From 006b65eed41059654b458fdba39f1ea3b5fd920e Mon Sep 17 00:00:00 2001 From: Victor Date: Fri, 27 Feb 2026 15:50:57 +0100 Subject: [PATCH] feat: fix player --- src/components/Slider.js | 32 +- src/components/player/ProgressSlider.js | 35 ++- src/providers/PlayerProvider.js | 379 +++++++++++++++++++----- src/providers/UserDataProvider.js | 16 +- src/screens/Library/MusicDetails.js | 89 +----- 5 files changed, 385 insertions(+), 166 deletions(-) diff --git a/src/components/Slider.js b/src/components/Slider.js index eb75769..8cf48ec 100644 --- a/src/components/Slider.js +++ b/src/components/Slider.js @@ -19,11 +19,10 @@ export default ({ }) => { const offset = useSharedValue(0) const boxWidth = useSharedValue(INITIAL_BOX_SIZE) - const [layout, setLayout] = useState(null) + const [sliderWidth, setSliderWidth] = useState(0) const seekingRef = useRef(false) - const SLIDER_WIDTH = layout?.width - const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE + const MAX_VALUE = Math.max(0, sliderWidth - INITIAL_BOX_SIZE) const handleSeekStart = () => { if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) { @@ -49,6 +48,11 @@ export default ({ }) .onChange((event) => { runOnJS(handleSeekStart)() + if (!(MAX_VALUE > 0)) { + offset.value = 0 + boxWidth.value = INITIAL_BOX_SIZE + return + } offset.value = Math.abs(offset.value) <= MAX_VALUE ? offset.value + event.changeX <= 0 @@ -62,7 +66,7 @@ export default ({ boxWidth.value = newWidth }) .onEnd(() => { - if (seekEnabled && typeof onSeek === 'function' && MAX_VALUE) { + if (seekEnabled && typeof onSeek === 'function' && MAX_VALUE > 0) { const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0 // Reanimated -> JS thread bridge runOnJS(onSeek)(ratio) @@ -77,15 +81,20 @@ export default ({ useEffect(() => { if (seekingRef.current) return - if (typeof progress === 'number' && layout?.width) { - const max = layout.width - INITIAL_BOX_SIZE + if (typeof progress === 'number' && sliderWidth > 0) { + const max = sliderWidth - INITIAL_BOX_SIZE + if (!(max > 0)) { + offset.value = 0 + boxWidth.value = INITIAL_BOX_SIZE + return + } 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]) + }, [progress, sliderWidth]) const boxStyle = useAnimatedStyle(() => { return { @@ -100,8 +109,13 @@ export default ({ }) return ( - setLayout(e.nativeEvent.layout)}> - + { + const nextWidth = Math.max(0, Math.round(e.nativeEvent.layout?.width || 0)) + setSliderWidth((prev) => (prev === nextWidth ? prev : nextWidth)) + }} + > + { const wasPlayingRef = useRef(false) + const pendingSeekPromiseRef = useRef(null) const [isSeeking, setIsSeeking] = useState(false) const [previewRatio, setPreviewRatio] = useState(null) @@ -54,22 +55,36 @@ const ProgressSlider = ({ const bounded = clamp01(ratio) setPreviewRatio(bounded) const targetMs = safeDuration * bounded - onSeek(targetMs) + const maybePromise = onSeek(targetMs) + pendingSeekPromiseRef.current = + maybePromise && typeof maybePromise.then === 'function' + ? maybePromise + : Promise.resolve() }, [disabled, onSeek, safeDuration] ) const handleSeekEnd = useCallback(() => { if (disabled || !safeDuration) return - setIsSeeking(false) - setPreviewRatio(null) - if (typeof onSeekEnd === 'function') { - onSeekEnd() + const finalize = async () => { + try { + if (typeof onSeekEnd === 'function') { + await onSeekEnd() + } + if (pendingSeekPromiseRef.current) { + await pendingSeekPromiseRef.current + } + } finally { + setIsSeeking(false) + setPreviewRatio(null) + pendingSeekPromiseRef.current = null + if (wasPlayingRef.current && typeof onPlay === 'function') { + onPlay() + } + wasPlayingRef.current = false + } } - if (wasPlayingRef.current && typeof onPlay === 'function') { - onPlay() - } - wasPlayingRef.current = false + finalize() }, [disabled, onPlay, onSeekEnd, safeDuration]) return ( @@ -78,7 +93,7 @@ const ProgressSlider = ({ isSeeking && previewRatio != null ? safeDuration * previewRatio : safePosition )} maxValue={formatTime(safeDuration)} - progress={progress} + progress={isSeeking && previewRatio != null ? previewRatio : progress} seekEnabled={!disabled && safeDuration > 0} onSeekStart={handleSeekStart} onSeek={handleSeek} diff --git a/src/providers/PlayerProvider.js b/src/providers/PlayerProvider.js index 2cd0f52..873a385 100644 --- a/src/providers/PlayerProvider.js +++ b/src/providers/PlayerProvider.js @@ -91,11 +91,13 @@ export const PlayerContext = createContext(DEFAULT_CONTEXT) const convertToMs = (value) => { const numeric = Number(value ?? 0) if (!Number.isFinite(numeric) || numeric <= 0) return 0 - if (Platform.OS === 'web') return numeric - if (numeric > 100000) return numeric - return numeric * 1000 + if (Platform.OS === 'web') return Math.round(numeric) + if (numeric > 100000) return Math.round(numeric) + return Math.round(numeric * 1000) } +const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve, 0)) + const ensureSource = (track = {}, options = {}) => { const candidate = track?.source ?? options?.source ?? null if (typeof candidate === 'number') return candidate @@ -141,6 +143,23 @@ const getTrackId = (track = {}, options = {}) => { ) } +const areSourcesEquivalent = (a, b) => { + if (a === b) return true + if (!a || !b) return false + if (typeof a !== typeof b) return false + if (typeof a === 'string' || typeof a === 'number') { + return a === b + } + if (typeof a === 'object' && typeof b === 'object') { + const aUri = typeof a?.uri === 'string' ? a.uri : null + const bUri = typeof b?.uri === 'string' ? b.uri : null + const aAssetId = typeof a?.assetId === 'number' ? a.assetId : null + const bAssetId = typeof b?.assetId === 'number' ? b.assetId : null + return aUri === bUri && aAssetId === bAssetId + } + return false +} + const normalizeTrack = (trackInput = {}, options = {}) => { const track = typeof trackInput === 'string' ? { uri: trackInput } : trackInput ? { ...trackInput } : {} @@ -197,6 +216,7 @@ const normalizeTrack = (trackInput = {}, options = {}) => { const PlayerProvider = ({ children }) => { const [activeRouteName] = useGlobal('activeRouteName') const [currentTrack, setCurrentTrack] = useState(null) + const currentTrackId = currentTrack?.id ?? null const queueRef = useRef([]) const queueInfoRef = useRef({ id: null, type: null, name: null }) const queueIndexRef = useRef(-1) @@ -215,8 +235,23 @@ const PlayerProvider = ({ children }) => { const autoPlayRef = useRef(false) const pendingSeekValueRef = useRef(null) + const pendingSeekPosRef = useRef(null) + const isSeekingRef = useRef(false) + const isApplyingPendingSeekRef = useRef(false) const didJustFinishRef = useRef(false) const trackInfoRef = useRef({}) + const playRequestIdRef = useRef(0) + + const reportError = useCallback((err) => { + setError((prev) => { + const nextMessage = String(err?.message || err || '') + const prevMessage = String(prev?.message || prev || '') + if (prevMessage && prevMessage === nextMessage) { + return prev + } + return err + }) + }, []) const setQueueIndexValue = useCallback((index = -1) => { const list = queueRef.current @@ -338,6 +373,20 @@ const PlayerProvider = ({ children }) => { const player = useAudioPlayer(source || null, 200) const status = useAudioPlayerStatus(player) const shouldHideOnRoute = activeRouteName ? HIDDEN_ROUTE_NAMES.has(activeRouteName) : false + const isStatusLoaded = useMemo(() => { + try { + return !!status?.isLoaded + } catch (_error) { + return false + } + }, [status?.isLoaded]) + const statusDidJustFinish = useMemo(() => { + try { + return !!status?.didJustFinish + } catch (_error) { + return false + } + }, [status?.didJustFinish]) const toPlayerSeekValue = useCallback((milliseconds) => { const ms = Math.max(0, Number(milliseconds) || 0) @@ -357,16 +406,57 @@ const PlayerProvider = ({ children }) => { useEffect(() => { if (!status) return - const durationMs = convertToMs(status.duration) - const positionMs = convertToMs(status.currentTime) - const isLoaded = !!status.isLoaded - const isPlaying = !!status.playing - const isBuffering = !!status.isBuffering - const didJustFinish = !!status.didJustFinish + let durationMs = 0 + let positionMs = 0 + let isLoaded = false + let isPlaying = false + let isBuffering = false + let didJustFinish = false + + try { + durationMs = convertToMs(status.duration) + positionMs = convertToMs(status.currentTime) + isLoaded = !!status.isLoaded + isPlaying = !!status.playing + isBuffering = !!status.isBuffering + didJustFinish = !!status.didJustFinish + } catch (err) { + console.warn( + 'PlayerProvider: impossible de lire le statut du player', + err?.message || err + ) + return + } + + const previousTrackInfo = currentTrackId ? trackInfoRef.current[currentTrackId] : null + const nextDurationMs = durationMs > 0 ? durationMs : previousTrackInfo?.durationMs || 0 + + const activePendingSeekMs = + pendingSeekPosRef.current !== null + ? Platform.OS === 'web' + ? pendingSeekPosRef.current + : pendingSeekPosRef.current * 1000 + : null + const queuedPendingSeekMs = + pendingSeekValueRef.current !== null + ? Platform.OS === 'web' + ? pendingSeekValueRef.current + : pendingSeekValueRef.current * 1000 + : null + const rawEffectivePositionMs = currentTrackId + ? activePendingSeekMs ?? queuedPendingSeekMs ?? positionMs + : 0 + const positionQuantizationStep = + activePendingSeekMs !== null || queuedPendingSeekMs !== null ? 1 : 80 + const effectivePositionMs = Math.round(rawEffectivePositionMs / positionQuantizationStep) * + positionQuantizationStep + const effectiveDurationMs = currentTrackId ? nextDurationMs : 0 + const effectiveIsPlaying = currentTrackId ? isPlaying : false + const effectiveIsBuffering = currentTrackId ? isBuffering : false setPlayback((prev) => { let nextStatus = prev.status - if (!currentTrack) { + if (!currentTrackId) { nextStatus = 'idle' } else if (!isLoaded) { nextStatus = 'loading' @@ -380,49 +470,83 @@ const PlayerProvider = ({ children }) => { nextStatus = 'paused' } + if ( + prev.status === nextStatus && + prev.isPlaying === effectiveIsPlaying && + prev.isBuffering === effectiveIsBuffering && + prev.positionMs === effectivePositionMs && + prev.durationMs === effectiveDurationMs + ) { + return prev + } + return { status: nextStatus, - isPlaying, - isBuffering, - positionMs, - durationMs, + isPlaying: effectiveIsPlaying, + isBuffering: effectiveIsBuffering, + positionMs: effectivePositionMs, + durationMs: effectiveDurationMs, } }) - if (currentTrack?.id) { - trackInfoRef.current[currentTrack.id] = { - durationMs: - durationMs > 0 ? durationMs : trackInfoRef.current[currentTrack.id]?.durationMs || 0, - positionMs, - isLoaded, - updatedAt: Date.now(), + if (currentTrackId) { + if ( + !previousTrackInfo || + previousTrackInfo.durationMs !== nextDurationMs || + previousTrackInfo.positionMs !== positionMs || + previousTrackInfo.isLoaded !== isLoaded + ) { + trackInfoRef.current[currentTrackId] = { + durationMs: nextDurationMs, + positionMs, + isLoaded, + updatedAt: Date.now(), + } } } - }, [status, currentTrack]) + }, [ + currentTrackId, + status?.duration, + status?.currentTime, + status?.isLoaded, + status?.playing, + status?.isBuffering, + status?.didJustFinish, + ]) useEffect(() => { - if (!player || !currentTrack) return + if (!player || !currentTrackId || !isStatusLoaded) return const run = async () => { - try { - const pendingSeek = pendingSeekValueRef.current + if (isApplyingPendingSeekRef.current) return + + const pendingSeek = pendingSeekValueRef.current + if (typeof pendingSeek === 'number' && pendingSeek >= 0) { pendingSeekValueRef.current = null - - if (typeof pendingSeek === 'number' && pendingSeek >= 0) { + isApplyingPendingSeekRef.current = true + try { await player.seekTo?.(pendingSeek) + } catch (err) { + pendingSeekValueRef.current = pendingSeek + reportError(err) + } finally { + isApplyingPendingSeekRef.current = false } + } - if (autoPlayRef.current) { - autoPlayRef.current = false + if (autoPlayRef.current) { + autoPlayRef.current = false + try { await player.play?.() + } catch (err) { + autoPlayRef.current = true + reportError(err) } - } catch (err) { - setError(err) } } run() - }, [player, currentTrack?.id]) + }, [player, currentTrackId, isStatusLoaded]) useEffect(() => { if (!player) return @@ -432,8 +556,17 @@ const PlayerProvider = ({ children }) => { }, [player, isLooping]) const seekDebounceRef = useRef(null) - const pendingSeekPosRef = useRef(null) - const isSeekingRef = useRef(false) + + useEffect(() => { + return () => { + if (seekDebounceRef.current) { + clearTimeout(seekDebounceRef.current) + seekDebounceRef.current = null + } + pendingSeekPosRef.current = null + isSeekingRef.current = false + } + }, [player]) const waitForActiveSeek = useCallback(async () => { if (!isSeekingRef.current) return @@ -458,23 +591,17 @@ const PlayerProvider = ({ children }) => { // Store pending seek position pendingSeekPosRef.current = seekValue - // Update local state immediately for UI responsiveness - setPlayback((prev) => ({ - ...prev, - positionMs: bounded, - })) - return new Promise((resolve) => { seekDebounceRef.current = setTimeout(async () => { try { isSeekingRef.current = true - if (status?.isLoaded) { + if (isStatusLoaded) { await player.seekTo?.(seekValue) } else { pendingSeekValueRef.current = seekValue } } catch (err) { - setError(err) + reportError(err) } finally { isSeekingRef.current = false pendingSeekPosRef.current = null @@ -483,7 +610,7 @@ const PlayerProvider = ({ children }) => { }, 100) // 100ms debounce }) }, - [player, status?.isLoaded, toPlayerSeekValue] + [player, isStatusLoaded, toPlayerSeekValue] ) const seekBy = useCallback( @@ -512,13 +639,13 @@ const PlayerProvider = ({ children }) => { pendingSeekPosRef.current = null try { isSeekingRef.current = true - if (status?.isLoaded) { + if (isStatusLoaded) { await player.seekTo?.(seekValue) } else { pendingSeekValueRef.current = seekValue } } catch (err) { - setError(err) + reportError(err) } finally { isSeekingRef.current = false } @@ -526,17 +653,43 @@ const PlayerProvider = ({ children }) => { // 2. Wait for any active seek to complete await waitForActiveSeek() - }, [player, status?.isLoaded, waitForActiveSeek]) + }, [player, isStatusLoaded, waitForActiveSeek]) + + const stopCurrentTrackBeforeSwitch = useCallback(async () => { + autoPlayRef.current = false + pendingSeekValueRef.current = null + + if (seekDebounceRef.current) { + clearTimeout(seekDebounceRef.current) + seekDebounceRef.current = null + } + pendingSeekPosRef.current = null + isSeekingRef.current = false + isApplyingPendingSeekRef.current = false + + try { + await flushPendingSeek() + } catch (_err) {} + + try { + await player?.pause?.() + } catch (err) { + reportError(err) + } + }, [flushPendingSeek, player, reportError]) const play = useCallback( async (trackInput, options = {}) => { + const requestId = playRequestIdRef.current + 1 + playRequestIdRef.current = requestId + const normalized = normalizeTrack(trackInput, options) if (!normalized.source) { console.warn('PlayerProvider: impossible de lancer la lecture, source audio manquante') return } - const sameTrack = currentTrack?.id && normalized.id === currentTrack.id + const sameTrack = currentTrackId && normalized.id === currentTrackId const targetPositionMs = typeof options?.startPositionMs === 'number' ? options.startPositionMs @@ -548,6 +701,13 @@ const PlayerProvider = ({ children }) => { setError(null) + if (seekDebounceRef.current) { + clearTimeout(seekDebounceRef.current) + seekDebounceRef.current = null + } + pendingSeekPosRef.current = null + isSeekingRef.current = false + if (Array.isArray(options.queue)) { updateQueue(options.queue, { id: options.queueId, @@ -559,19 +719,43 @@ const PlayerProvider = ({ children }) => { } if (sameTrack) { - setCurrentTrack((prev) => ({ ...prev, ...normalized })) + if (requestId !== playRequestIdRef.current) return + + setCurrentTrack((prev) => { + if (!prev || prev.id !== normalized.id) { + return normalized + } + const preservedSource = areSourcesEquivalent(prev.source, normalized.source) + ? prev.source + : normalized.source + const merged = { + ...prev, + ...normalized, + source: preservedSource, + } + const unchanged = + prev.title === merged.title && + prev.artist === merged.artist && + prev.artwork === merged.artwork && + prev.coverUrl === merged.coverUrl && + prev.source === merged.source + return unchanged ? prev : merged + }) + try { // Flush any pending seek first await flushPendingSeek() + if (requestId !== playRequestIdRef.current) return if (targetPositionMs > 0) { await player?.seekTo?.(targetSeekValue) + if (requestId !== playRequestIdRef.current) return } if (shouldAutoPlay) { await player?.play?.() } } catch (err) { - setError(err) + reportError(err) } if (!shouldAutoPlay) { setPlayback((prev) => ({ @@ -584,12 +768,31 @@ const PlayerProvider = ({ children }) => { return } - try { - if (player?.playing) { - await player.pause?.() - } - } catch (err) { - setError(err) + if (currentTrackId) { + await stopCurrentTrackBeforeSwitch() + if (requestId !== playRequestIdRef.current) return + + setPlayback((prev) => { + if ( + prev.status === 'idle' && + !prev.isPlaying && + !prev.isBuffering && + prev.positionMs === 0 && + prev.durationMs === 0 + ) { + return prev + } + return { + status: 'idle', + isPlaying: false, + isBuffering: false, + positionMs: 0, + durationMs: 0, + } + }) + setCurrentTrack(null) + await waitForNextTick() + if (requestId !== playRequestIdRef.current) return } autoPlayRef.current = shouldAutoPlay @@ -601,9 +804,35 @@ const PlayerProvider = ({ children }) => { isPlaying: false, positionMs: targetPositionMs, })) - setCurrentTrack(normalized) + setCurrentTrack((prev) => { + if (!prev) return normalized + if (prev.id !== normalized.id) return normalized + const preservedSource = areSourcesEquivalent(prev.source, normalized.source) + ? prev.source + : normalized.source + const merged = { + ...prev, + ...normalized, + source: preservedSource, + } + const unchanged = + prev.title === merged.title && + prev.artist === merged.artist && + prev.artwork === merged.artwork && + prev.coverUrl === merged.coverUrl && + prev.source === merged.source + return unchanged ? prev : merged + }) }, - [currentTrack?.id, player, toPlayerSeekValue, updateQueue, flushPendingSeek] + [ + currentTrackId, + player, + toPlayerSeekValue, + updateQueue, + flushPendingSeek, + stopCurrentTrackBeforeSwitch, + reportError, + ] ) const resume = useCallback(async () => { @@ -612,9 +841,9 @@ const PlayerProvider = ({ children }) => { await flushPendingSeek() await player?.play?.() } catch (err) { - setError(err) + reportError(err) } - }, [player, currentTrack, flushPendingSeek]) + }, [player, currentTrack, flushPendingSeek, reportError]) const pause = useCallback(async () => { try { @@ -622,9 +851,9 @@ const PlayerProvider = ({ children }) => { pendingSeekValueRef.current = null await player?.pause?.() } catch (err) { - setError(err) + reportError(err) } - }, [player]) + }, [player, reportError]) const togglePlay = useCallback( async (trackInput, options = {}) => { @@ -651,13 +880,21 @@ const PlayerProvider = ({ children }) => { ) const stop = useCallback(async () => { + playRequestIdRef.current += 1 try { autoPlayRef.current = false pendingSeekValueRef.current = null await player?.pause?.() } catch (err) { - setError(err) + reportError(err) } finally { + if (seekDebounceRef.current) { + clearTimeout(seekDebounceRef.current) + seekDebounceRef.current = null + } + pendingSeekPosRef.current = null + isSeekingRef.current = false + isApplyingPendingSeekRef.current = false setCurrentTrack(null) setPlayback({ status: 'idle', @@ -667,7 +904,7 @@ const PlayerProvider = ({ children }) => { durationMs: 0, }) } - }, [player]) + }, [player, reportError]) const setLooping = useCallback((value) => { setIsLooping(!!value) @@ -682,17 +919,17 @@ const PlayerProvider = ({ children }) => { }, []) useEffect(() => { - if (!currentTrack?.id) { + if (!currentTrackId) { if (queueIndexRef.current !== -1) { setQueueIndexValue(-1) } return } - const idx = queueRef.current.findIndex((item) => item.id === currentTrack.id) + const idx = queueRef.current.findIndex((item) => item.id === currentTrackId) if (idx !== queueIndexRef.current) { setQueueIndexValue(idx) } - }, [currentTrack?.id, setQueueIndexValue]) + }, [currentTrackId, setQueueIndexValue]) const handleTrackDidFinish = useCallback(() => { if (isLooping) return @@ -700,7 +937,7 @@ const PlayerProvider = ({ children }) => { if (!Array.isArray(items) || items.length === 0) return let idx = queueIndexRef.current - const currentId = currentTrack?.id ?? null + const currentId = currentTrackId if ((idx == null || idx < 0) && currentId) { idx = items.findIndex((item) => item.id === currentId) } @@ -718,12 +955,12 @@ const PlayerProvider = ({ children }) => { queueType: queueInfoRef.current.type, queueName: queueInfoRef.current.name, }).catch((err) => { - setError(err) + reportError(err) }) - }, [currentTrack?.id, isLooping, play]) + }, [currentTrackId, isLooping, play, reportError]) useEffect(() => { - const finished = !!status?.didJustFinish + const finished = statusDidJustFinish if (finished) { if (!didJustFinishRef.current) { didJustFinishRef.current = true @@ -732,7 +969,7 @@ const PlayerProvider = ({ children }) => { } else { didJustFinishRef.current = false } - }, [status?.didJustFinish, handleTrackDidFinish]) + }, [statusDidJustFinish, handleTrackDidFinish]) const contextValue = useMemo( () => ({ diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index 3c2c6cf..c39a3bb 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -3,7 +3,7 @@ import { useDataFromRef } from 'react-native-minuit/src/hooks' import useMinuit from 'react-native-minuit/src/hooks/useMinuit' import { createContext, useContext, useGlobal } from 'reactn' -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { checkIfEmailIsValid } from '../actions/signupActions' import { showPremiumRequiredAlert } from '../components/Alert' import firebase, { @@ -126,6 +126,7 @@ export default ({ children }) => { // Selected project state and helpers const [selectedProjectId, setSelectedProjectId] = useState(null) + const selectedProjectIdRef = useRef(null) const { data: selectedProject = null, setData: setSelectedProject } = useDataFromRef({ ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null, simpleRef: true, @@ -134,6 +135,10 @@ export default ({ children }) => { refreshArray: [selectedProjectId], }) + useEffect(() => { + selectedProjectIdRef.current = selectedProjectId + }, [selectedProjectId]) + const persistSelectedProjectId = useCallback( async (projectId) => { if (!currentUID) return @@ -144,7 +149,6 @@ export default ({ children }) => { }, { merge: true } ) - console.log('update selected project') } catch (error) { console.log( 'UserDataProvider: unable to persist selectedProjectId', @@ -158,6 +162,14 @@ export default ({ children }) => { const selectProject = useCallback( (projectId) => { const safeId = projectId || null + if (safeId === selectedProjectIdRef.current) { + if (!safeId) { + setSelectedProject(null) + } + return + } + + selectedProjectIdRef.current = safeId setSelectedProjectId(safeId) if (!safeId) { setSelectedProject(null) diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js index 522de6b..661fcc0 100644 --- a/src/screens/Library/MusicDetails.js +++ b/src/screens/Library/MusicDetails.js @@ -15,7 +15,7 @@ import { responsiveHeight, responsiveWidth } from 'react-native-responsive-dimen import { useGlobal } from 'reactn' import { background, icons } from '../../assets' import PressableScale from '../../components/PressableScale' -import Slider from '../../components/Slider' +import ProgressSlider from '../../components/player/ProgressSlider' import { arrayRemove, arrayUnion, increment, projectsRef, usersRef } from '../../config/firebase' import useDataFromRef from '../../hooks/useDataFromRef' import usePlayer from '../../hooks/usePlayer' @@ -44,9 +44,6 @@ const MusicDetails = ({ route }) => { const projectId = params?.projectId || null const [fav, setFav] = useState(false) const [currentUID] = useGlobal('currentUID') - const wasPlayingBeforeSeek = useRef(false) - const hasCapturedSeekStateRef = useRef(false) - const lastSeekTargetMsRef = useRef(null) const listenedMsRef = useRef(0) const incrementDoneRef = useRef(false) const timerRef = useRef(null) @@ -274,15 +271,6 @@ const MusicDetails = ({ route }) => { return clearTimer }, [isTrackPlaying, projectId]) - const fmt = (ms) => { - const total = Math.max(0, Math.floor((ms || 0) / 1000)) - const m = Math.floor(total / 60) - .toString() - .padStart(1, '0') - const s = (total % 60).toString().padStart(2, '0') - return `${m}:${s}` - } - const togglePlay = useCallback(async () => { if (!trackDescriptor) return try { @@ -308,42 +296,26 @@ const MusicDetails = ({ route }) => { resumeTrack, ]) - const handleSliderSeekStart = useCallback(async () => { + const handleSliderSeekStart = useCallback(() => { if (!trackDescriptor) return - lastSeekTargetMsRef.current = null - if (!hasCapturedSeekStateRef.current) { - hasCapturedSeekStateRef.current = true - wasPlayingBeforeSeek.current = isTrackPlaying - } - try { - if (!isCurrentTrack) { - await ensureLoaded({ startPositionMs: positionMs, autoPlay: false }) - } - if (isTrackPlaying) { - await pauseTrack() - } - } catch (e) { - console.log('MusicDetails seek start error', e?.message) - } - }, [trackDescriptor, isTrackPlaying, isCurrentTrack, ensureLoaded, positionMs, pauseTrack]) + }, [trackDescriptor]) const handleSliderSeek = useCallback( - async (ratio) => { + async (targetMs) => { const dur = sliderDurationMs || 0 if (!trackDescriptor || dur <= 0) return - const targetMs = Math.max(0, Math.floor(dur * ratio)) - lastSeekTargetMsRef.current = targetMs + const bounded = Math.max(0, Math.min(dur, Math.floor(targetMs))) try { if (!isCurrentTrack) { - await ensureLoaded({ startPositionMs: targetMs, autoPlay: false }) + await ensureLoaded({ startPositionMs: bounded, autoPlay: false }) } else { - await seekTrackTo(targetMs) + await seekTrackTo(bounded) } } catch (e) { console.log('MusicDetails seek error', e?.message) } }, - [sliderDurationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo] + [trackDescriptor, sliderDurationMs, isCurrentTrack, ensureLoaded, seekTrackTo] ) const handleToggleLoop = useCallback(async () => { @@ -392,34 +364,6 @@ const MusicDetails = ({ route }) => { resumeTrack, ]) - const handleSliderSeekEnd = useCallback(async () => { - const targetMs = - typeof lastSeekTargetMsRef.current === 'number' - ? Math.max(0, lastSeekTargetMsRef.current) - : null - try { - if (wasPlayingBeforeSeek.current) { - if (!isCurrentTrack) { - await ensureLoaded({ - startPositionMs: targetMs !== null && Number.isFinite(targetMs) ? targetMs : positionMs, - autoPlay: true, - }) - } else { - if (targetMs !== null && Number.isFinite(targetMs)) { - await seekTrackTo(targetMs) - } - await resumeTrack() - } - } - } catch (e) { - console.log('MusicDetails seek end error', e?.message) - } finally { - wasPlayingBeforeSeek.current = false - hasCapturedSeekStateRef.current = false - lastSeekTargetMsRef.current = null - } - }, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack, seekTrackTo]) - const handleSeekBySeconds = useCallback( async (deltaSeconds) => { if (!trackDescriptor) return @@ -816,18 +760,15 @@ const MusicDetails = ({ route }) => { {songUrl && ( - {/* Previous (rewind 10s) */}