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",
"unknownOption": "Unknown",
"editOrder": "Edit order"
"findNextSource": "Find next source"
},
"subtitles": {
"customChoice": "Drop or upload file",

View file

@ -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 (
<>
<Menu.BackLink
onClick={() => router.navigate("/")}
rightSide={
<button
type="button"
onClick={() => {
window.location.href = "/settings#source-order";
}}
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")}
</button>
<div className="flex items-center gap-2">
{currentSourceId && !manualSourceSelection && (
<button
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"
>
{t("player.menus.sources.findNextSource")}
</button>
)}
</div>
}
>
{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 (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);

View file

@ -47,6 +47,10 @@ export function RealPlayerView() {
const [resumeFromSourceId, setResumeFromSourceId] = useState<string | null>(
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() {
<SourceSelectPart media={scrapeMedia} />
) : (
<ScrapingPart
key={`scraping-${resumeFromSourceId || "default"}`}
key={`scraping-${resumeFromSourceId || storeResumeFromSourceId || "default"}`}
media={scrapeMedia}
startFromSourceId={resumeFromSourceId || undefined}
startFromSourceId={
resumeFromSourceId || storeResumeFromSourceId || undefined
}
onResult={(sources, sourceOrder) => {
setErrorData({
sourceOrder,
@ -234,6 +262,7 @@ export function RealPlayerView() {
setScrapeNotFound();
// Clear resume state after scraping
setResumeFromSourceId(null);
setResumeFromSourceIdInStore(null);
}}
onGetStream={playAfterScrape}
/>

View file

@ -105,6 +105,7 @@ export interface SourceSlice {
meta: PlayerMeta | null;
failedSourcesPerMedia: Record<string, string[]>; // mediaKey -> array of failed sourceIds
failedEmbedsPerMedia: Record<string, Record<string, string[]>>; // 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<SourceSlice> = (set, get) => ({
meta: null,
failedSourcesPerMedia: {},
failedEmbedsPerMedia: {},
resumeFromSourceId: null,
caption: {
selected: null,
asTrack: false,
@ -387,6 +390,11 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (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<SourceSlice> = (set, get) => ({
s.meta = null;
s.failedSourcesPerMedia = {};
s.failedEmbedsPerMedia = {};
s.resumeFromSourceId = null;
this.clearTranslateTask();
s.caption = {
selected: null,