mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-18 17:18:11 +00:00
wip
This commit is contained in:
@@ -5,7 +5,6 @@ import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Platform, View } from "react-native";
|
||||
import Animated, {
|
||||
runOnJS,
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withTiming,
|
||||
@@ -28,7 +27,11 @@ const Page: React.FC = () => {
|
||||
|
||||
// Exclude MediaSources/MediaStreams from initial fetch for faster loading
|
||||
// (especially important for plugins like Gelato)
|
||||
const { data: item, isError } = useItemQuery(id, isOffline, undefined, [
|
||||
const {
|
||||
data: item,
|
||||
isError,
|
||||
isLoading,
|
||||
} = useItemQuery(id, isOffline, undefined, [
|
||||
ItemFields.MediaSources,
|
||||
ItemFields.MediaSourceCount,
|
||||
ItemFields.MediaStreams,
|
||||
@@ -44,33 +47,14 @@ const Page: React.FC = () => {
|
||||
};
|
||||
});
|
||||
|
||||
const fadeOut = (callback: any) => {
|
||||
setTimeout(() => {
|
||||
opacity.value = withTiming(0, { duration: 500 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(callback)();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const fadeIn = (callback: any) => {
|
||||
setTimeout(() => {
|
||||
opacity.value = withTiming(1, { duration: 500 }, (finished) => {
|
||||
if (finished) {
|
||||
runOnJS(callback)();
|
||||
}
|
||||
});
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// Fast fade out when item loads (no setTimeout delay)
|
||||
useEffect(() => {
|
||||
if (item) {
|
||||
fadeOut(() => {});
|
||||
opacity.value = withTiming(0, { duration: 150 });
|
||||
} else {
|
||||
fadeIn(() => {});
|
||||
opacity.value = withTiming(1, { duration: 150 });
|
||||
}
|
||||
}, [item]);
|
||||
}, [item, opacity]);
|
||||
|
||||
if (isError)
|
||||
return (
|
||||
@@ -82,37 +66,46 @@ const Page: React.FC = () => {
|
||||
return (
|
||||
<OfflineModeProvider isOffline={isOffline}>
|
||||
<View className='flex flex-1 relative'>
|
||||
<Animated.View
|
||||
pointerEvents={"none"}
|
||||
style={[animatedStyle]}
|
||||
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen z-50 bg-black'
|
||||
>
|
||||
{Platform.isTV && ItemContentSkeletonTV ? (
|
||||
<ItemContentSkeletonTV />
|
||||
) : (
|
||||
<View style={{ paddingHorizontal: 16, width: "100%" }}>
|
||||
<View
|
||||
style={{
|
||||
height: item?.Type === "Episode" ? 300 : 450,
|
||||
}}
|
||||
className='bg-transparent rounded-lg mb-4 w-full'
|
||||
/>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
||||
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
||||
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
||||
<View className='flex flex-row space-x-1 mb-8'>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
{/* Always render ItemContent - it handles loading state internally on TV */}
|
||||
<ItemContent
|
||||
item={item}
|
||||
itemWithSources={itemWithSources}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Skeleton overlay - fades out when content loads */}
|
||||
{!item && (
|
||||
<Animated.View
|
||||
pointerEvents={"none"}
|
||||
style={[animatedStyle]}
|
||||
className='absolute top-0 left-0 flex flex-col items-start h-screen w-screen z-50 bg-black'
|
||||
>
|
||||
{Platform.isTV && ItemContentSkeletonTV ? (
|
||||
<ItemContentSkeletonTV />
|
||||
) : (
|
||||
<View style={{ paddingHorizontal: 16, width: "100%" }}>
|
||||
<View
|
||||
style={{
|
||||
height: item?.Type === "Episode" ? 300 : 450,
|
||||
}}
|
||||
className='bg-transparent rounded-lg mb-4 w-full'
|
||||
/>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-4 w-14' />
|
||||
<View className='h-10 bg-neutral-900 rounded-lg mb-2 w-1/2' />
|
||||
<View className='h-3 bg-neutral-900 rounded mb-3 w-8' />
|
||||
<View className='flex flex-row space-x-1 mb-8'>
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
<View className='h-6 bg-neutral-900 rounded mb-3 w-14' />
|
||||
</View>
|
||||
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
||||
<View className='h-10 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>
|
||||
<View className='h-3 bg-neutral-900 rounded w-2/3 mb-1' />
|
||||
<View className='h-10 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>
|
||||
)}
|
||||
</Animated.View>
|
||||
{item && <ItemContent item={item} itemWithSources={itemWithSources} />}
|
||||
)}
|
||||
</Animated.View>
|
||||
)}
|
||||
</View>
|
||||
</OfflineModeProvider>
|
||||
);
|
||||
|
||||
@@ -47,8 +47,9 @@ export type SelectedOptions = {
|
||||
};
|
||||
|
||||
interface ItemContentProps {
|
||||
item: BaseItemDto;
|
||||
item?: BaseItemDto | null;
|
||||
itemWithSources?: BaseItemDto | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// Mobile-specific implementation
|
||||
|
||||
@@ -51,86 +51,90 @@ export type SelectedOptions = {
|
||||
};
|
||||
|
||||
interface ItemContentTVProps {
|
||||
item: BaseItemDto;
|
||||
item?: BaseItemDto | null;
|
||||
itemWithSources?: BaseItemDto | null;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
// Focusable button component for TV with Apple TV-style animations
|
||||
const TVFocusableButton: React.FC<{
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
style?: any;
|
||||
variant?: "primary" | "secondary";
|
||||
}> = ({
|
||||
onPress,
|
||||
children,
|
||||
hasTVPreferredFocus,
|
||||
style,
|
||||
variant = "primary",
|
||||
}) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
const TVFocusableButton = React.forwardRef<
|
||||
View,
|
||||
{
|
||||
onPress: () => void;
|
||||
children: React.ReactNode;
|
||||
hasTVPreferredFocus?: boolean;
|
||||
style?: any;
|
||||
variant?: "primary" | "secondary";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ onPress, children, hasTVPreferredFocus, style, variant = "primary" },
|
||||
ref,
|
||||
) => {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const scale = useRef(new Animated.Value(1)).current;
|
||||
|
||||
const animateTo = (v: number) =>
|
||||
Animated.timing(scale, {
|
||||
toValue: v,
|
||||
duration: 150,
|
||||
easing: Easing.out(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
const animateTo = (v: number) =>
|
||||
Animated.timing(scale, {
|
||||
toValue: v,
|
||||
duration: 150,
|
||||
easing: Easing.out(Easing.quad),
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
|
||||
const isPrimary = variant === "primary";
|
||||
const isPrimary = variant === "primary";
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={onPress}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
animateTo(1.05);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
animateTo(1);
|
||||
}}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
transform: [{ scale }],
|
||||
shadowColor: isPrimary ? "#fff" : "#a855f7",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: focused ? 0.6 : 0,
|
||||
shadowRadius: focused ? 20 : 0,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
onPress={onPress}
|
||||
onFocus={() => {
|
||||
setFocused(true);
|
||||
animateTo(1.05);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocused(false);
|
||||
animateTo(1);
|
||||
}}
|
||||
hasTVPreferredFocus={hasTVPreferredFocus}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: focused
|
||||
? isPrimary
|
||||
? "#ffffff"
|
||||
: "#7c3aed"
|
||||
: isPrimary
|
||||
? "rgba(255, 255, 255, 0.9)"
|
||||
: "rgba(124, 58, 237, 0.8)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 32,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minWidth: 180,
|
||||
}}
|
||||
<Animated.View
|
||||
style={[
|
||||
{
|
||||
transform: [{ scale }],
|
||||
shadowColor: isPrimary ? "#fff" : "#a855f7",
|
||||
shadowOffset: { width: 0, height: 0 },
|
||||
shadowOpacity: focused ? 0.6 : 0,
|
||||
shadowRadius: focused ? 20 : 0,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
<View
|
||||
style={{
|
||||
backgroundColor: focused
|
||||
? isPrimary
|
||||
? "#ffffff"
|
||||
: "#7c3aed"
|
||||
: isPrimary
|
||||
? "rgba(255, 255, 255, 0.9)"
|
||||
: "rgba(124, 58, 237, 0.8)",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 18,
|
||||
paddingHorizontal: 32,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Info row component for metadata display
|
||||
const _InfoRow: React.FC<{ label: string; value: string }> = ({
|
||||
@@ -629,6 +633,9 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
|
||||
SelectedOptions | undefined
|
||||
>(undefined);
|
||||
|
||||
// Ref for programmatic focus on play button
|
||||
const playButtonRef = useRef<View>(null);
|
||||
|
||||
const {
|
||||
defaultAudioIndex,
|
||||
defaultBitrate,
|
||||
@@ -656,6 +663,16 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
|
||||
defaultMediaSource,
|
||||
]);
|
||||
|
||||
// Programmatically focus play button after content renders
|
||||
useEffect(() => {
|
||||
if (selectedOptions && playButtonRef.current) {
|
||||
const timer = setTimeout(() => {
|
||||
(playButtonRef.current as any)?.requestTVFocus?.();
|
||||
}, 50);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [selectedOptions]);
|
||||
|
||||
const handlePlay = () => {
|
||||
if (!item || !selectedOptions) return;
|
||||
|
||||
@@ -1069,6 +1086,7 @@ export const ItemContentTV: React.FC<ItemContentTVProps> = React.memo(
|
||||
}}
|
||||
>
|
||||
<TVFocusableButton
|
||||
ref={playButtonRef}
|
||||
onPress={handlePlay}
|
||||
hasTVPreferredFocus
|
||||
variant='primary'
|
||||
|
||||
Reference in New Issue
Block a user