add Find Next Source button

replace edit order button
This commit is contained in:
Pas 2026-01-10 18:29:00 -07:00
parent 45e5abd00e
commit 02a179b1d8
5 changed files with 76 additions and 14 deletions

View file

@ -823,7 +823,7 @@
}, },
"title": "Sources", "title": "Sources",
"unknownOption": "Unknown", "unknownOption": "Unknown",
"editOrder": "Edit order" "findNextSource": "Find next source"
}, },
"subtitles": { "subtitles": {
"customChoice": "Drop or upload file", "customChoice": "Drop or upload file",

View file

@ -10,6 +10,7 @@ import {
import { Menu } from "@/components/player/internals/ContextMenu"; import { Menu } from "@/components/player/internals/ContextMenu";
import { SelectableLink } from "@/components/player/internals/ContextMenu/Links"; import { SelectableLink } from "@/components/player/internals/ContextMenu/Links";
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; import { useOverlayRouter } from "@/hooks/useOverlayRouter";
import { playerStatus } from "@/stores/player/slices/source";
import { usePlayerStore } from "@/stores/player/store"; import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences"; import { usePreferencesStore } from "@/stores/preferences";
@ -156,6 +157,8 @@ export function SourceSelectionView({
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const metaType = usePlayerStore((s) => s.meta?.type); const metaType = usePlayerStore((s) => s.meta?.type);
const currentSourceId = usePlayerStore((s) => s.sourceId); 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 preferredSourceOrder = usePreferencesStore((s) => s.sourceOrder);
const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder); const enableSourceOrder = usePreferencesStore((s) => s.enableSourceOrder);
const lastSuccessfulSource = usePreferencesStore( const lastSuccessfulSource = usePreferencesStore(
@ -164,6 +167,9 @@ export function SourceSelectionView({
const enableLastSuccessfulSource = usePreferencesStore( const enableLastSuccessfulSource = usePreferencesStore(
(s) => s.enableLastSuccessfulSource, (s) => s.enableLastSuccessfulSource,
); );
const manualSourceSelection = usePreferencesStore(
(s) => s.manualSourceSelection,
);
const sources = useMemo(() => { const sources = useMemo(() => {
if (!metaType) return []; if (!metaType) return [];
@ -221,20 +227,32 @@ export function SourceSelectionView({
enableLastSuccessfulSource, 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 ( return (
<> <>
<Menu.BackLink <Menu.BackLink
onClick={() => router.navigate("/")} onClick={() => router.navigate("/")}
rightSide={ rightSide={
<button <div className="flex items-center gap-2">
type="button" {currentSourceId && !manualSourceSelection && (
onClick={() => { <button
window.location.href = "/settings#source-order"; type="button"
}} onClick={handleFindNextSource}
className="-mr-2 -my-1 px-2 p-[0.4em] rounded tabbable hover:bg-video-context-light hover:bg-opacity-10" className="-mr-2 -my-1 px-2 p-[0.4em] rounded tabbable hover:bg-video-context-light hover:bg-opacity-10"
> >
{t("player.menus.sources.editOrder")} {t("player.menus.sources.findNextSource")}
</button> </button>
)}
</div>
} }
> >
{t("player.menus.sources.title")} {t("player.menus.sources.title")}

View file

@ -211,7 +211,12 @@ export function useScrape() {
} }
// 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) { // BUT only if we're not resuming from a specific source (to preserve custom order)
if (
enableLastSuccessfulSource &&
lastSuccessfulSource &&
!startFromSourceId
) {
const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource); const lastSourceIndex = baseSourceOrder.indexOf(lastSuccessfulSource);
if (lastSourceIndex !== -1) { if (lastSourceIndex !== -1) {
baseSourceOrder = [ baseSourceOrder = [
@ -222,6 +227,7 @@ export function useScrape() {
} }
// If starting from a specific source ID, filter the order to start AFTER that source // 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; let filteredSourceOrder = baseSourceOrder;
if (startFromSourceId) { if (startFromSourceId) {
const startIndex = filteredSourceOrder.indexOf(startFromSourceId); const startIndex = filteredSourceOrder.indexOf(startFromSourceId);

View file

@ -47,6 +47,10 @@ export function RealPlayerView() {
const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>( const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>(
null, null,
); );
const storeResumeFromSourceId = usePlayerStore((s) => s.resumeFromSourceId);
const setResumeFromSourceIdInStore = usePlayerStore(
(s) => s.setResumeFromSourceId,
);
const [startAtParam] = useQueryParam("t"); const [startAtParam] = useQueryParam("t");
const { const {
status, status,
@ -77,6 +81,14 @@ export function RealPlayerView() {
}; };
}, [setLastSuccessfulSource]); }, [setLastSuccessfulSource]);
// Reset resume from source ID when leaving the player
useEffect(() => {
return () => {
setResumeFromSourceId(null);
setResumeFromSourceIdInStore(null);
};
}, [setResumeFromSourceIdInStore]);
const paramsData = JSON.stringify({ const paramsData = JSON.stringify({
media: params.media, media: params.media,
season: params.season, season: params.season,
@ -169,14 +181,28 @@ export function RealPlayerView() {
(startFromSourceId: string) => { (startFromSourceId: string) => {
// Set resume source first // Set resume source first
setResumeFromSourceId(startFromSourceId); setResumeFromSourceId(startFromSourceId);
setResumeFromSourceIdInStore(startFromSourceId);
// Then change status in next tick to ensure re-render // Then change status in next tick to ensure re-render
setTimeout(() => { setTimeout(() => {
setStatus(playerStatus.SCRAPING); setStatus(playerStatus.SCRAPING);
}, 0); }, 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( const playAfterScrape = useCallback(
(out: RunOutput | null) => { (out: RunOutput | null) => {
if (!out) return; if (!out) return;
@ -223,9 +249,11 @@ export function RealPlayerView() {
<SourceSelectPart media={scrapeMedia} /> <SourceSelectPart media={scrapeMedia} />
) : ( ) : (
<ScrapingPart <ScrapingPart
key={`scraping-${resumeFromSourceId || "default"}`} key={`scraping-${resumeFromSourceId || storeResumeFromSourceId || "default"}`}
media={scrapeMedia} media={scrapeMedia}
startFromSourceId={resumeFromSourceId || undefined} startFromSourceId={
resumeFromSourceId || storeResumeFromSourceId || undefined
}
onResult={(sources, sourceOrder) => { onResult={(sources, sourceOrder) => {
setErrorData({ setErrorData({
sourceOrder, sourceOrder,
@ -234,6 +262,7 @@ export function RealPlayerView() {
setScrapeNotFound(); setScrapeNotFound();
// Clear resume state after scraping // Clear resume state after scraping
setResumeFromSourceId(null); setResumeFromSourceId(null);
setResumeFromSourceIdInStore(null);
}} }}
onGetStream={playAfterScrape} onGetStream={playAfterScrape}
/> />

View file

@ -105,6 +105,7 @@ export interface SourceSlice {
meta: PlayerMeta | null; meta: PlayerMeta | null;
failedSourcesPerMedia: Record<string, string[]>; // mediaKey -> array of failed sourceIds failedSourcesPerMedia: Record<string, string[]>; // mediaKey -> array of failed sourceIds
failedEmbedsPerMedia: Record<string, Record<string, string[]>>; // mediaKey -> sourceId -> array of failed embedIds failedEmbedsPerMedia: Record<string, Record<string, string[]>>; // mediaKey -> sourceId -> array of failed embedIds
resumeFromSourceId: string | null;
setStatus(status: PlayerStatus): void; setStatus(status: PlayerStatus): void;
setSource( setSource(
stream: SourceSliceSource, stream: SourceSliceSource,
@ -129,6 +130,7 @@ export interface SourceSlice {
addFailedEmbed(sourceId: string, embedId: string): void; addFailedEmbed(sourceId: string, embedId: string): void;
clearFailedSources(mediaKey?: string): void; clearFailedSources(mediaKey?: string): void;
clearFailedEmbeds(mediaKey?: string): void; clearFailedEmbeds(mediaKey?: string): void;
setResumeFromSourceId(sourceId: string | null): void;
reset(): void; reset(): void;
} }
@ -190,6 +192,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
meta: null, meta: null,
failedSourcesPerMedia: {}, failedSourcesPerMedia: {},
failedEmbedsPerMedia: {}, failedEmbedsPerMedia: {},
resumeFromSourceId: null,
caption: { caption: {
selected: null, selected: null,
asTrack: false, asTrack: false,
@ -387,6 +390,11 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
} }
}); });
}, },
setResumeFromSourceId(sourceId: string | null) {
set((s) => {
s.resumeFromSourceId = sourceId;
});
},
reset() { reset() {
set((s) => { set((s) => {
s.source = null; s.source = null;
@ -402,6 +410,7 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
s.meta = null; s.meta = null;
s.failedSourcesPerMedia = {}; s.failedSourcesPerMedia = {};
s.failedEmbedsPerMedia = {}; s.failedEmbedsPerMedia = {};
s.resumeFromSourceId = null;
this.clearTranslateTask(); this.clearTranslateTask();
s.caption = { s.caption = {
selected: null, selected: null,