Compare commits

..

7 Commits

Author SHA1 Message Date
lostb1t
a096c86fe2 Refactor page.tsx to use local search params 2025-11-06 16:22:20 +01:00
lostb1t
6051d4ca1e Update page.tsx 2025-11-06 16:15:25 +01:00
lostb1t
e727d93303 Update useItemQuery.ts 2025-11-06 13:24:52 +01:00
lostb1t
b71e4bbcda Update page.tsx 2025-11-06 13:24:18 +01:00
lostb1t
c80131f560 Update useItemQuery.ts 2025-11-06 13:19:21 +01:00
lostb1t
a650dc4174 Update useItemQuery.ts 2025-11-06 13:15:41 +01:00
lostb1t
3bde693618 Update useItemQuery.ts 2025-11-06 12:59:56 +01:00
12 changed files with 85 additions and 142 deletions

View File

@@ -10,15 +10,10 @@ module.exports = ({ config }) => {
// Add the background downloader plugin only for non-TV builds // Add the background downloader plugin only for non-TV builds
config.plugins.push("./plugins/withRNBackgroundDownloader.js"); config.plugins.push("./plugins/withRNBackgroundDownloader.js");
} }
// Only override googleServicesFile if env var is set
const androidConfig = {};
if (process.env.GOOGLE_SERVICES_JSON) {
androidConfig.googleServicesFile = process.env.GOOGLE_SERVICES_JSON;
}
return { return {
...(Object.keys(androidConfig).length > 0 && { android: androidConfig }), android: {
googleServicesFile: process.env.GOOGLE_SERVICES_JSON,
},
...config, ...config,
}; };
}; };

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Streamyfin", "name": "Streamyfin",
"slug": "streamyfin", "slug": "streamyfin",
"version": "0.40.1", "version": "0.40.0",
"orientation": "default", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "streamyfin", "scheme": "streamyfin",
@@ -37,7 +37,7 @@
}, },
"android": { "android": {
"jsEngine": "hermes", "jsEngine": "hermes",
"versionCode": 73, "versionCode": 72,
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/icon-android-plain.png", "foregroundImage": "./assets/images/icon-android-plain.png",
"monochromeImage": "./assets/images/icon-android-themed.png", "monochromeImage": "./assets/images/icon-android-themed.png",

View File

@@ -20,7 +20,8 @@ const Page: React.FC = () => {
const { offline } = useLocalSearchParams() as { offline?: string }; const { offline } = useLocalSearchParams() as { offline?: string };
const isOffline = offline === "true"; const isOffline = offline === "true";
const { data: item, isError } = useItemQuery(id, isOffline); const { data: item, isError } = useItemQuery(itemId, false, undefined, [ItemFields.MediaSources]);
const { data: mediaSourcesitem, isError } = useItemQuery(id, isOffline);
const opacity = useSharedValue(1); const opacity = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => { const animatedStyle = useAnimatedStyle(() => {
@@ -90,7 +91,7 @@ const Page: React.FC = () => {
<View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' /> <View className='h-12 bg-neutral-900 rounded-lg w-full mb-2' />
<View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' /> <View className='h-24 bg-neutral-900 rounded-lg mb-1 w-full' />
</Animated.View> </Animated.View>
{item && <ItemContent item={item} isOffline={isOffline} />} {item && <ItemContent item={item} isOffline={isOffline} mediaSourcesItem={mediaSourcesItem} />}
</View> </View>
); );
}; };

View File

@@ -39,7 +39,7 @@ interface AppleTVCarouselProps {
const { width: screenWidth, height: screenHeight } = Dimensions.get("window"); const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
// Layout Constants // Layout Constants
export const APPLE_TV_CAROUSEL_HEIGHT = screenHeight / 1.45; const CAROUSEL_HEIGHT = screenHeight / 1.45;
const GRADIENT_HEIGHT_TOP = 150; const GRADIENT_HEIGHT_TOP = 150;
const GRADIENT_HEIGHT_BOTTOM = 150; const GRADIENT_HEIGHT_BOTTOM = 150;
const LOGO_HEIGHT = 80; const LOGO_HEIGHT = 80;
@@ -381,7 +381,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
<View <View
style={{ style={{
width: screenWidth, width: screenWidth,
height: APPLE_TV_CAROUSEL_HEIGHT, height: CAROUSEL_HEIGHT,
backgroundColor: "#000", backgroundColor: "#000",
}} }}
> >
@@ -549,7 +549,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
key={item.Id} key={item.Id}
style={{ style={{
width: screenWidth, width: screenWidth,
height: APPLE_TV_CAROUSEL_HEIGHT, height: CAROUSEL_HEIGHT,
position: "relative", position: "relative",
}} }}
> >
@@ -731,7 +731,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
return ( return (
<View <View
style={{ style={{
height: APPLE_TV_CAROUSEL_HEIGHT, height: CAROUSEL_HEIGHT,
backgroundColor: "#000", backgroundColor: "#000",
overflow: "hidden", overflow: "hidden",
}} }}
@@ -749,7 +749,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
return ( return (
<View <View
style={{ style={{
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1 height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
backgroundColor: "#000", backgroundColor: "#000",
overflow: "hidden", overflow: "hidden",
}} }}
@@ -758,7 +758,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
<Animated.View <Animated.View
style={[ style={[
{ {
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1 height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
flexDirection: "row", flexDirection: "row",
width: screenWidth * items.length, width: screenWidth * items.length,
}, },

View File

@@ -1,19 +1,6 @@
import { LinearGradient } from "expo-linear-gradient"; import { LinearGradient } from "expo-linear-gradient";
import type { import type { PropsWithChildren, ReactElement } from "react";
MutableRefObject, import { type NativeScrollEvent, View, type ViewProps } from "react-native";
PropsWithChildren,
ReactElement,
Ref,
} from "react";
import { useEffect } from "react";
import {
type NativeScrollEvent,
type ScrollViewProps,
type StyleProp,
View,
type ViewProps,
type ViewStyle,
} from "react-native";
import Animated, { import Animated, {
interpolate, interpolate,
useAnimatedRef, useAnimatedRef,
@@ -27,9 +14,6 @@ interface Props extends ViewProps {
episodePoster?: ReactElement; episodePoster?: ReactElement;
headerHeight?: number; headerHeight?: number;
onEndReached?: (() => void) | null | undefined; onEndReached?: (() => void) | null | undefined;
scrollViewProps?: Animated.AnimatedProps<ScrollViewProps>;
contentContainerStyle?: StyleProp<ViewStyle>;
scrollViewRef?: Ref<Animated.ScrollView>;
} }
export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
@@ -39,33 +23,10 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
headerHeight = 400, headerHeight = 400,
logo, logo,
onEndReached, onEndReached,
contentContainerStyle,
scrollViewProps,
scrollViewRef,
...props ...props
}: Props) => { }: Props) => {
const animatedScrollRef = useAnimatedRef<Animated.ScrollView>(); const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollViewOffset(animatedScrollRef); const scrollOffset = useScrollViewOffset(scrollRef);
const {
onScroll: externalOnScroll,
style: scrollStyle,
scrollEventThrottle: externalScrollEventThrottle,
...restScrollViewProps
} = scrollViewProps ?? {};
useEffect(() => {
if (!scrollViewRef) return;
const node = animatedScrollRef.current;
if (typeof scrollViewRef === "function") {
scrollViewRef(node);
return () => scrollViewRef(null);
}
(scrollViewRef as MutableRefObject<Animated.ScrollView | null>).current =
node;
}, [animatedScrollRef, scrollViewRef]);
const headerAnimatedStyle = useAnimatedStyle(() => { const headerAnimatedStyle = useAnimatedStyle(() => {
return { return {
@@ -101,17 +62,12 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
return ( return (
<View className='flex-1' {...props}> <View className='flex-1' {...props}>
<Animated.ScrollView <Animated.ScrollView
{...restScrollViewProps} style={{
style={[ position: "relative",
{ }}
position: "relative", ref={scrollRef}
}, scrollEventThrottle={16}
scrollStyle,
]}
ref={animatedScrollRef}
scrollEventThrottle={externalScrollEventThrottle ?? 16}
onScroll={(e) => { onScroll={(e) => {
externalOnScroll?.(e);
if (isCloseToBottom(e.nativeEvent)) onEndReached?.(); if (isCloseToBottom(e.nativeEvent)) onEndReached?.();
}} }}
> >
@@ -140,12 +96,9 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
</Animated.View> </Animated.View>
<View <View
style={[ style={{
{ top: -50,
top: -50, }}
},
contentContainerStyle,
]}
className='relative flex-1 bg-transparent pb-24' className='relative flex-1 bg-transparent pb-24'
> >
<LinearGradient <LinearGradient

View File

@@ -16,15 +16,14 @@ import { type QueryFunction, useQuery } from "@tanstack/react-query";
import { useNavigation, useRouter, useSegments } from "expo-router"; import { useNavigation, useRouter, useSegments } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import Animated from "react-native-reanimated";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
ActivityIndicator, ActivityIndicator,
Platform, Platform,
RefreshControl, RefreshControl,
ScrollView,
TouchableOpacity, TouchableOpacity,
View, View,
ScrollView,
} from "react-native"; } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
@@ -39,8 +38,7 @@ import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { eventBus } from "@/utils/eventBus"; import { eventBus } from "@/utils/eventBus";
import { ParallaxScrollView } from "@/components/ParallaxPage"; import { AppleTVCarousel } from "../AppleTVCarousel";
import { AppleTVCarousel, APPLE_TV_CAROUSEL_HEIGHT } from "../AppleTVCarousel";
type ScrollingCollectionListSection = { type ScrollingCollectionListSection = {
type: "ScrollingCollectionList"; type: "ScrollingCollectionList";
@@ -68,13 +66,12 @@ export const HomeIndex = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { settings, refreshStreamyfinPluginSettings } = useSettings(); const { settings, refreshStreamyfinPluginSettings } = useSettings();
const showCarousel = settings?.showHomeCarousel ?? true;
const navigation = useNavigation(); const navigation = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
const scrollViewRef = useRef<Animated.ScrollView | null>(null); const scrollViewRef = useRef<ScrollView>(null);
const { getDownloadedItems, cleanCacheDirectory } = useDownload(); const { getDownloadedItems, cleanCacheDirectory } = useDownload();
const prevIsConnected = useRef<boolean | null>(false); const prevIsConnected = useRef<boolean | null>(false);
@@ -131,7 +128,7 @@ export const HomeIndex = () => {
const unsubscribe = eventBus.on("scrollToTop", () => { const unsubscribe = eventBus.on("scrollToTop", () => {
if ((segments as string[])[2] === "(home)") if ((segments as string[])[2] === "(home)")
scrollViewRef.current?.scrollTo({ scrollViewRef.current?.scrollTo({
y: 0, y: Platform.isTV ? -152 : -100,
animated: true, animated: true,
}); });
}); });
@@ -458,36 +455,25 @@ export const HomeIndex = () => {
</View> </View>
); );
const headerHeight = showCarousel ? APPLE_TV_CAROUSEL_HEIGHT : 120;
const refreshProgressOffset = showCarousel ? 200 : 80;
return ( return (
<ParallaxScrollView <ScrollView
scrollViewRef={scrollViewRef} scrollToOverflowEnabled={true}
headerHeight={headerHeight} ref={scrollViewRef}
headerImage={ nestedScrollEnabled
showCarousel ? ( contentInsetAdjustmentBehavior='never'
<AppleTVCarousel initialIndex={0} /> refreshControl={
) : ( <RefreshControl
<View className='flex-1 bg-black' /> refreshing={loading}
) onRefresh={refetch}
tintColor='white' // For iOS
colors={["white"]} // For Android
progressViewOffset={200} // This offsets the refresh indicator to appear over the carousel
/>
} }
contentContainerStyle={showCarousel ? undefined : { top: 0 }} style={{ marginTop: Platform.isTV ? 0 : -100 }}
scrollViewProps={{ contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }}
refreshControl: (
<RefreshControl
refreshing={loading}
onRefresh={refetch}
tintColor='white'
colors={["white"]}
progressViewOffset={refreshProgressOffset}
/>
),
contentInsetAdjustmentBehavior: "never",
nestedScrollEnabled: true,
scrollToOverflowEnabled: true,
}}
> >
<AppleTVCarousel initialIndex={0} />
<View <View
style={{ style={{
paddingLeft: insets.left, paddingLeft: insets.left,
@@ -523,7 +509,7 @@ export const HomeIndex = () => {
</View> </View>
</View> </View>
<View className='h-24' /> <View className='h-24' />
</ParallaxScrollView> </ScrollView>
); );
}; };

View File

@@ -62,7 +62,6 @@ export const OtherSettings: React.FC = () => {
pluginSettings?.followDeviceOrientation?.locked === true && pluginSettings?.followDeviceOrientation?.locked === true &&
pluginSettings?.defaultVideoOrientation?.locked === true && pluginSettings?.defaultVideoOrientation?.locked === true &&
pluginSettings?.safeAreaInControlsEnabled?.locked === true && pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
pluginSettings?.showHomeCarousel?.locked === true &&
pluginSettings?.showCustomMenuLinks?.locked === true && pluginSettings?.showCustomMenuLinks?.locked === true &&
pluginSettings?.hiddenLibraries?.locked === true && pluginSettings?.hiddenLibraries?.locked === true &&
pluginSettings?.disableHapticFeedback?.locked === true, pluginSettings?.disableHapticFeedback?.locked === true,
@@ -159,19 +158,6 @@ export const OtherSettings: React.FC = () => {
/> />
</ListItem> </ListItem>
<ListItem
title={t("home.settings.other.show_home_carousel")}
disabled={pluginSettings?.showHomeCarousel?.locked}
>
<Switch
value={settings.showHomeCarousel}
disabled={pluginSettings?.showHomeCarousel?.locked}
onValueChange={(value) =>
updateSettings({ showHomeCarousel: value })
}
/>
</ListItem>
{/* {(Platform.OS === "ios" || Platform.isTVOS)&& ( {/* {(Platform.OS === "ios" || Platform.isTVOS)&& (
<ListItem <ListItem
title={t("home.settings.other.video_player")} title={t("home.settings.other.video_player")}

View File

@@ -45,14 +45,14 @@
}, },
"production": { "production": {
"environment": "production", "environment": "production",
"channel": "0.40.1", "channel": "0.40.0",
"android": { "android": {
"image": "latest" "image": "latest"
} }
}, },
"production-apk": { "production-apk": {
"environment": "production", "environment": "production",
"channel": "0.40.1", "channel": "0.40.0",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"
@@ -60,7 +60,7 @@
}, },
"production-apk-tv": { "production-apk-tv": {
"environment": "production", "environment": "production",
"channel": "0.40.1", "channel": "0.40.0",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"

View File

@@ -1,31 +1,56 @@
import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api"; import { getUserLibraryApi } from "@jellyfin/sdk/lib/utils/api";
import { ItemFields } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
export const useItemQuery = (itemId: string, isOffline: boolean) => { // Helper to exclude specific fields
export const excludeFields = (fieldsToExclude: ItemFields[]) => {
return Object.values(ItemFields).filter(
(field) => !fieldsToExclude.includes(field)
);
};
export const useItemQuery = (
itemId: string | undefined,
isOffline?: boolean,
fields?: ItemFields[],
excludeFields?: ItemFields[]
) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const { getDownloadedItemById } = useDownload(); const { getDownloadedItemById } = useDownload();
// Calculate final fields: use excludeFields if provided, otherwise use fields
const finalFields = excludeFields
? Object.values(ItemFields).filter(field => !excludeFields.includes(field))
: fields;
return useQuery({ return useQuery({
queryKey: ["item", itemId], queryKey: ["item", itemId, finalFields],
queryFn: async () => { queryFn: async () => {
if (!itemId) throw new Error('Item ID is required');
if (isOffline) { if (isOffline) {
return getDownloadedItemById(itemId)?.item; return getDownloadedItemById(itemId)?.item;
} }
if (!api || !user || !itemId) return null;
const res = await getUserLibraryApi(api).getItem({ if (!api || !user) return null;
itemId: itemId,
userId: user?.Id, const response = await getUserLibraryApi(api).getItem({
itemId,
userId: user.Id,
...(finalFields && { fields: finalFields }),
}); });
return res.data;
return response.data;
}, },
enabled: !!itemId,
staleTime: 0, staleTime: 0,
refetchOnMount: true, refetchOnMount: true,
refetchOnWindowFocus: true, refetchOnWindowFocus: true,
refetchOnReconnect: true, refetchOnReconnect: true,
networkMode: "always", networkMode: "always",
}); });
}; };

View File

@@ -64,7 +64,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin( setJellyfin(
() => () =>
new Jellyfin({ new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.40.1" }, clientInfo: { name: "Streamyfin", version: "0.40.0" },
deviceInfo: { deviceInfo: {
name: deviceName, name: deviceName,
id, id,
@@ -87,7 +87,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return { return {
authorization: `MediaBrowser Client="Streamyfin", Device=${ authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS" Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.40.1"`, }, DeviceId="${deviceId}", Version="0.40.0"`,
}; };
}, [deviceId]); }, [deviceId]);

View File

@@ -176,7 +176,6 @@
"UNKNOWN": "Unknown" "UNKNOWN": "Unknown"
}, },
"safe_area_in_controls": "Safe Area in Controls", "safe_area_in_controls": "Safe Area in Controls",
"show_home_carousel": "Show Home Carousel",
"video_player": "Video Player", "video_player": "Video Player",
"video_players": { "video_players": {
"VLC_3": "VLC 3", "VLC_3": "VLC 3",

View File

@@ -160,7 +160,6 @@ export type Settings = {
subtitleMode: SubtitlePlaybackMode; subtitleMode: SubtitlePlaybackMode;
rememberSubtitleSelections: boolean; rememberSubtitleSelections: boolean;
showHomeTitles: boolean; showHomeTitles: boolean;
showHomeCarousel: boolean;
defaultVideoOrientation: ScreenOrientation.OrientationLock; defaultVideoOrientation: ScreenOrientation.OrientationLock;
forwardSkipTime: number; forwardSkipTime: number;
rewindSkipTime: number; rewindSkipTime: number;
@@ -229,7 +228,6 @@ export const defaultValues: Settings = {
subtitleMode: SubtitlePlaybackMode.Default, subtitleMode: SubtitlePlaybackMode.Default,
rememberSubtitleSelections: true, rememberSubtitleSelections: true,
showHomeTitles: true, showHomeTitles: true,
showHomeCarousel: true,
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT, defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
forwardSkipTime: 30, forwardSkipTime: 30,
rewindSkipTime: 10, rewindSkipTime: 10,