mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 15:48:05 +00:00
Fix carousel behavior and settings option
This commit is contained in:
@@ -39,7 +39,7 @@ interface AppleTVCarouselProps {
|
||||
const { width: screenWidth, height: screenHeight } = Dimensions.get("window");
|
||||
|
||||
// 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_BOTTOM = 150;
|
||||
const LOGO_HEIGHT = 80;
|
||||
@@ -381,7 +381,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
<View
|
||||
style={{
|
||||
width: screenWidth,
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: APPLE_TV_CAROUSEL_HEIGHT,
|
||||
backgroundColor: "#000",
|
||||
}}
|
||||
>
|
||||
@@ -549,7 +549,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
key={item.Id}
|
||||
style={{
|
||||
width: screenWidth,
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: APPLE_TV_CAROUSEL_HEIGHT,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
@@ -731,7 +731,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: CAROUSEL_HEIGHT,
|
||||
height: APPLE_TV_CAROUSEL_HEIGHT,
|
||||
backgroundColor: "#000",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -749,7 +749,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
backgroundColor: "#000",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
@@ -758,7 +758,7 @@ export const AppleTVCarousel: React.FC<AppleTVCarouselProps> = ({
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
height: CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
height: APPLE_TV_CAROUSEL_HEIGHT, // Fixed height instead of flex: 1
|
||||
flexDirection: "row",
|
||||
width: screenWidth * items.length,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import type { PropsWithChildren, ReactElement } from "react";
|
||||
import { type NativeScrollEvent, View, type ViewProps } from "react-native";
|
||||
import type {
|
||||
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, {
|
||||
interpolate,
|
||||
useAnimatedRef,
|
||||
@@ -14,6 +27,9 @@ interface Props extends ViewProps {
|
||||
episodePoster?: ReactElement;
|
||||
headerHeight?: number;
|
||||
onEndReached?: (() => void) | null | undefined;
|
||||
scrollViewProps?: Animated.AnimatedProps<ScrollViewProps>;
|
||||
contentContainerStyle?: StyleProp<ViewStyle>;
|
||||
scrollViewRef?: Ref<Animated.ScrollView>;
|
||||
}
|
||||
|
||||
export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
@@ -23,10 +39,33 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
headerHeight = 400,
|
||||
logo,
|
||||
onEndReached,
|
||||
contentContainerStyle,
|
||||
scrollViewProps,
|
||||
scrollViewRef,
|
||||
...props
|
||||
}: Props) => {
|
||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const scrollOffset = useScrollViewOffset(scrollRef);
|
||||
const animatedScrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
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(() => {
|
||||
return {
|
||||
@@ -62,12 +101,17 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
return (
|
||||
<View className='flex-1' {...props}>
|
||||
<Animated.ScrollView
|
||||
style={{
|
||||
position: "relative",
|
||||
}}
|
||||
ref={scrollRef}
|
||||
scrollEventThrottle={16}
|
||||
{...restScrollViewProps}
|
||||
style={[
|
||||
{
|
||||
position: "relative",
|
||||
},
|
||||
scrollStyle,
|
||||
]}
|
||||
ref={animatedScrollRef}
|
||||
scrollEventThrottle={externalScrollEventThrottle ?? 16}
|
||||
onScroll={(e) => {
|
||||
externalOnScroll?.(e);
|
||||
if (isCloseToBottom(e.nativeEvent)) onEndReached?.();
|
||||
}}
|
||||
>
|
||||
@@ -96,9 +140,12 @@ export const ParallaxScrollView: React.FC<PropsWithChildren<Props>> = ({
|
||||
</Animated.View>
|
||||
|
||||
<View
|
||||
style={{
|
||||
top: -50,
|
||||
}}
|
||||
style={[
|
||||
{
|
||||
top: -50,
|
||||
},
|
||||
contentContainerStyle,
|
||||
]}
|
||||
className='relative flex-1 bg-transparent pb-24'
|
||||
>
|
||||
<LinearGradient
|
||||
|
||||
@@ -16,14 +16,15 @@ import { type QueryFunction, useQuery } from "@tanstack/react-query";
|
||||
import { useNavigation, useRouter, useSegments } from "expo-router";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Platform,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ScrollView,
|
||||
} from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { Button } from "@/components/Button";
|
||||
@@ -38,7 +39,8 @@ import { useDownload } from "@/providers/DownloadProvider";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
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: "ScrollingCollectionList";
|
||||
@@ -66,12 +68,13 @@ export const HomeIndex = () => {
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { settings, refreshStreamyfinPluginSettings } = useSettings();
|
||||
const showCarousel = settings?.showHomeCarousel ?? true;
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
const scrollViewRef = useRef<Animated.ScrollView | null>(null);
|
||||
|
||||
const { getDownloadedItems, cleanCacheDirectory } = useDownload();
|
||||
const prevIsConnected = useRef<boolean | null>(false);
|
||||
@@ -128,7 +131,7 @@ export const HomeIndex = () => {
|
||||
const unsubscribe = eventBus.on("scrollToTop", () => {
|
||||
if ((segments as string[])[2] === "(home)")
|
||||
scrollViewRef.current?.scrollTo({
|
||||
y: Platform.isTV ? -152 : -100,
|
||||
y: 0,
|
||||
animated: true,
|
||||
});
|
||||
});
|
||||
@@ -455,25 +458,36 @@ export const HomeIndex = () => {
|
||||
</View>
|
||||
);
|
||||
|
||||
const headerHeight = showCarousel ? APPLE_TV_CAROUSEL_HEIGHT : 120;
|
||||
const refreshProgressOffset = showCarousel ? 200 : 80;
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
scrollToOverflowEnabled={true}
|
||||
ref={scrollViewRef}
|
||||
nestedScrollEnabled
|
||||
contentInsetAdjustmentBehavior='never'
|
||||
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
|
||||
/>
|
||||
<ParallaxScrollView
|
||||
scrollViewRef={scrollViewRef}
|
||||
headerHeight={headerHeight}
|
||||
headerImage={
|
||||
showCarousel ? (
|
||||
<AppleTVCarousel initialIndex={0} />
|
||||
) : (
|
||||
<View className='flex-1 bg-black' />
|
||||
)
|
||||
}
|
||||
style={{ marginTop: Platform.isTV ? 0 : -100 }}
|
||||
contentContainerStyle={{ paddingTop: Platform.isTV ? 0 : 100 }}
|
||||
contentContainerStyle={showCarousel ? undefined : { top: 0 }}
|
||||
scrollViewProps={{
|
||||
refreshControl: (
|
||||
<RefreshControl
|
||||
refreshing={loading}
|
||||
onRefresh={refetch}
|
||||
tintColor='white'
|
||||
colors={["white"]}
|
||||
progressViewOffset={refreshProgressOffset}
|
||||
/>
|
||||
),
|
||||
contentInsetAdjustmentBehavior: "never",
|
||||
nestedScrollEnabled: true,
|
||||
scrollToOverflowEnabled: true,
|
||||
}}
|
||||
>
|
||||
<AppleTVCarousel initialIndex={0} />
|
||||
<View
|
||||
style={{
|
||||
paddingLeft: insets.left,
|
||||
@@ -509,7 +523,7 @@ export const HomeIndex = () => {
|
||||
</View>
|
||||
</View>
|
||||
<View className='h-24' />
|
||||
</ScrollView>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ export const OtherSettings: React.FC = () => {
|
||||
pluginSettings?.followDeviceOrientation?.locked === true &&
|
||||
pluginSettings?.defaultVideoOrientation?.locked === true &&
|
||||
pluginSettings?.safeAreaInControlsEnabled?.locked === true &&
|
||||
pluginSettings?.showHomeCarousel?.locked === true &&
|
||||
pluginSettings?.showCustomMenuLinks?.locked === true &&
|
||||
pluginSettings?.hiddenLibraries?.locked === true &&
|
||||
pluginSettings?.disableHapticFeedback?.locked === true,
|
||||
@@ -158,6 +159,19 @@ export const OtherSettings: React.FC = () => {
|
||||
/>
|
||||
</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)&& (
|
||||
<ListItem
|
||||
title={t("home.settings.other.video_player")}
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
"UNKNOWN": "Unknown"
|
||||
},
|
||||
"safe_area_in_controls": "Safe Area in Controls",
|
||||
"show_home_carousel": "Show Home Carousel",
|
||||
"video_player": "Video Player",
|
||||
"video_players": {
|
||||
"VLC_3": "VLC 3",
|
||||
|
||||
@@ -160,6 +160,7 @@ export type Settings = {
|
||||
subtitleMode: SubtitlePlaybackMode;
|
||||
rememberSubtitleSelections: boolean;
|
||||
showHomeTitles: boolean;
|
||||
showHomeCarousel: boolean;
|
||||
defaultVideoOrientation: ScreenOrientation.OrientationLock;
|
||||
forwardSkipTime: number;
|
||||
rewindSkipTime: number;
|
||||
@@ -228,6 +229,7 @@ export const defaultValues: Settings = {
|
||||
subtitleMode: SubtitlePlaybackMode.Default,
|
||||
rememberSubtitleSelections: true,
|
||||
showHomeTitles: true,
|
||||
showHomeCarousel: true,
|
||||
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
|
||||
forwardSkipTime: 30,
|
||||
rewindSkipTime: 10,
|
||||
|
||||
Reference in New Issue
Block a user