diff --git a/components/CurrentlyPlayingBar.tsx b/components/CurrentlyPlayingBar.tsx index 85c05e9d..ba4ecec4 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/CurrentlyPlayingBar.tsx @@ -1,27 +1,39 @@ -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { apiAtom } from "@/providers/JellyfinProvider"; import { usePlayback } from "@/providers/PlaybackProvider"; import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { writeToLog } from "@/utils/log"; +import { runtimeTicksToMinutes, runtimeTicksToSeconds } from "@/utils/time"; import { Ionicons } from "@expo/vector-icons"; import { BlurView } from "expo-blur"; import { useRouter, useSegments } from "expo-router"; import { useAtom } from "jotai"; import { useEffect, useMemo, useRef, useState } from "react"; -import { Alert, Platform, TouchableOpacity, View } from "react-native"; +import { + Alert, + Dimensions, + Pressable, + TouchableOpacity, + View, +} from "react-native"; +import { Slider } from "react-native-awesome-slider"; +import "react-native-gesture-handler"; import Animated, { + interpolate, + interpolateColor, useAnimatedStyle, useSharedValue, - withDecay, withTiming, } from "react-native-reanimated"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import Video from "react-native-video"; import { Text } from "./common/Text"; import { Loader } from "./Loader"; -import { Dimensions } from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Bubble, Slider } from "react-native-awesome-slider"; -import { runtimeTicksToMinutes } from "@/utils/time"; + +const PADDING = 8; +const BAR_HEIGHT = 70; +const CONTENT_HEIGHT = BAR_HEIGHT - PADDING * 2; +const COLORS = ["#262626", "#000000"]; export const CurrentlyPlayingBar: React.FC = () => { const segments = useSegments(); @@ -43,67 +55,147 @@ export const CurrentlyPlayingBar: React.FC = () => { const [api] = useAtom(apiAtom); const [size, setSize] = useState<"full" | "small">("small"); + const animationProgress = useSharedValue(0); + const controlsOpacity = useSharedValue(1); const screenHeight = Dimensions.get("window").height; const screenWiidth = Dimensions.get("window").width; + const BOTTOM_HEIGHT = useMemo(() => insets.bottom + 48, [insets.bottom]); const from = useMemo(() => segments[2], [segments]); - const backgroundValues = useSharedValue({ - bottom: 70, - height: 80, - padding: 0, - width: screenWiidth - 100, - left: 50, - }); - - const videoValues = useSharedValue({ - bottom: 90, - height: 70, - width: 125, - left: 16, - }); - - const buttonsValues = useSharedValue({ - bottom: 90, - opacity: 1, - right: 16, - }); - - const textValues = useSharedValue({ - height: 70, - bottom: 90, - left: 149, - width: 140, + const animatedBackgroundStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; + return { + bottom: interpolate(progress, [0, 1], [BOTTOM_HEIGHT, 0]), + width: interpolate( + progress, + [0, 1], + [screenWiidth - PADDING * 2 - insets.left - insets.right, screenWiidth] + ), + height: interpolate(progress, [0, 1], [BAR_HEIGHT, screenHeight]), + padding: interpolate(progress, [0, 1], [PADDING, 0]), + left: interpolate(progress, [0, 1], [insets.left + PADDING, 0]), + }; }); const animatedTextStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; return { - bottom: withTiming(textValues.value.bottom, { duration: 500 }), - left: withTiming(textValues.value.left, { duration: 500 }), - height: withTiming(textValues.value.height, { duration: 500 }), - width: withTiming(textValues.value.width, { duration: 500 }), + bottom: interpolate( + progress, + [0, 1], + [BOTTOM_HEIGHT, insets.bottom + PADDING * 5] + ), + left: interpolate( + progress, + [0, 1], + [ + (CONTENT_HEIGHT * 16) / 9 + 16 + 8 + insets.left, + PADDING * 4 + insets.left, + ] + ), + height: interpolate(progress, [0, 1], [BAR_HEIGHT, 64]), + width: interpolate(progress, [0, 1], [140, 140]), }; }); const animatedButtonStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; return { - bottom: withTiming(buttonsValues.value.bottom, { duration: 500 }), - opacity: withTiming(buttonsValues.value.opacity, { duration: 500 }), - right: withTiming(buttonsValues.value.right, { duration: 500 }), + bottom: interpolate( + progress, + [0, 1], + [BOTTOM_HEIGHT, screenHeight - insets.top - insets.bottom - PADDING * 5] + ), + right: interpolate( + progress, + [0, 1], + [16 + insets.right, 16 + insets.right + PADDING] + ), + height: interpolate(progress, [0, 1], [BAR_HEIGHT, BAR_HEIGHT]), }; }); - const animatedBackgroundStyle = useAnimatedStyle(() => { + const animatedVideoStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; return { - bottom: withTiming(backgroundValues.value.bottom, { duration: 500 }), - width: withTiming(backgroundValues.value.width, { duration: 500 }), - height: withTiming(backgroundValues.value.height, { duration: 500 }), - padding: withTiming(backgroundValues.value.padding, { duration: 500 }), - left: withTiming(backgroundValues.value.left, { duration: 500 }), + bottom: interpolate(progress, [0, 1], [BOTTOM_HEIGHT + PADDING, 0]), + height: interpolate(progress, [0, 1], [CONTENT_HEIGHT, screenHeight]), + width: interpolate( + progress, + [0, 1], + [(CONTENT_HEIGHT * 16) / 9, screenWiidth - insets.right - insets.left] + ), + left: interpolate( + progress, + [0, 1], + [PADDING * 2 + insets.left, insets.left] + ), + opacity: + size === "small" + ? 1 + : interpolate( + controlsOpacity.value, + [0, 1], + [1, 0.5] // 100% opacity when controls are hidden, 50% when visible + ), }; }); + const animatedColorStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; + return { + backgroundColor: interpolateColor( + progress, + [0, 1], + [COLORS[0], COLORS[1]] + ), + }; + }); + + const animatedSliderStyle = useAnimatedStyle(() => { + const progress = animationProgress.value; + return { + opacity: interpolate(progress, [0, 0.1], [0, 1]), + display: progress > 0 ? "flex" : "none", + }; + }); + + const showControls = () => { + controlsOpacity.value = withTiming(1, { duration: 300 }); + }; + + const hideControls = () => { + controlsOpacity.value = withTiming(0, { duration: 300 }); + }; + + const animatedControlsStyle = useAnimatedStyle(() => { + return { + opacity: controlsOpacity.value, + }; + }); + + // const PAN_GESTURE_EXTENT = screenHeight * 4; // Adjust this value as needed + // const panGesture = Gesture.Pan() + // .onStart(() => { + // animationProgress.value = size === "small" ? 0 : 1; + // }) + // .onUpdate((event) => { + // const delta = -event.translationY / PAN_GESTURE_EXTENT; + // const newProgress = animationProgress.value + delta; + // animationProgress.value = Math.max(0, Math.min(1, newProgress)); + // }) + // .onEnd(() => { + // if (animationProgress.value > 0.5) { + // animationProgress.value = withTiming(1, { duration: 300 }); + // size = "full"; + // } else { + // animationProgress.value = withTiming(0, { duration: 300 }); + // size = "small"; + // } + // }); + const poster = useMemo(() => { if (currentlyPlaying?.item.Type === "Audio") return `${api?.basePath}/Items/${currentlyPlaying.item.AlbumId}/Images/Primary?tag=${currentlyPlaying.item.AlbumPrimaryImageTag}&quality=90&maxHeight=200&maxWidth=200`; @@ -149,68 +241,90 @@ export const CurrentlyPlayingBar: React.FC = () => { }; }, [currentlyPlaying, startPosition, api, poster]); - const animatedVideoStyle = useAnimatedStyle(() => { - return { - height: withTiming(videoValues.value.height, { duration: 500 }), - width: withTiming(videoValues.value.width, { duration: 500 }), - bottom: withTiming(videoValues.value.bottom, { duration: 500 }), - left: withTiming(videoValues.value.left, { duration: 500 }), - }; - }); + // useEffect(() => { + // const BOTTOM_HEIGHT = insets.bottom + 48; - useEffect(() => { - if (size === "full") { - backgroundValues.value = { - bottom: 0, - height: screenHeight, - padding: 0, - width: screenWiidth, - left: 0, - }; - buttonsValues.value = { - bottom: screenHeight - insets.top - 38, - opacity: 1, - right: 16, - }; - videoValues.value = { - bottom: 0, - height: screenHeight, - width: screenWiidth, - left: 0, - }; - textValues.value = { - bottom: 78, - height: 64, - left: 16, - width: 140, - }; - } else { - backgroundValues.value = { - bottom: 70, - height: 80, - padding: 0, - width: screenWiidth - 16, - left: 8, - }; - buttonsValues.value = { - bottom: 90, - opacity: 1, - right: 16, - }; - videoValues.value = { - bottom: 78, - height: 64, - width: 113, - left: 16, - }; - textValues.value = { - bottom: 78, - height: 64, - left: 141, - width: 140, - }; - } - }, [size, screenHeight, insets]); + // backgroundValues.value = { + // bottom: interpolate(animationProgress.value, [0, 1], [BOTTOM_HEIGHT, 0]), + // height: interpolate( + // animationProgress.value, + // [0, 1], + // [BAR_HEIGHT, screenHeight] + // ), + // padding: interpolate(animationProgress.value, [0, 1], [PADDING, 0]), + // width: interpolate( + // animationProgress.value, + // [0, 1], + // [screenWiidth - PADDING * 2, screenWiidth] + // ), + // left: interpolate(animationProgress.value, [0, 1], [8, 0]), + // }; + + // buttonsValues.value = { + // bottom: interpolate( + // animationProgress.value, + // [0, 1], + // [BOTTOM_HEIGHT, screenHeight - insets.top - 48] + // ), + // right: interpolate( + // animationProgress.value, + // [0, 1], + // [16, 16 + insets.right] + // ), + // height: interpolate( + // animationProgress.value, + // [0, 1], + // [BAR_HEIGHT, BAR_HEIGHT] + // ), + // }; + + // videoValues.value = { + // bottom: interpolate( + // animationProgress.value, + // [0, 1], + // [BOTTOM_HEIGHT + PADDING, 0] + // ), + // height: interpolate( + // animationProgress.value, + // [0, 1], + // [CONTENT_HEIGHT, screenHeight] + // ), + // width: interpolate( + // animationProgress.value, + // [0, 1], + // [(CONTENT_HEIGHT * 16) / 9, screenWiidth - insets.right - insets.left] + // ), + // left: interpolate(animationProgress.value, [0, 1], [16, insets.left]), + // }; + + // textValues.value = { + // bottom: interpolate( + // animationProgress.value, + // [0, 1], + // [BOTTOM_HEIGHT, BOTTOM_HEIGHT] + // ), + // height: interpolate(animationProgress.value, [0, 1], [BAR_HEIGHT, 64]), + // left: interpolate( + // animationProgress.value, + // [0, 1], + // [(CONTENT_HEIGHT * 16) / 9 + 16 + 8, PADDING * 2 + insets.left] + // ), + // width: interpolate(animationProgress.value, [0, 1], [140, 140]), + // }; + + // colorProgress.value = withTiming(animationProgress.value, { + // duration: 500, + // }); + // }, [ + // animationProgress.value, + // screenHeight, + // screenWiidth, + // insets.bottom, + // insets.top, + // insets.right, + // insets.left, + // controlsVisible, + // ]); const progress = useSharedValue(0); const min = useSharedValue(0); @@ -221,229 +335,333 @@ export const CurrentlyPlayingBar: React.FC = () => { max.value = currentlyPlaying?.item.RunTimeTicks || 0; }, [currentlyPlaying?.item.RunTimeTicks]); + const hideControlsTimerRef = useRef(null); + + const showControlsAndResetTimer = () => { + showControls(); + if (size === "full") { + resetHideControlsTimer(); + } + }; + + const resetHideControlsTimer = () => { + if (hideControlsTimerRef.current) { + clearTimeout(hideControlsTimerRef.current); + } + hideControlsTimerRef.current = setTimeout(() => { + hideControls(); + }, 3000); + }; + + useEffect(() => { + if (size === "full" && controlsOpacity.value > 0) { + resetHideControlsTimer(); + } + + return () => { + if (hideControlsTimerRef.current) { + clearTimeout(hideControlsTimerRef.current); + } + }; + }, [controlsOpacity.value, size]); + if (!api || !currentlyPlaying) return null; return ( - <> - + + + - - - { - if (currentlyPlaying.item?.Type === "Audio") { - router.push( - // @ts-ignore - `/(auth)/(tabs)/${from}/albums/${currentlyPlaying.item.AlbumId}` - ); - } else { - router.push( - // @ts-ignore - `/(auth)/(tabs)/${from}/items/page?id=${currentlyPlaying.item?.Id}` - ); - } - }} - > - {currentlyPlaying.item?.Name} - - {currentlyPlaying.item?.Type === "Episode" && ( + + { - router.push( - // @ts-ignore - `/(auth)/(tabs)/${from}/series/${currentlyPlaying.item.SeriesId}` - ); + if (currentlyPlaying.item?.Type === "Audio") { + router.push( + // @ts-ignore + `/(auth)/(tabs)/${from}/albums/${currentlyPlaying.item.AlbumId}` + ); + } else { + router.push( + // @ts-ignore + `/(auth)/(tabs)/${from}/items/page?id=${currentlyPlaying.item?.Id}` + ); + } }} - className="text-xs opacity-50" > - {currentlyPlaying.item.SeriesName} + {currentlyPlaying.item?.Name} - )} - {currentlyPlaying.item?.Type === "Movie" && ( - + {currentlyPlaying.item?.Type === "Episode" && ( + { + router.push( + // @ts-ignore + `/(auth)/(tabs)/${from}/series/${currentlyPlaying.item.SeriesId}` + ); + }} + > + + {currentlyPlaying.item.SeriesName} + + + )} + {currentlyPlaying.item?.Type === "Movie" && ( {currentlyPlaying.item?.ProductionYear} - - )} - {currentlyPlaying.item?.Type === "Audio" && ( + )} + {currentlyPlaying.item?.Type === "Audio" && ( + { + router.push(`/albums/${currentlyPlaying.item?.AlbumId}`); + }} + > + + {currentlyPlaying.item?.Album} + + + )} + + + + + { - router.push(`/albums/${currentlyPlaying.item?.AlbumId}`); + if (size === "small") { + animationProgress.value = withTiming(1, { duration: 300 }); + setSize("full"); + hideControls(); + } else { + animationProgress.value = withTiming(0, { duration: 300 }); + setSize("small"); + showControls(); + } }} + className="aspect-square rounded flex flex-col items-center justify-center p-2" > - - {currentlyPlaying.item?.Album} - + - )} - - + { + if (isPlaying) { + pauseVideo(); + } else { + playVideo(); + } + }} + className="aspect-square rounded flex flex-col items-center justify-center p-2" + > + + + { + stopPlayback(); + }} + className="aspect-square rounded flex flex-col items-center justify-center p-2" + > + + + + - - { - if (size === "small") setSize("full"); - else setSize("small"); - }} - className="aspect-square rounded flex flex-col items-center justify-center p-2" + - - - { - stopPlayback(); - }} - className="aspect-square rounded flex flex-col items-center justify-center p-2" - > - - - - - - {videoSource && ( - - {size === "full" && ( - - { - sliding.current = true; - }} - onSlidingComplete={(val) => { - const tick = Math.floor(val); - videoRef.current?.seek(tick / 10000000); - sliding.current = false; - }} - onValueChange={(val) => { - const tick = Math.floor(val); - progress.value = tick; - }} - containerStyle={{ - borderRadius: 100, - }} - bubble={(s) => runtimeTicksToMinutes(s)} - sliderHeight={12} - thumbWidth={0} - progress={progress} - minimumValue={min} - maximumValue={max} - /> - - {runtimeTicksToMinutes(progress.value)} - - - )} + className="w-full h-full" + > + {videoSource && ( + + ); };