mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
fix: improve carousel behavior and settings option (#1166)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
96d6220f5e
commit
0dadfd3d90
@@ -10,11 +10,18 @@ import { LinearGradient } from "expo-linear-gradient";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Dimensions, Pressable, TouchableOpacity, View } from "react-native";
|
||||
import {
|
||||
Pressable,
|
||||
TouchableOpacity,
|
||||
useWindowDimensions,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
Easing,
|
||||
interpolate,
|
||||
runOnJS,
|
||||
type SharedValue,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
@@ -34,12 +41,10 @@ import { PlayedStatus } from "./PlayedStatus";
|
||||
interface AppleTVCarouselProps {
|
||||
initialIndex?: number;
|
||||
onItemChange?: (index: number) => void;
|
||||
scrollOffset?: SharedValue<number>;
|
||||
}
|
||||
|
||||
const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
|
||||
|
||||
// Layout Constants
|
||||
const CAROUSEL_HEIGHT = screenHeight / 1.45;
|
||||
const GRADIENT_HEIGHT_TOP = 150;
|
||||
const GRADIENT_HEIGHT_BOTTOM = 150;
|
||||
const LOGO_HEIGHT = 80;
|
||||
@@ -147,14 +152,21 @@ const DotIndicator = ({
|
||||
export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
initialIndex = 0,
|
||||
onItemChange,
|
||||
scrollOffset,
|
||||
}) => {
|
||||
const { settings } = useSettings();
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
const { isConnected, serverConnected } = useNetworkStatus();
|
||||
const router = useRouter();
|
||||
const { width: screenWidth, height: screenHeight } = useWindowDimensions();
|
||||
const isLandscape = screenWidth >= screenHeight;
|
||||
const carouselHeight = useMemo(
|
||||
() => (isLandscape ? screenHeight * 0.9 : screenHeight / 1.45),
|
||||
[isLandscape, screenHeight],
|
||||
);
|
||||
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||
const translateX = useSharedValue(-currentIndex * screenWidth);
|
||||
const translateX = useSharedValue(-initialIndex * screenWidth);
|
||||
|
||||
const isQueryEnabled =
|
||||
!!api && !!user?.Id && isConnected && serverConnected === true;
|
||||
@@ -281,7 +293,11 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
translateX.value = -newIndex * screenWidth;
|
||||
return newIndex;
|
||||
});
|
||||
}, [hasItems, items, initialIndex, translateX]);
|
||||
}, [hasItems, items, initialIndex, screenWidth, translateX]);
|
||||
|
||||
useEffect(() => {
|
||||
translateX.value = -currentIndex * screenWidth;
|
||||
}, [currentIndex, screenWidth, translateX]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasItems) {
|
||||
@@ -301,7 +317,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
setCurrentIndex(index);
|
||||
onItemChange?.(index);
|
||||
},
|
||||
[hasItems, items, onItemChange, translateX],
|
||||
[hasItems, items, onItemChange, screenWidth, translateX],
|
||||
);
|
||||
|
||||
const navigateToItem = useCallback(
|
||||
@@ -348,6 +364,28 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
};
|
||||
});
|
||||
|
||||
const headerAnimatedStyle = useAnimatedStyle(() => {
|
||||
if (!scrollOffset) return {};
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollOffset.value,
|
||||
[-carouselHeight, 0, carouselHeight],
|
||||
[-carouselHeight / 2, 0, carouselHeight * 0.75],
|
||||
),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
scrollOffset.value,
|
||||
[-carouselHeight, 0, carouselHeight],
|
||||
[2, 1, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const renderDots = () => {
|
||||
if (!hasItems || items.length <= 1) return null;
|
||||
|
||||
@@ -381,7 +419,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
<View
|
||||
style={{
|
||||
width: screenWidth,
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: carouselHeight,
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
@@ -549,20 +587,30 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
key={item.Id}
|
||||
style={{
|
||||
width: screenWidth,
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: carouselHeight,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{/* Background Backdrop */}
|
||||
<ItemImage
|
||||
item={item}
|
||||
variant='Backdrop'
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
}}
|
||||
/>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
},
|
||||
headerAnimatedStyle,
|
||||
]}
|
||||
>
|
||||
<ItemImage
|
||||
item={item}
|
||||
variant='Backdrop'
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
</Animated.View>
|
||||
|
||||
{/* Dark Overlay */}
|
||||
<View
|
||||
@@ -731,7 +779,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: carouselHeight,
|
||||
backgroundColor: "#000",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -749,7 +797,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
height: carouselHeight, // Fixed height instead of flex: 1
|
||||
backgroundColor: "#000",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -758,7 +806,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
height: carouselHeight, // Fixed height instead of flex: 1
|
||||
flexDirection: "row",
|
||||
width: screenWidth * items.length,
|
||||
},
|
||||
|
||||
@@ -21,10 +21,13 @@ import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import Animated, {
|
||||
useAnimatedRef,
|
||||
useScrollViewOffset,
|
||||
} from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Button } from "@/components/Button";
|
||||
import { Text } from "@/components/common/Text";
|
||||
@@ -64,14 +67,21 @@ export const HomeIndex = () => {
|
||||
const api = useAtomValue(apiAtom);
|
||||
const user = useAtomValue(userAtom);
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { settings, refreshStreamyfinPluginSettings } = useSettings();
|
||||
const showLargeHomeCarousel = settings.showLargeHomeCarousel ?? true;
|
||||
const headerOverlayOffset = Platform.isTV
|
||||
? 0
|
||||
: showLargeHomeCarousel
|
||||
? 60
|
||||
: 0;
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const animatedScrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const scrollOffset = useScrollViewOffset(animatedScrollRef);
|
||||
|
||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||
const prevIsConnected = useRef<boolean | null>(false);
|
||||
@@ -127,7 +137,7 @@ export const HomeIndex = () => {
|
||||
useEffect(() => {
|
||||
const unsubscribe = eventBus.on("scrollToTop", () => {
|
||||
if ((segments as string[])[2] === "(home)")
|
||||
scrollViewRef.current?.scrollTo({
|
||||
animatedScrollRef.current?.scrollTo({
|
||||
y: Platform.isTV ? -152 : -100,
|
||||
animated: true,
|
||||
});
|
||||
@@ -456,29 +466,37 @@ export const HomeIndex = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
<Animated.ScrollView
|
||||
scrollToOverflowEnabled={true}
|
||||
ref={scrollViewRef}
|
||||
ref={animatedScrollRef}
|
||||
nestedScrollEnabled
|
||||
contentInsetAdjustmentBehavior='never'
|
||||
scrollEventThrottle={16}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={loading}
|
||||
onRefresh={refetch}
|
||||
tintColor='white' // For iOS
|
||||
colors={["white"]} // For Android
|
||||
progressViewOffset={200} // This offsets the refresh indicator to appear over the carousel
|
||||
progressViewOffset={showLargeHomeCarousel ? 200 : 0} // This offsets the refresh indicator to appear over the carousel
|
||||
/>
|
||||
}
|
||||
style={{ marginTop: Platform.isTV ? 0 : -100 }}
|
||||
contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }}
|
||||
style={{ marginTop: -headerOverlayOffset }}
|
||||
contentContainerStyle={{ paddingTop: headerOverlayOffset }}
|
||||
>
|
||||
<AppleTVCarousel initialIndex={0} />
|
||||
{showLargeHomeCarousel && (
|
||||
<AppleTVCarousel initialIndex={0} scrollOffset={scrollOffset} />
|
||||
)}
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
paddingBottom: 16,
|
||||
paddingTop: Platform.isTV
|
||||
? 0
|
||||
: showLargeHomeCarousel
|
||||
? 0
|
||||
: insets.top + 60,
|
||||
}}
|
||||
>
|
||||
<View className='flex flex-col space-y-4'>
|
||||
@@ -509,7 +527,7 @@ export const HomeIndex = () => {
|
||||
</View>
|
||||
</View>
|
||||
<View className='h-24' />
|
||||
</ScrollView>
|
||||
</Animated.ScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -205,6 +205,14 @@ export const OtherSettings: React.FC = () => {
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem title={t("home.settings.other.show_large_home_carousel")}>
|
||||
<Switch
|
||||
value={settings.showLargeHomeCarousel}
|
||||
onValueChange={(value) =>
|
||||
updateSettings({ showLargeHomeCarousel: value })
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
onPress={() => router.push("/settings/hide-libraries/page")}
|
||||
title={t("home.settings.other.hide_libraries")}
|
||||
|
||||
@@ -182,6 +182,7 @@
|
||||
"VLC_4": "VLC 4 (Experimental + PiP)"
|
||||
},
|
||||
"show_custom_menu_links": "Show Custom Menu Links",
|
||||
"show_large_home_carousel": "Show Large Home Carousel",
|
||||
"hide_libraries": "Hide Libraries",
|
||||
"select_liraries_you_want_to_hide": "Select the libraries you want to hide from the Library tab and home page sections.",
|
||||
"disable_haptic_feedback": "Disable Haptic Feedback",
|
||||
|
||||
@@ -188,6 +188,7 @@ export type Settings = {
|
||||
enableLeftSideBrightnessSwipe: boolean;
|
||||
enableRightSideVolumeSwipe: boolean;
|
||||
usePopularPlugin: boolean;
|
||||
showLargeHomeCarousel: boolean;
|
||||
};
|
||||
|
||||
export interface Lockable<T> {
|
||||
@@ -256,6 +257,7 @@ export const defaultValues: Settings = {
|
||||
enableLeftSideBrightnessSwipe: true,
|
||||
enableRightSideVolumeSwipe: true,
|
||||
usePopularPlugin: true,
|
||||
showLargeHomeCarousel: true,
|
||||
};
|
||||
|
||||
const loadSettings = (): Partial<Settings> => {
|
||||
|
||||
Reference in New Issue
Block a user