Files
musicland/src/components/Slider.js
T
2025-10-01 16:49:12 +02:00

149 lines
4.0 KiB
JavaScript

import { useEffect, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
runOnJS,
useAnimatedStyle,
useSharedValue,
} from "react-native-reanimated";
import { Palette, Style } from "../styles";
import { FONT_FAMILY } from "../styles/Fonts";
import { LinearGradient } from "./LinearGradient/LinearGradient";
const INITIAL_BOX_SIZE = 6;
export default ({
value,
maxValue,
progress,
onSeek,
onSeekStart,
onSeekEnd,
seekEnabled = false,
}) => {
const offset = useSharedValue(0);
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
const [layout, setLayout] = useState(null);
const SLIDER_WIDTH = layout?.width;
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
const pan = Gesture.Pan()
.enabled(seekEnabled)
.onBegin(() => {
if (seekEnabled && typeof onSeekStart === "function") {
// Notify JS thread that user started seeking (e.g., pause audio)
runOnJS(onSeekStart)();
}
})
.onChange((event) => {
offset.value =
Math.abs(offset.value) <= MAX_VALUE
? offset.value + event.changeX <= 0
? 0
: offset.value + event.changeX >= MAX_VALUE
? MAX_VALUE
: offset.value + event.changeX
: offset.value;
const newWidth = INITIAL_BOX_SIZE + offset.value;
boxWidth.value = newWidth;
})
.onEnd(() => {
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
const ratio =
MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
// Reanimated -> JS thread bridge
runOnJS(onSeek)(ratio);
})
.onFinalize(() => {
if (seekEnabled && typeof onSeekEnd === "function") {
runOnJS(onSeekEnd)();
}
});
// Reflect external progress into the slider UI
useEffect(() => {
if (typeof progress === "number" && layout?.width) {
const max = layout.width - INITIAL_BOX_SIZE;
const clamped = Math.max(0, Math.min(1, progress));
const newOffset = clamped * max;
offset.value = newOffset;
boxWidth.value = INITIAL_BOX_SIZE + newOffset;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [progress, layout?.width]);
const boxStyle = useAnimatedStyle(() => {
return {
width: INITIAL_BOX_SIZE + offset.value,
};
});
const sliderStyle = useAnimatedStyle(() => {
return {
transform: [{ translateX: offset.value }],
};
});
return (
<View onLayout={(e) => setLayout(e.nativeEvent.layout)}>
<View style={{ ...styles.sliderTrack, width: SLIDER_WIDTH }}>
<Animated.View style={[styles.box, boxStyle]}>
<LinearGradient
colors={["#F94697", "#7023F7"]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 0 }}
style={{ flex: 1, borderRadius: 20 }}
/>
</Animated.View>
<GestureDetector gesture={pan}>
<Animated.View style={[styles.sliderHandle, sliderStyle]} />
</GestureDetector>
</View>
<View style={{ ...Style.containerSpaceBetween, marginTop: 10 }}>
<Text style={styles.time}>{value}</Text>
<Text style={styles.time}>{maxValue}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
box: {
height: INITIAL_BOX_SIZE,
borderRadius: 20,
position: "absolute",
zIndex: 1,
},
sliderHandle: {
width: 20,
height: 20,
backgroundColor: "#f8f9ff",
borderRadius: 25,
position: "absolute",
zIndex: 2,
borderWidth: 4,
borderColor: "#9B4DFF",
shadowColor: "#8951FC",
shadowOffset: {
width: 0,
height: 3,
},
shadowOpacity: 0.17,
shadowRadius: 3.05,
elevation: 4,
},
sliderTrack: {
height: 6,
backgroundColor: "#0F0C19",
borderRadius: 25,
justifyContent: "center",
},
time: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
},
});