mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-24 12:08:05 +00:00
Compare commits
1 Commits
develop
...
feat-tv-ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ece40534ac |
@@ -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
|
||||||
const CAROUSEL_HEIGHT = screenHeight / 1.45;
|
export const APPLE_TV_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: CAROUSEL_HEIGHT,
|
height: APPLE_TV_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: CAROUSEL_HEIGHT,
|
height: APPLE_TV_CAROUSEL_HEIGHT,
|
||||||
position: "relative",
|
position: "relative",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -731,7 +731,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
|||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
height: CAROUSEL_HEIGHT,
|
height: APPLE_TV_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: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
height: APPLE_TV_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: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
width: screenWidth * items.length,
|
width: screenWidth * items.length,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
import { LinearGradient } from "expo-linear-gradient";
|
import { LinearGradient } from "expo-linear-gradient";
|
||||||
import type { PropsWithChildren, ReactElement } from "react";
|
import type {
|
||||||
import { type NativeScrollEvent, View, type ViewProps } from "react-native";
|
MutableRefObject,
|
||||||
|
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,
|
||||||
@@ -14,6 +27,9 @@ 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>> = ({
|
||||||
@@ -23,10 +39,33 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
headerHeight = 400,
|
headerHeight = 400,
|
||||||
logo,
|
logo,
|
||||||
onEndReached,
|
onEndReached,
|
||||||
|
contentContainerStyle,
|
||||||
|
scrollViewProps,
|
||||||
|
scrollViewRef,
|
||||||
...props
|
...props
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
const animatedScrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||||
const scrollOffset = useScrollViewOffset(scrollRef);
|
const scrollOffset = useScrollViewOffset(animatedScrollRef);
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -62,12 +101,17 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
|||||||
return (
|
return (
|
||||||
<View className='flex-1' {...props}>
|
<View className='flex-1' {...props}>
|
||||||
<Animated.ScrollView
|
<Animated.ScrollView
|
||||||
style={{
|
{...restScrollViewProps}
|
||||||
position: "relative",
|
style={[
|
||||||
}}
|
{
|
||||||
ref={scrollRef}
|
position: "relative",
|
||||||
scrollEventThrottle={16}
|
},
|
||||||
|
scrollStyle,
|
||||||
|
]}
|
||||||
|
ref={animatedScrollRef}
|
||||||
|
scrollEventThrottle={externalScrollEventThrottle ?? 16}
|
||||||
onScroll={(e) => {
|
onScroll={(e) => {
|
||||||
|
externalOnScroll?.(e);
|
||||||
if (isCloseToBottom(e.nativeEvent)) onEndReached?.();
|
if (isCloseToBottom(e.nativeEvent)) onEndReached?.();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -96,9 +140,12 @@ 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
|
||||||
|
|||||||
@@ -16,14 +16,15 @@ 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";
|
||||||
@@ -38,7 +39,8 @@ 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 { AppleTVCarousel } from "../AppleTVCarousel";
|
import { ParallaxScrollView } from "@/components/ParallaxPage";
|
||||||
|
import { AppleTVCarousel, APPLE_TV_CAROUSEL_HEIGHT } from "../AppleTVCarousel";
|
||||||
|
|
||||||
type ScrollingCollectionListSection = {
|
type ScrollingCollectionListSection = {
|
||||||
type: "ScrollingCollectionList";
|
type: "ScrollingCollectionList";
|
||||||
@@ -66,12 +68,13 @@ 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<ScrollView>(null);
|
const scrollViewRef = useRef<Animated.ScrollView | null>(null);
|
||||||
|
|
||||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||||
const prevIsConnected = useRef<boolean | null>(false);
|
const prevIsConnected = useRef<boolean | null>(false);
|
||||||
@@ -128,7 +131,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: Platform.isTV ? -152 : -100,
|
y: 0,
|
||||||
animated: true,
|
animated: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -455,25 +458,36 @@ export const HomeIndex = () => {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const headerHeight = showCarousel ? APPLE_TV_CAROUSEL_HEIGHT : 120;
|
||||||
|
const refreshProgressOffset = showCarousel ? 200 : 80;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollView
|
<ParallaxScrollView
|
||||||
scrollToOverflowEnabled={true}
|
scrollViewRef={scrollViewRef}
|
||||||
ref={scrollViewRef}
|
headerHeight={headerHeight}
|
||||||
nestedScrollEnabled
|
headerImage={
|
||||||
contentInsetAdjustmentBehavior='never'
|
showCarousel ? (
|
||||||
refreshControl={
|
<AppleTVCarousel initialIndex={0} />
|
||||||
<RefreshControl
|
) : (
|
||||||
refreshing={loading}
|
<View className='flex-1 bg-black' />
|
||||||
onRefresh={refetch}
|
)
|
||||||
tintColor='white' // For iOS
|
|
||||||
colors={["white"]} // For Android
|
|
||||||
progressViewOffset={200} // This offsets the refresh indicator to appear over the carousel
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
style={{ marginTop: Platform.isTV ? 0 : -100 }}
|
contentContainerStyle={showCarousel ? undefined : { top: 0 }}
|
||||||
contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }}
|
scrollViewProps={{
|
||||||
|
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,
|
||||||
@@ -509,7 +523,7 @@ export const HomeIndex = () => {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View className='h-24' />
|
<View className='h-24' />
|
||||||
</ScrollView>
|
</ParallaxScrollView>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ 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,
|
||||||
@@ -158,6 +159,19 @@ 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")}
|
||||||
|
|||||||
@@ -176,6 +176,7 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ 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;
|
||||||
@@ -228,6 +229,7 @@ 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user