feat: fix player

This commit is contained in:
Victor
2026-02-27 15:50:57 +01:00
parent ae93cd4521
commit 006b65eed4
5 changed files with 385 additions and 166 deletions
+23 -9
View File
@@ -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 (
<View onLayout={(e) => setLayout(e.nativeEvent.layout)}>
<View style={{ ...styles.sliderTrack, width: SLIDER_WIDTH }}>
<View
onLayout={(e) => {
const nextWidth = Math.max(0, Math.round(e.nativeEvent.layout?.width || 0))
setSliderWidth((prev) => (prev === nextWidth ? prev : nextWidth))
}}
>
<View style={{ ...styles.sliderTrack, width: sliderWidth || undefined }}>
<Animated.View style={[styles.box, boxStyle]}>
<LinearGradient
colors={['#F94697', '#7023F7']}
+20 -5
View File
@@ -25,6 +25,7 @@ const ProgressSlider = ({
disabled = false,
}) => {
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
const finalize = async () => {
try {
if (typeof onSeekEnd === 'function') {
await onSeekEnd()
}
if (pendingSeekPromiseRef.current) {
await pendingSeekPromiseRef.current
}
} finally {
setIsSeeking(false)
setPreviewRatio(null)
if (typeof onSeekEnd === 'function') {
onSeekEnd()
}
pendingSeekPromiseRef.current = null
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}
+301 -64
View File
@@ -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,
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
pendingSeekValueRef.current = null
if (isApplyingPendingSeekRef.current) return
const pendingSeek = pendingSeekValueRef.current
if (typeof pendingSeek === 'number' && pendingSeek >= 0) {
pendingSeekValueRef.current = null
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
try {
await player.play?.()
}
} catch (err) {
setError(err)
autoPlayRef.current = true
reportError(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?.()
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
}
} catch (err) {
setError(err)
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(
() => ({
+14 -2
View File
@@ -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)
+15 -74
View File
@@ -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 }) => {
</View>
{songUrl && (
<View style={{ paddingTop: 22 }}>
<Slider
value={fmt(positionMs)}
maxValue={fmt(sliderDurationMs)}
progress={
sliderDurationMs
? Math.min(1, Math.max(0, (positionMs || 0) / sliderDurationMs))
: 0
}
seekEnabled={!!songUrl}
<ProgressSlider
positionMs={positionMs}
durationMs={sliderDurationMs}
isPlaying={isTrackPlaying}
onSeekStart={handleSliderSeekStart}
onSeek={handleSliderSeek}
onSeekEnd={handleSliderSeekEnd}
onPause={pauseTrack}
onPlay={resumeTrack}
disabled={!songUrl}
/>
<View style={{ ...Style.containerRow, gap: 24, alignSelf: 'center' }}>
{/* Previous (rewind 10s) */}