diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index e683c815..7e4d0df3 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -823,7 +823,7 @@ }, "title": "Sources", "unknownOption": "Unknown", - "editOrder": "Edit order" + "findNextSource": "Find next source" }, "subtitles": { "customChoice": "Drop or upload file", diff --git a/src/components/player/atoms/settings/SourceSelectingView.tsx b/src/components/player/atoms/settings/SourceSelectingView.tsx index 5e9938fb..86ba15c8 100644 --- a/src/components/player/atoms/settings/SourceSelectingView.tsx +++ b/src/components/player/atoms/settings/SourceSelectingView.tsx @@ -10,6 +10,7 @@ import { import { Menu } from "@/components/player/internals/ContextMenu"; import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; import { useOverlayRouter } from "@/hooks/useOverlayRouter"; +import { playerStatus } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; import { usePreferencesStore } from "@/stores/preferences"; @@ -156,6 +157,8 @@ export function SourceSelectionView({ const router = useOverlayRouter(id); const metaType = usePlayerStore((s) => s.meta?.type); const currentSourceId = usePlayerStore((s) => s.sourceId); + const setResumeFromSourceId = usePlayerStore((s) => s.setResumeFromSourceId); + const setStatus = usePlayerStore((s) => s.setStatus); const preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder); const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); const lastSuccessfulSource = usePreferencesStore( @@ -164,6 +167,9 @@ export function SourceSelectionView({ const enableLastSuccessfulSource = usePreferencesStore( (s) => s.enableLastSuccessfulSource, ); + const manualSourceSelection = usePreferencesStore( + (s) => s.manualSourceSelection, + ); const sources = useMemo(() => { if (!metaType) return []; @@ -221,20 +227,32 @@ export function SourceSelectionView({ enableLastSuccessfulSource, ]); + const handleFindNextSource = () => { + if (!currentSourceId) return; + // Set the resume source ID in the store + setResumeFromSourceId(currentSourceId); + // Close the settings overlay + router.close(); + // Set status to SCRAPING to trigger scraping from next source + setStatus(playerStatus.SCRAPING); + }; + return ( <> router.navigate("/")} rightSide={ - +
+ {currentSourceId && !manualSourceSelection && ( + + )} +
} > {t("player.menus.sources.title")} diff --git a/src/hooks/useProviderScrape.tsx b/src/hooks/useProviderScrape.tsx index 39b2b561..383e01f5 100644 --- a/src/hooks/useProviderScrape.tsx +++ b/src/hooks/useProviderScrape.tsx @@ -211,7 +211,12 @@ export function useScrape() { } // If we have a last successful source and the feature is enabled, prioritize it - if (enableLastSuccessfulSource && lastSuccessfulSource) { + // BUT only if we're not resuming from a specific source (to preserve custom order) + if ( + enableLastSuccessfulSource && + lastSuccessfulSource && + !startFromSourceId + ) { const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource); if (lastSourceIndex !== -1) { baseSourceOrder = [ @@ -222,6 +227,7 @@ export function useScrape() { } // If starting from a specific source ID, filter the order to start AFTER that source + // This preserves the custom order while starting from the next source let filteredSourceOrder = baseSourceOrder; if (startFromSourceId) { const startIndex = filteredSourceOrder.indexOf(startFromSourceId); diff --git a/src/pages/PlayerView.tsx b/src/pages/PlayerView.tsx index 43558fe3..76c0eed3 100644 --- a/src/pages/PlayerView.tsx +++ b/src/pages/PlayerView.tsx @@ -47,6 +47,10 @@ export function RealPlayerView() { const [resumeFromSourceId, setResumeFromSourceId] = useState( null, ); + const storeResumeFromSourceId = usePlayerStore((s) => s.resumeFromSourceId); + const setResumeFromSourceIdInStore = usePlayerStore( + (s) => s.setResumeFromSourceId, + ); const [startAtParam] = useQueryParam("t"); const { status, @@ -77,6 +81,14 @@ export function RealPlayerView() { }; }, [setLastSuccessfulSource]); + // Reset resume from source ID when leaving the player + useEffect(() => { + return () => { + setResumeFromSourceId(null); + setResumeFromSourceIdInStore(null); + }; + }, [setResumeFromSourceIdInStore]); + const paramsData = JSON.stringify({ media: params.media, season: params.season, @@ -169,14 +181,28 @@ export function RealPlayerView() { (startFromSourceId: string) => { // Set resume source first setResumeFromSourceId(startFromSourceId); + setResumeFromSourceIdInStore(startFromSourceId); // Then change status in next tick to ensure re-render setTimeout(() => { setStatus(playerStatus.SCRAPING); }, 0); }, - [setStatus], + [setStatus, setResumeFromSourceIdInStore], ); + // Sync store value to local state when it changes (e.g., from settings) + // or when status changes to SCRAPING + useEffect(() => { + if (storeResumeFromSourceId && status === playerStatus.SCRAPING) { + if ( + !resumeFromSourceId || + resumeFromSourceId !== storeResumeFromSourceId + ) { + setResumeFromSourceId(storeResumeFromSourceId); + } + } + }, [storeResumeFromSourceId, resumeFromSourceId, status]); + const playAfterScrape = useCallback( (out: RunOutput | null) => { if (!out) return; @@ -223,9 +249,11 @@ export function RealPlayerView() { ) : ( { setErrorData({ sourceOrder, @@ -234,6 +262,7 @@ export function RealPlayerView() { setScrapeNotFound(); // Clear resume state after scraping setResumeFromSourceId(null); + setResumeFromSourceIdInStore(null); }} onGetStream={playAfterScrape} /> diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index dab67ce2..b8147616 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -105,6 +105,7 @@ export interface SourceSlice { meta: PlayerMeta | null; failedSourcesPerMedia: Record; // mediaKey -> array of failed sourceIds failedEmbedsPerMedia: Record>; // mediaKey -> sourceId -> array of failed embedIds + resumeFromSourceId: string | null; setStatus(status: PlayerStatus): void; setSource( stream: SourceSliceSource, @@ -129,6 +130,7 @@ export interface SourceSlice { addFailedEmbed(sourceId: string, embedId: string): void; clearFailedSources(mediaKey?: string): void; clearFailedEmbeds(mediaKey?: string): void; + setResumeFromSourceId(sourceId: string | null): void; reset(): void; } @@ -190,6 +192,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ meta: null, failedSourcesPerMedia: {}, failedEmbedsPerMedia: {}, + resumeFromSourceId: null, caption: { selected: null, asTrack: false, @@ -387,6 +390,11 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ } }); }, + setResumeFromSourceId(sourceId: string | null) { + set((s) => { + s.resumeFromSourceId = sourceId; + }); + }, reset() { set((s) => { s.source = null; @@ -402,6 +410,7 @@ export const createSourceSlice: MakeSlice = (set, get) => ({ s.meta = null; s.failedSourcesPerMedia = {}; s.failedEmbedsPerMedia = {}; + s.resumeFromSourceId = null; this.clearTranslateTask(); s.caption = { selected: null,