fix(player): retain subtitle language and mode across episodes

Carry the live subtitle/audio selection to the next episode on all TV
navigation paths (next/prev buttons, autoplay) and feed TV subtitle modal
selections back into player state via onSubtitleIndexChange so the chosen
track is what gets carried.

Rank subtitles by language plus forced/hearing-impaired mode, with a
no-language fallback (mode + codec + position), and use an explicit match
flag so a deliberate "off" selection is retained too.
This commit is contained in:
Fredrik Burmester
2026-05-30 20:15:24 +02:00
parent e044859aaf
commit 6876ce046f
4 changed files with 167 additions and 64 deletions

View File

@@ -233,14 +233,8 @@ export const Controls: FC<Props> = ({
const api = useAtomValue(apiAtom);
const { settings } = useSettings();
const router = useRouter();
const {
bitrateValue,
subtitleIndex: paramSubtitleIndex,
audioIndex: paramAudioIndex,
} = useLocalSearchParams<{
const { bitrateValue } = useLocalSearchParams<{
bitrateValue: string;
subtitleIndex: string;
audioIndex: string;
}>();
const { nextItem: internalNextItem } = usePlaybackManager({
@@ -583,10 +577,22 @@ export const Controls: FC<Props> = ({
const handleOpenSubtitleSheet = useCallback(() => {
setLastOpenedModal("subtitle");
// Filter out the "Disable" option from VideoContext tracks since the modal adds its own "None" option
const tracksWithoutDisable = (videoContextSubtitleTracks ?? []).filter(
(track) => track.index !== -1,
);
// Filter out the "Disable" option from VideoContext tracks since the modal adds its own "None" option.
// Wrap each setTrack so selecting a subtitle ALSO updates the player's live
// index via onSubtitleIndexChange. The modal is a separate route, so the
// VideoContext router.setParams inside setTrack targets the modal — not the
// player — leaving currentSubtitleIndex stale. Without this sync, the next
// episode carries the previously-shown subtitle instead of the one the user
// just picked. (The audio sheet already uses onAudioIndexChange directly.)
const tracksWithoutDisable = (videoContextSubtitleTracks ?? [])
.filter((track) => track.index !== -1)
.map((track) => ({
...track,
setTrack: () => {
track.setTrack();
onSubtitleIndexChange?.(track.index);
},
}));
showSubtitleModal({
item,
mediaSourceId: mediaSource?.Id,
@@ -598,6 +604,7 @@ export const Controls: FC<Props> = ({
(t) => t.index === -1,
);
disableTrack?.setTrack();
onSubtitleIndexChange?.(-1);
},
onLocalSubtitleDownloaded: handleLocalSubtitleDownloaded,
refreshSubtitleTracks: onRefreshSubtitleTracks
@@ -611,6 +618,7 @@ export const Controls: FC<Props> = ({
mediaSource?.Id,
videoContextSubtitleTracks,
subtitleIndex,
onSubtitleIndexChange,
handleLocalSubtitleDownloaded,
onRefreshSubtitleTracks,
refreshSubtitleTracks,
@@ -1031,13 +1039,12 @@ export const Controls: FC<Props> = ({
return;
}
// Use the live selection passed down from the player (currentSubtitleIndex
// / currentAudioIndex), not the stale URL params the episode started with.
// This path runs on autoplay; the manual "Next" button uses goToNextItemProp.
const previousIndexes = {
subtitleIndex: paramSubtitleIndex
? Number.parseInt(paramSubtitleIndex, 10)
: undefined,
audioIndex: paramAudioIndex
? Number.parseInt(paramAudioIndex, 10)
: undefined,
subtitleIndex,
audioIndex,
};
const {
@@ -1064,8 +1071,8 @@ export const Controls: FC<Props> = ({
[
nextItem,
settings,
paramSubtitleIndex,
paramAudioIndex,
subtitleIndex,
audioIndex,
mediaSource,
bitrateValue,
router,