From b03f5da5bc991c78d74e5ac14489910d78895970 Mon Sep 17 00:00:00 2001 From: leon-morival Date: Thu, 2 Oct 2025 15:01:07 +0200 Subject: [PATCH] slider web --- src/components/Slider.web.js | 27 ++-- src/screens/Library/MusicDetails.web.js | 174 ++++++++++++++++++------ 2 files changed, 146 insertions(+), 55 deletions(-) diff --git a/src/components/Slider.web.js b/src/components/Slider.web.js index 4ea1850..42e1b46 100644 --- a/src/components/Slider.web.js +++ b/src/components/Slider.web.js @@ -6,7 +6,6 @@ import { LinearGradient } from "./LinearGradient/LinearGradient"; const INITIAL_BOX_SIZE = 6; const HANDLE_SIZE = 20; -const HANDLE_TOP_OFFSET = (INITIAL_BOX_SIZE - HANDLE_SIZE) / 2; const clamp01 = (value) => Math.min(1, Math.max(0, value)); @@ -21,7 +20,7 @@ const Slider = ({ }) => { const [layoutWidth, setLayoutWidth] = useState(0); const [ratio, setRatio] = useState( - typeof progress === "number" ? clamp01(progress) : 0, + typeof progress === "number" ? clamp01(progress) : 0 ); const draggingRef = useRef(false); @@ -43,7 +42,7 @@ const Slider = ({ const nextRatio = clamp01(clampedX / available); setRatio(nextRatio); }, - [layoutWidth], + [layoutWidth] ); const handleGrant = useCallback( @@ -55,7 +54,7 @@ const Slider = ({ onSeekStart(); } }, - [seekEnabled, updateRatioFromX, onSeekStart], + [seekEnabled, updateRatioFromX, onSeekStart] ); const handleMove = useCallback( @@ -63,7 +62,7 @@ const Slider = ({ if (!seekEnabled || !draggingRef.current) return; updateRatioFromX(event?.nativeEvent?.locationX || 0); }, - [seekEnabled, updateRatioFromX], + [seekEnabled, updateRatioFromX] ); const finishSeeking = useCallback(() => { @@ -78,12 +77,15 @@ const Slider = ({ } }, [seekEnabled, ratio, onSeek, onSeekEnd]); - const handleLayout = useCallback((event) => { - const width = event?.nativeEvent?.layout?.width || 0; - if (width !== layoutWidth) { - setLayoutWidth(width); - } - }, [layoutWidth]); + const handleLayout = useCallback( + (event) => { + const width = event?.nativeEvent?.layout?.width || 0; + if (width !== layoutWidth) { + setLayoutWidth(width); + } + }, + [layoutWidth] + ); const maxOffset = Math.max(layoutWidth - INITIAL_BOX_SIZE, 0); const offset = maxOffset * ratio; @@ -159,7 +161,7 @@ const styles = StyleSheet.create({ backgroundColor: "#f8f9ff", borderRadius: HANDLE_SIZE / 2, position: "absolute", - top: HANDLE_TOP_OFFSET, + // top: HANDLE_TOP_OFFSET, zIndex: 2, borderWidth: 4, borderColor: "#9B4DFF", @@ -170,7 +172,6 @@ const styles = StyleSheet.create({ }, shadowOpacity: 0.17, shadowRadius: 3.05, - elevation: 4, }, time: { fontSize: 14, diff --git a/src/screens/Library/MusicDetails.web.js b/src/screens/Library/MusicDetails.web.js index 703919b..035b46c 100644 --- a/src/screens/Library/MusicDetails.web.js +++ b/src/screens/Library/MusicDetails.web.js @@ -1,4 +1,3 @@ -import { useAudioPlayer } from "expo-audio"; import { BlurView } from "expo-blur"; import { Image as ExpoImage } from "expo-image"; import React, { useEffect, useMemo, useRef, useState } from "react"; @@ -31,6 +30,111 @@ import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import Style, { gutters, size } from "../../styles/Style"; +const useHtmlAudioPlayer = (source) => { + const src = typeof source === "string" ? source : source?.uri || null; + const audioRef = useRef(null); + const [state, setState] = useState({ + durationMs: 0, + positionMs: 0, + isPlaying: false, + }); + + useEffect(() => { + setState({ durationMs: 0, positionMs: 0, isPlaying: false }); + + if (!src) { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ""; + audioRef.current.load(); + audioRef.current = null; + } + return undefined; + } + + const audio = new Audio(src); + audio.preload = "auto"; + audio.crossOrigin = "anonymous"; + audioRef.current = audio; + + const syncDuration = () => { + const duration = Number.isFinite(audio.duration) ? audio.duration * 1000 : 0; + setState((prev) => ({ ...prev, durationMs: duration })); + }; + + const handleLoadedMetadata = () => syncDuration(); + const handleDurationChange = () => syncDuration(); + const handleTimeUpdate = () => { + setState((prev) => ({ + ...prev, + positionMs: audio.currentTime * 1000, + })); + }; + const handlePlay = () => setState((prev) => ({ ...prev, isPlaying: true })); + const handlePause = () => setState((prev) => ({ ...prev, isPlaying: false })); + const handleEnded = () => + setState((prev) => ({ + ...prev, + positionMs: Number.isFinite(audio.duration) + ? audio.duration * 1000 + : prev.positionMs, + isPlaying: false, + })); + + audio.addEventListener("loadedmetadata", handleLoadedMetadata); + audio.addEventListener("durationchange", handleDurationChange); + audio.addEventListener("timeupdate", handleTimeUpdate); + audio.addEventListener("play", handlePlay); + audio.addEventListener("pause", handlePause); + audio.addEventListener("ended", handleEnded); + + return () => { + audio.pause(); + audio.removeEventListener("loadedmetadata", handleLoadedMetadata); + audio.removeEventListener("durationchange", handleDurationChange); + audio.removeEventListener("timeupdate", handleTimeUpdate); + audio.removeEventListener("play", handlePlay); + audio.removeEventListener("pause", handlePause); + audio.removeEventListener("ended", handleEnded); + audio.src = ""; + audio.load(); + audioRef.current = null; + }; + }, [src]); + + const play = React.useCallback(async () => { + if (!audioRef.current) return; + try { + await audioRef.current.play(); + } catch (e) { + throw e; + } + }, []); + + const pause = React.useCallback(() => { + if (!audioRef.current) return; + audioRef.current.pause(); + }, []); + + const seekTo = React.useCallback((seconds) => { + if (!audioRef.current) return; + const next = Math.max(0, Number(seconds) || 0); + const duration = audioRef.current.duration; + const bounded = Number.isFinite(duration) ? Math.min(next, duration) : next; + audioRef.current.currentTime = bounded; + setState((prev) => ({ ...prev, positionMs: bounded * 1000 })); + }, []); + + return { + play, + pause, + seekTo, + playing: state.isPlaying, + positionMs: state.positionMs, + durationMs: state.durationMs, + }; +}; + // 20 secondes const timeBeforeIncrement = 20000; @@ -40,8 +144,6 @@ const MusicDetails = ({ route }) => { const projectId = params?.projectId || null; const [fav, setFav] = useState(false); const [currentUID] = useGlobal("currentUID"); - const [isPlaying, setIsPlaying] = useState(false); - const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 }); const wasPlayingBeforeSeek = React.useRef(false); const listenedMsRef = React.useRef(0); const incrementDoneRef = React.useRef(false); @@ -75,7 +177,14 @@ const MusicDetails = ({ route }) => { const coverUrl = project?.coverUrl || null; const songUrl = project?.songUrl || null; - const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined); + const { + play: playAudio, + pause: pauseAudio, + seekTo: seekAudio, + playing: isPlaying, + positionMs, + durationMs, + } = useHtmlAudioPlayer(songUrl ? { uri: songUrl } : undefined); // Karaoke aligned words (from timestamps) const alignedWords = useMemo(() => { @@ -99,17 +208,6 @@ const MusicDetails = ({ route }) => { incrementDoneRef.current = false; }, [songUrl]); - // Poll player state to update progress and play state - useEffect(() => { - const id = setInterval(() => { - const dur = (player?.duration || 0) * 1000; - const pos = (player?.currentTime || 0) * 1000; - setProgressInfo({ pos, dur }); - setIsPlaying(!!player?.playing); - }, 300); - return () => clearInterval(id); - }, [player]); - // Start/stop a timer to accumulate listened milliseconds while playing useEffect(() => { const clearTimer = () => { @@ -154,14 +252,12 @@ const MusicDetails = ({ route }) => { }; const togglePlay = async () => { - if (!player || !songUrl) return; + if (!songUrl) return; try { - if (player.playing) { - await player.pause?.(); - setIsPlaying(false); + if (isPlaying) { + await pauseAudio(); } else { - await player.play?.(); - setIsPlaying(true); + await playAudio(); } } catch (e) { console.log("MusicDetails audio error", e?.message); @@ -170,10 +266,10 @@ const MusicDetails = ({ route }) => { const onSeek = async (ratio) => { try { - const dur = progressInfo.dur || 0; + const dur = durationMs || 0; const pos = Math.floor(dur * ratio); - if (player && dur > 0) { - await player.seekTo?.(Math.floor((pos || 0) / 1000)); + if (dur > 0) { + await seekAudio(Math.floor((pos || 0) / 1000)); } } catch (e) { console.log("MusicDetails seek error", e?.message); @@ -182,9 +278,9 @@ const MusicDetails = ({ route }) => { const seekBy = async (deltaSeconds) => { try { - const cur = Math.floor((progressInfo.pos || 0) / 1000); + const cur = Math.floor((positionMs || 0) / 1000); const next = Math.max(0, cur + deltaSeconds); - if (player) await player.seekTo?.(next); + await seekAudio(next); } catch (e) { console.log("MusicDetails seekBy error", e?.message); } @@ -214,8 +310,8 @@ const MusicDetails = ({ route }) => { // Current time in seconds for highlighting const currentTimeS = useMemo( - () => Math.max(0, (progressInfo.pos || 0) / 1000), - [progressInfo.pos] + () => Math.max(0, (positionMs || 0) / 1000), + [positionMs] ); // Preview lead: show words 0.5s earlier const visibleTimeS = useMemo( @@ -422,20 +518,15 @@ const MusicDetails = ({ route }) => { {songUrl && ( { try { - wasPlayingBeforeSeek.current = !!player?.playing; - if (player?.playing) { - await player.pause?.(); - setIsPlaying(false); + wasPlayingBeforeSeek.current = !!isPlaying; + if (isPlaying) { + await pauseAudio(); } } catch (e) { console.log("Pause on seek start error", e?.message); @@ -444,9 +535,8 @@ const MusicDetails = ({ route }) => { onSeek={onSeek} onSeekEnd={async () => { try { - if (player && wasPlayingBeforeSeek.current) { - await player.play?.(); - setIsPlaying(true); + if (wasPlayingBeforeSeek.current) { + await playAudio(); } wasPlayingBeforeSeek.current = false; } catch (e) {