From 369af003fb2db23709000490df98a627f85d2cb6 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Fri, 17 Jan 2025 01:08:41 +0000 Subject: [PATCH] Squashed commit of the following: commit 3d1be4c0f19789dd3c5a3572ec1acd744a2edb80 Author: Samuel Newman Date: Fri Jan 17 01:08:05 2025 +0000 extend animation commit c9f199413b018efcbd9d8d2a58dd05eb41e7acb7 Author: Samuel Newman Date: Fri Jan 17 01:01:24 2025 +0000 fix gap commit 22e520795f50efda176f21a5e967cb27d0cdd907 Author: Samuel Newman Date: Fri Jan 17 00:50:16 2025 +0000 thinner bar, format time commit c32427f21405294ed3567545629a2964c4af59fe Author: Samuel Newman Date: Fri Jan 17 00:47:57 2025 +0000 fix 2 in 3 screens commit cbf84c08d64ca0a08ba9070ef5db918f89aa4296 Author: Samuel Newman Date: Fri Jan 17 00:45:46 2025 +0000 rm unneeded var commit 7e0e100177bb1cd0e64c0841bb7685c7f1eb857f Author: Samuel Newman Date: Fri Jan 17 00:41:18 2025 +0000 scrubberrrrr --- src/screens/Feeds/VibeScreen.tsx | 317 +++++++++++++++++++++++++------ 1 file changed, 263 insertions(+), 54 deletions(-) diff --git a/src/screens/Feeds/VibeScreen.tsx b/src/screens/Feeds/VibeScreen.tsx index 333d60596d..120f781b3c 100644 --- a/src/screens/Feeds/VibeScreen.tsx +++ b/src/screens/Feeds/VibeScreen.tsx @@ -8,9 +8,17 @@ import { ViewToken, } from 'react-native' import {Gesture, GestureDetector} from 'react-native-gesture-handler' -import {runOnJS} from 'react-native-reanimated' +import Animated, { + runOnJS, + runOnUI, + SharedValue, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' import {SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-context' -import {useEvent} from 'expo' +import {useEvent, useEventListener} from 'expo' import {Image} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' @@ -45,9 +53,10 @@ import {useSetMinimalShellMode} from '#/state/shell' import {useSetLightStatusBar} from '#/state/shell/light-status-bar' import {List} from '#/view/com/util/List' import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls' +import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/Header' -import {atoms as a, ThemeProvider, useTheme} from '#/alf' +import {atoms as a, ThemeProvider, tokens, useTheme} from '#/alf' import {Button, ButtonText} from '#/components/Button' import * as Layout from '#/components/Layout' import {ListFooter} from '#/components/Lists' @@ -60,10 +69,13 @@ function createThreeVideoPlayers( ): [VideoPlayer, VideoPlayer, VideoPlayer] { const p1 = createVideoPlayer(sources?.[0] ?? '') p1.loop = true + p1.timeUpdateEventInterval = 0.1 const p2 = createVideoPlayer(sources?.[1] ?? '') p2.loop = true + p2.timeUpdateEventInterval = 0.1 const p3 = createVideoPlayer(sources?.[2] ?? '') p3.loop = true + p3.timeUpdateEventInterval = 0.1 return [p1, p2, p3] } @@ -369,7 +381,9 @@ function VideoItem({ ) )} {postShadow !== POST_TOMBSTONE ? ( - player && + player && ( + + ) ) : ( + active: boolean }) { const insets = useSafeAreaInsets() const t = useTheme() @@ -461,6 +477,7 @@ function Overlay({ const {status} = useEvent(player, 'statusChange', {status: player.status}) const doubleTapRef = useRef | null>(null) const [queueLike] = usePostLikeMutationQueue(post, 'ImmersiveVideo') + const seekingAnimationSV = useSharedValue(0) const pushToProfile = useNonReactiveCallback(() => { navigation.navigate('Profile', {name: post.author.did}) @@ -508,59 +525,86 @@ function Overlay({ facets: record?.facets, }) + const animatedStyle = useAnimatedStyle(() => ({ + opacity: 1 - seekingAnimationSV.get(), + })) + return ( <> - - - - - - - - - {sanitizeDisplayName( - post.author.displayName || post.author.handle, - )} - - - {sanitizeHandle(post.author.handle, '@')} - + + + + + + + + + + {sanitizeDisplayName( + post.author.displayName || post.author.handle, + )} + + + {sanitizeHandle(post.author.handle, '@')} + + - - {record?.text?.trim() && ( - - )} - {record && ( - - navigation.navigate('PostThread', { - name: post.author.did, - rkey, - }) - } - big - /> - )} - - - + {record?.text?.trim() && ( + + )} + {record && ( + + navigation.navigate('PostThread', { + name: post.author.did, + rkey, + }) + } + big + /> + )} + + + + {player && active ? ( + + ) : ( + + )} + {isAndroid && status === 'loading' && ( ) } + +function Scrubber({ + player, + seekingAnimationSV, +}: { + player: VideoPlayer + seekingAnimationSV: SharedValue +}) { + const {width: screenWidth} = useWindowDimensions() + const t = useTheme() + const insets = useSafeAreaInsets() + const currentTimeSV = useSharedValue(0) + const durationSV = useSharedValue(0) + const [currentSeekTime, setCurrentSeekTime] = useState(0) + const [duration, setDuration] = useState(0) + + const updateTime = (currentTime: number, duration: number) => { + 'worklet' + currentTimeSV.set(currentTime) + durationSV.set(duration) + } + + useEventListener(player, 'timeUpdate', evt => { + runOnUI(updateTime)(evt.currentTime, player.duration) + }) + + const isSeekingSV = useSharedValue(false) + const seekProgressSV = useSharedValue(0) + + useAnimatedReaction( + () => Math.round(seekProgressSV.get()), + (progress, prevProgress) => { + if (progress !== prevProgress) { + runOnJS(setCurrentSeekTime)(progress) + } + }, + ) + + useAnimatedReaction( + () => Math.round(durationSV.get()), + (duration, prevDuration) => { + if (duration !== prevDuration) { + runOnJS(setDuration)(duration) + } + }, + ) + + const seekBy = useCallback( + (time: number) => { + player.seekBy(time) + }, + [player], + ) + + const gesture = useMemo(() => { + return Gesture.Pan() + .failOffsetY([-10, 10]) + .onStart(() => { + 'worklet' + seekProgressSV.set(currentTimeSV.get()) + isSeekingSV.set(true) + seekingAnimationSV.set(withTiming(1, {duration: 500})) + }) + .onUpdate(evt => { + 'worklet' + const progress = evt.x / screenWidth + seekProgressSV.set( + clamp(progress * durationSV.get(), 0, durationSV.get()), + ) + }) + .onEnd(evt => { + 'worklet' + isSeekingSV.get() + + const progress = evt.x / screenWidth + const newTime = clamp(progress * durationSV.get(), 0, durationSV.get()) + + // it's seek by, so offset by the current time + runOnJS(seekBy)(newTime - currentTimeSV.get()) + + isSeekingSV.set(false) + seekingAnimationSV.set(withTiming(0, {duration: 500})) + }) + }, [ + seekingAnimationSV, + seekBy, + screenWidth, + currentTimeSV, + durationSV, + isSeekingSV, + seekProgressSV, + ]) + + const timeStyle = useAnimatedStyle(() => { + return { + display: seekingAnimationSV.get() === 0 ? 'none' : 'flex', + opacity: seekingAnimationSV.get(), + } + }) + + const barStyle = useAnimatedStyle(() => { + const currentTime = isSeekingSV.get() + ? seekProgressSV.get() + : currentTimeSV.get() + const progress = currentTime === 0 ? 0 : currentTime / durationSV.get() + return { + height: seekingAnimationSV.get() * 3 + 1, + width: `${progress * 100}%`, + } + }) + + return ( + <> + + + + {formatTime(currentSeekTime)} + + + {' / '} + + + {formatTime(duration)} + + + + + + + + + + ) +} + +function clamp(num: number, min: number, max: number) { + 'worklet' + return Math.min(Math.max(num, min), max) +}