fix(subtitles): order detail-page & TV subtitle menus like jellyfin-web

The detail-page selector (MediaSourceButton, the #1176 replacement for
TrackSheet) and the TV detail/refresh paths (ItemContent.tv, Controls.tv
refreshSubtitleTracks) still listed subtitles in raw MediaStreams order
(externals first). Apply compareTracksForMenu there too so every menu
matches web. The in-player TV modal was already covered (fed from the
sorted VideoContext tracks).
This commit is contained in:
Gauvain
2026-06-30 00:39:29 +02:00
parent 9a7b9c9de2
commit 1c158dea4e
3 changed files with 16 additions and 7 deletions

View File

@@ -56,6 +56,7 @@ import { useSettings } from "@/utils/atoms/settings";
import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById"; import { getPrimaryImageUrlById } from "@/utils/jellyfin/image/getPrimaryImageUrlById";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { formatDuration, runtimeTicksToMinutes } from "@/utils/time"; import { formatDuration, runtimeTicksToMinutes } from "@/utils/time";
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window"); const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
@@ -232,12 +233,13 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
return streams ?? []; return streams ?? [];
}, [selectedOptions?.mediaSource]); }, [selectedOptions?.mediaSource]);
// Get available subtitle tracks (raw MediaStream[] for label lookup) // Get available subtitle tracks (raw MediaStream[] for label lookup),
// ordered like jellyfin-web (embedded first, externals last, forced/default up).
const subtitleStreams = useMemo(() => { const subtitleStreams = useMemo(() => {
const streams = selectedOptions?.mediaSource?.MediaStreams?.filter( const streams = selectedOptions?.mediaSource?.MediaStreams?.filter(
(s) => s.Type === "Subtitle", (s) => s.Type === "Subtitle",
); );
return streams ?? []; return streams ? [...streams].sort(compareTracksForMenu) : [];
}, [selectedOptions?.mediaSource]); }, [selectedOptions?.mediaSource]);
// Store handleSubtitleChange in a ref for stable callback reference // Store handleSubtitleChange in a ref for stable callback reference

View File

@@ -7,6 +7,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { ActivityIndicator, TouchableOpacity, View } from "react-native"; import { ActivityIndicator, TouchableOpacity, View } from "react-native";
import type { ThemeColors } from "@/hooks/useImageColorsReturn"; import type { ThemeColors } from "@/hooks/useImageColorsReturn";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { BITRATES } from "./BitRateSheet"; import { BITRATES } from "./BitRateSheet";
import type { SelectedOptions } from "./ItemContent"; import type { SelectedOptions } from "./ItemContent";
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown"; import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
@@ -63,9 +64,12 @@ export const MediaSourceButton: React.FC<Props> = ({
const subtitleStreams = useMemo( const subtitleStreams = useMemo(
() => () =>
selectedOptions.mediaSource?.MediaStreams?.filter( // Order like jellyfin-web (embedded first, externals last, forced/default up).
(x) => x.Type === "Subtitle", [
) || [], ...(selectedOptions.mediaSource?.MediaStreams?.filter(
(x) => x.Type === "Subtitle",
) || []),
].sort(compareTracksForMenu),
[selectedOptions.mediaSource], [selectedOptions.mediaSource],
); );

View File

@@ -51,6 +51,7 @@ import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import type { TVOptionItem } from "@/utils/atoms/tvOptionModal"; import type { TVOptionItem } from "@/utils/atoms/tvOptionModal";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { compareTracksForMenu } from "@/utils/jellyfin/subtitleUtils";
import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time"; import { formatTimeString, msToTicks, ticksToMs } from "@/utils/time";
import { CONTROLS_CONSTANTS } from "./constants"; import { CONTROLS_CONSTANTS } from "./constants";
import { useVideoContext } from "./contexts/VideoContext"; import { useVideoContext } from "./contexts/VideoContext";
@@ -317,8 +318,10 @@ export const Controls: FC<Props> = ({
try { try {
const streams = (await onRefreshSubtitleTracks?.()) ?? []; const streams = (await onRefreshSubtitleTracks?.()) ?? [];
// Skip streams without a real index: `?? -1` would alias them to the // Skip streams without a real index: `?? -1` would alias them to the
// "disable subtitles" sentinel and mis-route selection. // "disable subtitles" sentinel and mis-route selection. Order like
return streams // jellyfin-web (embedded first, externals last, forced/default up).
return [...streams]
.sort(compareTracksForMenu)
.filter((stream) => typeof stream.Index === "number") .filter((stream) => typeof stream.Index === "number")
.map((stream) => { .map((stream) => {
const index = stream.Index as number; const index = stream.Index as number;