feat: fix player
This commit is contained in:
@@ -19,11 +19,10 @@ export default ({
|
|||||||
}) => {
|
}) => {
|
||||||
const offset = useSharedValue(0)
|
const offset = useSharedValue(0)
|
||||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE)
|
const boxWidth = useSharedValue(INITIAL_BOX_SIZE)
|
||||||
const [layout, setLayout] = useState(null)
|
const [sliderWidth, setSliderWidth] = useState(0)
|
||||||
const seekingRef = useRef(false)
|
const seekingRef = useRef(false)
|
||||||
|
|
||||||
const SLIDER_WIDTH = layout?.width
|
const MAX_VALUE = Math.max(0, sliderWidth - INITIAL_BOX_SIZE)
|
||||||
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE
|
|
||||||
|
|
||||||
const handleSeekStart = () => {
|
const handleSeekStart = () => {
|
||||||
if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) {
|
if (seekEnabled && typeof onSeekStart === 'function' && !seekingRef.current) {
|
||||||
@@ -49,6 +48,11 @@ export default ({
|
|||||||
})
|
})
|
||||||
.onChange((event) => {
|
.onChange((event) => {
|
||||||
runOnJS(handleSeekStart)()
|
runOnJS(handleSeekStart)()
|
||||||
|
if (!(MAX_VALUE > 0)) {
|
||||||
|
offset.value = 0
|
||||||
|
boxWidth.value = INITIAL_BOX_SIZE
|
||||||
|
return
|
||||||
|
}
|
||||||
offset.value =
|
offset.value =
|
||||||
Math.abs(offset.value) <= MAX_VALUE
|
Math.abs(offset.value) <= MAX_VALUE
|
||||||
? offset.value + event.changeX <= 0
|
? offset.value + event.changeX <= 0
|
||||||
@@ -62,7 +66,7 @@ export default ({
|
|||||||
boxWidth.value = newWidth
|
boxWidth.value = newWidth
|
||||||
})
|
})
|
||||||
.onEnd(() => {
|
.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
|
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0
|
||||||
// Reanimated -> JS thread bridge
|
// Reanimated -> JS thread bridge
|
||||||
runOnJS(onSeek)(ratio)
|
runOnJS(onSeek)(ratio)
|
||||||
@@ -77,15 +81,20 @@ export default ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (seekingRef.current) return
|
if (seekingRef.current) return
|
||||||
|
|
||||||
if (typeof progress === 'number' && layout?.width) {
|
if (typeof progress === 'number' && sliderWidth > 0) {
|
||||||
const max = layout.width - INITIAL_BOX_SIZE
|
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 clamped = Math.max(0, Math.min(1, progress))
|
||||||
const newOffset = clamped * max
|
const newOffset = clamped * max
|
||||||
offset.value = newOffset
|
offset.value = newOffset
|
||||||
boxWidth.value = INITIAL_BOX_SIZE + newOffset
|
boxWidth.value = INITIAL_BOX_SIZE + newOffset
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [progress, layout?.width])
|
}, [progress, sliderWidth])
|
||||||
|
|
||||||
const boxStyle = useAnimatedStyle(() => {
|
const boxStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
@@ -100,8 +109,13 @@ export default ({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View onLayout={(e) => setLayout(e.nativeEvent.layout)}>
|
<View
|
||||||
<View style={{ ...styles.sliderTrack, width: SLIDER_WIDTH }}>
|
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]}>
|
<Animated.View style={[styles.box, boxStyle]}>
|
||||||
<LinearGradient
|
<LinearGradient
|
||||||
colors={['#F94697', '#7023F7']}
|
colors={['#F94697', '#7023F7']}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const ProgressSlider = ({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
}) => {
|
}) => {
|
||||||
const wasPlayingRef = useRef(false)
|
const wasPlayingRef = useRef(false)
|
||||||
|
const pendingSeekPromiseRef = useRef(null)
|
||||||
const [isSeeking, setIsSeeking] = useState(false)
|
const [isSeeking, setIsSeeking] = useState(false)
|
||||||
const [previewRatio, setPreviewRatio] = useState(null)
|
const [previewRatio, setPreviewRatio] = useState(null)
|
||||||
|
|
||||||
@@ -54,22 +55,36 @@ const ProgressSlider = ({
|
|||||||
const bounded = clamp01(ratio)
|
const bounded = clamp01(ratio)
|
||||||
setPreviewRatio(bounded)
|
setPreviewRatio(bounded)
|
||||||
const targetMs = safeDuration * bounded
|
const targetMs = safeDuration * bounded
|
||||||
onSeek(targetMs)
|
const maybePromise = onSeek(targetMs)
|
||||||
|
pendingSeekPromiseRef.current =
|
||||||
|
maybePromise && typeof maybePromise.then === 'function'
|
||||||
|
? maybePromise
|
||||||
|
: Promise.resolve()
|
||||||
},
|
},
|
||||||
[disabled, onSeek, safeDuration]
|
[disabled, onSeek, safeDuration]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSeekEnd = useCallback(() => {
|
const handleSeekEnd = useCallback(() => {
|
||||||
if (disabled || !safeDuration) return
|
if (disabled || !safeDuration) return
|
||||||
setIsSeeking(false)
|
const finalize = async () => {
|
||||||
setPreviewRatio(null)
|
try {
|
||||||
if (typeof onSeekEnd === 'function') {
|
if (typeof onSeekEnd === 'function') {
|
||||||
onSeekEnd()
|
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') {
|
finalize()
|
||||||
onPlay()
|
|
||||||
}
|
|
||||||
wasPlayingRef.current = false
|
|
||||||
}, [disabled, onPlay, onSeekEnd, safeDuration])
|
}, [disabled, onPlay, onSeekEnd, safeDuration])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -78,7 +93,7 @@ const ProgressSlider = ({
|
|||||||
isSeeking && previewRatio != null ? safeDuration * previewRatio : safePosition
|
isSeeking && previewRatio != null ? safeDuration * previewRatio : safePosition
|
||||||
)}
|
)}
|
||||||
maxValue={formatTime(safeDuration)}
|
maxValue={formatTime(safeDuration)}
|
||||||
progress={progress}
|
progress={isSeeking && previewRatio != null ? previewRatio : progress}
|
||||||
seekEnabled={!disabled && safeDuration > 0}
|
seekEnabled={!disabled && safeDuration > 0}
|
||||||
onSeekStart={handleSeekStart}
|
onSeekStart={handleSeekStart}
|
||||||
onSeek={handleSeek}
|
onSeek={handleSeek}
|
||||||
|
|||||||
+308
-71
@@ -91,11 +91,13 @@ export const PlayerContext = createContext(DEFAULT_CONTEXT)
|
|||||||
const convertToMs = (value) => {
|
const convertToMs = (value) => {
|
||||||
const numeric = Number(value ?? 0)
|
const numeric = Number(value ?? 0)
|
||||||
if (!Number.isFinite(numeric) || numeric <= 0) return 0
|
if (!Number.isFinite(numeric) || numeric <= 0) return 0
|
||||||
if (Platform.OS === 'web') return numeric
|
if (Platform.OS === 'web') return Math.round(numeric)
|
||||||
if (numeric > 100000) return numeric
|
if (numeric > 100000) return Math.round(numeric)
|
||||||
return numeric * 1000
|
return Math.round(numeric * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const waitForNextTick = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
const ensureSource = (track = {}, options = {}) => {
|
const ensureSource = (track = {}, options = {}) => {
|
||||||
const candidate = track?.source ?? options?.source ?? null
|
const candidate = track?.source ?? options?.source ?? null
|
||||||
if (typeof candidate === 'number') return candidate
|
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 normalizeTrack = (trackInput = {}, options = {}) => {
|
||||||
const track =
|
const track =
|
||||||
typeof trackInput === 'string' ? { uri: trackInput } : trackInput ? { ...trackInput } : {}
|
typeof trackInput === 'string' ? { uri: trackInput } : trackInput ? { ...trackInput } : {}
|
||||||
@@ -197,6 +216,7 @@ const normalizeTrack = (trackInput = {}, options = {}) => {
|
|||||||
const PlayerProvider = ({ children }) => {
|
const PlayerProvider = ({ children }) => {
|
||||||
const [activeRouteName] = useGlobal('activeRouteName')
|
const [activeRouteName] = useGlobal('activeRouteName')
|
||||||
const [currentTrack, setCurrentTrack] = useState(null)
|
const [currentTrack, setCurrentTrack] = useState(null)
|
||||||
|
const currentTrackId = currentTrack?.id ?? null
|
||||||
const queueRef = useRef([])
|
const queueRef = useRef([])
|
||||||
const queueInfoRef = useRef({ id: null, type: null, name: null })
|
const queueInfoRef = useRef({ id: null, type: null, name: null })
|
||||||
const queueIndexRef = useRef(-1)
|
const queueIndexRef = useRef(-1)
|
||||||
@@ -215,8 +235,23 @@ const PlayerProvider = ({ children }) => {
|
|||||||
|
|
||||||
const autoPlayRef = useRef(false)
|
const autoPlayRef = useRef(false)
|
||||||
const pendingSeekValueRef = useRef(null)
|
const pendingSeekValueRef = useRef(null)
|
||||||
|
const pendingSeekPosRef = useRef(null)
|
||||||
|
const isSeekingRef = useRef(false)
|
||||||
|
const isApplyingPendingSeekRef = useRef(false)
|
||||||
const didJustFinishRef = useRef(false)
|
const didJustFinishRef = useRef(false)
|
||||||
const trackInfoRef = useRef({})
|
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 setQueueIndexValue = useCallback((index = -1) => {
|
||||||
const list = queueRef.current
|
const list = queueRef.current
|
||||||
@@ -338,6 +373,20 @@ const PlayerProvider = ({ children }) => {
|
|||||||
const player = useAudioPlayer(source || null, 200)
|
const player = useAudioPlayer(source || null, 200)
|
||||||
const status = useAudioPlayerStatus(player)
|
const status = useAudioPlayerStatus(player)
|
||||||
const shouldHideOnRoute = activeRouteName ? HIDDEN_ROUTE_NAMES.has(activeRouteName) : false
|
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 toPlayerSeekValue = useCallback((milliseconds) => {
|
||||||
const ms = Math.max(0, Number(milliseconds) || 0)
|
const ms = Math.max(0, Number(milliseconds) || 0)
|
||||||
@@ -357,16 +406,57 @@ const PlayerProvider = ({ children }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!status) return
|
if (!status) return
|
||||||
|
|
||||||
const durationMs = convertToMs(status.duration)
|
let durationMs = 0
|
||||||
const positionMs = convertToMs(status.currentTime)
|
let positionMs = 0
|
||||||
const isLoaded = !!status.isLoaded
|
let isLoaded = false
|
||||||
const isPlaying = !!status.playing
|
let isPlaying = false
|
||||||
const isBuffering = !!status.isBuffering
|
let isBuffering = false
|
||||||
const didJustFinish = !!status.didJustFinish
|
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) => {
|
setPlayback((prev) => {
|
||||||
let nextStatus = prev.status
|
let nextStatus = prev.status
|
||||||
if (!currentTrack) {
|
if (!currentTrackId) {
|
||||||
nextStatus = 'idle'
|
nextStatus = 'idle'
|
||||||
} else if (!isLoaded) {
|
} else if (!isLoaded) {
|
||||||
nextStatus = 'loading'
|
nextStatus = 'loading'
|
||||||
@@ -380,49 +470,83 @@ const PlayerProvider = ({ children }) => {
|
|||||||
nextStatus = 'paused'
|
nextStatus = 'paused'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
prev.status === nextStatus &&
|
||||||
|
prev.isPlaying === effectiveIsPlaying &&
|
||||||
|
prev.isBuffering === effectiveIsBuffering &&
|
||||||
|
prev.positionMs === effectivePositionMs &&
|
||||||
|
prev.durationMs === effectiveDurationMs
|
||||||
|
) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: nextStatus,
|
status: nextStatus,
|
||||||
isPlaying,
|
isPlaying: effectiveIsPlaying,
|
||||||
isBuffering,
|
isBuffering: effectiveIsBuffering,
|
||||||
positionMs,
|
positionMs: effectivePositionMs,
|
||||||
durationMs,
|
durationMs: effectiveDurationMs,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
if (currentTrack?.id) {
|
if (currentTrackId) {
|
||||||
trackInfoRef.current[currentTrack.id] = {
|
if (
|
||||||
durationMs:
|
!previousTrackInfo ||
|
||||||
durationMs > 0 ? durationMs : trackInfoRef.current[currentTrack.id]?.durationMs || 0,
|
previousTrackInfo.durationMs !== nextDurationMs ||
|
||||||
positionMs,
|
previousTrackInfo.positionMs !== positionMs ||
|
||||||
isLoaded,
|
previousTrackInfo.isLoaded !== isLoaded
|
||||||
updatedAt: Date.now(),
|
) {
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
if (!player || !currentTrack) return
|
if (!player || !currentTrackId || !isStatusLoaded) return
|
||||||
|
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
try {
|
if (isApplyingPendingSeekRef.current) return
|
||||||
const pendingSeek = pendingSeekValueRef.current
|
|
||||||
|
const pendingSeek = pendingSeekValueRef.current
|
||||||
|
if (typeof pendingSeek === 'number' && pendingSeek >= 0) {
|
||||||
pendingSeekValueRef.current = null
|
pendingSeekValueRef.current = null
|
||||||
|
isApplyingPendingSeekRef.current = true
|
||||||
if (typeof pendingSeek === 'number' && pendingSeek >= 0) {
|
try {
|
||||||
await player.seekTo?.(pendingSeek)
|
await player.seekTo?.(pendingSeek)
|
||||||
|
} catch (err) {
|
||||||
|
pendingSeekValueRef.current = pendingSeek
|
||||||
|
reportError(err)
|
||||||
|
} finally {
|
||||||
|
isApplyingPendingSeekRef.current = false
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (autoPlayRef.current) {
|
if (autoPlayRef.current) {
|
||||||
autoPlayRef.current = false
|
autoPlayRef.current = false
|
||||||
|
try {
|
||||||
await player.play?.()
|
await player.play?.()
|
||||||
|
} catch (err) {
|
||||||
|
autoPlayRef.current = true
|
||||||
|
reportError(err)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
setError(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
run()
|
run()
|
||||||
}, [player, currentTrack?.id])
|
}, [player, currentTrackId, isStatusLoaded])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!player) return
|
if (!player) return
|
||||||
@@ -432,8 +556,17 @@ const PlayerProvider = ({ children }) => {
|
|||||||
}, [player, isLooping])
|
}, [player, isLooping])
|
||||||
|
|
||||||
const seekDebounceRef = useRef(null)
|
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 () => {
|
const waitForActiveSeek = useCallback(async () => {
|
||||||
if (!isSeekingRef.current) return
|
if (!isSeekingRef.current) return
|
||||||
@@ -458,23 +591,17 @@ const PlayerProvider = ({ children }) => {
|
|||||||
// Store pending seek position
|
// Store pending seek position
|
||||||
pendingSeekPosRef.current = seekValue
|
pendingSeekPosRef.current = seekValue
|
||||||
|
|
||||||
// Update local state immediately for UI responsiveness
|
|
||||||
setPlayback((prev) => ({
|
|
||||||
...prev,
|
|
||||||
positionMs: bounded,
|
|
||||||
}))
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
seekDebounceRef.current = setTimeout(async () => {
|
seekDebounceRef.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
isSeekingRef.current = true
|
isSeekingRef.current = true
|
||||||
if (status?.isLoaded) {
|
if (isStatusLoaded) {
|
||||||
await player.seekTo?.(seekValue)
|
await player.seekTo?.(seekValue)
|
||||||
} else {
|
} else {
|
||||||
pendingSeekValueRef.current = seekValue
|
pendingSeekValueRef.current = seekValue
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
} finally {
|
} finally {
|
||||||
isSeekingRef.current = false
|
isSeekingRef.current = false
|
||||||
pendingSeekPosRef.current = null
|
pendingSeekPosRef.current = null
|
||||||
@@ -483,7 +610,7 @@ const PlayerProvider = ({ children }) => {
|
|||||||
}, 100) // 100ms debounce
|
}, 100) // 100ms debounce
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[player, status?.isLoaded, toPlayerSeekValue]
|
[player, isStatusLoaded, toPlayerSeekValue]
|
||||||
)
|
)
|
||||||
|
|
||||||
const seekBy = useCallback(
|
const seekBy = useCallback(
|
||||||
@@ -512,13 +639,13 @@ const PlayerProvider = ({ children }) => {
|
|||||||
pendingSeekPosRef.current = null
|
pendingSeekPosRef.current = null
|
||||||
try {
|
try {
|
||||||
isSeekingRef.current = true
|
isSeekingRef.current = true
|
||||||
if (status?.isLoaded) {
|
if (isStatusLoaded) {
|
||||||
await player.seekTo?.(seekValue)
|
await player.seekTo?.(seekValue)
|
||||||
} else {
|
} else {
|
||||||
pendingSeekValueRef.current = seekValue
|
pendingSeekValueRef.current = seekValue
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
} finally {
|
} finally {
|
||||||
isSeekingRef.current = false
|
isSeekingRef.current = false
|
||||||
}
|
}
|
||||||
@@ -526,17 +653,43 @@ const PlayerProvider = ({ children }) => {
|
|||||||
|
|
||||||
// 2. Wait for any active seek to complete
|
// 2. Wait for any active seek to complete
|
||||||
await waitForActiveSeek()
|
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(
|
const play = useCallback(
|
||||||
async (trackInput, options = {}) => {
|
async (trackInput, options = {}) => {
|
||||||
|
const requestId = playRequestIdRef.current + 1
|
||||||
|
playRequestIdRef.current = requestId
|
||||||
|
|
||||||
const normalized = normalizeTrack(trackInput, options)
|
const normalized = normalizeTrack(trackInput, options)
|
||||||
if (!normalized.source) {
|
if (!normalized.source) {
|
||||||
console.warn('PlayerProvider: impossible de lancer la lecture, source audio manquante')
|
console.warn('PlayerProvider: impossible de lancer la lecture, source audio manquante')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const sameTrack = currentTrack?.id && normalized.id === currentTrack.id
|
const sameTrack = currentTrackId && normalized.id === currentTrackId
|
||||||
const targetPositionMs =
|
const targetPositionMs =
|
||||||
typeof options?.startPositionMs === 'number'
|
typeof options?.startPositionMs === 'number'
|
||||||
? options.startPositionMs
|
? options.startPositionMs
|
||||||
@@ -548,6 +701,13 @@ const PlayerProvider = ({ children }) => {
|
|||||||
|
|
||||||
setError(null)
|
setError(null)
|
||||||
|
|
||||||
|
if (seekDebounceRef.current) {
|
||||||
|
clearTimeout(seekDebounceRef.current)
|
||||||
|
seekDebounceRef.current = null
|
||||||
|
}
|
||||||
|
pendingSeekPosRef.current = null
|
||||||
|
isSeekingRef.current = false
|
||||||
|
|
||||||
if (Array.isArray(options.queue)) {
|
if (Array.isArray(options.queue)) {
|
||||||
updateQueue(options.queue, {
|
updateQueue(options.queue, {
|
||||||
id: options.queueId,
|
id: options.queueId,
|
||||||
@@ -559,19 +719,43 @@ const PlayerProvider = ({ children }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sameTrack) {
|
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 {
|
try {
|
||||||
// Flush any pending seek first
|
// Flush any pending seek first
|
||||||
await flushPendingSeek()
|
await flushPendingSeek()
|
||||||
|
if (requestId !== playRequestIdRef.current) return
|
||||||
|
|
||||||
if (targetPositionMs > 0) {
|
if (targetPositionMs > 0) {
|
||||||
await player?.seekTo?.(targetSeekValue)
|
await player?.seekTo?.(targetSeekValue)
|
||||||
|
if (requestId !== playRequestIdRef.current) return
|
||||||
}
|
}
|
||||||
if (shouldAutoPlay) {
|
if (shouldAutoPlay) {
|
||||||
await player?.play?.()
|
await player?.play?.()
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
}
|
}
|
||||||
if (!shouldAutoPlay) {
|
if (!shouldAutoPlay) {
|
||||||
setPlayback((prev) => ({
|
setPlayback((prev) => ({
|
||||||
@@ -584,12 +768,31 @@ const PlayerProvider = ({ children }) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
if (currentTrackId) {
|
||||||
if (player?.playing) {
|
await stopCurrentTrackBeforeSwitch()
|
||||||
await player.pause?.()
|
if (requestId !== playRequestIdRef.current) return
|
||||||
}
|
|
||||||
} catch (err) {
|
setPlayback((prev) => {
|
||||||
setError(err)
|
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
|
autoPlayRef.current = shouldAutoPlay
|
||||||
@@ -601,9 +804,35 @@ const PlayerProvider = ({ children }) => {
|
|||||||
isPlaying: false,
|
isPlaying: false,
|
||||||
positionMs: targetPositionMs,
|
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 () => {
|
const resume = useCallback(async () => {
|
||||||
@@ -612,9 +841,9 @@ const PlayerProvider = ({ children }) => {
|
|||||||
await flushPendingSeek()
|
await flushPendingSeek()
|
||||||
await player?.play?.()
|
await player?.play?.()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
}
|
}
|
||||||
}, [player, currentTrack, flushPendingSeek])
|
}, [player, currentTrack, flushPendingSeek, reportError])
|
||||||
|
|
||||||
const pause = useCallback(async () => {
|
const pause = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -622,9 +851,9 @@ const PlayerProvider = ({ children }) => {
|
|||||||
pendingSeekValueRef.current = null
|
pendingSeekValueRef.current = null
|
||||||
await player?.pause?.()
|
await player?.pause?.()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
}
|
}
|
||||||
}, [player])
|
}, [player, reportError])
|
||||||
|
|
||||||
const togglePlay = useCallback(
|
const togglePlay = useCallback(
|
||||||
async (trackInput, options = {}) => {
|
async (trackInput, options = {}) => {
|
||||||
@@ -651,13 +880,21 @@ const PlayerProvider = ({ children }) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const stop = useCallback(async () => {
|
const stop = useCallback(async () => {
|
||||||
|
playRequestIdRef.current += 1
|
||||||
try {
|
try {
|
||||||
autoPlayRef.current = false
|
autoPlayRef.current = false
|
||||||
pendingSeekValueRef.current = null
|
pendingSeekValueRef.current = null
|
||||||
await player?.pause?.()
|
await player?.pause?.()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err)
|
reportError(err)
|
||||||
} finally {
|
} finally {
|
||||||
|
if (seekDebounceRef.current) {
|
||||||
|
clearTimeout(seekDebounceRef.current)
|
||||||
|
seekDebounceRef.current = null
|
||||||
|
}
|
||||||
|
pendingSeekPosRef.current = null
|
||||||
|
isSeekingRef.current = false
|
||||||
|
isApplyingPendingSeekRef.current = false
|
||||||
setCurrentTrack(null)
|
setCurrentTrack(null)
|
||||||
setPlayback({
|
setPlayback({
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
@@ -667,7 +904,7 @@ const PlayerProvider = ({ children }) => {
|
|||||||
durationMs: 0,
|
durationMs: 0,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [player])
|
}, [player, reportError])
|
||||||
|
|
||||||
const setLooping = useCallback((value) => {
|
const setLooping = useCallback((value) => {
|
||||||
setIsLooping(!!value)
|
setIsLooping(!!value)
|
||||||
@@ -682,17 +919,17 @@ const PlayerProvider = ({ children }) => {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!currentTrack?.id) {
|
if (!currentTrackId) {
|
||||||
if (queueIndexRef.current !== -1) {
|
if (queueIndexRef.current !== -1) {
|
||||||
setQueueIndexValue(-1)
|
setQueueIndexValue(-1)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const idx = queueRef.current.findIndex((item) => item.id === currentTrack.id)
|
const idx = queueRef.current.findIndex((item) => item.id === currentTrackId)
|
||||||
if (idx !== queueIndexRef.current) {
|
if (idx !== queueIndexRef.current) {
|
||||||
setQueueIndexValue(idx)
|
setQueueIndexValue(idx)
|
||||||
}
|
}
|
||||||
}, [currentTrack?.id, setQueueIndexValue])
|
}, [currentTrackId, setQueueIndexValue])
|
||||||
|
|
||||||
const handleTrackDidFinish = useCallback(() => {
|
const handleTrackDidFinish = useCallback(() => {
|
||||||
if (isLooping) return
|
if (isLooping) return
|
||||||
@@ -700,7 +937,7 @@ const PlayerProvider = ({ children }) => {
|
|||||||
if (!Array.isArray(items) || items.length === 0) return
|
if (!Array.isArray(items) || items.length === 0) return
|
||||||
|
|
||||||
let idx = queueIndexRef.current
|
let idx = queueIndexRef.current
|
||||||
const currentId = currentTrack?.id ?? null
|
const currentId = currentTrackId
|
||||||
if ((idx == null || idx < 0) && currentId) {
|
if ((idx == null || idx < 0) && currentId) {
|
||||||
idx = items.findIndex((item) => item.id === currentId)
|
idx = items.findIndex((item) => item.id === currentId)
|
||||||
}
|
}
|
||||||
@@ -718,12 +955,12 @@ const PlayerProvider = ({ children }) => {
|
|||||||
queueType: queueInfoRef.current.type,
|
queueType: queueInfoRef.current.type,
|
||||||
queueName: queueInfoRef.current.name,
|
queueName: queueInfoRef.current.name,
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
setError(err)
|
reportError(err)
|
||||||
})
|
})
|
||||||
}, [currentTrack?.id, isLooping, play])
|
}, [currentTrackId, isLooping, play, reportError])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const finished = !!status?.didJustFinish
|
const finished = statusDidJustFinish
|
||||||
if (finished) {
|
if (finished) {
|
||||||
if (!didJustFinishRef.current) {
|
if (!didJustFinishRef.current) {
|
||||||
didJustFinishRef.current = true
|
didJustFinishRef.current = true
|
||||||
@@ -732,7 +969,7 @@ const PlayerProvider = ({ children }) => {
|
|||||||
} else {
|
} else {
|
||||||
didJustFinishRef.current = false
|
didJustFinishRef.current = false
|
||||||
}
|
}
|
||||||
}, [status?.didJustFinish, handleTrackDidFinish])
|
}, [statusDidJustFinish, handleTrackDidFinish])
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useDataFromRef } from 'react-native-minuit/src/hooks'
|
|||||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
|
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
|
||||||
import { createContext, useContext, useGlobal } from 'reactn'
|
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 { checkIfEmailIsValid } from '../actions/signupActions'
|
||||||
import { showPremiumRequiredAlert } from '../components/Alert'
|
import { showPremiumRequiredAlert } from '../components/Alert'
|
||||||
import firebase, {
|
import firebase, {
|
||||||
@@ -126,6 +126,7 @@ export default ({ children }) => {
|
|||||||
|
|
||||||
// Selected project state and helpers
|
// Selected project state and helpers
|
||||||
const [selectedProjectId, setSelectedProjectId] = useState(null)
|
const [selectedProjectId, setSelectedProjectId] = useState(null)
|
||||||
|
const selectedProjectIdRef = useRef(null)
|
||||||
const { data: selectedProject = null, setData: setSelectedProject } = useDataFromRef({
|
const { data: selectedProject = null, setData: setSelectedProject } = useDataFromRef({
|
||||||
ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null,
|
ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null,
|
||||||
simpleRef: true,
|
simpleRef: true,
|
||||||
@@ -134,6 +135,10 @@ export default ({ children }) => {
|
|||||||
refreshArray: [selectedProjectId],
|
refreshArray: [selectedProjectId],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedProjectIdRef.current = selectedProjectId
|
||||||
|
}, [selectedProjectId])
|
||||||
|
|
||||||
const persistSelectedProjectId = useCallback(
|
const persistSelectedProjectId = useCallback(
|
||||||
async (projectId) => {
|
async (projectId) => {
|
||||||
if (!currentUID) return
|
if (!currentUID) return
|
||||||
@@ -144,7 +149,6 @@ export default ({ children }) => {
|
|||||||
},
|
},
|
||||||
{ merge: true }
|
{ merge: true }
|
||||||
)
|
)
|
||||||
console.log('update selected project')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(
|
console.log(
|
||||||
'UserDataProvider: unable to persist selectedProjectId',
|
'UserDataProvider: unable to persist selectedProjectId',
|
||||||
@@ -158,6 +162,14 @@ export default ({ children }) => {
|
|||||||
const selectProject = useCallback(
|
const selectProject = useCallback(
|
||||||
(projectId) => {
|
(projectId) => {
|
||||||
const safeId = projectId || null
|
const safeId = projectId || null
|
||||||
|
if (safeId === selectedProjectIdRef.current) {
|
||||||
|
if (!safeId) {
|
||||||
|
setSelectedProject(null)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedProjectIdRef.current = safeId
|
||||||
setSelectedProjectId(safeId)
|
setSelectedProjectId(safeId)
|
||||||
if (!safeId) {
|
if (!safeId) {
|
||||||
setSelectedProject(null)
|
setSelectedProject(null)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { responsiveHeight, responsiveWidth } from 'react-native-responsive-dimen
|
|||||||
import { useGlobal } from 'reactn'
|
import { useGlobal } from 'reactn'
|
||||||
import { background, icons } from '../../assets'
|
import { background, icons } from '../../assets'
|
||||||
import PressableScale from '../../components/PressableScale'
|
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 { arrayRemove, arrayUnion, increment, projectsRef, usersRef } from '../../config/firebase'
|
||||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||||
import usePlayer from '../../hooks/usePlayer'
|
import usePlayer from '../../hooks/usePlayer'
|
||||||
@@ -44,9 +44,6 @@ const MusicDetails = ({ route }) => {
|
|||||||
const projectId = params?.projectId || null
|
const projectId = params?.projectId || null
|
||||||
const [fav, setFav] = useState(false)
|
const [fav, setFav] = useState(false)
|
||||||
const [currentUID] = useGlobal('currentUID')
|
const [currentUID] = useGlobal('currentUID')
|
||||||
const wasPlayingBeforeSeek = useRef(false)
|
|
||||||
const hasCapturedSeekStateRef = useRef(false)
|
|
||||||
const lastSeekTargetMsRef = useRef(null)
|
|
||||||
const listenedMsRef = useRef(0)
|
const listenedMsRef = useRef(0)
|
||||||
const incrementDoneRef = useRef(false)
|
const incrementDoneRef = useRef(false)
|
||||||
const timerRef = useRef(null)
|
const timerRef = useRef(null)
|
||||||
@@ -274,15 +271,6 @@ const MusicDetails = ({ route }) => {
|
|||||||
return clearTimer
|
return clearTimer
|
||||||
}, [isTrackPlaying, projectId])
|
}, [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 () => {
|
const togglePlay = useCallback(async () => {
|
||||||
if (!trackDescriptor) return
|
if (!trackDescriptor) return
|
||||||
try {
|
try {
|
||||||
@@ -308,42 +296,26 @@ const MusicDetails = ({ route }) => {
|
|||||||
resumeTrack,
|
resumeTrack,
|
||||||
])
|
])
|
||||||
|
|
||||||
const handleSliderSeekStart = useCallback(async () => {
|
const handleSliderSeekStart = useCallback(() => {
|
||||||
if (!trackDescriptor) return
|
if (!trackDescriptor) return
|
||||||
lastSeekTargetMsRef.current = null
|
}, [trackDescriptor])
|
||||||
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])
|
|
||||||
|
|
||||||
const handleSliderSeek = useCallback(
|
const handleSliderSeek = useCallback(
|
||||||
async (ratio) => {
|
async (targetMs) => {
|
||||||
const dur = sliderDurationMs || 0
|
const dur = sliderDurationMs || 0
|
||||||
if (!trackDescriptor || dur <= 0) return
|
if (!trackDescriptor || dur <= 0) return
|
||||||
const targetMs = Math.max(0, Math.floor(dur * ratio))
|
const bounded = Math.max(0, Math.min(dur, Math.floor(targetMs)))
|
||||||
lastSeekTargetMsRef.current = targetMs
|
|
||||||
try {
|
try {
|
||||||
if (!isCurrentTrack) {
|
if (!isCurrentTrack) {
|
||||||
await ensureLoaded({ startPositionMs: targetMs, autoPlay: false })
|
await ensureLoaded({ startPositionMs: bounded, autoPlay: false })
|
||||||
} else {
|
} else {
|
||||||
await seekTrackTo(targetMs)
|
await seekTrackTo(bounded)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('MusicDetails seek error', e?.message)
|
console.log('MusicDetails seek error', e?.message)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[sliderDurationMs, trackDescriptor, isCurrentTrack, ensureLoaded, seekTrackTo]
|
[trackDescriptor, sliderDurationMs, isCurrentTrack, ensureLoaded, seekTrackTo]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleToggleLoop = useCallback(async () => {
|
const handleToggleLoop = useCallback(async () => {
|
||||||
@@ -392,34 +364,6 @@ const MusicDetails = ({ route }) => {
|
|||||||
resumeTrack,
|
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(
|
const handleSeekBySeconds = useCallback(
|
||||||
async (deltaSeconds) => {
|
async (deltaSeconds) => {
|
||||||
if (!trackDescriptor) return
|
if (!trackDescriptor) return
|
||||||
@@ -816,18 +760,15 @@ const MusicDetails = ({ route }) => {
|
|||||||
</View>
|
</View>
|
||||||
{songUrl && (
|
{songUrl && (
|
||||||
<View style={{ paddingTop: 22 }}>
|
<View style={{ paddingTop: 22 }}>
|
||||||
<Slider
|
<ProgressSlider
|
||||||
value={fmt(positionMs)}
|
positionMs={positionMs}
|
||||||
maxValue={fmt(sliderDurationMs)}
|
durationMs={sliderDurationMs}
|
||||||
progress={
|
isPlaying={isTrackPlaying}
|
||||||
sliderDurationMs
|
|
||||||
? Math.min(1, Math.max(0, (positionMs || 0) / sliderDurationMs))
|
|
||||||
: 0
|
|
||||||
}
|
|
||||||
seekEnabled={!!songUrl}
|
|
||||||
onSeekStart={handleSliderSeekStart}
|
onSeekStart={handleSliderSeekStart}
|
||||||
onSeek={handleSliderSeek}
|
onSeek={handleSliderSeek}
|
||||||
onSeekEnd={handleSliderSeekEnd}
|
onPause={pauseTrack}
|
||||||
|
onPlay={resumeTrack}
|
||||||
|
disabled={!songUrl}
|
||||||
/>
|
/>
|
||||||
<View style={{ ...Style.containerRow, gap: 24, alignSelf: 'center' }}>
|
<View style={{ ...Style.containerRow, gap: 24, alignSelf: 'center' }}>
|
||||||
{/* Previous (rewind 10s) */}
|
{/* Previous (rewind 10s) */}
|
||||||
|
|||||||
Reference in New Issue
Block a user