diff --git a/src/screens/VideoFeed/components/Scrubber.tsx b/src/screens/VideoFeed/components/Scrubber.tsx new file mode 100644 index 0000000000..8c45f465f4 --- /dev/null +++ b/src/screens/VideoFeed/components/Scrubber.tsx @@ -0,0 +1,216 @@ +import {useCallback, useMemo, useState} from 'react' +import {View} from 'react-native' +import { + Gesture, + GestureDetector, + NativeGesture, +} from 'react-native-gesture-handler' +import Animated, { + runOnJS, + runOnUI, + SharedValue, + useAnimatedReaction, + useAnimatedStyle, + useSharedValue, + withTiming, +} from 'react-native-reanimated' +import { + useSafeAreaFrame, + useSafeAreaInsets, +} from 'react-native-safe-area-context' +import {useEventListener} from 'expo' +import {VideoPlayer} from 'expo-video' + +import {formatTime} from '#/view/com/util/post-embeds/VideoEmbedInner/web-controls/utils' +import {tokens} from '#/alf' +import {atoms as a} from '#/alf' +import {Text} from '#/components/Typography' + +export function Scrubber({ + player, + seekingAnimationSV, + scrollGesture, +}: { + player: VideoPlayer + seekingAnimationSV: SharedValue + scrollGesture: NativeGesture +}) { + const {width: screenWidth} = useSafeAreaFrame() + 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 scrubPanGesture = useMemo(() => { + return Gesture.Pan() + .blocksExternalGesture(scrollGesture) + .activeOffsetX([-1, 1]) + .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})) + }) + }, [ + scrollGesture, + 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)} + + + + + + + + + + + ) +} + +/** + * Magic number that matches the Scrubber height + */ +export function ScrubberPlaceholder() { + const {bottom} = useSafeAreaInsets() + return ( + + ) +} + +function clamp(num: number, min: number, max: number) { + 'worklet' + return Math.min(Math.max(num, min), max) +} diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 5190f1e910..0d495ebd7c 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -13,20 +13,15 @@ import { NativeGesture, } from 'react-native-gesture-handler' import Animated, { - runOnJS, - runOnUI, - SharedValue, - useAnimatedReaction, useAnimatedStyle, useSharedValue, - withTiming, } from 'react-native-reanimated' import { SafeAreaView, useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context' -import {useEvent, useEventListener} from 'expo' +import {useEvent} from 'expo' import {Image, ImageStyle} from 'expo-image' import {LinearGradient} from 'expo-linear-gradient' import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video' @@ -73,10 +68,9 @@ 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 {UserAvatar} from '#/view/com/util/UserAvatar' import {Header} from '#/screens/VideoFeed/components/Header' -import {atoms as a, platform, ThemeProvider, tokens, useTheme} from '#/alf' +import {atoms as a, platform, ThemeProvider, useTheme} from '#/alf' import {setNavigationBar} from '#/alf/util/navigationBar' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' @@ -85,6 +79,7 @@ import {Link} from '#/components/Link' import {ListFooter} from '#/components/Lists' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {Scrubber, ScrubberPlaceholder} from './components/Scrubber' function createThreeVideoPlayers( sources?: [string, string, string], @@ -574,7 +569,6 @@ function Overlay({ variant="outline" color="secondary_inverted" style={[a.mb_xs, a.bg_transparent]} - hoverStyle={[]} onPress={() => profile.viewer?.following ? queueUnfollow() : queueFollow() }> @@ -646,193 +640,6 @@ function Overlay({ ) } -/** - * Magic number that matches the Scrubber height - */ -function ScrubberPlaceholder() { - const {bottom} = useSafeAreaInsets() - return ( - - ) -} - -function Scrubber({ - player, - seekingAnimationSV, - scrollGesture, -}: { - player: VideoPlayer - seekingAnimationSV: SharedValue - scrollGesture: NativeGesture -}) { - const {width: screenWidth} = useSafeAreaFrame() - 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 scrubPanGesture = useMemo(() => { - return Gesture.Pan() - .blocksExternalGesture(scrollGesture) - .activeOffsetX([-1, 1]) - .failOffsetY([-10, 10]) - .onBegin(() => { - 'worklet' - }) - .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})) - }) - }, [ - scrollGesture, - 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 ExpandableRichTextView({ value, authorHandle, @@ -955,11 +762,6 @@ function PlayPauseTapArea({ ) } -function clamp(num: number, min: number, max: number) { - 'worklet' - return Math.min(Math.max(num, min), max) -} - /* * If the video is taller than 9:16 */