mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
Add auto-resume on playback error setting and logic
Introduces a new user preference to automatically resume playback from the next available source when a playback error occurs. Updates settings UI, preferences store, and player error handling to support this feature, including new translations and backend support. Manual resume remains available if the feature is disabled.
This commit is contained in:
parent
32f7178a1e
commit
4ced25623f
10 changed files with 233 additions and 40 deletions
|
|
@ -841,9 +841,11 @@
|
|||
"errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.",
|
||||
"errorNotSupported": "The media or media provider object is not supported."
|
||||
},
|
||||
"autoResumeText": "There was an error trying to play the media 😖. Automatically trying the next source...",
|
||||
"copyDebugInfo": "Copy debug info",
|
||||
"debugInfo": "Check console for more details.",
|
||||
"homeButton": "Go home",
|
||||
"resumeButton": "Try next source",
|
||||
"text": "There was an error trying to play the media 😖. Please try again or try a different source!",
|
||||
"title": "Failed to play video!"
|
||||
},
|
||||
|
|
@ -1135,6 +1137,9 @@
|
|||
"manualSource": "Manual source selection",
|
||||
"manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.",
|
||||
"manualSourceLabel": "Manual source selection",
|
||||
"autoResumeOnPlaybackError": "Auto resume on playback error",
|
||||
"autoResumeOnPlaybackErrorDescription": "Automatically continue searching for other sources when the current source fails during playback. If disabled, you'll see an error screen with a manual resume option.",
|
||||
"autoResumeOnPlaybackErrorLabel": "Auto resume on playback error",
|
||||
"lastSuccessfulSource": "Last used source",
|
||||
"lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.",
|
||||
"lastSuccessfulSourceEnableLabel": "Last used source"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ export interface SettingsInput {
|
|||
homeSectionOrder?: string[] | null;
|
||||
manualSourceSelection?: boolean;
|
||||
enableDoubleClickToSeek?: boolean;
|
||||
enableAutoResumeOnPlaybackError?: boolean;
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
|
|
@ -69,6 +70,7 @@ export interface SettingsResponse {
|
|||
homeSectionOrder?: string[] | null;
|
||||
manualSourceSelection?: boolean;
|
||||
enableDoubleClickToSeek?: boolean;
|
||||
enableAutoResumeOnPlaybackError?: boolean;
|
||||
}
|
||||
|
||||
export function updateSettings(
|
||||
|
|
|
|||
|
|
@ -167,30 +167,53 @@ export function useScrape() {
|
|||
const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds);
|
||||
|
||||
const startScraping = useCallback(
|
||||
async (media: ScrapeMedia) => {
|
||||
// Create source order that prioritizes last successful source
|
||||
let filteredSourceOrder = enableSourceOrder
|
||||
? preferredSourceOrder.filter((id) => !disabledSources.includes(id))
|
||||
: undefined;
|
||||
async (media: ScrapeMedia, startFromSourceId?: string) => {
|
||||
const providerInstance = getProviders();
|
||||
const allSources = providerInstance.listSources();
|
||||
|
||||
// Start with all available sources (filtered by disabled ones)
|
||||
let baseSourceOrder = allSources
|
||||
.filter((source) => !disabledSources.includes(source.id))
|
||||
.map((source) => source.id);
|
||||
|
||||
// Apply custom source ordering if enabled
|
||||
if (enableSourceOrder && preferredSourceOrder.length > 0) {
|
||||
const orderedSources: string[] = [];
|
||||
const remainingSources = [...baseSourceOrder];
|
||||
|
||||
// Add sources in preferred order
|
||||
for (const sourceId of preferredSourceOrder) {
|
||||
const sourceIndex = remainingSources.indexOf(sourceId);
|
||||
if (sourceIndex !== -1) {
|
||||
orderedSources.push(sourceId);
|
||||
remainingSources.splice(sourceIndex, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Add remaining sources
|
||||
baseSourceOrder = [...orderedSources, ...remainingSources];
|
||||
}
|
||||
|
||||
// If we have a last successful source and the feature is enabled, prioritize it
|
||||
if (enableLastSuccessfulSource && lastSuccessfulSource) {
|
||||
// Get all available sources (either from custom order or default)
|
||||
const availableSources = filteredSourceOrder || [];
|
||||
|
||||
// If the last successful source is not disabled and exists in available sources,
|
||||
// move it to the front
|
||||
if (
|
||||
!disabledSources.includes(lastSuccessfulSource) &&
|
||||
availableSources.includes(lastSuccessfulSource)
|
||||
) {
|
||||
filteredSourceOrder = [
|
||||
const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource);
|
||||
if (lastSourceIndex !== -1) {
|
||||
baseSourceOrder = [
|
||||
lastSuccessfulSource,
|
||||
...availableSources.filter((id) => id !== lastSuccessfulSource),
|
||||
...baseSourceOrder.filter((id) => id !== lastSuccessfulSource),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// If starting from a specific source ID, filter the order to start AFTER that source
|
||||
let filteredSourceOrder = baseSourceOrder;
|
||||
if (startFromSourceId) {
|
||||
const startIndex = filteredSourceOrder.indexOf(startFromSourceId);
|
||||
if (startIndex !== -1) {
|
||||
filteredSourceOrder = filteredSourceOrder.slice(startIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out disabled embeds from the embed order
|
||||
const filteredEmbedOrder = enableEmbedOrder
|
||||
? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id))
|
||||
|
|
@ -223,9 +246,7 @@ export function useScrape() {
|
|||
const providers = getProviders();
|
||||
const output = await providers.runAll({
|
||||
media,
|
||||
// Only pass sourceOrder if enableSourceOrder is true, and filter out disabled sources
|
||||
sourceOrder: filteredSourceOrder,
|
||||
// Only pass embedOrder if enableEmbedOrder is true
|
||||
embedOrder: filteredEmbedOrder,
|
||||
events: {
|
||||
init: initEvent,
|
||||
|
|
@ -256,8 +277,16 @@ export function useScrape() {
|
|||
],
|
||||
);
|
||||
|
||||
const resumeScraping = useCallback(
|
||||
async (media: ScrapeMedia, startFromSourceId: string) => {
|
||||
return startScraping(media, startFromSourceId);
|
||||
},
|
||||
[startScraping],
|
||||
);
|
||||
|
||||
return {
|
||||
startScraping,
|
||||
resumeScraping,
|
||||
sourceOrder,
|
||||
sources,
|
||||
currentSource,
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ export function useSettingsState(
|
|||
homeSectionOrder: string[],
|
||||
manualSourceSelection: boolean,
|
||||
enableDoubleClickToSeek: boolean,
|
||||
enableAutoResumeOnPlaybackError: boolean,
|
||||
) {
|
||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||
useDerived(proxyUrls);
|
||||
|
|
@ -262,6 +263,12 @@ export function useSettingsState(
|
|||
resetEnableDoubleClickToSeek,
|
||||
enableDoubleClickToSeekChanged,
|
||||
] = useDerived(enableDoubleClickToSeek);
|
||||
const [
|
||||
enableAutoResumeOnPlaybackErrorState,
|
||||
setEnableAutoResumeOnPlaybackErrorState,
|
||||
resetEnableAutoResumeOnPlaybackError,
|
||||
enableAutoResumeOnPlaybackErrorChanged,
|
||||
] = useDerived(enableAutoResumeOnPlaybackError);
|
||||
|
||||
function reset() {
|
||||
resetTheme();
|
||||
|
|
@ -299,6 +306,7 @@ export function useSettingsState(
|
|||
resetHomeSectionOrder();
|
||||
resetManualSourceSelection();
|
||||
resetEnableDoubleClickToSeek();
|
||||
resetEnableAutoResumeOnPlaybackError();
|
||||
}
|
||||
|
||||
const changed =
|
||||
|
|
@ -336,7 +344,8 @@ export function useSettingsState(
|
|||
enableHoldToBoostChanged ||
|
||||
homeSectionOrderChanged ||
|
||||
manualSourceSelectionChanged ||
|
||||
enableDoubleClickToSeekChanged;
|
||||
enableDoubleClickToSeekChanged ||
|
||||
enableAutoResumeOnPlaybackErrorChanged;
|
||||
|
||||
return {
|
||||
reset,
|
||||
|
|
@ -516,5 +525,10 @@ export function useSettingsState(
|
|||
set: setEnableDoubleClickToSeekState,
|
||||
changed: enableDoubleClickToSeekChanged,
|
||||
},
|
||||
enableAutoResumeOnPlaybackError: {
|
||||
state: enableAutoResumeOnPlaybackErrorState,
|
||||
set: setEnableAutoResumeOnPlaybackErrorState,
|
||||
changed: enableAutoResumeOnPlaybackErrorChanged,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
|
|||
import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart";
|
||||
import { useLastNonPlayerLink } from "@/stores/history";
|
||||
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { getProgressPercentage, useProgressStore } from "@/stores/progress";
|
||||
import { needsOnboarding } from "@/utils/onboarding";
|
||||
|
|
@ -41,6 +42,9 @@ export function RealPlayerView() {
|
|||
sources: Record<string, ScrapingSegment>;
|
||||
sourceOrder: ScrapingItems[];
|
||||
} | null>(null);
|
||||
const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [startAtParam] = useQueryParam("t");
|
||||
const {
|
||||
status,
|
||||
|
|
@ -51,6 +55,7 @@ export function RealPlayerView() {
|
|||
setShouldStartFromBeginning,
|
||||
setStatus,
|
||||
} = usePlayer();
|
||||
const sourceId = usePlayerStore((s) => s.sourceId);
|
||||
const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
|
||||
const backUrl = useLastNonPlayerLink();
|
||||
const manualSourceSelection = usePreferencesStore(
|
||||
|
|
@ -158,6 +163,18 @@ export function RealPlayerView() {
|
|||
setStatus(playerStatus.SCRAPING);
|
||||
}, [setShouldStartFromBeginning, setStatus]);
|
||||
|
||||
const handleResumeScraping = useCallback(
|
||||
(startFromSourceId: string) => {
|
||||
// Set resume source first
|
||||
setResumeFromSourceId(startFromSourceId);
|
||||
// Then change status in next tick to ensure re-render
|
||||
setTimeout(() => {
|
||||
setStatus(playerStatus.SCRAPING);
|
||||
}, 0);
|
||||
},
|
||||
[setStatus],
|
||||
);
|
||||
|
||||
const playAfterScrape = useCallback(
|
||||
(out: RunOutput | null) => {
|
||||
if (!out) return;
|
||||
|
|
@ -198,13 +215,17 @@ export function RealPlayerView() {
|
|||
<SourceSelectPart media={scrapeMedia} />
|
||||
) : (
|
||||
<ScrapingPart
|
||||
key={`scraping-${resumeFromSourceId || "default"}`}
|
||||
media={scrapeMedia}
|
||||
startFromSourceId={resumeFromSourceId || undefined}
|
||||
onResult={(sources, sourceOrder) => {
|
||||
setErrorData({
|
||||
sourceOrder,
|
||||
sources,
|
||||
});
|
||||
setScrapeNotFound();
|
||||
// Clear resume state after scraping
|
||||
setResumeFromSourceId(null);
|
||||
}}
|
||||
onGetStream={playAfterScrape}
|
||||
/>
|
||||
|
|
@ -213,7 +234,12 @@ export function RealPlayerView() {
|
|||
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
|
||||
<ScrapeErrorPart data={errorData} />
|
||||
) : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null}
|
||||
{status === playerStatus.PLAYBACK_ERROR ? (
|
||||
<PlaybackErrorPart
|
||||
onResume={handleResumeScraping}
|
||||
currentSourceId={sourceId}
|
||||
/>
|
||||
) : null}
|
||||
</PlayerPart>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -486,6 +486,13 @@ export function SettingsPage() {
|
|||
(s) => s.setEnableDoubleClickToSeek,
|
||||
);
|
||||
|
||||
const enableAutoResumeOnPlaybackError = usePreferencesStore(
|
||||
(s) => s.enableAutoResumeOnPlaybackError,
|
||||
);
|
||||
const setEnableAutoResumeOnPlaybackError = usePreferencesStore(
|
||||
(s) => s.setEnableAutoResumeOnPlaybackError,
|
||||
);
|
||||
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
||||
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
||||
|
|
@ -557,6 +564,7 @@ export function SettingsPage() {
|
|||
homeSectionOrder,
|
||||
manualSourceSelection,
|
||||
enableDoubleClickToSeek,
|
||||
enableAutoResumeOnPlaybackError,
|
||||
);
|
||||
|
||||
const availableSources = useMemo(() => {
|
||||
|
|
@ -622,7 +630,8 @@ export function SettingsPage() {
|
|||
state.enableHoldToBoost.changed ||
|
||||
state.homeSectionOrder.changed ||
|
||||
state.manualSourceSelection.changed ||
|
||||
state.enableDoubleClickToSeek
|
||||
state.enableDoubleClickToSeek.changed ||
|
||||
state.enableAutoResumeOnPlaybackError
|
||||
) {
|
||||
await updateSettings(backendUrl, account, {
|
||||
applicationLanguage: state.appLanguage.state,
|
||||
|
|
@ -651,6 +660,8 @@ export function SettingsPage() {
|
|||
homeSectionOrder: state.homeSectionOrder.state,
|
||||
manualSourceSelection: state.manualSourceSelection.state,
|
||||
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
|
||||
enableAutoResumeOnPlaybackError:
|
||||
state.enableAutoResumeOnPlaybackError.state,
|
||||
});
|
||||
}
|
||||
if (state.deviceName.changed) {
|
||||
|
|
@ -705,6 +716,9 @@ export function SettingsPage() {
|
|||
setHomeSectionOrder(state.homeSectionOrder.state);
|
||||
setManualSourceSelection(state.manualSourceSelection.state);
|
||||
setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state);
|
||||
setEnableAutoResumeOnPlaybackError(
|
||||
state.enableAutoResumeOnPlaybackError.state,
|
||||
);
|
||||
|
||||
if (state.profile.state) {
|
||||
updateProfile(state.profile.state);
|
||||
|
|
@ -757,6 +771,7 @@ export function SettingsPage() {
|
|||
setHomeSectionOrder,
|
||||
setManualSourceSelection,
|
||||
setEnableDoubleClickToSeek,
|
||||
setEnableAutoResumeOnPlaybackError,
|
||||
]);
|
||||
return (
|
||||
<SubPageLayout>
|
||||
|
|
@ -838,6 +853,12 @@ export function SettingsPage() {
|
|||
setManualSourceSelection={state.manualSourceSelection.set}
|
||||
enableDoubleClickToSeek={state.enableDoubleClickToSeek.state}
|
||||
setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set}
|
||||
enableAutoResumeOnPlaybackError={
|
||||
state.enableAutoResumeOnPlaybackError.state
|
||||
}
|
||||
setEnableAutoResumeOnPlaybackError={
|
||||
state.enableAutoResumeOnPlaybackError.set
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,26 +14,64 @@ import { usePreferencesStore } from "@/stores/preferences";
|
|||
|
||||
import { ErrorCardInModal } from "../errors/ErrorCard";
|
||||
|
||||
export function PlaybackErrorPart() {
|
||||
export interface PlaybackErrorPartProps {
|
||||
onResume?: (startFromSourceId: string) => void;
|
||||
currentSourceId?: string | null;
|
||||
}
|
||||
|
||||
export function PlaybackErrorPart(props: PlaybackErrorPartProps) {
|
||||
const { t } = useTranslation();
|
||||
const playbackError = usePlayerStore((s) => s.interface.error);
|
||||
const modal = useModal("error");
|
||||
const settingsRouter = useOverlayRouter("settings");
|
||||
const hasOpenedSettings = useRef(false);
|
||||
const hasAutoResumed = useRef(false);
|
||||
const setLastSuccessfulSource = usePreferencesStore(
|
||||
(s) => s.setLastSuccessfulSource,
|
||||
);
|
||||
const enableAutoResumeOnPlaybackError = usePreferencesStore(
|
||||
(s) => s.enableAutoResumeOnPlaybackError,
|
||||
);
|
||||
|
||||
// Automatically open the settings overlay when a playback error occurs
|
||||
// Automatically open the settings overlay when a playback error occurs (unless auto-resume is enabled)
|
||||
useEffect(() => {
|
||||
if (playbackError && !hasOpenedSettings.current) {
|
||||
if (
|
||||
playbackError &&
|
||||
!hasOpenedSettings.current &&
|
||||
!enableAutoResumeOnPlaybackError
|
||||
) {
|
||||
hasOpenedSettings.current = true;
|
||||
// Reset the last successful source when a playback error occurs
|
||||
setLastSuccessfulSource(null);
|
||||
settingsRouter.open();
|
||||
settingsRouter.navigate("/source");
|
||||
}
|
||||
}, [playbackError, settingsRouter, setLastSuccessfulSource]);
|
||||
}, [
|
||||
playbackError,
|
||||
settingsRouter,
|
||||
setLastSuccessfulSource,
|
||||
enableAutoResumeOnPlaybackError,
|
||||
]);
|
||||
|
||||
// Automatically resume scraping from the next source if enabled
|
||||
useEffect(() => {
|
||||
if (
|
||||
playbackError &&
|
||||
!hasAutoResumed.current &&
|
||||
enableAutoResumeOnPlaybackError &&
|
||||
props.currentSourceId &&
|
||||
props.onResume
|
||||
) {
|
||||
hasAutoResumed.current = true;
|
||||
// Immediately call resume without delay since we don't need the overlay
|
||||
props.onResume!(props.currentSourceId!);
|
||||
}
|
||||
}, [
|
||||
playbackError,
|
||||
enableAutoResumeOnPlaybackError,
|
||||
props.currentSourceId,
|
||||
props.onResume,
|
||||
]);
|
||||
|
||||
const handleOpenSourcePicker = () => {
|
||||
settingsRouter.open();
|
||||
|
|
@ -45,7 +83,33 @@ export function PlaybackErrorPart() {
|
|||
<ErrorContainer>
|
||||
<IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
|
||||
<Title>{t("player.playbackError.title")}</Title>
|
||||
<Paragraph>{t("player.playbackError.text")}</Paragraph>
|
||||
<Paragraph>
|
||||
{enableAutoResumeOnPlaybackError
|
||||
? t("player.playbackError.autoResumeText")
|
||||
: t("player.playbackError.text")}
|
||||
</Paragraph>
|
||||
<div className="flex gap-3">
|
||||
{props.currentSourceId &&
|
||||
props.onResume &&
|
||||
!enableAutoResumeOnPlaybackError && (
|
||||
<Button
|
||||
onClick={() => props.onResume!(props.currentSourceId!)}
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
{t("player.playbackError.resumeButton")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleOpenSourcePicker}
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
{t("player.menus.sources.title")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => modal.show()}
|
||||
|
|
@ -55,14 +119,6 @@ export function PlaybackErrorPart() {
|
|||
>
|
||||
{t("errors.showError")}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleOpenSourcePicker}
|
||||
theme="purple"
|
||||
padding="md:px-12 p-2.5"
|
||||
className="mt-6"
|
||||
>
|
||||
{t("player.menus.sources.title")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
|
|
|
|||
|
|
@ -31,11 +31,13 @@ export interface ScrapingProps {
|
|||
sources: Record<string, ScrapingSegment>,
|
||||
sourceOrder: ScrapingItems[],
|
||||
) => void;
|
||||
startFromSourceId?: string;
|
||||
}
|
||||
|
||||
export function ScrapingPart(props: ScrapingProps) {
|
||||
const { report } = useReportProviders();
|
||||
const { startScraping, sourceOrder, sources, currentSource } = useScrape();
|
||||
const { startScraping, resumeScraping, sourceOrder, sources, currentSource } =
|
||||
useScrape();
|
||||
const isMounted = useMountedState();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -60,12 +62,17 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||
};
|
||||
}, [sourceOrder, sources]);
|
||||
|
||||
const started = useRef(false);
|
||||
const started = useRef<string | null>(null);
|
||||
useEffect(() => {
|
||||
if (started.current) return;
|
||||
started.current = true;
|
||||
// Only start scraping if we haven't started with this startFromSourceId before
|
||||
const currentKey = props.startFromSourceId || "default";
|
||||
if (started.current === currentKey) return;
|
||||
started.current = currentKey;
|
||||
|
||||
(async () => {
|
||||
const output = await startScraping(props.media);
|
||||
const output = props.startFromSourceId
|
||||
? await resumeScraping(props.media, props.startFromSourceId)
|
||||
: await startScraping(props.media);
|
||||
if (!isMounted()) return;
|
||||
props.onResult?.(
|
||||
resultRef.current.sources,
|
||||
|
|
@ -80,7 +87,7 @@ export function ScrapingPart(props: ScrapingProps) {
|
|||
);
|
||||
props.onGetStream?.(output);
|
||||
})().catch(() => setFailedStartScrape(true));
|
||||
}, [startScraping, props, report, isMounted]);
|
||||
}, [startScraping, resumeScraping, props, report, isMounted]);
|
||||
|
||||
let currentProviderIndex = sourceOrder.findIndex(
|
||||
(s) => s.id === currentSource || s.children.includes(currentSource ?? ""),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ export function PreferencesPart(props: {
|
|||
setManualSourceSelection: (v: boolean) => void;
|
||||
enableDoubleClickToSeek: boolean;
|
||||
setEnableDoubleClickToSeek: (v: boolean) => void;
|
||||
enableAutoResumeOnPlaybackError: boolean;
|
||||
setEnableAutoResumeOnPlaybackError: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
|
||||
|
|
@ -270,6 +272,29 @@ export function PreferencesPart(props: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Auto Resume on Playback Error */}
|
||||
<div>
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.preferences.autoResumeOnPlaybackError")}
|
||||
</p>
|
||||
<p className="max-w-[25rem] font-medium">
|
||||
{t("settings.preferences.autoResumeOnPlaybackErrorDescription")}
|
||||
</p>
|
||||
<div
|
||||
onClick={() =>
|
||||
props.setEnableAutoResumeOnPlaybackError(
|
||||
!props.enableAutoResumeOnPlaybackError,
|
||||
)
|
||||
}
|
||||
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
|
||||
>
|
||||
<Toggle enabled={props.enableAutoResumeOnPlaybackError} />
|
||||
<p className="flex-1 text-white font-bold">
|
||||
{t("settings.preferences.autoResumeOnPlaybackErrorLabel")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last Successful Source Preference */}
|
||||
<div>
|
||||
<p className="text-white font-bold mb-3">
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ export interface PreferencesStore {
|
|||
homeSectionOrder: string[];
|
||||
manualSourceSelection: boolean;
|
||||
enableDoubleClickToSeek: boolean;
|
||||
enableAutoResumeOnPlaybackError: boolean;
|
||||
|
||||
setEnableThumbnails(v: boolean): void;
|
||||
setEnableAutoplay(v: boolean): void;
|
||||
|
|
@ -58,6 +59,7 @@ export interface PreferencesStore {
|
|||
setHomeSectionOrder(v: string[]): void;
|
||||
setManualSourceSelection(v: boolean): void;
|
||||
setEnableDoubleClickToSeek(v: boolean): void;
|
||||
setEnableAutoResumeOnPlaybackError(v: boolean): void;
|
||||
}
|
||||
|
||||
export const usePreferencesStore = create(
|
||||
|
|
@ -90,6 +92,7 @@ export const usePreferencesStore = create(
|
|||
homeSectionOrder: ["watching", "bookmarks"],
|
||||
manualSourceSelection: false,
|
||||
enableDoubleClickToSeek: false,
|
||||
enableAutoResumeOnPlaybackError: true,
|
||||
setEnableThumbnails(v) {
|
||||
set((s) => {
|
||||
s.enableThumbnails = v;
|
||||
|
|
@ -230,6 +233,11 @@ export const usePreferencesStore = create(
|
|||
s.enableDoubleClickToSeek = v;
|
||||
});
|
||||
},
|
||||
setEnableAutoResumeOnPlaybackError(v) {
|
||||
set((s) => {
|
||||
s.enableAutoResumeOnPlaybackError = v;
|
||||
});
|
||||
},
|
||||
})),
|
||||
{
|
||||
name: "__MW::preferences",
|
||||
|
|
|
|||
Loading…
Reference in a new issue