mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-07 09:31:53 +01:00
Sync subtitle and audio indexes between server and offline
This commit is contained in:
@@ -25,6 +25,7 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
|
||||
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
|
||||
import { useOrientation } from "@/hooks/useOrientation";
|
||||
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
@@ -53,6 +54,7 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
({ item, itemWithSources }) => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const isOffline = useOfflineMode();
|
||||
const { getDownloadedItemById } = useDownload();
|
||||
const { settings } = useSettings();
|
||||
const { orientation } = useOrientation();
|
||||
const navigation = useNavigation();
|
||||
@@ -91,17 +93,32 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
|
||||
// Needs to automatically change the selected to the default values for default indexes.
|
||||
useEffect(() => {
|
||||
// When offline, use the indices stored in userData (the last-used tracks for this file)
|
||||
// rather than the server's defaults, so MediaSourceButton reflects what will actually play.
|
||||
const downloadedItem =
|
||||
isOffline && item.Id ? getDownloadedItemById(item.Id) : null;
|
||||
const offlineUserData = downloadedItem?.userData;
|
||||
|
||||
setSelectedOptions(() => ({
|
||||
bitrate: defaultBitrate,
|
||||
mediaSource: defaultMediaSource ?? undefined,
|
||||
subtitleIndex: defaultSubtitleIndex ?? -1,
|
||||
audioIndex: defaultAudioIndex,
|
||||
subtitleIndex:
|
||||
offlineUserData && !offlineUserData.isTranscoded
|
||||
? offlineUserData.subtitleStreamIndex
|
||||
: (defaultSubtitleIndex ?? -1),
|
||||
audioIndex:
|
||||
offlineUserData && !offlineUserData.isTranscoded
|
||||
? offlineUserData.audioStreamIndex
|
||||
: defaultAudioIndex,
|
||||
}));
|
||||
}, [
|
||||
defaultAudioIndex,
|
||||
defaultBitrate,
|
||||
defaultSubtitleIndex,
|
||||
defaultMediaSource,
|
||||
isOffline,
|
||||
item.Id,
|
||||
getDownloadedItemById,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -232,14 +249,12 @@ export const ItemContent: React.FC<ItemContentProps> = React.memo(
|
||||
colors={itemColors}
|
||||
/>
|
||||
<View className='w-1' />
|
||||
{!isOffline && (
|
||||
<MediaSourceButton
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
item={itemWithSources}
|
||||
colors={itemColors}
|
||||
/>
|
||||
)}
|
||||
<MediaSourceButton
|
||||
selectedOptions={selectedOptions}
|
||||
setSelectedOptions={setSelectedOptions}
|
||||
item={itemWithSources}
|
||||
colors={itemColors}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
{item.Type === "Episode" && (
|
||||
|
||||
@@ -7,6 +7,8 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import type { ThemeColors } from "@/hooks/useImageColorsReturn";
|
||||
import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { useOfflineMode } from "@/providers/OfflineModeProvider";
|
||||
import { BITRATES } from "./BitRateSheet";
|
||||
import type { SelectedOptions } from "./ItemContent";
|
||||
import { type OptionGroup, PlatformDropdown } from "./PlatformDropdown";
|
||||
@@ -28,6 +30,14 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
}: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const isOffline = useOfflineMode();
|
||||
const { getDownloadedItemById } = useDownload();
|
||||
|
||||
// For transcoded downloads there's only one burned-in track — nothing to pick
|
||||
const isTranscodedDownload = useMemo(() => {
|
||||
if (!isOffline || !item?.Id) return false;
|
||||
return getDownloadedItemById(item.Id)?.userData?.isTranscoded === true;
|
||||
}, [isOffline, item?.Id, getDownloadedItemById]);
|
||||
|
||||
const effectiveColors = colors || {
|
||||
primary: "#7c3aed",
|
||||
@@ -72,34 +82,36 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
const optionGroups: OptionGroup[] = useMemo(() => {
|
||||
const groups: OptionGroup[] = [];
|
||||
|
||||
// Bitrate group
|
||||
groups.push({
|
||||
title: t("item_card.quality"),
|
||||
options: BITRATES.map((bitrate) => ({
|
||||
type: "radio" as const,
|
||||
label: bitrate.key,
|
||||
value: bitrate,
|
||||
selected: bitrate.value === selectedOptions.bitrate?.value,
|
||||
onPress: () =>
|
||||
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
|
||||
})),
|
||||
});
|
||||
|
||||
// Media Source group (only if multiple sources)
|
||||
if (item?.MediaSources && item.MediaSources.length > 1) {
|
||||
if (!isOffline) {
|
||||
// Bitrate group
|
||||
groups.push({
|
||||
title: t("item_card.video"),
|
||||
options: item.MediaSources.map((source) => ({
|
||||
title: t("item_card.quality"),
|
||||
options: BITRATES.map((bitrate) => ({
|
||||
type: "radio" as const,
|
||||
label: getMediaSourceDisplayName(source),
|
||||
value: source,
|
||||
selected: source.Id === selectedOptions.mediaSource?.Id,
|
||||
label: bitrate.key,
|
||||
value: bitrate,
|
||||
selected: bitrate.value === selectedOptions.bitrate?.value,
|
||||
onPress: () =>
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, mediaSource: source },
|
||||
),
|
||||
setSelectedOptions((prev) => prev && { ...prev, bitrate }),
|
||||
})),
|
||||
});
|
||||
|
||||
// Media Source group (only if multiple sources)
|
||||
if (item?.MediaSources && item.MediaSources.length > 1) {
|
||||
groups.push({
|
||||
title: t("item_card.video"),
|
||||
options: item.MediaSources.map((source) => ({
|
||||
type: "radio" as const,
|
||||
label: getMediaSourceDisplayName(source),
|
||||
value: source,
|
||||
selected: source.Id === selectedOptions.mediaSource?.Id,
|
||||
onPress: () =>
|
||||
setSelectedOptions(
|
||||
(prev) => prev && { ...prev, mediaSource: source },
|
||||
),
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Audio track group
|
||||
@@ -150,6 +162,7 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
return groups;
|
||||
}, [
|
||||
item,
|
||||
isOffline,
|
||||
selectedOptions,
|
||||
audioStreams,
|
||||
subtitleStreams,
|
||||
@@ -178,6 +191,8 @@ export const MediaSourceButton: React.FC<Props> = ({
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
if (isTranscodedDownload) return null;
|
||||
|
||||
return (
|
||||
<PlatformDropdown
|
||||
groups={optionGroups}
|
||||
|
||||
@@ -302,9 +302,16 @@ export const PlayButton: React.FC<Props> = ({
|
||||
|
||||
// If already in offline mode, play downloaded file directly
|
||||
if (isOffline && downloadedItem) {
|
||||
const isTranscoded = downloadedItem.userData?.isTranscoded === true;
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
audioIndex: isTranscoded
|
||||
? (downloadedItem.userData?.audioStreamIndex?.toString() ?? "")
|
||||
: (selectedOptions.audioIndex?.toString() ?? ""),
|
||||
subtitleIndex: isTranscoded
|
||||
? (downloadedItem.userData?.subtitleStreamIndex?.toString() ?? "-1")
|
||||
: (selectedOptions.subtitleIndex?.toString() ?? "-1"),
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
@@ -331,9 +338,19 @@ export const PlayButton: React.FC<Props> = ({
|
||||
<Button
|
||||
onPress={() => {
|
||||
hideModal();
|
||||
const isTranscoded =
|
||||
downloadedItem.userData?.isTranscoded === true;
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
audioIndex: isTranscoded
|
||||
? (downloadedItem.userData?.audioStreamIndex?.toString() ??
|
||||
"")
|
||||
: (selectedOptions.audioIndex?.toString() ?? ""),
|
||||
subtitleIndex: isTranscoded
|
||||
? (downloadedItem.userData?.subtitleStreamIndex?.toString() ??
|
||||
"-1")
|
||||
: (selectedOptions.subtitleIndex?.toString() ?? "-1"),
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
@@ -374,9 +391,19 @@ export const PlayButton: React.FC<Props> = ({
|
||||
{
|
||||
text: t("player.downloaded_file_yes"),
|
||||
onPress: () => {
|
||||
const isTranscoded =
|
||||
downloadedItem.userData?.isTranscoded === true;
|
||||
const queryParams = new URLSearchParams({
|
||||
itemId: item.Id!,
|
||||
offline: "true",
|
||||
audioIndex: isTranscoded
|
||||
? (downloadedItem.userData?.audioStreamIndex?.toString() ??
|
||||
"")
|
||||
: (selectedOptions.audioIndex?.toString() ?? ""),
|
||||
subtitleIndex: isTranscoded
|
||||
? (downloadedItem.userData?.subtitleStreamIndex?.toString() ??
|
||||
"-1")
|
||||
: (selectedOptions.subtitleIndex?.toString() ?? "-1"),
|
||||
playbackPosition:
|
||||
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user