Compare commits

..

1 Commits

Author SHA1 Message Date
Lance Chant
360066cdde fix: apple top shelf currently cropping images
the backdrop url is currently being used, primary is the last resort, so
changing image shape should hopefully help

Signed-off-by: Lance Chant <13349722+lancechant@users.noreply.github.com>
2026-06-13 12:19:17 +02:00
7 changed files with 51 additions and 146 deletions

View File

@@ -147,9 +147,9 @@ export default function DirectPlayerPage() {
const audioIndexFromUrl = audioIndexStr const audioIndexFromUrl = audioIndexStr
? Number.parseInt(audioIndexStr, 10) ? Number.parseInt(audioIndexStr, 10)
: undefined; : undefined;
const subtitleIndexFromUrl = subtitleIndexStr const subtitleIndex = subtitleIndexStr
? Number.parseInt(subtitleIndexStr, 10) ? Number.parseInt(subtitleIndexStr, 10)
: undefined; : -1;
const bitrateValue = bitrateValueStr const bitrateValue = bitrateValueStr
? Number.parseInt(bitrateValueStr, 10) ? Number.parseInt(bitrateValueStr, 10)
: BITRATES[0].value; : BITRATES[0].value;
@@ -185,23 +185,6 @@ export default function DirectPlayerPage() {
return undefined; return undefined;
}, [audioIndexFromUrl, offline, downloadedItem?.userData?.audioStreamIndex]); }, [audioIndexFromUrl, offline, downloadedItem?.userData?.audioStreamIndex]);
// Resolve subtitle index: use URL param if provided, otherwise use stored index for offline playback
const subtitleIndex = useMemo(() => {
if (subtitleIndexFromUrl !== undefined) {
return subtitleIndexFromUrl;
}
if (
offline &&
downloadedItem?.userData?.subtitleStreamIndex !== undefined
) {
return downloadedItem.userData.subtitleStreamIndex;
}
return -1;
}, [
subtitleIndexFromUrl,
offline,
downloadedItem?.userData?.subtitleStreamIndex,
]);
// Initialize TV audio/subtitle indices from URL params. // Initialize TV audio/subtitle indices from URL params.
// No undefined guard: when a new episode's URL omits audioIndex, reset to // No undefined guard: when a new episode's URL omits audioIndex, reset to
// undefined (media default) rather than leaking the previous episode's track. // undefined (media default) rather than leaking the previous episode's track.

View File

