Files
jellyfin-audio-player/src/screens/modals/Player/components/ProgressBar.tsx
2025-05-24 00:08:56 +02:00

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);