fix(player): correct prev/next episode buttons at boundaries & for missing episodes

Previous/next were derived from the adjacentTo response's array length,
which wrongly surfaced the current episode as "previous" on the first
episode (e.g. S1E1) and offered "Virtual" (missing) episodes — like an
empty Specials entry — as navigation/autoplay targets.

Derive them from the current item's actual index in the list and skip
neighbours that are the current item or have LocationType 'Virtual'. This
also stops autoplay from advancing into a missing episode.
This commit is contained in:
Gauvain
2026-06-02 00:04:00 +02:00
parent 211923b2ab
commit c7c3aa8a34

View File

@@ -109,30 +109,35 @@ export const usePlaybackManager = ({
staleTime: 0,
});
/**
* Derive prev/next from the current item's real position in the adjacent
* list rather than from the array length. `getEpisodes({ adjacentTo })` does
* not guarantee a fixed [prev, current, next] shape — at the first/last
* episode it can still return the current item as the first/last entry — so
* length-based indexing wrongly surfaces the current episode as "previous".
*/
const currentIndex = useMemo(
() => adjacentItems?.findIndex((e) => e.Id === item?.Id) ?? -1,
[adjacentItems, item],
);
/** A neighbour is only navigable if it has an actual media file (not a
* "Virtual"/missing episode placeholder, e.g. an absent Special). */
const isNavigable = (episode?: BaseItemDto | null): episode is BaseItemDto =>
!!episode && episode.Id !== item?.Id && episode.LocationType !== "Virtual";
const previousItem = useMemo(() => {
if (!adjacentItems || adjacentItems.length <= 1) {
return null;
}
if (adjacentItems.length === 2) {
return adjacentItems[0].Id === item?.Id ? null : adjacentItems[0];
}
return adjacentItems[0];
}, [adjacentItems, item]);
if (!adjacentItems || currentIndex <= 0) return null;
const candidate = adjacentItems[currentIndex - 1];
return isNavigable(candidate) ? candidate : null;
}, [adjacentItems, currentIndex, item]);
/** The next item in the series */
const nextItem = useMemo(() => {
if (!adjacentItems || adjacentItems.length <= 1) {
return null;
}
if (adjacentItems.length === 2) {
return adjacentItems[1].Id === item?.Id ? null : adjacentItems[1];
}
return adjacentItems[2];
}, [adjacentItems, item]);
if (!adjacentItems || currentIndex < 0) return null;
const candidate = adjacentItems[currentIndex + 1];
return isNavigable(candidate) ? candidate : null;
}, [adjacentItems, currentIndex, item]);
/**
* Reports playback progress.