diff --git a/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx b/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx
index 15ae2870..73870886 100644
--- a/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx
+++ b/app/(auth)/(tabs)/(home,libraries,search)/series/[id].tsx
@@ -61,7 +61,7 @@ const page: React.FC = () => {
return (
= ({
}, []);
return (
-
+
-
+
Audio
-
-
-
- {tc(selectedAudioSteam?.DisplayTitle, 7)}
-
-
-
+
+
+ {selectedAudioSteam?.DisplayTitle}
+
+
{
onChange: (value: Bitrate) => void;
selected: Bitrate;
+ inverted?: boolean;
}
export const BitrateSelector: React.FC = ({
onChange,
selected,
+ inverted,
...props
}) => {
+ const sorted = useMemo(() => {
+ if (inverted)
+ return BITRATES.sort(
+ (a, b) => (a.value || Infinity) - (b.value || Infinity)
+ );
+ return BITRATES.sort(
+ (a, b) => (b.value || Infinity) - (a.value || Infinity)
+ );
+ }, []);
+
return (
-
+
-
+
Quality
-
-
-
- {BITRATES.find((b) => b.value === selected.value)?.key}
-
-
-
+
+
+ {BITRATES.find((b) => b.value === selected.value)?.key}
+
+
Bitrates
- {BITRATES?.map((b, index: number) => (
+ {sorted.map((b) => (
{
onChange(b);
}}
diff --git a/components/Chromecast.tsx b/components/Chromecast.tsx
index f3ff8f16..60c4400d 100644
--- a/components/Chromecast.tsx
+++ b/components/Chromecast.tsx
@@ -39,7 +39,7 @@ export const Chromecast: React.FC = ({
if (background === "transparent")
return (
diff --git a/components/DownloadItem.tsx b/components/DownloadItem.tsx
index d2ff0893..a07c0e9a 100644
--- a/components/DownloadItem.tsx
+++ b/components/DownloadItem.tsx
@@ -2,8 +2,17 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { runningProcesses } from "@/utils/atoms/downloads";
import { queueActions, queueAtom } from "@/utils/atoms/queue";
-import { getPlaybackInfo } from "@/utils/jellyfin/media/getPlaybackInfo";
+import { useSettings } from "@/utils/atoms/settings";
+import ios from "@/utils/profiles/ios";
+import native from "@/utils/profiles/native";
+import old from "@/utils/profiles/old";
import Ionicons from "@expo/vector-icons/Ionicons";
+import {
+ BottomSheetBackdrop,
+ BottomSheetBackdropProps,
+ BottomSheetModal,
+ BottomSheetView,
+} from "@gorhom/bottom-sheet";
import {
BaseItemDto,
MediaSourceInfo,
@@ -12,19 +21,16 @@ import AsyncStorage from "@react-native-async-storage/async-storage";
import { useQuery } from "@tanstack/react-query";
import { router } from "expo-router";
import { useAtom } from "jotai";
-import {
- TouchableOpacity,
- TouchableOpacityProps,
- View,
- ViewProps,
-} from "react-native";
+import { useCallback, useMemo, useRef, useState } from "react";
+import { TouchableOpacity, View, ViewProps } from "react-native";
+import { AudioTrackSelector } from "./AudioTrackSelector";
+import { Bitrate, BitrateSelector } from "./BitrateSelector";
+import { Button } from "./Button";
+import { Text } from "./common/Text";
import { Loader } from "./Loader";
+import { MediaSourceSelector } from "./MediaSourceSelector";
import ProgressCircle from "./ProgressCircle";
-import { DownloadQuality, useSettings } from "@/utils/atoms/settings";
-import { useCallback } from "react";
-import ios from "@/utils/profiles/ios";
-import native from "@/utils/profiles/native";
-import old from "@/utils/profiles/old";
+import { SubtitleTrackSelector } from "./SubtitleTrackSelector";
interface DownloadProps extends ViewProps {
item: BaseItemDto;
@@ -35,100 +41,134 @@ export const DownloadItem: React.FC = ({ item, ...props }) => {
const [user] = useAtom(userAtom);
const [process] = useAtom(runningProcesses);
const [queue, setQueue] = useAtom(queueAtom);
-
const [settings] = useSettings();
-
const { startRemuxing } = useRemuxHlsToMp4(item);
- const initiateDownload = useCallback(
- async (qualitySetting: DownloadQuality) => {
- if (!api || !user?.Id || !item.Id) {
- throw new Error(
- "DownloadItem ~ initiateDownload: No api or user or item"
- );
- }
+ const [selectedMediaSource, setSelectedMediaSource] =
+ useState(null);
+ const [selectedAudioStream, setSelectedAudioStream] = useState(-1);
+ const [selectedSubtitleStream, setSelectedSubtitleStream] =
+ useState(0);
+ const [maxBitrate, setMaxBitrate] = useState({
+ key: "Max",
+ value: undefined,
+ });
- let deviceProfile: any = ios;
+ /**
+ * Bottom sheet
+ */
+ const bottomSheetModalRef = useRef(null);
+ const snapPoints = useMemo(() => ["50%"], []);
- if (settings?.deviceProfile === "Native") {
- deviceProfile = native;
- } else if (settings?.deviceProfile === "Old") {
- deviceProfile = old;
- }
+ const handlePresentModalPress = useCallback(() => {
+ bottomSheetModalRef.current?.present();
+ }, []);
- let maxStreamingBitrate: number | undefined = undefined;
+ const handleSheetChanges = useCallback((index: number) => {
+ console.log("handleSheetChanges", index);
+ }, []);
- if (qualitySetting === "high") {
- maxStreamingBitrate = 8000000;
- } else if (qualitySetting === "low") {
- maxStreamingBitrate = 2000000;
- }
+ const closeModal = useCallback(() => {
+ bottomSheetModalRef.current?.dismiss();
+ }, []);
- const response = await api.axiosInstance.post(
- `${api.basePath}/Items/${item.Id}/PlaybackInfo`,
- {
- DeviceProfile: deviceProfile,
- UserId: user.Id,
- MaxStreamingBitrate: maxStreamingBitrate,
- StartTimeTicks: 0,
- EnableTranscoding: maxStreamingBitrate ? true : undefined,
- AutoOpenLiveStream: true,
- MediaSourceId: item.Id,
- AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
- },
- {
- headers: {
- Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
- },
- }
+ /**
+ * Start download
+ */
+ const initiateDownload = useCallback(async () => {
+ if (!api || !user?.Id || !item.Id || !selectedMediaSource?.Id) {
+ throw new Error(
+ "DownloadItem ~ initiateDownload: No api or user or item"
);
+ }
- let url: string | undefined = undefined;
+ let deviceProfile: any = ios;
- const mediaSource = response.data.MediaSources?.[0] as MediaSourceInfo;
+ if (settings?.deviceProfile === "Native") {
+ deviceProfile = native;
+ } else if (settings?.deviceProfile === "Old") {
+ deviceProfile = old;
+ }
- if (!mediaSource) {
- throw new Error("No media source");
+ const response = await api.axiosInstance.post(
+ `${api.basePath}/Items/${item.Id}/PlaybackInfo`,
+ {
+ DeviceProfile: deviceProfile,
+ UserId: user.Id,
+ MaxStreamingBitrate: maxBitrate.value,
+ StartTimeTicks: 0,
+ EnableTranscoding: maxBitrate.value ? true : undefined,
+ AutoOpenLiveStream: true,
+ AllowVideoStreamCopy: maxBitrate.value ? false : true,
+ MediaSourceId: selectedMediaSource?.Id,
+ AudioStreamIndex: selectedAudioStream,
+ SubtitleStreamIndex: selectedSubtitleStream,
+ },
+ {
+ headers: {
+ Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
+ },
}
+ );
- if (mediaSource.SupportsDirectPlay) {
- if (item.MediaType === "Video") {
- console.log("Using direct stream for video!");
- url = `${api.basePath}/Videos/${item.Id}/stream.mp4?mediaSourceId=${item.Id}&static=true`;
- } else if (item.MediaType === "Audio") {
- console.log("Using direct stream for audio!");
- const searchParams = new URLSearchParams({
- UserId: user.Id,
- DeviceId: api.deviceInfo.id,
- MaxStreamingBitrate: "140000000",
- Container:
- "opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg",
- TranscodingContainer: "mp4",
- TranscodingProtocol: "hls",
- AudioCodec: "aac",
- api_key: api.accessToken,
- StartTimeTicks: "0",
- EnableRedirection: "true",
- EnableRemoteMedia: "false",
- });
- url = `${api.basePath}/Audio/${
- item.Id
- }/universal?${searchParams.toString()}`;
- }
+ let url: string | undefined = undefined;
+
+ const mediaSource: MediaSourceInfo = response.data.MediaSources.find(
+ (source: MediaSourceInfo) => source.Id === selectedMediaSource?.Id
+ );
+
+ if (!mediaSource) {
+ throw new Error("No media source");
+ }
+
+ if (mediaSource.SupportsDirectPlay) {
+ if (item.MediaType === "Video") {
+ console.log("Using direct stream for video!");
+ url = `${api.basePath}/Videos/${item.Id}/stream.mp4?mediaSourceId=${item.Id}&static=true&mediaSourceId=${mediaSource.Id}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`;
+ } else if (item.MediaType === "Audio") {
+ console.log("Using direct stream for audio!");
+ const searchParams = new URLSearchParams({
+ UserId: user.Id,
+ DeviceId: api.deviceInfo.id,
+ MaxStreamingBitrate: "140000000",
+ Container:
+ "opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg",
+ TranscodingContainer: "mp4",
+ TranscodingProtocol: "hls",
+ AudioCodec: "aac",
+ api_key: api.accessToken,
+ StartTimeTicks: "0",
+ EnableRedirection: "true",
+ EnableRemoteMedia: "false",
+ });
+ url = `${api.basePath}/Audio/${
+ item.Id
+ }/universal?${searchParams.toString()}`;
}
+ }
- if (mediaSource.TranscodingUrl) {
- console.log("Using transcoded stream!");
- url = `${api.basePath}${mediaSource.TranscodingUrl}`;
- } else {
- throw new Error("No transcoding url");
- }
+ if (mediaSource.TranscodingUrl) {
+ console.log("Using transcoded stream!");
+ url = `${api.basePath}${mediaSource.TranscodingUrl}`;
+ } else {
+ throw new Error("No transcoding url");
+ }
- return await startRemuxing(url);
- },
- [api, item, startRemuxing, user?.Id]
- );
+ return await startRemuxing(url);
+ }, [
+ api,
+ item,
+ startRemuxing,
+ user?.Id,
+ selectedMediaSource,
+ selectedAudioStream,
+ selectedSubtitleStream,
+ maxBitrate,
+ ]);
+ /**
+ * Check if item is downloaded
+ */
const { data: downloaded, isFetching } = useQuery({
queryKey: ["downloaded", item.Id],
queryFn: async () => {
@@ -143,6 +183,17 @@ export const DownloadItem: React.FC = ({ item, ...props }) => {
enabled: !!item.Id,
});
+ const renderBackdrop = useCallback(
+ (props: BottomSheetBackdropProps) => (
+
+ ),
+ []
+ );
+
return (
= ({ item, ...props }) => {
) : (
- {
- queueActions.enqueue(queue, setQueue, {
- id: item.Id!,
- execute: async () => {
- if (!settings?.downloadQuality?.value) {
- throw new Error("No download quality selected");
- }
- await initiateDownload(settings?.downloadQuality?.value);
- },
- item,
- });
- }}
- >
+
)}
+
+
+
+
+ Download options
+
+
+ setMaxBitrate(val)}
+ selected={maxBitrate}
+ />
+
+ {selectedMediaSource && (
+
+
+
+
+ )}
+
+
+
+
+
);
};
diff --git a/components/ItemContent.tsx b/components/ItemContent.tsx
index f64e17a2..536d43e6 100644
--- a/components/ItemContent.tsx
+++ b/components/ItemContent.tsx
@@ -32,16 +32,23 @@ import { useCastDevice } from "react-native-google-cast";
import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader";
import { MediaSourceSelector } from "./MediaSourceSelector";
-import { useImageColors } from "@/hooks/useImageColors";
+import Animated, {
+ useSharedValue,
+ useAnimatedStyle,
+ withTiming,
+ runOnJS,
+} from "react-native-reanimated";
+import { Loader } from "./Loader";
+import { set } from "lodash";
export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
- const [settings] = useSettings();
+ const opacity = useSharedValue(0);
const castDevice = useCastDevice();
const navigation = useNavigation();
-
+ const [settings] = useSettings();
const [selectedMediaSource, setSelectedMediaSource] =
useState(null);
const [selectedAudioStream, setSelectedAudioStream] = useState(-1);
@@ -52,6 +59,27 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
value: undefined,
});
+ const [loadingImage, setLoadingImage] = useState(true);
+ const [loadingLogo, setLoadingLogo] = useState(true);
+
+ const animatedStyle = useAnimatedStyle(() => {
+ return {
+ opacity: opacity.value,
+ };
+ });
+
+ const fadeIn = () => {
+ opacity.value = withTiming(1, { duration: 300 });
+ };
+
+ const fadeOut = (callback: any) => {
+ opacity.value = withTiming(0, { duration: 300 }, (finished) => {
+ if (finished) {
+ runOnJS(callback)();
+ }
+ });
+ };
+
const headerHeightRef = useRef(0);
const {
@@ -70,9 +98,32 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
return res;
},
enabled: !!id && !!api,
- staleTime: 60 * 1000,
+ staleTime: 60 * 1000 * 5,
});
+ const [localItem, setLocalItem] = useState(item);
+
+ useEffect(() => {
+ if (item) {
+ if (localItem) {
+ // Fade out current item
+ fadeOut(() => {
+ // Update local item after fade out
+ setLocalItem(item);
+ // Then fade in
+ fadeIn();
+ });
+ } else {
+ // If there's no current item, just set and fade in
+ setLocalItem(item);
+ fadeIn();
+ }
+ } else {
+ // If item is null, fade out and clear local item
+ fadeOut(() => setLocalItem(null));
+ }
+ }, [item]);
+
useEffect(() => {
navigation.setOptions({
headerRight: () =>
@@ -88,7 +139,7 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
useEffect(() => {
if (item?.Type === "Episode") headerHeightRef.current = 400;
- else headerHeightRef.current = 500;
+ else if (item?.Type === "Movie") headerHeightRef.current = 500;
}, [item]);
const { data: sessionData } = useQuery({
@@ -155,110 +206,123 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const logoUrl = useMemo(() => getLogoImageUrlById({ api, item }), [item]);
- const loading = useMemo(
- () => isLoading || isFetching,
- [isLoading, isFetching]
- );
+ const loading = useMemo(() => {
+ return Boolean(
+ isLoading || isFetching || loadingImage || (logoUrl && loadingLogo)
+ );
+ }, [isLoading, isFetching, loadingImage, loadingLogo, logoUrl]);
return (
-
- {item ? (
-
- ) : (
-
- )}
- >
- }
- logo={
- <>
- {logoUrl ? (
-
- ) : null}
- >
- }
- >
-
-
-
-
- {item ? (
-
- setMaxBitrate(val)}
- selected={maxBitrate}
+
+ {loading && (
+
+
+
+ )}
+
+
+ {localItem && (
+ setLoadingImage(false)}
+ onError={() => setLoadingImage(false)}
+ />
+ )}
+
+ >
+ }
+ logo={
+ <>
+ {logoUrl ? (
+ setLoadingLogo(false)}
+ onError={() => setLoadingLogo(false)}
/>
-
- {selectedMediaSource && (
-
-
+ }
+ >
+
+
+
+
+ {localItem ? (
+
+ setMaxBitrate(val)}
+ selected={maxBitrate}
/>
-
+ {selectedMediaSource && (
+ <>
+
+
+ >
+ )}
+
+ ) : (
+
+
+
)}
-
- ) : (
-
-
-
-
+
+
+
+
+
+ {item?.Type === "Episode" && (
+
)}
-
+
+
+
+
+ {item?.Type === "Episode" && (
+
+ )}
+
+
+
-
- {item?.Type === "Episode" && (
-
- )}
-
-
-
-
-
- {item?.Type === "Episode" && (
-
- )}
-
-
-
-
-
+
+
);
});
diff --git a/components/ItemHeader.tsx b/components/ItemHeader.tsx
index 1eb15d64..dcffe023 100644
--- a/components/ItemHeader.tsx
+++ b/components/ItemHeader.tsx
@@ -11,17 +11,25 @@ interface Props extends ViewProps {
export const ItemHeader: React.FC = ({ item, ...props }) => {
if (!item)
return (
-
-
-
-
-
-
+
+
+
+
+
);
return (
-
+
{item.Type === "Episode" && }
{item.Type === "Movie" && }
diff --git a/components/MediaSourceSelector.tsx b/components/MediaSourceSelector.tsx
index 44a3766a..b32ceb4b 100644
--- a/components/MediaSourceSelector.tsx
+++ b/components/MediaSourceSelector.tsx
@@ -37,16 +37,19 @@ export const MediaSourceSelector: React.FC = ({
}, [mediaSources]);
return (
-
+
-
+
Video
-
-
- {tc(selectedMediaSource, 7)}
-
-
+
+ {selectedMediaSource}
+
= ({
onChange(source);
}}
>
-
- {
- source.MediaStreams?.find((s) => s.Type === "Video")
- ?.DisplayTitle
- }
-
+ {source.Name}
))}
diff --git a/components/ParallaxPage.tsx b/components/ParallaxPage.tsx
index 764ed9ef..daebca6b 100644
--- a/components/ParallaxPage.tsx
+++ b/components/ParallaxPage.tsx
@@ -1,6 +1,6 @@
import { LinearGradient } from "expo-linear-gradient";
import { type PropsWithChildren, type ReactElement } from "react";
-import { View } from "react-native";
+import { View, ViewProps } from "react-native";
import Animated, {
interpolate,
useAnimatedRef,
@@ -8,19 +8,20 @@ import Animated, {
useScrollViewOffset,
} from "react-native-reanimated";
-type Props = PropsWithChildren<{
+interface Props extends ViewProps {
headerImage: ReactElement;
logo?: ReactElement;
episodePoster?: ReactElement;
headerHeight?: number;
-}>;
+}
-export const ParallaxScrollView: React.FC = ({
+export const ParallaxScrollView: React.FC> = ({
children,
headerImage,
episodePoster,
headerHeight = 400,
logo,
+ ...props
}: Props) => {
const scrollRef = useAnimatedRef();
const scrollOffset = useScrollViewOffset(scrollRef);
@@ -47,7 +48,7 @@ export const ParallaxScrollView: React.FC = ({
});
return (
-
+
{
item?: BaseItemDto | null;
@@ -26,6 +34,47 @@ export const PlayButton: React.FC = ({ item, url, ...props }) => {
const [color] = useAtom(itemThemeColorAtom);
+ // Create a shared value for animation progress
+ const progress = useSharedValue(0);
+
+ // Create shared values for start and end colors
+ const startColor = useSharedValue(color);
+ const endColor = useSharedValue(color);
+
+ useEffect(() => {
+ // When color changes, update end color and animate progress
+ endColor.value = color;
+ progress.value = 0; // Reset progress
+ progress.value = withTiming(1, { duration: 300 }); // Animate to 1 over 500ms
+ }, [color]);
+
+ // Animated style for primary color
+ const animatedPrimaryStyle = useAnimatedStyle(() => ({
+ backgroundColor: interpolateColor(
+ progress.value,
+ [0, 1],
+ [startColor.value.average, endColor.value.average]
+ ),
+ }));
+
+ // Animated style for text color
+ const animatedTextStyle = useAnimatedStyle(() => ({
+ color: interpolateColor(
+ progress.value,
+ [0, 1],
+ [startColor.value.text, endColor.value.text]
+ ),
+ }));
+
+ // Update start color after animation completes
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ startColor.value = color;
+ }, 500); // Should match the duration in withTiming
+
+ return () => clearTimeout(timeout);
+ }, [color]);
+
const onPress = async () => {
if (!url || !item) return;
@@ -85,37 +134,43 @@ export const PlayButton: React.FC = ({ item, url, ...props }) => {
return (
+
+
-
-
+ className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
+ >
-
+
{runtimeTicksToMinutes(item?.RunTimeTicks)}
-
-
- {client && }
+
+
+
+
+ {client && (
+
+
+
+ )}
diff --git a/components/SubtitleTrackSelector.tsx b/components/SubtitleTrackSelector.tsx
index c70ad7dc..3b53a7d9 100644
--- a/components/SubtitleTrackSelector.tsx
+++ b/components/SubtitleTrackSelector.tsx
@@ -44,20 +44,24 @@ export const SubtitleTrackSelector: React.FC = ({
if (subtitleStreams.length === 0) return null;
return (
-
+
-
+
Subtitle
-
-
-
- {selectedSubtitleSteam
- ? tc(selectedSubtitleSteam?.DisplayTitle, 7)
- : "None"}
-
-
-
+
+
+ {selectedSubtitleSteam
+ ? tc(selectedSubtitleSteam?.DisplayTitle, 7)
+ : "None"}
+
+
= ({
return (
router.back()}
- className=" bg-black rounded-full p-2 border border-neutral-900"
+ className=" bg-neutral-800/80 rounded-full p-2"
{...touchableOpacityProps}
>
= ({ item, initialSeasonIndex }) => {
const { data: episodes, isFetching } = useQuery({
queryKey: ["episodes", item.Id, selectedSeasonId],
queryFn: async () => {
- if (!api || !user?.Id || !item.Id) return [];
- const response = await api.axiosInstance.get(
- `${api.basePath}/Shows/${item.Id}/Episodes`,
- {
- params: {
- userId: user?.Id,
- seasonId: selectedSeasonId,
- Fields:
- "ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount,Overview",
- },
- headers: {
- Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
- },
- }
- );
+ if (!api || !user?.Id || !item.Id || !selectedSeasonId) return [];
+ const res = await getTvShowsApi(api).getEpisodes({
+ seriesId: item.Id,
+ userId: user.Id,
+ seasonId: selectedSeasonId,
+ enableUserData: true,
+ fields: ["MediaSources", "MediaStreams", "Overview"],
+ });
- return response.data.Items as BaseItemDto[];
+ return res.data.Items;
},
enabled: !!api && !!user?.Id && !!item.Id && !!selectedSeasonId,
});
+ const queryClient = useQueryClient();
+ useEffect(() => {
+ for (let e of episodes || []) {
+ queryClient.prefetchQuery({
+ queryKey: ["item", e.Id],
+ queryFn: async () => {
+ if (!e.Id) return;
+ const res = await getUserItemData({
+ api,
+ userId: user?.Id,
+ itemId: e.Id,
+ });
+ return res;
+ },
+ staleTime: 60 * 5 * 1000,
+ });
+ }
+ }, [episodes]);
+
// Used for height calculation
const [nrOfEpisodes, setNrOfEpisodes] = useState(0);
useEffect(() => {
@@ -164,26 +180,6 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => {
))}
- {/* Old View. Might have a setting later to manually select view. */}
- {/* {episodes && (
-
- (
- {
- router.push(`/(auth)/items/${item.Id}`);
- }}
- className="flex flex-col w-48"
- >
-
-
-
- )}
- />
-
- )} */}
{isFetching ? (
{
onValueChange={(value) => updateSettings({ autoRotate: value })}
/>
- {
))}
-
+ */}
Start videos in fullscreen
diff --git a/utils/atoms/primaryColor.ts b/utils/atoms/primaryColor.ts
index d7036163..99dbc709 100644
--- a/utils/atoms/primaryColor.ts
+++ b/utils/atoms/primaryColor.ts
@@ -62,8 +62,8 @@ export const itemThemeColorAtom = atom(
const newColors = { ...currentColors, ...update };
// Recalculate text color if primary color changes
- if (update.primary) {
- newColors.text = calculateTextColor(update.primary);
+ if (update.average) {
+ newColors.text = calculateTextColor(update.average);
}
set(baseThemeColorAtom, newColors);
diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts
index 1ca210a7..075d486f 100644
--- a/utils/jellyfin/media/getStreamUrl.ts
+++ b/utils/jellyfin/media/getStreamUrl.ts
@@ -79,7 +79,7 @@ export const getStreamUrl = async ({
if (mediaSource.SupportsDirectPlay || forceDirectPlay === true) {
if (item.MediaType === "Video") {
console.log("Using direct stream for video!");
- return `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${mediaSource.Id}&static=true`;
+ return `${api.basePath}/Videos/${itemId}/stream.mp4?playSessionId=${sessionData.PlaySessionId}&mediaSourceId=${mediaSource.Id}&static=true&subtitleStreamIndex=${subtitleStreamIndex}&audioStreamIndex=${audioStreamIndex}&deviceId=${api.deviceInfo.id}&api_key=${api.accessToken}`;
} else if (item.MediaType === "Audio") {
console.log("Using direct stream for audio!");
const searchParams = new URLSearchParams({