From 0dadfd3d902f9a1d4a0f41300c75d1bbe4dfdb21 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Fri, 7 Nov 2025 15:14:35 +0100 Subject: [PATCH] fix: improve carousel behavior and settings option (#1166) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- components/AppleTVCarousel.tsx | 90 ++++++++++++++++++++------- components/settings/HomeIndex.tsx | 42 +++++++++---- components/settings/OtherSettings.tsx | 8 +++ translations/en.json | 1 + utils/atoms/settings.ts | 2 + 5 files changed, 110 insertions(+), 33 deletions(-) diff --git a/components/AppleTVCarousel.tsx b/components/AppleTVCarousel.tsx index c439069d..fc1a8715 100644 --- a/components/AppleTVCarousel.tsx +++ b/components/AppleTVCarousel.tsx @@ -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; } -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 = ({ 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 = ({ 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 = ({ 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 = ({ }; }); + 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 = ({ @@ -549,20 +587,30 @@ export const AppleTVCarousel: React.FC = ({ key={item.Id} style={{ width: screenWidth, - height: CAROUSEL_HEIGHT, + height: carouselHeight, position: "relative", }} > {/* Background Backdrop */} - + + + {/* Dark Overlay */} = ({ return ( = ({ return ( = ({ { 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(null); + const animatedScrollRef = useAnimatedRef(); + const scrollOffset = useScrollViewOffset(animatedScrollRef); const { getDownloadedItems, cleanCacheDirectory } = useDownload(); const prevIsConnected = useRef(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 ( - } - style={{ marginTop: Platform.isTV ? 0 : -100 }} - contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }} + style={{ marginTop: -headerOverlayOffset }} + contentContainerStyle={{ paddingTop: headerOverlayOffset }} > - + {showLargeHomeCarousel && ( + + )} @@ -509,7 +527,7 @@ export const HomeIndex = () => { - + ); }; diff --git a/components/settings/OtherSettings.tsx b/components/settings/OtherSettings.tsx index d4d7e598..e05e57b0 100644 --- a/components/settings/OtherSettings.tsx +++ b/components/settings/OtherSettings.tsx @@ -205,6 +205,14 @@ export const OtherSettings: React.FC = () => { } /> + + + updateSettings({ showLargeHomeCarousel: value }) + } + /> + router.push("/settings/hide-libraries/page")} title={t("home.settings.other.hide_libraries")} diff --git a/translations/en.json b/translations/en.json index 14b6f6cd..148c08c9 100644 --- a/translations/en.json +++ b/translations/en.json @@ -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", diff --git a/utils/atoms/settings.ts b/utils/atoms/settings.ts index 03c4e3a8..376b360f 100644 --- a/utils/atoms/settings.ts +++ b/utils/atoms/settings.ts @@ -188,6 +188,7 @@ export type Settings = { enableLeftSideBrightnessSwipe: boolean; enableRightSideVolumeSwipe: boolean; usePopularPlugin: boolean; + showLargeHomeCarousel: boolean; }; export interface Lockable { @@ -256,6 +257,7 @@ export const defaultValues: Settings = { enableLeftSideBrightnessSwipe: true, enableRightSideVolumeSwipe: true, usePopularPlugin: true, + showLargeHomeCarousel: true, }; const loadSettings = (): Partial => {