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:
Pas 2025-11-30 17:28:41 -07:00
parent 32f7178a1e
commit 4ced25623f
10 changed files with 233 additions and 40 deletions

View file

@ -841,9 +841,11 @@
"errorNetwork": "Some kind of network error occurred which prevented the media from being successfully fetched, despite having previously been available.", "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." "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", "copyDebugInfo": "Copy debug info",
"debugInfo": "Check console for more details.", "debugInfo": "Check console for more details.",
"homeButton": "Go home", "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!", "text": "There was an error trying to play the media 😖. Please try again or try a different source!",
"title": "Failed to play video!" "title": "Failed to play video!"
}, },
@ -1135,6 +1137,9 @@
"manualSource": "Manual source selection", "manualSource": "Manual source selection",
"manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.", "manualSourceDescription": "Require picking a source before scraping. Disables automatic source selection and opens the source picker when starting playback.",
"manualSourceLabel": "Manual source selection", "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", "lastSuccessfulSource": "Last used source",
"lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.", "lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.",
"lastSuccessfulSourceEnableLabel": "Last used source" "lastSuccessfulSourceEnableLabel": "Last used source"

View file

@ -35,6 +35,7 @@ export interface SettingsInput {
homeSectionOrder?: string[] | null; homeSectionOrder?: string[] | null;
manualSourceSelection?: boolean; manualSourceSelection?: boolean;
enableDoubleClickToSeek?: boolean; enableDoubleClickToSeek?: boolean;
enableAutoResumeOnPlaybackError?: boolean;
} }
export interface SettingsResponse { export interface SettingsResponse {
@ -69,6 +70,7 @@ export interface SettingsResponse {
homeSectionOrder?: string[] | null; homeSectionOrder?: string[] | null;
manualSourceSelection?: boolean; manualSourceSelection?: boolean;
enableDoubleClickToSeek?: boolean; enableDoubleClickToSeek?: boolean;
enableAutoResumeOnPlaybackError?: boolean;
} }
export function updateSettings( export function updateSettings(

View file

@ -167,30 +167,53 @@ export function useScrape() {
const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds); const disabledEmbeds = usePreferencesStore((s) => s.disabledEmbeds);
const startScraping = useCallback( const startScraping = useCallback(
async (media: ScrapeMedia) => { async (media: ScrapeMedia, startFromSourceId?: string) => {
// Create source order that prioritizes last successful source const providerInstance = getProviders();
let filteredSourceOrder = enableSourceOrder const allSources = providerInstance.listSources();
? preferredSourceOrder.filter((id) => !disabledSources.includes(id))
: undefined; // 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 we have a last successful source and the feature is enabled, prioritize it
if (enableLastSuccessfulSource && lastSuccessfulSource) { if (enableLastSuccessfulSource && lastSuccessfulSource) {
// Get all available sources (either from custom order or default) const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource);
const availableSources = filteredSourceOrder || []; if (lastSourceIndex !== -1) {
baseSourceOrder = [
// 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 = [
lastSuccessfulSource, 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 // Filter out disabled embeds from the embed order
const filteredEmbedOrder = enableEmbedOrder const filteredEmbedOrder = enableEmbedOrder
? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) ? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id))
@ -223,9 +246,7 @@ export function useScrape() {
const providers = getProviders(); const providers = getProviders();
const output = await providers.runAll({ const output = await providers.runAll({
media, media,
// Only pass sourceOrder if enableSourceOrder is true, and filter out disabled sources
sourceOrder: filteredSourceOrder, sourceOrder: filteredSourceOrder,
// Only pass embedOrder if enableEmbedOrder is true
embedOrder: filteredEmbedOrder, embedOrder: filteredEmbedOrder,
events: { events: {
init: initEvent, init: initEvent,
@ -256,8 +277,16 @@ export function useScrape() {
], ],
); );
const resumeScraping = useCallback(
async (media: ScrapeMedia, startFromSourceId: string) => {
return startScraping(media, startFromSourceId);
},
[startScraping],
);
return { return {
startScraping, startScraping,
resumeScraping,
sourceOrder, sourceOrder,
sources, sources,
currentSource, currentSource,

View file

@ -79,6 +79,7 @@ export function useSettingsState(
homeSectionOrder: string[], homeSectionOrder: string[],
manualSourceSelection: boolean, manualSourceSelection: boolean,
enableDoubleClickToSeek: boolean, enableDoubleClickToSeek: boolean,
enableAutoResumeOnPlaybackError: boolean,
) { ) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] = const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls); useDerived(proxyUrls);
@ -262,6 +263,12 @@ export function useSettingsState(
resetEnableDoubleClickToSeek, resetEnableDoubleClickToSeek,
enableDoubleClickToSeekChanged, enableDoubleClickToSeekChanged,
] = useDerived(enableDoubleClickToSeek); ] = useDerived(enableDoubleClickToSeek);
const [
enableAutoResumeOnPlaybackErrorState,
setEnableAutoResumeOnPlaybackErrorState,
resetEnableAutoResumeOnPlaybackError,
enableAutoResumeOnPlaybackErrorChanged,
] = useDerived(enableAutoResumeOnPlaybackError);
function reset() { function reset() {
resetTheme(); resetTheme();
@ -299,6 +306,7 @@ export function useSettingsState(
resetHomeSectionOrder(); resetHomeSectionOrder();
resetManualSourceSelection(); resetManualSourceSelection();
resetEnableDoubleClickToSeek(); resetEnableDoubleClickToSeek();
resetEnableAutoResumeOnPlaybackError();
} }
const changed = const changed =
@ -336,7 +344,8 @@ export function useSettingsState(
enableHoldToBoostChanged || enableHoldToBoostChanged ||
homeSectionOrderChanged || homeSectionOrderChanged ||
manualSourceSelectionChanged || manualSourceSelectionChanged ||
enableDoubleClickToSeekChanged; enableDoubleClickToSeekChanged ||
enableAutoResumeOnPlaybackErrorChanged;
return { return {
reset, reset,
@ -516,5 +525,10 @@ export function useSettingsState(
set: setEnableDoubleClickToSeekState, set: setEnableDoubleClickToSeekState,
changed: enableDoubleClickToSeekChanged, changed: enableDoubleClickToSeekChanged,
}, },
enableAutoResumeOnPlaybackError: {
state: enableAutoResumeOnPlaybackErrorState,
set: setEnableAutoResumeOnPlaybackErrorState,
changed: enableAutoResumeOnPlaybackErrorChanged,
},
}; };
} }

View file

@ -25,6 +25,7 @@ import { ScrapingPart } from "@/pages/parts/player/ScrapingPart";
import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart"; import { SourceSelectPart } from "@/pages/parts/player/SourceSelectPart";
import { useLastNonPlayerLink } from "@/stores/history"; import { useLastNonPlayerLink } from "@/stores/history";
import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
import { getProgressPercentage, useProgressStore } from "@/stores/progress"; import { getProgressPercentage, useProgressStore } from "@/stores/progress";
import { needsOnboarding } from "@/utils/onboarding"; import { needsOnboarding } from "@/utils/onboarding";
@ -41,6 +42,9 @@ export function RealPlayerView() {
sources: Record<string, ScrapingSegment>; sources: Record<string, ScrapingSegment>;
sourceOrder: ScrapingItems[]; sourceOrder: ScrapingItems[];
} | null>(null); } | null>(null);
const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>(
null,
);
const [startAtParam] = useQueryParam("t"); const [startAtParam] = useQueryParam("t");
const { const {
status, status,
@ -51,6 +55,7 @@ export function RealPlayerView() {
setShouldStartFromBeginning, setShouldStartFromBeginning,
setStatus, setStatus,
} = usePlayer(); } = usePlayer();
const sourceId = usePlayerStore((s) => s.sourceId);
const { setPlayerMeta, scrapeMedia } = usePlayerMeta(); const { setPlayerMeta, scrapeMedia } = usePlayerMeta();
const backUrl = useLastNonPlayerLink(); const backUrl = useLastNonPlayerLink();
const manualSourceSelection = usePreferencesStore( const manualSourceSelection = usePreferencesStore(
@ -158,6 +163,18 @@ export function RealPlayerView() {
setStatus(playerStatus.SCRAPING); setStatus(playerStatus.SCRAPING);
}, [setShouldStartFromBeginning, setStatus]); }, [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( const playAfterScrape = useCallback(
(out: RunOutput | null) => { (out: RunOutput | null) => {
if (!out) return; if (!out) return;
@ -198,13 +215,17 @@ export function RealPlayerView() {
<SourceSelectPart media={scrapeMedia} /> <SourceSelectPart media={scrapeMedia} />
) : ( ) : (
<ScrapingPart <ScrapingPart
key={`scraping-${resumeFromSourceId || "default"}`}
media={scrapeMedia} media={scrapeMedia}
startFromSourceId={resumeFromSourceId || undefined}
onResult={(sources, sourceOrder) => { onResult={(sources, sourceOrder) => {
setErrorData({ setErrorData({
sourceOrder, sourceOrder,
sources, sources,
}); });
setScrapeNotFound(); setScrapeNotFound();
// Clear resume state after scraping
setResumeFromSourceId(null);
}} }}
onGetStream={playAfterScrape} onGetStream={playAfterScrape}
/> />
@ -213,7 +234,12 @@ export function RealPlayerView() {
{status === playerStatus.SCRAPE_NOT_FOUND && errorData ? ( {status === playerStatus.SCRAPE_NOT_FOUND && errorData ? (
<ScrapeErrorPart data={errorData} /> <ScrapeErrorPart data={errorData} />
) : null} ) : null}
{status === playerStatus.PLAYBACK_ERROR ? <PlaybackErrorPart /> : null} {status === playerStatus.PLAYBACK_ERROR ? (
<PlaybackErrorPart
onResume={handleResumeScraping}
currentSourceId={sourceId}
/>
) : null}
</PlayerPart> </PlayerPart>
); );
} }

View file

@ -486,6 +486,13 @@ export function SettingsPage() {
(s) => s.setEnableDoubleClickToSeek, (s) => s.setEnableDoubleClickToSeek,
); );
const enableAutoResumeOnPlaybackError = usePreferencesStore(
(s) => s.enableAutoResumeOnPlaybackError,
);
const setEnableAutoResumeOnPlaybackError = usePreferencesStore(
(s) => s.setEnableAutoResumeOnPlaybackError,
);
const account = useAuthStore((s) => s.account); const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName); const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
@ -557,6 +564,7 @@ export function SettingsPage() {
homeSectionOrder, homeSectionOrder,
manualSourceSelection, manualSourceSelection,
enableDoubleClickToSeek, enableDoubleClickToSeek,
enableAutoResumeOnPlaybackError,
); );
const availableSources = useMemo(() => { const availableSources = useMemo(() => {
@ -622,7 +630,8 @@ export function SettingsPage() {
state.enableHoldToBoost.changed || state.enableHoldToBoost.changed ||
state.homeSectionOrder.changed || state.homeSectionOrder.changed ||
state.manualSourceSelection.changed || state.manualSourceSelection.changed ||
state.enableDoubleClickToSeek state.enableDoubleClickToSeek.changed ||
state.enableAutoResumeOnPlaybackError
) { ) {
await updateSettings(backendUrl, account, { await updateSettings(backendUrl, account, {
applicationLanguage: state.appLanguage.state, applicationLanguage: state.appLanguage.state,
@ -651,6 +660,8 @@ export function SettingsPage() {
homeSectionOrder: state.homeSectionOrder.state, homeSectionOrder: state.homeSectionOrder.state,
manualSourceSelection: state.manualSourceSelection.state, manualSourceSelection: state.manualSourceSelection.state,
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state, enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
enableAutoResumeOnPlaybackError:
state.enableAutoResumeOnPlaybackError.state,
}); });
} }
if (state.deviceName.changed) { if (state.deviceName.changed) {
@ -705,6 +716,9 @@ export function SettingsPage() {
setHomeSectionOrder(state.homeSectionOrder.state); setHomeSectionOrder(state.homeSectionOrder.state);
setManualSourceSelection(state.manualSourceSelection.state); setManualSourceSelection(state.manualSourceSelection.state);
setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state); setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state);
setEnableAutoResumeOnPlaybackError(
state.enableAutoResumeOnPlaybackError.state,
);
if (state.profile.state) { if (state.profile.state) {
updateProfile(state.profile.state); updateProfile(state.profile.state);
@ -757,6 +771,7 @@ export function SettingsPage() {
setHomeSectionOrder, setHomeSectionOrder,
setManualSourceSelection, setManualSourceSelection,
setEnableDoubleClickToSeek, setEnableDoubleClickToSeek,
setEnableAutoResumeOnPlaybackError,
]); ]);
return ( return (
<SubPageLayout> <SubPageLayout>
@ -838,6 +853,12 @@ export function SettingsPage() {
setManualSourceSelection={state.manualSourceSelection.set} setManualSourceSelection={state.manualSourceSelection.set}
enableDoubleClickToSeek={state.enableDoubleClickToSeek.state} enableDoubleClickToSeek={state.enableDoubleClickToSeek.state}
setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set} setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set}
enableAutoResumeOnPlaybackError={
state.enableAutoResumeOnPlaybackError.state
}
setEnableAutoResumeOnPlaybackError={
state.enableAutoResumeOnPlaybackError.set
}
/> />
</div> </div>
)} )}

View file

@ -14,26 +14,64 @@ import { usePreferencesStore } from "@/stores/preferences";
import { ErrorCardInModal } from "../errors/ErrorCard"; 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 { t } = useTranslation();
const playbackError = usePlayerStore((s) => s.interface.error); const playbackError = usePlayerStore((s) => s.interface.error);
const modal = useModal("error"); const modal = useModal("error");
const settingsRouter = useOverlayRouter("settings"); const settingsRouter = useOverlayRouter("settings");
const hasOpenedSettings = useRef(false); const hasOpenedSettings = useRef(false);
const hasAutoResumed = useRef(false);
const setLastSuccessfulSource = usePreferencesStore( const setLastSuccessfulSource = usePreferencesStore(
(s) => s.setLastSuccessfulSource, (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(() => { useEffect(() => {
if (playbackError && !hasOpenedSettings.current) { if (
playbackError &&
!hasOpenedSettings.current &&
!enableAutoResumeOnPlaybackError
) {
hasOpenedSettings.current = true; hasOpenedSettings.current = true;
// Reset the last successful source when a playback error occurs // Reset the last successful source when a playback error occurs
setLastSuccessfulSource(null); setLastSuccessfulSource(null);
settingsRouter.open(); settingsRouter.open();
settingsRouter.navigate("/source"); 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 = () => { const handleOpenSourcePicker = () => {
settingsRouter.open(); settingsRouter.open();
@ -45,7 +83,33 @@ export function PlaybackErrorPart() {
<ErrorContainer> <ErrorContainer>
<IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill> <IconPill icon={Icons.WAND}>{t("player.playbackError.badge")}</IconPill>
<Title>{t("player.playbackError.title")}</Title> <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"> <div className="flex gap-3">
<Button <Button
onClick={() => modal.show()} onClick={() => modal.show()}
@ -55,14 +119,6 @@ export function PlaybackErrorPart() {
> >
{t("errors.showError")} {t("errors.showError")}
</Button> </Button>
<Button
onClick={handleOpenSourcePicker}
theme="purple"
padding="md:px-12 p-2.5"
className="mt-6"
>
{t("player.menus.sources.title")}
</Button>
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button

View file

@ -31,11 +31,13 @@ export interface ScrapingProps {
sources: Record<string, ScrapingSegment>, sources: Record<string, ScrapingSegment>,
sourceOrder: ScrapingItems[], sourceOrder: ScrapingItems[],
) => void; ) => void;
startFromSourceId?: string;
} }
export function ScrapingPart(props: ScrapingProps) { export function ScrapingPart(props: ScrapingProps) {
const { report } = useReportProviders(); const { report } = useReportProviders();
const { startScraping, sourceOrder, sources, currentSource } = useScrape(); const { startScraping, resumeScraping, sourceOrder, sources, currentSource } =
useScrape();
const isMounted = useMountedState(); const isMounted = useMountedState();
const { t } = useTranslation(); const { t } = useTranslation();
@ -60,12 +62,17 @@ export function ScrapingPart(props: ScrapingProps) {
}; };
}, [sourceOrder, sources]); }, [sourceOrder, sources]);
const started = useRef(false); const started = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
if (started.current) return; // Only start scraping if we haven't started with this startFromSourceId before
started.current = true; const currentKey = props.startFromSourceId || "default";
if (started.current === currentKey) return;
started.current = currentKey;
(async () => { (async () => {
const output = await startScraping(props.media); const output = props.startFromSourceId
? await resumeScraping(props.media, props.startFromSourceId)
: await startScraping(props.media);
if (!isMounted()) return; if (!isMounted()) return;
props.onResult?.( props.onResult?.(
resultRef.current.sources, resultRef.current.sources,
@ -80,7 +87,7 @@ export function ScrapingPart(props: ScrapingProps) {
); );
props.onGetStream?.(output); props.onGetStream?.(output);
})().catch(() => setFailedStartScrape(true)); })().catch(() => setFailedStartScrape(true));
}, [startScraping, props, report, isMounted]); }, [startScraping, resumeScraping, props, report, isMounted]);
let currentProviderIndex = sourceOrder.findIndex( let currentProviderIndex = sourceOrder.findIndex(
(s) => s.id === currentSource || s.children.includes(currentSource ?? ""), (s) => s.id === currentSource || s.children.includes(currentSource ?? ""),

View file

@ -39,6 +39,8 @@ export function PreferencesPart(props: {
setManualSourceSelection: (v: boolean) => void; setManualSourceSelection: (v: boolean) => void;
enableDoubleClickToSeek: boolean; enableDoubleClickToSeek: boolean;
setEnableDoubleClickToSeek: (v: boolean) => void; setEnableDoubleClickToSeek: (v: boolean) => void;
enableAutoResumeOnPlaybackError: boolean;
setEnableAutoResumeOnPlaybackError: (v: boolean) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code)); const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
@ -270,6 +272,29 @@ export function PreferencesPart(props: {
</div> </div>
</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 */} {/* Last Successful Source Preference */}
<div> <div>
<p className="text-white font-bold mb-3"> <p className="text-white font-bold mb-3">

View file

@ -30,6 +30,7 @@ export interface PreferencesStore {
homeSectionOrder: string[]; homeSectionOrder: string[];
manualSourceSelection: boolean; manualSourceSelection: boolean;
enableDoubleClickToSeek: boolean; enableDoubleClickToSeek: boolean;
enableAutoResumeOnPlaybackError: boolean;
setEnableThumbnails(v: boolean): void; setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void; setEnableAutoplay(v: boolean): void;
@ -58,6 +59,7 @@ export interface PreferencesStore {
setHomeSectionOrder(v: string[]): void; setHomeSectionOrder(v: string[]): void;
setManualSourceSelection(v: boolean): void; setManualSourceSelection(v: boolean): void;
setEnableDoubleClickToSeek(v: boolean): void; setEnableDoubleClickToSeek(v: boolean): void;
setEnableAutoResumeOnPlaybackError(v: boolean): void;
} }
export const usePreferencesStore = create( export const usePreferencesStore = create(
@ -90,6 +92,7 @@ export const usePreferencesStore = create(
homeSectionOrder: ["watching", "bookmarks"], homeSectionOrder: ["watching", "bookmarks"],
manualSourceSelection: false, manualSourceSelection: false,
enableDoubleClickToSeek: false, enableDoubleClickToSeek: false,
enableAutoResumeOnPlaybackError: true,
setEnableThumbnails(v) { setEnableThumbnails(v) {
set((s) => { set((s) => {
s.enableThumbnails = v; s.enableThumbnails = v;
@ -230,6 +233,11 @@ export const usePreferencesStore = create(
s.enableDoubleClickToSeek = v; s.enableDoubleClickToSeek = v;
}); });
}, },
setEnableAutoResumeOnPlaybackError(v) {
set((s) => {
s.enableAutoResumeOnPlaybackError = v;
});
},
})), })),
{ {
name: "__MW::preferences", name: "__MW::preferences",