mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-07 04:53:01 +01:00
fix: animated progress
This commit is contained in:
@@ -1,91 +1,62 @@
|
|||||||
import { usePlayback } from "@/providers/PlaybackProvider";
|
import { usePlayback } from "@/providers/PlaybackProvider";
|
||||||
|
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
||||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useAtom } from "jotai";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
import { TouchableOpacity, View } from "react-native";
|
import { TouchableOpacity, View } from "react-native";
|
||||||
import CastContext, {
|
import CastContext, {
|
||||||
PlayServicesState,
|
PlayServicesState,
|
||||||
useRemoteMediaClient,
|
useRemoteMediaClient,
|
||||||
} from "react-native-google-cast";
|
} from "react-native-google-cast";
|
||||||
import { Button } from "./Button";
|
|
||||||
import { Text } from "./common/Text";
|
|
||||||
import { useAtom } from "jotai";
|
|
||||||
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
|
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useSharedValue,
|
Easing,
|
||||||
useAnimatedStyle,
|
interpolate,
|
||||||
withTiming,
|
|
||||||
interpolateColor,
|
interpolateColor,
|
||||||
runOnJS,
|
|
||||||
useAnimatedReaction,
|
useAnimatedReaction,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useDerivedValue,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
|
import { Button } from "./Button";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {
|
||||||
item?: BaseItemDto | null;
|
item?: BaseItemDto | null;
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ANIMATION_DURATION = 500;
|
||||||
|
const MIN_PLAYBACK_WIDTH = 15;
|
||||||
|
|
||||||
export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
||||||
const { showActionSheetWithOptions } = useActionSheet();
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
const client = useRemoteMediaClient();
|
|
||||||
const { setCurrentlyPlayingState } = usePlayback();
|
const { setCurrentlyPlayingState } = usePlayback();
|
||||||
|
|
||||||
const [color] = useAtom(itemThemeColorAtom);
|
const client = useRemoteMediaClient();
|
||||||
|
|
||||||
// Create a shared value for animation progress
|
const [colorAtom] = useAtom(itemThemeColorAtom);
|
||||||
const progress = useSharedValue(0);
|
|
||||||
|
|
||||||
// Create shared values for start and end colors
|
const memoizedItem = useMemo(() => item, [item?.Id]); // Memoize the item
|
||||||
const startColor = useSharedValue(color);
|
const memoizedColor = useMemo(() => colorAtom, [colorAtom]); // Memoize the color
|
||||||
const endColor = useSharedValue(color);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const startWidth = useSharedValue(0);
|
||||||
// When color changes, update end color and animate progress
|
const targetWidth = useSharedValue(0);
|
||||||
endColor.value = color;
|
const endColor = useSharedValue(memoizedColor);
|
||||||
progress.value = 0; // Reset progress
|
const startColor = useSharedValue(memoizedColor);
|
||||||
progress.value = withTiming(1, { duration: 300 }); // Animate to 1 over 500ms
|
const widthProgress = useSharedValue(0);
|
||||||
}, [color]);
|
const colorChangeProgress = useSharedValue(0);
|
||||||
|
|
||||||
// Animated style for primary color
|
|
||||||
const animatedPrimaryStyle = useAnimatedStyle(() => ({
|
|
||||||
backgroundColor: interpolateColor(
|
|
||||||
progress.value,
|
|
||||||
[0, 1],
|
|
||||||
[startColor.value.average, endColor.value.average]
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Animated style for text color
|
|
||||||
const animatedTextStyle = useAnimatedStyle(() => ({
|
|
||||||
color: interpolateColor(
|
|
||||||
progress.value,
|
|
||||||
[0, 1],
|
|
||||||
[startColor.value.text, endColor.value.text]
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Update start color after animation completes
|
|
||||||
useEffect(() => {
|
|
||||||
const timeout = setTimeout(() => {
|
|
||||||
startColor.value = color;
|
|
||||||
}, 500); // Should match the duration in withTiming
|
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
|
||||||
}, [color]);
|
|
||||||
|
|
||||||
const onPress = async () => {
|
const onPress = async () => {
|
||||||
if (!url || !item) return;
|
if (!url || !item) return;
|
||||||
|
|
||||||
if (!client) {
|
if (!client) {
|
||||||
setCurrentlyPlayingState({ item, url });
|
setCurrentlyPlayingState({ item, url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = ["Chromecast", "Device", "Cancel"];
|
const options = ["Chromecast", "Device", "Cancel"];
|
||||||
const cancelButtonIndex = 2;
|
const cancelButtonIndex = 2;
|
||||||
|
|
||||||
showActionSheetWithOptions(
|
showActionSheetWithOptions(
|
||||||
{
|
{
|
||||||
options,
|
options,
|
||||||
@@ -123,38 +94,123 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const playbackPercent = useMemo(() => {
|
const derivedTargetWidth = useDerivedValue(() => {
|
||||||
if (!item || !item.RunTimeTicks) return 0;
|
if (!memoizedItem || !memoizedItem.RunTimeTicks) return 0;
|
||||||
const userData = item.UserData;
|
const userData = memoizedItem.UserData;
|
||||||
if (!userData) return 0;
|
if (userData && userData.PlaybackPositionTicks) {
|
||||||
const PlaybackPositionTicks = userData.PlaybackPositionTicks;
|
return userData.PlaybackPositionTicks > 0
|
||||||
if (!PlaybackPositionTicks) return 0;
|
? Math.max(
|
||||||
return (PlaybackPositionTicks / item.RunTimeTicks) * 100;
|
(userData.PlaybackPositionTicks / memoizedItem.RunTimeTicks) * 100,
|
||||||
}, [item]);
|
MIN_PLAYBACK_WIDTH
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}, [memoizedItem]);
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => derivedTargetWidth.value,
|
||||||
|
(newWidth) => {
|
||||||
|
targetWidth.value = newWidth;
|
||||||
|
widthProgress.value = 0;
|
||||||
|
widthProgress.value = withTiming(1, {
|
||||||
|
duration: ANIMATION_DURATION,
|
||||||
|
easing: Easing.bezier(0.7, 0, 0.3, 1.0),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[item]
|
||||||
|
);
|
||||||
|
|
||||||
|
useAnimatedReaction(
|
||||||
|
() => memoizedColor,
|
||||||
|
(newColor) => {
|
||||||
|
endColor.value = newColor;
|
||||||
|
colorChangeProgress.value = 0;
|
||||||
|
colorChangeProgress.value = withTiming(1, {
|
||||||
|
duration: ANIMATION_DURATION,
|
||||||
|
easing: Easing.bezier(0.9, 0, 0.31, 0.99),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[memoizedColor]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout_2 = setTimeout(() => {
|
||||||
|
startColor.value = memoizedColor;
|
||||||
|
startWidth.value = targetWidth.value;
|
||||||
|
}, ANIMATION_DURATION);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout_2);
|
||||||
|
};
|
||||||
|
}, [memoizedColor, memoizedItem]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ANIMATED STYLES
|
||||||
|
*/
|
||||||
|
const animatedAverageStyle = useAnimatedStyle(() => ({
|
||||||
|
backgroundColor: interpolateColor(
|
||||||
|
colorChangeProgress.value,
|
||||||
|
[0, 1],
|
||||||
|
[startColor.value.average, endColor.value.average]
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const animatedPrimaryStyle = useAnimatedStyle(() => ({
|
||||||
|
backgroundColor: interpolateColor(
|
||||||
|
colorChangeProgress.value,
|
||||||
|
[0, 1],
|
||||||
|
[startColor.value.primary, endColor.value.primary]
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const animatedWidthStyle = useAnimatedStyle(() => ({
|
||||||
|
width: `${interpolate(
|
||||||
|
widthProgress.value,
|
||||||
|
[0, 1],
|
||||||
|
[startWidth.value, targetWidth.value]
|
||||||
|
)}%`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const animatedTextStyle = useAnimatedStyle(() => ({
|
||||||
|
color: interpolateColor(
|
||||||
|
colorChangeProgress.value,
|
||||||
|
[0, 1],
|
||||||
|
[startColor.value.text, endColor.value.text]
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
/**
|
||||||
|
* *********************
|
||||||
|
*/
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity onPress={onPress} className="relative" {...props}>
|
<TouchableOpacity
|
||||||
|
accessibilityLabel="Play button"
|
||||||
|
accessibilityHint="Tap to play the media"
|
||||||
|
onPress={onPress}
|
||||||
|
className="relative"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<View className="absolute w-full h-full top-0 left-0 rounded-xl z-10 overflow-hidden">
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
animatedPrimaryStyle,
|
||||||
|
animatedWidthStyle,
|
||||||
|
{
|
||||||
|
height: "100%",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[animatedAverageStyle]}
|
||||||
animatedPrimaryStyle,
|
className="absolute w-full h-full top-0 left-0 rounded-xl"
|
||||||
{
|
|
||||||
width:
|
|
||||||
playbackPercent === 0
|
|
||||||
? "100%"
|
|
||||||
: `${Math.max(playbackPercent, 15)}%`,
|
|
||||||
height: "100%",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
className="absolute w-full h-full top-0 left-0 rounded-xl z-10"
|
|
||||||
/>
|
|
||||||
<Animated.View
|
|
||||||
style={[animatedPrimaryStyle]}
|
|
||||||
className="absolute w-full h-full top-0 left-0 rounded-xl "
|
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: color.primary,
|
borderColor: colorAtom.primary,
|
||||||
borderStyle: "solid",
|
borderStyle: "solid",
|
||||||
}}
|
}}
|
||||||
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
className="flex flex-row items-center justify-center bg-transparent rounded-xl z-20 h-12 w-full "
|
||||||
|
|||||||
Reference in New Issue
Block a user