From d575d711084379c584c9727c58c0a119019f689b Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Sun, 2 Nov 2025 22:10:48 -0700 Subject: [PATCH] add lastSuccessfulSource feature to sort the last successful source to the top --- src/assets/locales/en.json | 5 ++- src/backend/accounts/settings.ts | 4 ++ .../player/atoms/NextEpisodeButton.tsx | 12 ++++++ .../atoms/settings/SourceSelectingView.tsx | 38 ++++++++++++++++++- .../player/internals/SkipEpisodeButton.tsx | 11 +++++- src/hooks/auth/useAuthData.ts | 16 ++++++++ src/hooks/useProviderScrape.tsx | 30 ++++++++++++++- src/hooks/useSettingsState.ts | 28 ++++++++++++++ src/pages/PlayerView.tsx | 10 +++++ src/pages/Settings.tsx | 26 +++++++++++++ src/pages/parts/player/SourceSelectPart.tsx | 37 +++++++++++++++++- src/pages/parts/settings/PreferencesPart.tsx | 26 +++++++++++++ src/stores/preferences/index.tsx | 16 ++++++++ 13 files changed, 251 insertions(+), 8 deletions(-) diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index f9fd3941..de33a8e3 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1107,7 +1107,10 @@ "embedOrderEnableLabel": "Custom embed order", "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" + "manualSourceLabel": "Manual source selection", + "lastSuccessfulSource": "Last successful source", + "lastSuccessfulSourceDescription": "Automatically prioritize the source that successfully provided content for the previous episode. This helps ensure continuity when watching series.", + "lastSuccessfulSourceEnableLabel": "Enable last successful source" }, "reset": "Reset", "save": "Save", diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts index 1460aa9c..14f9ca67 100644 --- a/src/backend/accounts/settings.ts +++ b/src/backend/accounts/settings.ts @@ -21,6 +21,8 @@ export interface SettingsInput { forceCompactEpisodeView?: boolean; sourceOrder?: string[] | null; enableSourceOrder?: boolean; + lastSuccessfulSource?: string | null; + enableLastSuccessfulSource?: boolean; disabledSources?: string[] | null; embedOrder?: string[] | null; enableEmbedOrder?: boolean; @@ -52,6 +54,8 @@ export interface SettingsResponse { forceCompactEpisodeView?: boolean; sourceOrder?: string[] | null; enableSourceOrder?: boolean; + lastSuccessfulSource?: string | null; + enableLastSuccessfulSource?: boolean; disabledSources?: string[] | null; embedOrder?: string[] | null; enableEmbedOrder?: boolean; diff --git a/src/components/player/atoms/NextEpisodeButton.tsx b/src/components/player/atoms/NextEpisodeButton.tsx index 86e082c5..a59d2efb 100644 --- a/src/components/player/atoms/NextEpisodeButton.tsx +++ b/src/components/player/atoms/NextEpisodeButton.tsx @@ -106,12 +106,16 @@ export function NextEpisodeButton(props: { const time = usePlayerStore((s) => s.progress.time); const enableAutoplay = usePreferencesStore((s) => s.enableAutoplay); const enableSkipCredits = usePreferencesStore((s) => s.enableSkipCredits); + const setLastSuccessfulSource = usePreferencesStore( + (s) => s.setLastSuccessfulSource, + ); const showingState = shouldShowNextEpisodeButton(time, duration); const status = usePlayerStore((s) => s.status); const setShouldStartFromBeginning = usePlayerStore( (s) => s.setShouldStartFromBeginning, ); const updateItem = useProgressStore((s) => s.updateItem); + const sourceId = usePlayerStore((s) => s.sourceId); const isLastEpisode = !meta?.episode?.number || !meta?.episodes?.at(-1)?.number @@ -147,6 +151,12 @@ export function NextEpisodeButton(props: { const loadNextEpisode = useCallback(() => { if (!meta || !nextEp) return; + + // Store the current source as the last successful source + if (sourceId) { + setLastSuccessfulSource(sourceId); + } + const metaCopy = { ...meta }; metaCopy.episode = nextEp; metaCopy.season = @@ -173,6 +183,8 @@ export function NextEpisodeButton(props: { updateItem, isLastEpisode, nextSeason, + sourceId, + setLastSuccessfulSource, ]); const startCurrentEpisodeFromBeginning = useCallback(() => { diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index c9ec789e..344c87d3 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -144,6 +144,12 @@ export function SourceSelectionView({ const currentSourceId = usePlayerStore((s) => s.sourceId); const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); + const lastSuccessfulSource = usePreferencesStore( + (s) => s.lastSuccessfulSource, + ); + const enableLastSuccessfulSource = usePreferencesStore( + (s) => s.enableLastSuccessfulSource, + ); const disabledSources = usePreferencesStore((s) => s.disabledSources); const sources = useMemo(() => { @@ -154,13 +160,34 @@ export function SourceSelectionView({ .filter((v) => !disabledSources.includes(v.id)); if (!enableSourceOrder || preferredSourceOrder.length === 0) { + // Even without custom source order, prioritize last successful source if enabled + if (enableLastSuccessfulSource && lastSuccessfulSource) { + const lastSourceIndex = allSources.findIndex( + (s) => s.id === lastSuccessfulSource, + ); + if (lastSourceIndex !== -1) { + const lastSource = allSources.splice(lastSourceIndex, 1)[0]; + return [lastSource, ...allSources]; + } + } return allSources; } - // Sort sources according to preferred order + // Sort sources according to preferred order, but prioritize last successful source const orderedSources = []; const remainingSources = [...allSources]; + // First, add the last successful source if it exists, is available, and the feature is enabled + if (enableLastSuccessfulSource && lastSuccessfulSource) { + const lastSourceIndex = remainingSources.findIndex( + (s) => s.id === lastSuccessfulSource, + ); + if (lastSourceIndex !== -1) { + orderedSources.push(remainingSources[lastSourceIndex]); + remainingSources.splice(lastSourceIndex, 1); + } + } + // Add sources in preferred order for (const sourceId of preferredSourceOrder) { const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId); @@ -174,7 +201,14 @@ export function SourceSelectionView({ orderedSources.push(...remainingSources); return orderedSources; - }, [metaType, preferredSourceOrder, enableSourceOrder, disabledSources]); + }, [ + metaType, + preferredSourceOrder, + enableSourceOrder, + disabledSources, + lastSuccessfulSource, + enableLastSuccessfulSource, + ]); return ( <> diff --git a/src/components/player/internals/SkipEpisodeButton.tsx b/src/components/player/internals/SkipEpisodeButton.tsx index f02208ad..2cf04aa6 100644 --- a/src/components/player/internals/SkipEpisodeButton.tsx +++ b/src/components/player/internals/SkipEpisodeButton.tsx @@ -5,6 +5,7 @@ import { usePlayerMeta } from "@/components/player/hooks/usePlayerMeta"; import { VideoPlayerButton } from "@/components/player/internals/Button"; import { PlayerMeta } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; +import { usePreferencesStore } from "@/stores/preferences"; import { useProgressStore } from "@/stores/progress"; interface SkipEpisodeButtonProps { @@ -19,13 +20,19 @@ export function SkipEpisodeButton(props: SkipEpisodeButtonProps) { (s) => s.setShouldStartFromBeginning, ); const updateItem = useProgressStore((s) => s.updateItem); - + const sourceId = usePlayerStore((s) => s.sourceId); + const setLastSuccessfulSource = usePreferencesStore( + (s) => s.setLastSuccessfulSource, + ); const nextEp = meta?.episodes?.find( (v) => v.number === (meta?.episode?.number ?? 0) + 1, ); const loadNextEpisode = useCallback(() => { if (!meta || !nextEp) return; + if (sourceId) { + setLastSuccessfulSource(sourceId); + } const metaCopy = { ...meta }; metaCopy.episode = nextEp; setShouldStartFromBeginning(true); @@ -43,6 +50,8 @@ export function SkipEpisodeButton(props: SkipEpisodeButtonProps) { props, setShouldStartFromBeginning, updateItem, + sourceId, + setLastSuccessfulSource, ]); // Don't show button if not in control, not a show, or no next episode diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts index 38339071..68cc2ac8 100644 --- a/src/hooks/auth/useAuthData.ts +++ b/src/hooks/auth/useAuthData.ts @@ -58,6 +58,12 @@ export function useAuthData() { const setEnableSourceOrder = usePreferencesStore( (s) => s.setEnableSourceOrder, ); + const setLastSuccessfulSource = usePreferencesStore( + (s) => s.setLastSuccessfulSource, + ); + const setEnableLastSuccessfulSource = usePreferencesStore( + (s) => s.setEnableLastSuccessfulSource, + ); const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); const setEmbedOrder = usePreferencesStore((s) => s.setEmbedOrder); const setEnableEmbedOrder = usePreferencesStore((s) => s.setEnableEmbedOrder); @@ -193,6 +199,14 @@ export function useAuthData() { setEnableSourceOrder(settings.enableSourceOrder); } + if (settings.lastSuccessfulSource !== undefined) { + setLastSuccessfulSource(settings.lastSuccessfulSource); + } + + if (settings.enableLastSuccessfulSource !== undefined) { + setEnableLastSuccessfulSource(settings.enableLastSuccessfulSource); + } + if (settings.disabledSources !== undefined) { setDisabledSources(settings.disabledSources ?? []); } @@ -265,6 +279,8 @@ export function useAuthData() { setForceCompactEpisodeView, setSourceOrder, setEnableSourceOrder, + setLastSuccessfulSource, + setEnableLastSuccessfulSource, setDisabledSources, setEmbedOrder, setEnableEmbedOrder, diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 5ecca8f1..be0e73c9 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -155,6 +155,12 @@ export function useScrape() { const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); + const lastSuccessfulSource = usePreferencesStore( + (s) => s.lastSuccessfulSource, + ); + const enableLastSuccessfulSource = usePreferencesStore( + (s) => s.enableLastSuccessfulSource, + ); const disabledSources = usePreferencesStore((s) => s.disabledSources); const preferredEmbedOrder = usePreferencesStore((s) => s.embedOrder); const enableEmbedOrder = usePreferencesStore((s) => s.enableEmbedOrder); @@ -162,11 +168,29 @@ export function useScrape() { const startScraping = useCallback( async (media: ScrapeMedia) => { - // Filter out disabled sources from the source order - const filteredSourceOrder = enableSourceOrder + // Create source order that prioritizes last successful source + let filteredSourceOrder = enableSourceOrder ? preferredSourceOrder.filter((id) => !disabledSources.includes(id)) : undefined; + // 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 = [ + lastSuccessfulSource, + ...availableSources.filter((id) => id !== lastSuccessfulSource), + ]; + } + } + // Filter out disabled embeds from the embed order const filteredEmbedOrder = enableEmbedOrder ? preferredEmbedOrder.filter((id) => !disabledEmbeds.includes(id)) @@ -223,6 +247,8 @@ export function useScrape() { startScrape, preferredSourceOrder, enableSourceOrder, + lastSuccessfulSource, + enableLastSuccessfulSource, disabledSources, preferredEmbedOrder, enableEmbedOrder, diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 33be4592..48fc0c61 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -60,6 +60,8 @@ export function useSettingsState( enableDetailsModal: boolean, sourceOrder: string[], enableSourceOrder: boolean, + lastSuccessfulSource: string | null, + enableLastSuccessfulSource: boolean, disabledSources: string[], embedOrder: string[], enableEmbedOrder: boolean, @@ -164,6 +166,18 @@ export function useSettingsState( resetEnableSourceOrder, enableSourceOrderChanged, ] = useDerived(enableSourceOrder); + const [ + lastSuccessfulSourceState, + setLastSuccessfulSourceState, + resetLastSuccessfulSource, + lastSuccessfulSourceChanged, + ] = useDerived(lastSuccessfulSource); + const [ + enableLastSuccessfulSourceState, + setEnableLastSuccessfulSourceState, + resetEnableLastSuccessfulSource, + enableLastSuccessfulSourceChanged, + ] = useDerived(enableLastSuccessfulSource); const [ disabledSourcesState, setDisabledSourcesState, @@ -259,6 +273,8 @@ export function useSettingsState( resetEnableImageLogos(); resetSourceOrder(); resetEnableSourceOrder(); + resetLastSuccessfulSource(); + resetEnableLastSuccessfulSource(); resetDisabledSources(); resetEmbedOrder(); resetEnableEmbedOrder(); @@ -293,6 +309,8 @@ export function useSettingsState( enableImageLogosChanged || sourceOrderChanged || enableSourceOrderChanged || + lastSuccessfulSourceChanged || + enableLastSuccessfulSourceChanged || disabledSourcesChanged || embedOrderChanged || enableEmbedOrderChanged || @@ -400,6 +418,16 @@ export function useSettingsState( set: setEnableSourceOrderState, changed: enableSourceOrderChanged, }, + lastSuccessfulSource: { + state: lastSuccessfulSourceState, + set: setLastSuccessfulSourceState, + changed: lastSuccessfulSourceChanged, + }, + enableLastSuccessfulSource: { + state: enableLastSuccessfulSourceState, + set: setEnableLastSuccessfulSourceState, + changed: enableLastSuccessfulSourceChanged, + }, proxyTmdb: { state: proxyTmdbState, set: setProxyTmdbState, diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 4dc69a71..13e4131d 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -56,10 +56,20 @@ export function RealPlayerView() { const manualSourceSelection = usePreferencesStore( (s) => s.manualSourceSelection, ); + const setLastSuccessfulSource = usePreferencesStore( + (s) => s.setLastSuccessfulSource, + ); const router = useOverlayRouter("settings"); const openedWatchPartyRef = useRef(false); const progressItems = useProgressStore((s) => s.items); + // Reset last successful source when leaving the player + useEffect(() => { + return () => { + setLastSuccessfulSource(null); + }; + }, [setLastSuccessfulSource]); + const paramsData = JSON.stringify({ media: params.media, season: params.season, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index c590282f..4f209711 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -282,6 +282,20 @@ export function SettingsPage() { (s) => s.setEnableSourceOrder, ); + const lastSuccessfulSource = usePreferencesStore( + (s) => s.lastSuccessfulSource, + ); + const setLastSuccessfulSource = usePreferencesStore( + (s) => s.setLastSuccessfulSource, + ); + + const enableLastSuccessfulSource = usePreferencesStore( + (s) => s.enableLastSuccessfulSource, + ); + const setEnableLastSuccessfulSource = usePreferencesStore( + (s) => s.setEnableLastSuccessfulSource, + ); + const disabledSources = usePreferencesStore((s) => s.disabledSources); const setDisabledSources = usePreferencesStore((s) => s.setDisabledSources); @@ -406,6 +420,8 @@ export function SettingsPage() { enableDetailsModal, sourceOrder, enableSourceOrder, + lastSuccessfulSource, + enableLastSuccessfulSource, disabledSources, embedOrder, enableEmbedOrder, @@ -475,6 +491,8 @@ export function SettingsPage() { state.enableImageLogos.changed || state.sourceOrder.changed || state.enableSourceOrder.changed || + state.lastSuccessfulSource.changed || + state.enableLastSuccessfulSource.changed || state.disabledSources.changed || state.proxyTmdb.changed || state.enableCarouselView.changed || @@ -500,6 +518,8 @@ export function SettingsPage() { enableImageLogos: state.enableImageLogos.state, sourceOrder: state.sourceOrder.state, enableSourceOrder: state.enableSourceOrder.state, + lastSuccessfulSource: state.lastSuccessfulSource.state, + enableLastSuccessfulSource: state.enableLastSuccessfulSource.state, disabledSources: state.disabledSources.state, proxyTmdb: state.proxyTmdb.state, enableCarouselView: state.enableCarouselView.state, @@ -537,6 +557,8 @@ export function SettingsPage() { setEnableImageLogos(state.enableImageLogos.state); setSourceOrder(state.sourceOrder.state); setEnableSourceOrder(state.enableSourceOrder.state); + setLastSuccessfulSource(state.lastSuccessfulSource.state); + setEnableLastSuccessfulSource(state.enableLastSuccessfulSource.state); setDisabledSources(state.disabledSources.state); setAppLanguage(state.appLanguage.state); setTheme(state.theme.state); @@ -584,6 +606,8 @@ export function SettingsPage() { setEnableImageLogos, setSourceOrder, setEnableSourceOrder, + setLastSuccessfulSource, + setEnableLastSuccessfulSource, setDisabledSources, setAppLanguage, setTheme, @@ -650,6 +674,8 @@ export function SettingsPage() { setSourceOrder={state.sourceOrder.set} enableSourceOrder={state.enableSourceOrder.state} setenableSourceOrder={state.enableSourceOrder.set} + enableLastSuccessfulSource={state.enableLastSuccessfulSource.state} + setEnableLastSuccessfulSource={state.enableLastSuccessfulSource.set} disabledSources={state.disabledSources.state} setDisabledSources={state.disabledSources.set} enableLowPerformanceMode={state.enableLowPerformanceMode.state} diff --git a/src/pages/parts/player/SourceSelectPart.tsx b/src/pages/parts/player/SourceSelectPart.tsx index cd371b7d..58198ad1 100644 --- a/src/pages/parts/player/SourceSelectPart.tsx +++ b/src/pages/parts/player/SourceSelectPart.tsx @@ -129,6 +129,12 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) { const routerId = "manualSourceSelect"; const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); + const lastSuccessfulSource = usePreferencesStore( + (s) => s.lastSuccessfulSource, + ); + const enableLastSuccessfulSource = usePreferencesStore( + (s) => s.enableLastSuccessfulSource, + ); const sources = useMemo(() => { const metaType = props.media.type; @@ -138,13 +144,34 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) { .filter((v) => v.mediaTypes?.includes(metaType)); if (!enableSourceOrder || preferredSourceOrder.length === 0) { + // Even without custom source order, prioritize last successful source if enabled + if (enableLastSuccessfulSource && lastSuccessfulSource) { + const lastSourceIndex = allSources.findIndex( + (s) => s.id === lastSuccessfulSource, + ); + if (lastSourceIndex !== -1) { + const lastSource = allSources.splice(lastSourceIndex, 1)[0]; + return [lastSource, ...allSources]; + } + } return allSources; } - // Sort sources according to preferred order + // Sort sources according to preferred order, but prioritize last successful source const orderedSources = []; const remainingSources = [...allSources]; + // First, add the last successful source if it exists, is available, and the feature is enabled + if (enableLastSuccessfulSource && lastSuccessfulSource) { + const lastSourceIndex = remainingSources.findIndex( + (s) => s.id === lastSuccessfulSource, + ); + if (lastSourceIndex !== -1) { + orderedSources.push(remainingSources[lastSourceIndex]); + remainingSources.splice(lastSourceIndex, 1); + } + } + // Add sources in preferred order for (const sourceId of preferredSourceOrder) { const sourceIndex = remainingSources.findIndex((s) => s.id === sourceId); @@ -158,7 +185,13 @@ export function SourceSelectPart(props: { media: ScrapeMedia }) { orderedSources.push(...remainingSources); return orderedSources; - }, [props.media.type, preferredSourceOrder, enableSourceOrder]); + }, [ + props.media.type, + preferredSourceOrder, + enableSourceOrder, + lastSuccessfulSource, + enableLastSuccessfulSource, + ]); if (selectedSourceId) { return ( diff --git a/src/pages/parts/settings/PreferencesPart.tsx b/src/pages/parts/settings/PreferencesPart.tsx index 98e009fc..ed5575f2 100644 --- a/src/pages/parts/settings/PreferencesPart.tsx +++ b/src/pages/parts/settings/PreferencesPart.tsx @@ -27,6 +27,8 @@ export function PreferencesPart(props: { setSourceOrder: (v: string[]) => void; enableSourceOrder: boolean; setenableSourceOrder: (v: boolean) => void; + enableLastSuccessfulSource: boolean; + setEnableLastSuccessfulSource: (v: boolean) => void; disabledSources: string[]; setDisabledSources: (v: string[]) => void; enableLowPerformanceMode: boolean; @@ -267,6 +269,30 @@ export function PreferencesPart(props: {

+ + {/* Last Successful Source Preference */} +
+

+ {t("settings.preferences.lastSuccessfulSource")} +

+

+ {t("settings.preferences.lastSuccessfulSourceDescription")} +

+
+ props.setEnableLastSuccessfulSource( + !props.enableLastSuccessfulSource, + ) + } + 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" + > + +

+ {t("settings.preferences.lastSuccessfulSourceEnableLabel")} +

+
+
+

{t("settings.preferences.sourceOrder")}

diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index 256227ed..59a1f2aa 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -14,6 +14,8 @@ export interface PreferencesStore { forceCompactEpisodeView: boolean; sourceOrder: string[]; enableSourceOrder: boolean; + lastSuccessfulSource: string | null; + enableLastSuccessfulSource: boolean; disabledSources: string[]; embedOrder: string[]; enableEmbedOrder: boolean; @@ -39,6 +41,8 @@ export interface PreferencesStore { setForceCompactEpisodeView(v: boolean): void; setSourceOrder(v: string[]): void; setEnableSourceOrder(v: boolean): void; + setLastSuccessfulSource(v: string | null): void; + setEnableLastSuccessfulSource(v: boolean): void; setDisabledSources(v: string[]): void; setEmbedOrder(v: string[]): void; setEnableEmbedOrder(v: boolean): void; @@ -68,6 +72,8 @@ export const usePreferencesStore = create( forceCompactEpisodeView: false, sourceOrder: [], enableSourceOrder: false, + lastSuccessfulSource: null, + enableLastSuccessfulSource: true, disabledSources: [], embedOrder: [], enableEmbedOrder: false, @@ -136,6 +142,16 @@ export const usePreferencesStore = create( s.enableSourceOrder = v; }); }, + setLastSuccessfulSource(v) { + set((s) => { + s.lastSuccessfulSource = v; + }); + }, + setEnableLastSuccessfulSource(v) { + set((s) => { + s.enableLastSuccessfulSource = v; + }); + }, setDisabledSources(v) { set((s) => { s.disabledSources = v;