move scrubber to own file
This commit is contained in:
@@ -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<number>
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
a.absolute,
|
||||||
|
{
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: insets.bottom + 48,
|
||||||
|
},
|
||||||
|
timeStyle,
|
||||||
|
]}
|
||||||
|
pointerEvents="none">
|
||||||
|
<Text style={[a.text_center, a.font_bold]}>
|
||||||
|
<Text style={[a.text_5xl, {fontVariant: ['tabular-nums']}]}>
|
||||||
|
{formatTime(currentSeekTime)}
|
||||||
|
</Text>
|
||||||
|
<Text style={[a.text_2xl, {opacity: 0.8}]}>{' / '}</Text>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
a.text_5xl,
|
||||||
|
{opacity: 0.8},
|
||||||
|
{fontVariant: ['tabular-nums']},
|
||||||
|
]}>
|
||||||
|
{formatTime(duration)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
</Animated.View>
|
||||||
|
|
||||||
|
<GestureDetector gesture={scrubPanGesture}>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.relative,
|
||||||
|
a.w_full,
|
||||||
|
a.justify_end,
|
||||||
|
{
|
||||||
|
paddingBottom: insets.bottom,
|
||||||
|
height:
|
||||||
|
// bottom padding
|
||||||
|
insets.bottom +
|
||||||
|
// actual height
|
||||||
|
tokens.space.xl,
|
||||||
|
},
|
||||||
|
a.z_10,
|
||||||
|
]}>
|
||||||
|
<Animated.View style={[{backgroundColor: 'white'}, barStyle]} />
|
||||||
|
</View>
|
||||||
|
</GestureDetector>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Magic number that matches the Scrubber height
|
||||||
|
*/
|
||||||
|
export function ScrubberPlaceholder() {
|
||||||
|
const {bottom} = useSafeAreaInsets()
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.w_full,
|
||||||
|
{
|
||||||
|
// same as Scrubber
|
||||||
|
height: bottom + tokens.space.xl,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(num: number, min: number, max: number) {
|
||||||
|
'worklet'
|
||||||
|
return Math.min(Math.max(num, min), max)
|
||||||
|
}
|
||||||
@@ -13,20 +13,15 @@ import {
|
|||||||
NativeGesture,
|
NativeGesture,
|
||||||
} from 'react-native-gesture-handler'
|
} from 'react-native-gesture-handler'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
runOnJS,
|
|
||||||
runOnUI,
|
|
||||||
SharedValue,
|
|
||||||
useAnimatedReaction,
|
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
withTiming,
|
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {
|
import {
|
||||||
SafeAreaView,
|
SafeAreaView,
|
||||||
useSafeAreaFrame,
|
useSafeAreaFrame,
|
||||||
useSafeAreaInsets,
|
useSafeAreaInsets,
|
||||||
} from 'react-native-safe-area-context'
|
} from 'react-native-safe-area-context'
|
||||||
import {useEvent, useEventListener} from 'expo'
|
import {useEvent} from 'expo'
|
||||||
import {Image, ImageStyle} from 'expo-image'
|
import {Image, ImageStyle} from 'expo-image'
|
||||||
import {LinearGradient} from 'expo-linear-gradient'
|
import {LinearGradient} from 'expo-linear-gradient'
|
||||||
import {createVideoPlayer, VideoPlayer, VideoView} from 'expo-video'
|
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 {useSetLightStatusBar} from '#/state/shell/light-status-bar'
|
||||||
import {List} from '#/view/com/util/List'
|
import {List} from '#/view/com/util/List'
|
||||||
import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
|
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 {UserAvatar} from '#/view/com/util/UserAvatar'
|
||||||
import {Header} from '#/screens/VideoFeed/components/Header'
|
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 {setNavigationBar} from '#/alf/util/navigationBar'
|
||||||
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
|
||||||
import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
|
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 {ListFooter} from '#/components/Lists'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {Scrubber, ScrubberPlaceholder} from './components/Scrubber'
|
||||||
|
|
||||||
function createThreeVideoPlayers(
|
function createThreeVideoPlayers(
|
||||||
sources?: [string, string, string],
|
sources?: [string, string, string],
|
||||||
@@ -574,7 +569,6 @@ function Overlay({
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
color="secondary_inverted"
|
color="secondary_inverted"
|
||||||
style={[a.mb_xs, a.bg_transparent]}
|
style={[a.mb_xs, a.bg_transparent]}
|
||||||
hoverStyle={[]}
|
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
profile.viewer?.following ? queueUnfollow() : queueFollow()
|
profile.viewer?.following ? queueUnfollow() : queueFollow()
|
||||||
}>
|
}>
|
||||||
@@ -646,193 +640,6 @@ function Overlay({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Magic number that matches the Scrubber height
|
|
||||||
*/
|
|
||||||
function ScrubberPlaceholder() {
|
|
||||||
const {bottom} = useSafeAreaInsets()
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.w_full,
|
|
||||||
{
|
|
||||||
// same as Scrubber
|
|
||||||
height: bottom + tokens.space.xl,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Scrubber({
|
|
||||||
player,
|
|
||||||
seekingAnimationSV,
|
|
||||||
scrollGesture,
|
|
||||||
}: {
|
|
||||||
player: VideoPlayer
|
|
||||||
seekingAnimationSV: SharedValue<number>
|
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Animated.View
|
|
||||||
style={[
|
|
||||||
a.absolute,
|
|
||||||
{
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: insets.bottom + 48,
|
|
||||||
},
|
|
||||||
timeStyle,
|
|
||||||
]}
|
|
||||||
pointerEvents="none">
|
|
||||||
<Text style={[a.text_center, a.font_bold]}>
|
|
||||||
<Text style={[a.text_5xl, {fontVariant: ['tabular-nums']}]}>
|
|
||||||
{formatTime(currentSeekTime)}
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.text_2xl, {opacity: 0.8}]}>{' / '}</Text>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
a.text_5xl,
|
|
||||||
{opacity: 0.8},
|
|
||||||
{fontVariant: ['tabular-nums']},
|
|
||||||
]}>
|
|
||||||
{formatTime(duration)}
|
|
||||||
</Text>
|
|
||||||
</Text>
|
|
||||||
</Animated.View>
|
|
||||||
|
|
||||||
<GestureDetector gesture={scrubPanGesture}>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.relative,
|
|
||||||
a.w_full,
|
|
||||||
a.justify_end,
|
|
||||||
{
|
|
||||||
paddingBottom: insets.bottom,
|
|
||||||
height:
|
|
||||||
// bottom padding
|
|
||||||
insets.bottom +
|
|
||||||
// actual height
|
|
||||||
tokens.space.xl,
|
|
||||||
},
|
|
||||||
a.z_10,
|
|
||||||
]}>
|
|
||||||
<Animated.View style={[{backgroundColor: 'white'}, barStyle]} />
|
|
||||||
</View>
|
|
||||||
</GestureDetector>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ExpandableRichTextView({
|
function ExpandableRichTextView({
|
||||||
value,
|
value,
|
||||||
authorHandle,
|
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
|
* If the video is taller than 9:16
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user