mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-07 13:02:52 +01:00
fix(tv): only defer subtitle selection when it navigates
The close-first + runAfterInteractions pattern is needed only in the player, where selecting a subtitle can navigate (replacePlayer for a burn-in switch while transcoding). On the item detail page the selection just updates state, so deferring it until after the modal is dismissed re-renders the page after focus returns and yanks TV focus — leaving navigation stuck. Gate the deferral behind deferApplyUntilDismissed (set by the in-player caller only); the detail page runs the selection before closing, preserving focus.
This commit is contained in:
@@ -646,16 +646,23 @@ export default function TVSubtitleModal() {
|
|||||||
|
|
||||||
const handleTrackSelect = useCallback(
|
const handleTrackSelect = useCallback(
|
||||||
(option: { setTrack?: () => void }) => {
|
(option: { setTrack?: () => void }) => {
|
||||||
// Close FIRST, apply after the modal route is dismissed: setTrack can
|
if (modalState?.deferApplyUntilDismissed) {
|
||||||
// trigger replacePlayer (burn-in switches while transcoding), and a
|
// Player: setTrack can navigate (replacePlayer for a burn-in switch
|
||||||
// router.replace fired while this modal is the active route targets the
|
// while transcoding); a router.replace fired while this modal is the
|
||||||
// MODAL instead of the player screen — the navigation is swallowed and
|
// active route targets the MODAL and is swallowed. Close FIRST, apply
|
||||||
// the selection silently no-ops (same trap as the setParams note in
|
// after dismissal.
|
||||||
// Controls.tv's handleOpenSubtitleSheet).
|
|
||||||
handleClose();
|
handleClose();
|
||||||
InteractionManager.runAfterInteractions(() => option.setTrack?.());
|
InteractionManager.runAfterInteractions(() => option.setTrack?.());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Detail page: setTrack only updates state. Run it BEFORE closing so the
|
||||||
|
// re-render happens while the modal is up; deferring it until after
|
||||||
|
// dismissal re-renders the detail page after focus returns and yanks TV
|
||||||
|
// focus, leaving navigation stuck.
|
||||||
|
option.setTrack?.();
|
||||||
|
handleClose();
|
||||||
},
|
},
|
||||||
[handleClose],
|
[handleClose, modalState?.deferApplyUntilDismissed],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
const handleDownload = useCallback(
|
||||||
|
|||||||
@@ -601,6 +601,9 @@ export const Controls: FC<Props> = ({
|
|||||||
mediaSourceId: mediaSource?.Id,
|
mediaSourceId: mediaSource?.Id,
|
||||||
subtitleTracks: tracksWithoutDisable,
|
subtitleTracks: tracksWithoutDisable,
|
||||||
currentSubtitleIndex: subtitleIndex ?? -1,
|
currentSubtitleIndex: subtitleIndex ?? -1,
|
||||||
|
// In-player selection can navigate (replacePlayer for burn-in switches);
|
||||||
|
// apply it after the modal route is dismissed so it isn't swallowed.
|
||||||
|
deferApplyUntilDismissed: true,
|
||||||
onDisableSubtitles: () => {
|
onDisableSubtitles: () => {
|
||||||
// Find and call the "Disable" track's setTrack from VideoContext
|
// Find and call the "Disable" track's setTrack from VideoContext
|
||||||
const disableTrack = videoContextSubtitleTracks?.find(
|
const disableTrack = videoContextSubtitleTracks?.find(
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ interface ShowSubtitleModalParams {
|
|||||||
onServerSubtitleDownloaded?: () => void;
|
onServerSubtitleDownloaded?: () => void;
|
||||||
onLocalSubtitleDownloaded?: (path: string) => void;
|
onLocalSubtitleDownloaded?: (path: string) => void;
|
||||||
refreshSubtitleTracks?: () => Promise<Track[]>;
|
refreshSubtitleTracks?: () => Promise<Track[]>;
|
||||||
|
deferApplyUntilDismissed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useTVSubtitleModal = () => {
|
export const useTVSubtitleModal = () => {
|
||||||
@@ -30,6 +31,7 @@ export const useTVSubtitleModal = () => {
|
|||||||
onServerSubtitleDownloaded: params.onServerSubtitleDownloaded,
|
onServerSubtitleDownloaded: params.onServerSubtitleDownloaded,
|
||||||
onLocalSubtitleDownloaded: params.onLocalSubtitleDownloaded,
|
onLocalSubtitleDownloaded: params.onLocalSubtitleDownloaded,
|
||||||
refreshSubtitleTracks: params.refreshSubtitleTracks,
|
refreshSubtitleTracks: params.refreshSubtitleTracks,
|
||||||
|
deferApplyUntilDismissed: params.deferApplyUntilDismissed,
|
||||||
});
|
});
|
||||||
router.push("/(auth)/tv-subtitle-modal");
|
router.push("/(auth)/tv-subtitle-modal");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ export type TVSubtitleModalState = {
|
|||||||
onServerSubtitleDownloaded?: () => void;
|
onServerSubtitleDownloaded?: () => void;
|
||||||
onLocalSubtitleDownloaded?: (path: string) => void;
|
onLocalSubtitleDownloaded?: (path: string) => void;
|
||||||
refreshSubtitleTracks?: () => Promise<Track[]>;
|
refreshSubtitleTracks?: () => Promise<Track[]>;
|
||||||
|
/**
|
||||||
|
* Run the selection callback AFTER the modal route is dismissed. Needed when
|
||||||
|
* `setTrack` navigates (the player's replacePlayer for a burn-in switch),
|
||||||
|
* which would be swallowed by the still-active modal route. Leave false for
|
||||||
|
* callers whose selection only updates state (the item detail page), so the
|
||||||
|
* update runs before dismissal and doesn't yank focus after it returns.
|
||||||
|
*/
|
||||||
|
deferApplyUntilDismissed?: boolean;
|
||||||
} | null;
|
} | null;
|
||||||
|
|
||||||
export const tvSubtitleModalAtom = atom<TVSubtitleModalState>(null);
|
export const tvSubtitleModalAtom = atom<TVSubtitleModalState>(null);
|
||||||
|
|||||||
Reference in New Issue
Block a user