@@ -24,7 +24,6 @@ import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColorsReturn } from "@/hooks/useImageColorsReturn"; import { useImageColorsReturn } from "@/hooks/useImageColorsReturn";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -60,9 +59,6 @@ const ItemContentMobile: React.FC<ItemContentProps> = ({
}) => { }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const isOffline = useOfflineMode(); const isOffline = useOfflineMode();
const { getDownloadedItemById } = useDownload();
const downloadedItem =
isOffline && item?.Id ? getDownloadedItemById(item.Id) : null;
const { settings } = useSettings(); const { settings } = useSettings();
const { orientation } = useOrientation(); const { orientation } = useOrientation();
const navigation = useNavigation(); const navigation = useNavigation();
@@ -109,30 +105,17 @@ const ItemContentMobile: React.FC<ItemContentProps> = ({
// Needs to automatically change the selected to the default values for default indexes. // Needs to automatically change the selected to the default values for default indexes.
useEffect(() => { 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 offlineUserData = downloadedItem?.userData;
setSelectedOptions(() => ({ setSelectedOptions(() => ({
bitrate: defaultBitrate, bitrate: defaultBitrate,
mediaSource: defaultMediaSource ?? undefined, mediaSource: defaultMediaSource ?? undefined,
subtitleIndex: subtitleIndex: defaultSubtitleIndex ?? -1,
offlineUserData && !offlineUserData.isTranscoded audioIndex: defaultAudioIndex,
? offlineUserData.subtitleStreamIndex
: (defaultSubtitleIndex ?? -1),
audioIndex:
offlineUserData && !offlineUserData.isTranscoded
? offlineUserData.audioStreamIndex
: defaultAudioIndex,
})); }));
}, [ }, [
defaultAudioIndex, defaultAudioIndex,
defaultBitrate, defaultBitrate,
defaultSubtitleIndex, defaultSubtitleIndex,
defaultMediaSource, defaultMediaSource,
downloadedItem?.userData?.audioStreamIndex,
downloadedItem?.userData?.subtitleStreamIndex,
downloadedItem?.userData?.isTranscoded,
]); ]);
useEffect(() => { useEffect(() => {

View File

@@ -7,8 +7,6 @@ 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 { useDownload } from "@/providers/DownloadProvider";
import { useOfflineMode } from "@/providers/OfflineModeProvider";
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";
@@ -30,14 +28,6 @@ export const MediaSourceButton: React.FC<Props> = ({
}: Props) => { }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); 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 || { const effectiveColors = colors || {
primary: "#7c3aed", primary: "#7c3aed",
@@ -82,7 +72,6 @@ export const MediaSourceButton: React.FC<Props> = ({
const optionGroups: OptionGroup[] = useMemo(() => { const optionGroups: OptionGroup[] = useMemo(() => {
const groups: OptionGroup[] = []; const groups: OptionGroup[] = [];
if (!isOffline) {
// Bitrate group // Bitrate group
groups.push({ groups.push({
title: t("item_card.quality"), title: t("item_card.quality"),
@@ -112,7 +101,6 @@ export const MediaSourceButton: React.FC<Props> = ({
})), })),
}); });
} }
}
// Audio track group // Audio track group
if (audioStreams.length > 0) { if (audioStreams.length > 0) {
@@ -162,7 +150,6 @@ export const MediaSourceButton: React.FC<Props> = ({
return groups; return groups;
}, [ }, [
item, item,
isOffline,
selectedOptions, selectedOptions,
audioStreams, audioStreams,
subtitleStreams, subtitleStreams,
@@ -191,8 +178,6 @@ export const MediaSourceButton: React.FC<Props> = ({
</TouchableOpacity> </TouchableOpacity>
); );
if (isTranscodedDownload) return null;
return ( return (
<PlatformDropdown <PlatformDropdown
groups={optionGroups} groups={optionGroups}

View File

@@ -96,23 +96,14 @@ export const PlayButton: React.FC<Props> = ({
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item.Id!, itemId: item.Id!,
audioIndex: selectedOptions.audioIndex?.toString() ?? "",
subtitleIndex: selectedOptions.subtitleIndex?.toString() ?? "",
mediaSourceId: selectedOptions.mediaSource?.Id ?? "", mediaSourceId: selectedOptions.mediaSource?.Id ?? "",
bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "", bitrateValue: selectedOptions.bitrate?.value?.toString() ?? "",
playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0", playbackPosition: item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
offline: isOffline ? "true" : "false", offline: isOffline ? "true" : "false",
}); });
if (selectedOptions.audioIndex !== undefined) {
queryParams.set("audioIndex", selectedOptions.audioIndex.toString());
}
if (selectedOptions.subtitleIndex !== undefined) {
queryParams.set(
"subtitleIndex",
selectedOptions.subtitleIndex.toString(),
);
}
const queryString = queryParams.toString(); const queryString = queryParams.toString();
if (!client) { if (!client) {
@@ -301,29 +292,6 @@ export const PlayButton: React.FC<Props> = ({
t, t,
]); ]);
const buildOfflineQueryParams = useCallback(
(downloadedItem: NonNullable<ReturnType<typeof getDownloadedItemById>>) => {
const isTranscoded = downloadedItem.userData?.isTranscoded === true;
const audioIdx = isTranscoded
? downloadedItem.userData?.audioStreamIndex
: selectedOptions.audioIndex;
const subtitleIdx = isTranscoded
? downloadedItem.userData?.subtitleStreamIndex
: selectedOptions.subtitleIndex;
const params = new URLSearchParams({
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
if (audioIdx !== undefined) params.set("audioIndex", audioIdx.toString());
if (subtitleIdx !== undefined)
params.set("subtitleIndex", subtitleIdx.toString());
return params;
},
[item, selectedOptions],
);
const onPress = useCallback(async () => { const onPress = useCallback(async () => {
if (!item) return; if (!item) return;
@@ -334,7 +302,13 @@ export const PlayButton: React.FC<Props> = ({
// If already in offline mode, play downloaded file directly // If already in offline mode, play downloaded file directly
if (isOffline && downloadedItem) { if (isOffline && downloadedItem) {
goToPlayer(buildOfflineQueryParams(downloadedItem).toString()); const queryParams = new URLSearchParams({
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
return; return;
} }
@@ -357,9 +331,13 @@ export const PlayButton: React.FC<Props> = ({
<Button <Button
onPress={() => { onPress={() => {
hideModal(); hideModal();
goToPlayer( const queryParams = new URLSearchParams({
buildOfflineQueryParams(downloadedItem).toString(), itemId: item.Id!,
); offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
}} }}
color='purple' color='purple'
> >
@@ -396,7 +374,13 @@ export const PlayButton: React.FC<Props> = ({
{ {
text: t("player.downloaded_file_yes"), text: t("player.downloaded_file_yes"),
onPress: () => { onPress: () => {
goToPlayer(buildOfflineQueryParams(downloadedItem).toString()); const queryParams = new URLSearchParams({
itemId: item.Id!,
offline: "true",
playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
});
goToPlayer(queryParams.toString());
}, },
isPreferred: true, isPreferred: true,
}, },

View File

@@ -1,7 +1,6 @@
import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useCallback } from "react"; import { useCallback } from "react";
import useRouter from "@/hooks/useAppRouter"; import useRouter from "@/hooks/useAppRouter";
import { getDownloadedItemById } from "@/providers/Downloads/database";
import { usePlaySettings } from "@/providers/PlaySettingsProvider"; import { usePlaySettings } from "@/providers/PlaySettingsProvider";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
@@ -16,27 +15,12 @@ export const useDownloadedFileOpener = () => {
console.error("Attempted to open a file without an ID."); console.error("Attempted to open a file without an ID.");
return; return;
} }
const downloadedItem = getDownloadedItemById(item.Id);
const queryParams = new URLSearchParams({ const queryParams = new URLSearchParams({
itemId: item.Id, itemId: item.Id,
offline: "true", offline: "true",
playbackPosition: playbackPosition:
item.UserData?.PlaybackPositionTicks?.toString() ?? "0", item.UserData?.PlaybackPositionTicks?.toString() ?? "0",
}); });
if (downloadedItem?.userData?.audioStreamIndex !== undefined) {
queryParams.set(
"audioIndex",
downloadedItem.userData.audioStreamIndex.toString(),
);
}
if (downloadedItem?.userData?.subtitleStreamIndex !== undefined) {
queryParams.set(
"subtitleIndex",
downloadedItem.userData.subtitleStreamIndex.toString(),
);
}
try { try {
router.push(`/player/direct-player?${queryParams.toString()}`); router.push(`/player/direct-player?${queryParams.toString()}`);
} catch (error) { } catch (error) {

View File

@@ -191,20 +191,6 @@ export const usePlaybackManager = ({
: playedPercentage, : playedPercentage,
}, },
}, },
// Sync selected audio/subtitle tracks so next playback resumes with
// the same tracks the user had active — but only for non-transcoded
// downloads where the user can freely switch tracks.
userData: localItem.userData.isTranscoded
? localItem.userData
: {
...localItem.userData,
audioStreamIndex:
playbackProgressInfo.AudioStreamIndex ??
localItem.userData.audioStreamIndex,
subtitleStreamIndex:
playbackProgressInfo.SubtitleStreamIndex ??
localItem.userData.subtitleStreamIndex,
},
}); });
// Force invalidate queries so they refetch from updated local database // Force invalidate queries so they refetch from updated local database
queryClient.invalidateQueries({ queryKey: ["item", itemId] }); queryClient.invalidateQueries({ queryKey: ["item", itemId] });

View File

@@ -65,7 +65,7 @@ final class TopShelfProvider: TVTopShelfContentProvider {
let item = TVTopShelfSectionedItem(identifier: cacheItem.id) let item = TVTopShelfSectionedItem(identifier: cacheItem.id)
item.title = cacheItem.title item.title = cacheItem.title
item.imageShape = .poster item.imageShape = .hdtv
item.displayAction = TVTopShelfAction(url: route) item.displayAction = TVTopShelfAction(url: route)
if let playRoute = cacheItem.playRoute, let playURL = URL(string: playRoute) { if let playRoute = cacheItem.playRoute, let playURL = URL(string: playRoute) {