mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-04-21 16:24:41 +01:00
chore: keep up to date with master
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect } from "react";
|
||||
import React, { useCallback, useEffect } from "react";
|
||||
import { Platform, TouchableOpacity, ViewProps } from "react-native";
|
||||
import GoogleCast, {
|
||||
CastButton,
|
||||
CastContext,
|
||||
useCastDevice,
|
||||
useDevices,
|
||||
@@ -39,18 +40,32 @@ export const Chromecast: React.FC<Props> = ({
|
||||
})();
|
||||
}, [client, devices, castDevice, sessionManager, discoveryManager]);
|
||||
|
||||
// Android requires the cast button to be present for startDiscovery to work
|
||||
const AndroidCastButton = useCallback(
|
||||
() =>
|
||||
Platform.OS === "android" ? (
|
||||
<CastButton tintColor="transparent" />
|
||||
) : (
|
||||
<></>
|
||||
),
|
||||
[Platform.OS]
|
||||
);
|
||||
|
||||
if (background === "transparent")
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
className="rounded-full h-10 w-10 flex items-center justify-center b"
|
||||
{...props}
|
||||
>
|
||||
<Feather name="cast" size={22} color={"white"} />
|
||||
</TouchableOpacity>
|
||||
<>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (mediaStatus?.currentItemId) CastContext.showExpandedControls();
|
||||
else CastContext.showCastDialog();
|
||||
}}
|
||||
className="rounded-full h-10 w-10 flex items-center justify-center b"
|
||||
{...props}
|
||||
>
|
||||
<Feather name="cast" size={22} color={"white"} />
|
||||
</TouchableOpacity>
|
||||
<AndroidCastButton />
|
||||
</>
|
||||
);
|
||||
|
||||
if (Platform.OS === "android")
|
||||
@@ -82,6 +97,7 @@ export const Chromecast: React.FC<Props> = ({
|
||||
>
|
||||
<Feather name="cast" size={22} color={"white"} />
|
||||
</BlurView>
|
||||
<AndroidCastButton />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -246,7 +246,7 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
|
||||
</View>
|
||||
)}
|
||||
|
||||
<PlayButton item={item} url={playUrl} className="grow" />
|
||||
<PlayButton className="grow" />
|
||||
</View>
|
||||
|
||||
{item.Type === "Episode" && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { apiAtom } from "@/providers/JellyfinProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
import { getParentBackdropImageUrl } from "@/utils/jellyfin/image/getParentBackdropImageUrl";
|
||||
import { getPrimaryImageUrl } from "@/utils/jellyfin/image/getPrimaryImageUrl";
|
||||
@@ -6,10 +6,11 @@ import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Linking, TouchableOpacity, View } from "react-native";
|
||||
import { useAtom, useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo } from "react";
|
||||
import { Alert, Linking, TouchableOpacity, View } from "react-native";
|
||||
import CastContext, {
|
||||
CastButton,
|
||||
PlayServicesState,
|
||||
useMediaStatus,
|
||||
useRemoteMediaClient,
|
||||
@@ -28,32 +29,31 @@ import { Button } from "./Button";
|
||||
import { Text } from "./common/Text";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
|
||||
import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import { usePlaySettings } from "@/providers/PlaySettingsProvider";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof Button> {
|
||||
item?: BaseItemDto | null;
|
||||
url?: string | null;
|
||||
}
|
||||
interface Props extends React.ComponentProps<typeof Button> {}
|
||||
|
||||
const ANIMATION_DURATION = 500;
|
||||
const MIN_PLAYBACK_WIDTH = 15;
|
||||
|
||||
export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
export const PlayButton: React.FC<Props> = ({ ...props }) => {
|
||||
const { playSettings, playUrl: url } = usePlaySettings();
|
||||
const { showActionSheetWithOptions } = useActionSheet();
|
||||
const client = useRemoteMediaClient();
|
||||
const mediaStatus = useMediaStatus();
|
||||
|
||||
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||
const [api] = useAtom(apiAtom);
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const memoizedItem = useMemo(() => item, [item?.Id]); // Memoize the item
|
||||
const memoizedColor = useMemo(() => colorAtom, [colorAtom]); // Memoize the color
|
||||
|
||||
const startWidth = useSharedValue(0);
|
||||
const targetWidth = useSharedValue(0);
|
||||
const endColor = useSharedValue(memoizedColor);
|
||||
const startColor = useSharedValue(memoizedColor);
|
||||
const endColor = useSharedValue(colorAtom);
|
||||
const startColor = useSharedValue(colorAtom);
|
||||
const widthProgress = useSharedValue(0);
|
||||
const colorChangeProgress = useSharedValue(0);
|
||||
const [settings] = useSettings();
|
||||
@@ -62,7 +62,11 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
return !url?.includes("m3u8");
|
||||
}, [url]);
|
||||
|
||||
const onPress = async () => {
|
||||
const item = useMemo(() => {
|
||||
return playSettings?.item;
|
||||
}, [playSettings?.item]);
|
||||
|
||||
const onPress = useCallback(async () => {
|
||||
if (!url || !item) {
|
||||
console.warn(
|
||||
"No URL or item provided to PlayButton",
|
||||
@@ -98,7 +102,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
await CastContext.getPlayServicesState().then((state) => {
|
||||
await CastContext.getPlayServicesState().then(async (state) => {
|
||||
if (state && state !== PlayServicesState.SUCCESS)
|
||||
CastContext.showPlayServicesErrorDialog(state);
|
||||
else {
|
||||
@@ -108,10 +112,34 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
CastContext.showExpandedControls();
|
||||
return;
|
||||
}
|
||||
|
||||
// Get a new URL with the Chromecast device profile:
|
||||
const data = await getStreamUrl({
|
||||
api,
|
||||
deviceProfile: chromecastProfile,
|
||||
item,
|
||||
mediaSourceId: playSettings?.mediaSource?.Id,
|
||||
startTimeTicks: 0,
|
||||
maxStreamingBitrate: playSettings?.bitrate?.value,
|
||||
audioStreamIndex: playSettings?.audioIndex ?? 0,
|
||||
subtitleStreamIndex: playSettings?.subtitleIndex ?? -1,
|
||||
userId: user?.Id,
|
||||
forceDirectPlay: settings?.forceDirectPlay,
|
||||
});
|
||||
|
||||
if (!data?.url) {
|
||||
console.warn("No URL returned from getStreamUrl", data);
|
||||
Alert.alert(
|
||||
"Client error",
|
||||
"Could not create stream for Chromecast"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
client
|
||||
.loadMedia({
|
||||
mediaInfo: {
|
||||
contentUrl: url,
|
||||
contentUrl: data?.url,
|
||||
contentType: "video/mp4",
|
||||
metadata:
|
||||
item.Type === "Episode"
|
||||
@@ -184,21 +212,32 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
}, [
|
||||
url,
|
||||
item,
|
||||
client,
|
||||
settings,
|
||||
api,
|
||||
user,
|
||||
playSettings,
|
||||
router,
|
||||
showActionSheetWithOptions,
|
||||
mediaStatus,
|
||||
]);
|
||||
|
||||
const derivedTargetWidth = useDerivedValue(() => {
|
||||
if (!memoizedItem || !memoizedItem.RunTimeTicks) return 0;
|
||||
const userData = memoizedItem.UserData;
|
||||
if (!item || !item.RunTimeTicks) return 0;
|
||||
const userData = item.UserData;
|
||||
if (userData && userData.PlaybackPositionTicks) {
|
||||
return userData.PlaybackPositionTicks > 0
|
||||
? Math.max(
|
||||
(userData.PlaybackPositionTicks / memoizedItem.RunTimeTicks) * 100,
|
||||
(userData.PlaybackPositionTicks / item.RunTimeTicks) * 100,
|
||||
MIN_PLAYBACK_WIDTH
|
||||
)
|
||||
: 0;
|
||||
}
|
||||
return 0;
|
||||
}, [memoizedItem]);
|
||||
}, [item]);
|
||||
|
||||
useAnimatedReaction(
|
||||
() => derivedTargetWidth.value,
|
||||
@@ -214,7 +253,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
);
|
||||
|
||||
useAnimatedReaction(
|
||||
() => memoizedColor,
|
||||
() => colorAtom,
|
||||
(newColor) => {
|
||||
endColor.value = newColor;
|
||||
colorChangeProgress.value = 0;
|
||||
@@ -223,19 +262,19 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
|
||||
});
|
||||
},
|
||||
[memoizedColor]
|
||||
[colorAtom]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const timeout_2 = setTimeout(() => {
|
||||
startColor.value = memoizedColor;
|
||||
startColor.value = colorAtom;
|
||||
startWidth.value = targetWidth.value;
|
||||
}, ANIMATION_DURATION);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeout_2);
|
||||
};
|
||||
}, [memoizedColor, memoizedItem]);
|
||||
}, [colorAtom, item]);
|
||||
|
||||
/**
|
||||
* ANIMATED STYLES
|
||||
@@ -318,6 +357,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||
{client && (
|
||||
<Animated.Text style={animatedTextStyle}>
|
||||
<Feather name="cast" size={22} />
|
||||
<CastButton tintColor="transparent" />
|
||||
</Animated.Text>
|
||||
)}
|
||||
{!client && settings?.openInVLC && (
|
||||
|
||||
@@ -11,12 +11,9 @@ import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Image } from "expo-image";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { TouchableOpacityProps, View } from "react-native";
|
||||
import { getColors } from "react-native-image-colors";
|
||||
import { TouchableItemRouter } from "../common/TouchableItemRouter";
|
||||
import { useImageColors } from "@/hooks/useImageColors";
|
||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||
|
||||
interface Props extends TouchableOpacityProps {
|
||||
library: BaseItemDto;
|
||||
@@ -53,10 +50,6 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||
[library]
|
||||
);
|
||||
|
||||
// If we want to use image colors for library cards
|
||||
// const [color] = useAtom(itemThemeColorAtom)
|
||||
// useImageColors({ url });
|
||||
|
||||
const { data: itemsCount } = useQuery({
|
||||
queryKey: ["library-count", library.Id],
|
||||
queryFn: async () => {
|
||||
@@ -68,6 +61,7 @@ export const LibraryItemCard: React.FC<Props> = ({ library, ...props }) => {
|
||||
});
|
||||
return response.data.TotalRecordCount;
|
||||
},
|
||||
staleTime: 1000 * 60 * 60,
|
||||
});
|
||||
|
||||
if (!url) return null;
|
||||
|
||||
@@ -85,47 +85,7 @@ export const Controls: React.FC<Props> = ({
|
||||
|
||||
const windowDimensions = Dimensions.get("window");
|
||||
|
||||
const op = useSharedValue<number>(1);
|
||||
const tr = useSharedValue<number>(10);
|
||||
const animatedStyles = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: op.value,
|
||||
};
|
||||
});
|
||||
const animatedTopStyles = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: op.value,
|
||||
transform: [
|
||||
{
|
||||
translateY: -tr.value,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
const animatedBottomStyles = useAnimatedStyle(() => {
|
||||
return {
|
||||
opacity: op.value,
|
||||
transform: [
|
||||
{
|
||||
translateY: tr.value,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (showControls || isBuffering) {
|
||||
op.value = withTiming(1, { duration: 200 });
|
||||
tr.value = withTiming(0, { duration: 200 });
|
||||
} else {
|
||||
op.value = withTiming(0, { duration: 200 });
|
||||
tr.value = withTiming(10, { duration: 200 });
|
||||
}
|
||||
}, [showControls, isBuffering]);
|
||||
|
||||
const { previousItem, nextItem } = useAdjacentItems({
|
||||
item: offline ? undefined : item,
|
||||
});
|
||||
const { previousItem, nextItem } = useAdjacentItems({ item });
|
||||
const { trickPlayUrl, calculateTrickplayUrl, trickplayInfo } = useTrickplay(
|
||||
item,
|
||||
!offline
|
||||
@@ -398,7 +358,7 @@ export const Controls: React.FC<Props> = ({
|
||||
toggleControls();
|
||||
}}
|
||||
>
|
||||
<Animated.View
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
@@ -406,11 +366,11 @@ export const Controls: React.FC<Props> = ({
|
||||
left: 0,
|
||||
width: windowDimensions.width + 100,
|
||||
height: windowDimensions.height + 100,
|
||||
opacity: showControls ? 1 : 0,
|
||||
},
|
||||
animatedStyles,
|
||||
]}
|
||||
className={`bg-black/50 z-0`}
|
||||
></Animated.View>
|
||||
></View>
|
||||
</Pressable>
|
||||
|
||||
<View
|
||||
@@ -429,14 +389,14 @@ export const Controls: React.FC<Props> = ({
|
||||
<Loader />
|
||||
</View>
|
||||
|
||||
<Animated.View
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
top: insets.top,
|
||||
right: insets.right,
|
||||
opacity: showControls ? 1 : 0,
|
||||
},
|
||||
animatedTopStyles,
|
||||
]}
|
||||
pointerEvents={showControls ? "auto" : "none"}
|
||||
className={`flex flex-row items-center space-x-2 z-10 p-4 `}
|
||||
@@ -460,9 +420,9 @@ export const Controls: React.FC<Props> = ({
|
||||
>
|
||||
<Ionicons name="close" size={24} color="white" />
|
||||
</TouchableOpacity>
|
||||
</Animated.View>
|
||||
</View>
|
||||
|
||||
<Animated.View
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
position: "absolute",
|
||||
@@ -470,8 +430,8 @@ export const Controls: React.FC<Props> = ({
|
||||
maxHeight: windowDimensions.height,
|
||||
left: insets.left,
|
||||
bottom: Platform.OS === "ios" ? insets.bottom : insets.bottom,
|
||||
opacity: showControls ? 1 : 0,
|
||||
},
|
||||
animatedBottomStyles,
|
||||
]}
|
||||
pointerEvents={showControls ? "auto" : "none"}
|
||||
className={`flex flex-col p-4 `}
|
||||
@@ -606,7 +566,7 @@ export const Controls: React.FC<Props> = ({
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user