219 lines
7.2 KiB
TypeScript
219 lines
7.2 KiB
TypeScript
import React, { useEffect } from 'react';
|
|
import TrackPlayer, { useProgress } from 'react-native-track-player';
|
|
import styled from 'styled-components/native';
|
|
import ProgressTrack, {
|
|
calculateProgressTranslation,
|
|
getMinutes,
|
|
getSeconds,
|
|
ProgressTrackContainer
|
|
} from '@/components/Progresstrack';
|
|
import { Gesture, GestureDetector, gestureHandlerRootHOC } from 'react-native-gesture-handler';
|
|
import Reanimated, {
|
|
useSharedValue,
|
|
useAnimatedStyle,
|
|
withTiming,
|
|
Easing,
|
|
useDerivedValue,
|
|
runOnJS,
|
|
} from 'react-native-reanimated';
|
|
import ReText from '@/components/ReText';
|
|
import useCurrentTrack from '@/utility/useCurrentTrack';
|
|
import useDefaultStyles from '@/components/Colors';
|
|
|
|
const DRAG_HANDLE_SIZE = 20;
|
|
const PADDING_TOP = 12;
|
|
|
|
const Container = styled.View`
|
|
padding-top: ${PADDING_TOP}px;
|
|
margin-top: ${PADDING_TOP}px;
|
|
`;
|
|
|
|
const NumberBar = styled.View`
|
|
flex-direction: row;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
padding: 8px 0;
|
|
gap: 16px;
|
|
`;
|
|
|
|
const Number = styled(ReText)`
|
|
font-size: 13px;
|
|
flex: 1;
|
|
`;
|
|
|
|
const DragHandle = styled(Reanimated.View)`
|
|
width: ${DRAG_HANDLE_SIZE}px;
|
|
height: ${DRAG_HANDLE_SIZE}px;
|
|
border-radius: ${DRAG_HANDLE_SIZE}px;
|
|
position: absolute;
|
|
left: -${DRAG_HANDLE_SIZE / 2}px;
|
|
top: ${PADDING_TOP - DRAG_HANDLE_SIZE / 2 + 2.5}px;
|
|
z-index: 14;
|
|
`;
|
|
|
|
function ProgressBar() {
|
|
const styles = useDefaultStyles();
|
|
const { position, buffered } = useProgress(990);
|
|
const { track } = useCurrentTrack();
|
|
|
|
const width = useSharedValue(0);
|
|
const pos = useSharedValue(0);
|
|
const buf = useSharedValue(0);
|
|
const dur = useSharedValue(0);
|
|
|
|
const isDragging = useSharedValue(false);
|
|
const offset = useSharedValue(0);
|
|
|
|
const bufferAnimation = useDerivedValue(() => {
|
|
return calculateProgressTranslation(buf.value, dur.value, width.value);
|
|
}, [[dur, buf, width.value]]);
|
|
|
|
const progressAnimation = useDerivedValue(() => {
|
|
if (isDragging.value) {
|
|
return calculateProgressTranslation(offset.value, width.value, width.value);
|
|
} else {
|
|
return calculateProgressTranslation(pos.value, dur.value, width.value);
|
|
}
|
|
});
|
|
|
|
const timePassed = useDerivedValue(() => {
|
|
if (isDragging.value) {
|
|
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
|
|
return getMinutes(currentPosition) + ':' + getSeconds(currentPosition);
|
|
} else {
|
|
return getMinutes(pos.value) + ':' + getSeconds(pos.value);
|
|
}
|
|
}, [pos]);
|
|
|
|
const timeRemaining = useDerivedValue(() => {
|
|
if (isDragging.value) {
|
|
const currentPosition = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
|
|
const remaining = (currentPosition - dur.value) * -1;
|
|
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
|
|
} else {
|
|
const remaining = (pos.value - dur.value) * -1;
|
|
return `-${getMinutes(remaining)}:${getSeconds((remaining))}`;
|
|
}
|
|
}, [pos, dur]);
|
|
|
|
const pan = Gesture.Pan()
|
|
.minDistance(1)
|
|
.activeOffsetX(1)
|
|
.activeOffsetY(1)
|
|
.onBegin((e) => {
|
|
isDragging.value = true;
|
|
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
|
|
}).onUpdate((e) => {
|
|
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
|
|
}).onFinalize(() => {
|
|
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
|
|
isDragging.value = false;
|
|
runOnJS(TrackPlayer.seekTo)(pos.value);
|
|
});
|
|
const tap = Gesture.Tap()
|
|
.onBegin((e) => {
|
|
isDragging.value = true;
|
|
offset.value = Math.min(Math.max(DRAG_HANDLE_SIZE / 2, e.x), width.value - DRAG_HANDLE_SIZE / 2);
|
|
}).onFinalize(() => {
|
|
pos.value = (offset.value - DRAG_HANDLE_SIZE / 2) / (width.value - DRAG_HANDLE_SIZE) * dur.value;
|
|
isDragging.value = false;
|
|
runOnJS(TrackPlayer.seekTo)(pos.value);
|
|
});
|
|
const gesture = Gesture.Exclusive(pan, tap);
|
|
|
|
useEffect(() => {
|
|
pos.value = position;
|
|
buf.value = buffered;
|
|
dur.value = (track?.duration || 0) / 10_000_000;
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [position, buffered, track?.duration]);
|
|
|
|
const dragHandleStyles = useAnimatedStyle(() => {
|
|
return {
|
|
transform: [
|
|
{ translateX: offset.value },
|
|
{
|
|
scale: withTiming(isDragging.value ? 1 : 0, {
|
|
duration: 100,
|
|
easing: Easing.out(Easing.ease),
|
|
})
|
|
}
|
|
],
|
|
};
|
|
});
|
|
|
|
const bufferStyles = useAnimatedStyle(() => ({
|
|
transform: [
|
|
{ translateX: bufferAnimation.value }
|
|
]
|
|
}));
|
|
|
|
const progressStyles = useAnimatedStyle(() => {
|
|
return {
|
|
transform: [
|
|
{ translateX: progressAnimation.value }
|
|
]
|
|
};
|
|
});
|
|
|
|
const timePassedStyles = useAnimatedStyle(() => {
|
|
return {
|
|
transform: [
|
|
{ translateY: withTiming(isDragging.value && offset.value < 48 ? 12 : 0, {
|
|
duration: 145,
|
|
easing: Easing.ease
|
|
}) },
|
|
],
|
|
};
|
|
});
|
|
|
|
const timeRemainingStyles = useAnimatedStyle(() => {
|
|
return {
|
|
transform: [
|
|
{ translateY: withTiming(isDragging.value && offset.value > width.value - 48 ? 12 : 0, {
|
|
duration: 150,
|
|
easing: Easing.ease
|
|
}) },
|
|
],
|
|
};
|
|
});
|
|
|
|
return (
|
|
<GestureDetector gesture={gesture}>
|
|
<Container onLayout={(e) => { width.value = e.nativeEvent.layout.width; }}>
|
|
<ProgressTrackContainer>
|
|
<ProgressTrack
|
|
opacity={0.15}
|
|
style={styles.themeBackground}
|
|
/>
|
|
<ProgressTrack
|
|
style={[
|
|
styles.themeBackground,
|
|
bufferStyles,
|
|
]}
|
|
opacity={0.15}
|
|
/>
|
|
<ProgressTrack
|
|
style={[
|
|
progressStyles,
|
|
styles.themeBackground,
|
|
]}
|
|
/>
|
|
</ProgressTrackContainer>
|
|
<DragHandle
|
|
style={[
|
|
styles.themeBackground,
|
|
dragHandleStyles,
|
|
]}
|
|
/>
|
|
<NumberBar style={{ flex: 1 }}>
|
|
<Number text={timePassed} style={[timePassedStyles]} />
|
|
<Number text={timeRemaining} style={[timeRemainingStyles, { textAlign: 'right' }]} />
|
|
</NumberBar>
|
|
</Container>
|
|
</GestureDetector>
|
|
);
|
|
}
|
|
|
|
export default gestureHandlerRootHOC(ProgressBar);
|