feat: merge audio
This commit is contained in:
@@ -27,7 +27,7 @@ import Style, { gutters, size } from '../../styles/Style'
|
||||
import { getArtistDisplayName } from '../../utils/artistName'
|
||||
import { ensureAuthenticated } from '../../utils/authRedirect'
|
||||
import { createMusicSharePayload, openShareSheet } from '../../utils/shareSheet'
|
||||
// Import de la fonction utilitaire (à créer dans src/utils/lyricsUtils.js)
|
||||
// Assure-toi que le fichier lyricsUtils.js contient la fonction processKaraokeSections fournie précédemment
|
||||
import { processKaraokeSections } from './utils/lyricsUtils'
|
||||
|
||||
const timeBeforeIncrement = 20000
|
||||
@@ -50,7 +50,7 @@ const MusicDetails = ({ route }) => {
|
||||
|
||||
const { isLooping, setLooping } = usePlayer() || {}
|
||||
|
||||
// --- DATA ---
|
||||
// --- DATA FETCHING ---
|
||||
const { data: project } = useDataFromRef({
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
@@ -65,7 +65,7 @@ const MusicDetails = ({ route }) => {
|
||||
condition: !!project?.userId,
|
||||
})
|
||||
|
||||
// --- MEMOS & STABILISATION ---
|
||||
// --- METADATA & PLAYER STABILIZATION ---
|
||||
const title = project?.title || 'Sans titre'
|
||||
const artist = useMemo(() => {
|
||||
const ownerName = getArtistDisplayName(owner, '')
|
||||
@@ -76,7 +76,7 @@ const MusicDetails = ({ route }) => {
|
||||
const coverUrl = project?.coverUrl || null
|
||||
const songUrl = project?.songUrl || null
|
||||
|
||||
// IMPORTANT: Stabiliser cet objet pour éviter les sauts iOS
|
||||
// On mémoïse le descriptor pour éviter les re-chargements intempestifs sur iOS
|
||||
const trackDescriptor = useMemo(() => {
|
||||
if (!songUrl) return null
|
||||
const id = projectId ? `project-${projectId}` : `song-${songUrl}`
|
||||
@@ -91,11 +91,10 @@ const MusicDetails = ({ route }) => {
|
||||
metadata: { projectId, screen: 'MusicDetails' },
|
||||
context: { projectId, screen: 'MusicDetails' },
|
||||
}
|
||||
console.log('[DEBUG PLAYER] Nouveau Track Descriptor:', desc.id)
|
||||
console.log('[PLAYER] Nouveau descriptor généré:', id)
|
||||
return desc
|
||||
}, [projectId, songUrl, title, artist, coverUrl])
|
||||
|
||||
// --- CONTROLLER ---
|
||||
const {
|
||||
isCurrent,
|
||||
isPlaying,
|
||||
@@ -108,9 +107,30 @@ const MusicDetails = ({ route }) => {
|
||||
seekBy,
|
||||
} = useTrackController(trackDescriptor)
|
||||
|
||||
const loopEnabled = isCurrent && !!isLooping
|
||||
// --- DURATION CALCULATIONS (Pour le clic sur le slider) ---
|
||||
const estimatedDurationMs = useMemo(() => {
|
||||
const toMs = (val) => {
|
||||
const n = Number(val); return (!n || n <= 0) ? 0 : (n > 1000 ? Math.round(n) : Math.round(n * 1000))
|
||||
}
|
||||
const idx = Number(project?.songIndex) || 0
|
||||
const ts = project?.musicTimestamps?.[idx]
|
||||
|
||||
// --- LOGIQUE LYRICS ---
|
||||
// Priorité 1: Metadata
|
||||
const fromMeta = toMs(ts?.durationMs || ts?.duration || ts?.audioDuration)
|
||||
if (fromMeta > 0) return fromMeta
|
||||
|
||||
// Priorité 2: Dernier mot synchronisé
|
||||
const words = ts?.alignedWords || []
|
||||
if (words.length > 0) {
|
||||
const last = words[words.length - 1]
|
||||
return Math.round((last.endS || last.startS || 0) * 1000)
|
||||
}
|
||||
return 0
|
||||
}, [project])
|
||||
|
||||
const sliderDurationMs = durationMs > 0 ? durationMs : estimatedDurationMs
|
||||
|
||||
// --- LYRICS LOGIC ---
|
||||
const sections = useMemo(() => {
|
||||
const idx = Number(project?.songIndex) || 0
|
||||
const ts = project?.musicTimestamps?.[idx]
|
||||
@@ -118,9 +138,7 @@ const MusicDetails = ({ route }) => {
|
||||
}, [project?.musicTimestamps, project?.songIndex])
|
||||
|
||||
const flatLines = useMemo(() => sections.flatMap((s) => s.lines), [sections])
|
||||
|
||||
const currentTimeS = (positionMs || 0) / 1000
|
||||
const visibleTimeS = currentTimeS + 0.3 // Offset pour fluidité iOS
|
||||
const visibleTimeS = (positionMs || 0) / 1000 + 0.3 // Offset de 300ms pour fluidité visuelle
|
||||
|
||||
const currentLineIdx = useMemo(() => {
|
||||
if (!flatLines.length) return -1
|
||||
@@ -128,31 +146,26 @@ const MusicDetails = ({ route }) => {
|
||||
}, [flatLines, visibleTimeS])
|
||||
|
||||
// --- EFFECTS ---
|
||||
|
||||
// Sync favoris
|
||||
useEffect(() => {
|
||||
if (project && currentUID) {
|
||||
setFav(Array.isArray(project.likedBy) ? project.likedBy.includes(currentUID) : false)
|
||||
}
|
||||
}, [project?.likedBy, currentUID])
|
||||
|
||||
// Timer pour les vues
|
||||
// Vues
|
||||
useEffect(() => {
|
||||
const clearTimer = () => { if (timerRef.current) clearInterval(timerRef.current) }
|
||||
const clear = () => timerRef.current && clearInterval(timerRef.current)
|
||||
if (isPlaying && projectId && !incrementDoneRef.current) {
|
||||
timerRef.current = setInterval(async () => {
|
||||
timerRef.current = setInterval(() => {
|
||||
listenedMsRef.current += 1000
|
||||
if (listenedMsRef.current >= timeBeforeIncrement) {
|
||||
console.log('[DEBUG] Incrémentation vue pour:', projectId)
|
||||
projectsRef.doc(projectId).update({ views: increment(1) }).catch(() => {})
|
||||
incrementDoneRef.current = true
|
||||
projectsRef.doc(projectId).set({ views: increment(1) }, { merge: true }).catch(() => {})
|
||||
clearTimer()
|
||||
clear()
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
clearTimer()
|
||||
}
|
||||
return clearTimer
|
||||
} else { clear() }
|
||||
return clear
|
||||
}, [isPlaying, projectId])
|
||||
|
||||
// Auto-scroll
|
||||
@@ -163,56 +176,59 @@ const MusicDetails = ({ route }) => {
|
||||
}
|
||||
}, [currentLineIdx])
|
||||
|
||||
// Autoplay requested
|
||||
// Autoplay
|
||||
useEffect(() => {
|
||||
if (autoPlayRequested && !hasAutoPlayedRef.current && trackDescriptor) {
|
||||
const start = async () => {
|
||||
const run = async () => {
|
||||
try {
|
||||
if (!isCurrent) await ensureLoaded({ startPositionMs: 0, autoPlay: true })
|
||||
else if (!isPlaying) await resume()
|
||||
hasAutoPlayedRef.current = true
|
||||
} catch (e) { console.log('[PLAYER ERROR] Autoplay:', e.message) }
|
||||
} catch (e) { console.log('[PLAYER] Autoplay error:', e.message) }
|
||||
}
|
||||
start()
|
||||
run()
|
||||
}
|
||||
}, [autoPlayRequested, trackDescriptor, isCurrent, isPlaying, ensureLoaded, resume])
|
||||
|
||||
// --- ACTIONS ---
|
||||
// --- CALLBACKS ---
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (!trackDescriptor) return
|
||||
try {
|
||||
if (!isCurrent) {
|
||||
console.log('[PLAYER] Chargement nouveau media:', trackDescriptor.title)
|
||||
console.log('[PLAYER] Chargement via togglePlay')
|
||||
await ensureLoaded({ startPositionMs: positionMs, autoPlay: true })
|
||||
} else {
|
||||
isPlaying ? await pause() : await resume()
|
||||
}
|
||||
} catch (e) { console.log('[PLAYER ERROR] Toggle:', e.message) }
|
||||
} catch (e) { console.log('[PLAYER] Toggle error:', e.message) }
|
||||
}, [trackDescriptor, isCurrent, isPlaying, ensureLoaded, positionMs, pause, resume])
|
||||
|
||||
const handleSliderSeek = useCallback(async (ms) => {
|
||||
if (!trackDescriptor) return
|
||||
const handleSliderSeek = useCallback(async (targetMs) => {
|
||||
if (!trackDescriptor || sliderDurationMs <= 0) return
|
||||
const ms = Math.max(0, Math.min(sliderDurationMs, Math.floor(targetMs)))
|
||||
console.log(`[PLAYER] Seek demandé vers: ${ms}ms`)
|
||||
try {
|
||||
if (!isCurrent) await ensureLoaded({ startPositionMs: ms, autoPlay: false })
|
||||
else await seekTo(Math.floor(ms))
|
||||
} catch (e) { console.log('[PLAYER ERROR] Seek:', e.message) }
|
||||
}, [trackDescriptor, isCurrent, ensureLoaded, seekTo])
|
||||
if (!isCurrent) {
|
||||
await ensureLoaded({ startPositionMs: ms, autoPlay: isPlaying })
|
||||
} else {
|
||||
await seekTo(ms)
|
||||
}
|
||||
} catch (e) { console.log('[PLAYER] Seek error:', e.message) }
|
||||
}, [trackDescriptor, sliderDurationMs, isCurrent, ensureLoaded, isPlaying, seekTo])
|
||||
|
||||
const handleLyricsSeek = async (timeS) => {
|
||||
const ms = timeS * 1000
|
||||
const ms = Math.max(0, timeS * 1000)
|
||||
try {
|
||||
if (!isCurrent) await ensureLoaded({ startPositionMs: ms, autoPlay: true })
|
||||
else {
|
||||
await seekTo(ms)
|
||||
if (!isPlaying) await resume()
|
||||
}
|
||||
} catch (e) { console.log('[PLAYER ERROR] LyricsSeek:', e.message) }
|
||||
} catch (e) { console.log('[PLAYER] Lyrics Seek error:', e.message) }
|
||||
}
|
||||
|
||||
// --- UI PARTS (Options / Share) ---
|
||||
const sharePayload = useMemo(() => createMusicSharePayload({ projectId, title, artist }), [artist, projectId, title])
|
||||
|
||||
const handleOpenOptions = () => {
|
||||
const sharePayload = createMusicSharePayload({ projectId, title, artist })
|
||||
SheetManager.show('MusicOptions', {
|
||||
payload: {
|
||||
projectId, title, ownerId: project?.userId || owner?.id || null,
|
||||
@@ -241,7 +257,7 @@ const MusicDetails = ({ route }) => {
|
||||
})}
|
||||
>
|
||||
<View style={{ flex: 1, paddingTop: 20, paddingBottom: gutters * 2 }}>
|
||||
{/* HEADER SECTION */}
|
||||
{/* INFOS MEDIA */}
|
||||
<View style={{ gap: 28 }}>
|
||||
{coverUrl && (
|
||||
<ExpoImage
|
||||
@@ -265,13 +281,12 @@ const MusicDetails = ({ route }) => {
|
||||
disabled={!isCurrent}
|
||||
style={{ ...size({ size: 32 }), alignItems: 'center', justifyContent: 'center', opacity: isCurrent ? 1 : 0.4 }}
|
||||
>
|
||||
<Feather name="repeat" size={24} color={loopEnabled ? Palette.primary : Palette.white} />
|
||||
<Feather name="repeat" size={24} color={isCurrent && isLooping ? Palette.primary : Palette.white} />
|
||||
</PressableScale>
|
||||
<PressableScale
|
||||
onPress={async () => {
|
||||
if (!projectId || !ensureAuthenticated(currentUID)) return
|
||||
const next = !fav
|
||||
setFav(next)
|
||||
const next = !fav; setFav(next)
|
||||
projectsRef.doc(projectId).update({
|
||||
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
|
||||
}).catch(() => setFav(!next))
|
||||
@@ -292,7 +307,7 @@ const MusicDetails = ({ route }) => {
|
||||
<View style={{ paddingTop: 22 }}>
|
||||
<ProgressSlider
|
||||
positionMs={positionMs}
|
||||
durationMs={durationMs || 0}
|
||||
durationMs={sliderDurationMs}
|
||||
isPlaying={isPlaying}
|
||||
onSeek={handleSliderSeek}
|
||||
onPause={pause}
|
||||
@@ -344,7 +359,9 @@ const MusicDetails = ({ route }) => {
|
||||
))}
|
||||
</ScrollView>
|
||||
) : (
|
||||
<Text style={{ color: 'gray', textAlign: 'center', marginTop: 20 }}>Chargement des paroles...</Text>
|
||||
<Text style={{ color: 'rgba(255,255,255,0.4)', textAlign: 'center', marginTop: 40, fontFamily: FONT_FAMILY.InterRegular }}>
|
||||
{songUrl ? "Chargement des paroles..." : "Aucune parole disponible"}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user