import { Feather } from '@expo/vector-icons' import { Image as ExpoImage } from 'expo-image' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Linking, Pressable, Image as RNImage, ScrollView, StyleSheet, Text, View, } from 'react-native' import { SheetManager } from 'react-native-actions-sheet' import { responsiveHeight, responsiveWidth } from 'react-native-responsive-dimensions' import { useGlobal } from 'reactn' import { background, icons } from '../../assets' import PressableScale from '../../components/PressableScale' 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' import useTrackController from '../../hooks/useTrackController' import Page from '../../layouts/Page' import { Palette } from '../../styles' import { FONT_FAMILY } from '../../styles/Fonts' import Style, { gutters, size } from '../../styles/Style' import { getArtistDisplayName } from '../../utils/artistName' import { ensureAuthenticated } from '../../utils/authRedirect' import { createMusicSharePayload, openShareSheet } from '../../utils/shareSheet' // Assure-toi que le fichier lyricsUtils.js contient la fonction processKaraokeSections fournie précédemment import { processKaraokeSections } from './utils/lyricsUtils' const timeBeforeIncrement = 20000 const MusicDetails = ({ route }) => { const { params } = route || {} const action = params?.action const autoPlayRequested = params?.autoPlay const projectId = params?.projectId || null const [fav, setFav] = useState(false) const [currentUID] = useGlobal('currentUID') const listenedMsRef = useRef(0) const incrementDoneRef = useRef(false) const timerRef = useRef(null) const hasAutoPlayedRef = useRef(false) const lyricsRef = useRef(null) const lineYRef = useRef({}) const { isLooping, setLooping } = usePlayer() || {} // --- DATA FETCHING --- const { data: project } = useDataFromRef({ ref: projectId ? projectsRef.doc(projectId) : null, simpleRef: true, listener: true, condition: !!projectId, }) const { data: owner } = useDataFromRef({ ref: project?.userId ? usersRef.doc(project.userId) : null, simpleRef: true, listener: true, condition: !!project?.userId, }) // --- METADATA & PLAYER STABILIZATION --- const title = project?.title || 'Sans titre' const artist = useMemo(() => { const ownerName = getArtistDisplayName(owner, '') if (ownerName) return ownerName return getArtistDisplayName(project, 'MusicLand') }, [owner, project]) const coverUrl = project?.coverUrl || null const songUrl = project?.songUrl || null // 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}` const desc = { id, uri: songUrl, songUrl, title, artist, artwork: coverUrl, coverUrl, metadata: { projectId, screen: 'MusicDetails' }, context: { projectId, screen: 'MusicDetails' }, } console.log('[PLAYER] Nouveau descriptor généré:', id) return desc }, [projectId, songUrl, title, artist, coverUrl]) const { isCurrent, isPlaying, positionMs, durationMs, ensureLoaded, pause, resume, seekTo, seekBy, } = useTrackController(trackDescriptor) // --- 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] // 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] return processKaraokeSections(ts?.alignedWords || []) }, [project?.musicTimestamps, project?.songIndex]) const flatLines = useMemo(() => sections.flatMap((s) => s.lines), [sections]) const visibleTimeS = (positionMs || 0) / 1000 const currentLineIdx = useMemo(() => { if (!flatLines.length) return -1 return flatLines.findIndex(l => visibleTimeS >= l.startS && visibleTimeS <= l.endS) }, [flatLines, visibleTimeS]) // --- EFFECTS --- useEffect(() => { if (project && currentUID) { setFav(Array.isArray(project.likedBy) ? project.likedBy.includes(currentUID) : false) } }, [project?.likedBy, currentUID]) // Vues useEffect(() => { const clear = () => timerRef.current && clearInterval(timerRef.current) if (isPlaying && projectId && !incrementDoneRef.current) { timerRef.current = setInterval(() => { listenedMsRef.current += 1000 if (listenedMsRef.current >= timeBeforeIncrement) { projectsRef.doc(projectId).update({ views: increment(1) }).catch(() => {}) incrementDoneRef.current = true clear() } }, 1000) } else { clear() } return clear }, [isPlaying, projectId]) // Auto-scroll useEffect(() => { const y = lineYRef.current?.[currentLineIdx] if (lyricsRef.current && typeof y === 'number') { lyricsRef.current.scrollTo({ y: Math.max(0, y - 80), animated: true }) } }, [currentLineIdx]) // Autoplay useEffect(() => { if (autoPlayRequested && !hasAutoPlayedRef.current && trackDescriptor) { 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] Autoplay error:', e.message) } } run() } }, [autoPlayRequested, trackDescriptor, isCurrent, isPlaying, ensureLoaded, resume]) // --- CALLBACKS --- const togglePlay = useCallback(async () => { if (!trackDescriptor) return try { if (!isCurrent) { console.log('[PLAYER] Chargement via togglePlay') await ensureLoaded({ startPositionMs: positionMs, autoPlay: true }) } else { isPlaying ? await pause() : await resume() } } catch (e) { console.log('[PLAYER] Toggle error:', e.message) } }, [trackDescriptor, isCurrent, isPlaying, ensureLoaded, positionMs, pause, resume]) 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: 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 = 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] Lyrics Seek error:', e.message) } } const handleOpenOptions = () => { const sharePayload = createMusicSharePayload({ projectId, title, artist }) SheetManager.show('MusicOptions', { payload: { projectId, title, ownerId: project?.userId || owner?.id || null, onReport: () => SheetManager.show('Report', { payload: { targetType: 'music', projectId, title, ownerId: project?.userId } }), onAddToPlaylist: () => SheetManager.show('Playlist', { payload: { projectId } }), onDownload: () => songUrl && Linking.openURL(songUrl).catch(() => {}), downloadDisabled: !songUrl, onShare: () => sharePayload && openShareSheet(sharePayload), sharePayload, }, }) } return ( ( SheetManager.show('DeleteAudio')}> ), })} > {/* INFOS MEDIA */} {coverUrl && ( )} {title} {artist} setLooping?.(!isLooping)} disabled={!isCurrent} style={{ ...size({ size: 32 }), alignItems: 'center', justifyContent: 'center', opacity: isCurrent ? 1 : 0.4 }} > { if (!projectId || !ensureAuthenticated(currentUID)) return const next = !fav; setFav(next) projectsRef.doc(projectId).update({ likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID), }).catch(() => setFav(!next)) }} > {/* PLAYER SLIDER & CONTROLS */} {songUrl && ( seekBy(-10000)}> seekBy(10000)}> )} {/* LYRICS SECTION */} {sections.length > 0 ? ( {sections.map((section, sIdx) => ( 0 && styles.sectionSpacing]}> {section.label}{'\n'} {section.lines.map((line) => ( { lineYRef.current[line.globalIdx] = e.nativeEvent.layout.y }} style={{ marginBottom: 8, flexDirection: 'row', flexWrap: 'wrap' }} > {line.words.map((w, j) => ( = w.startS ? styles.karaokeWordActive : styles.karaokeWord} onPress={() => handleLyricsSeek(w.startS)} > {w.word}{' '} ))} ))} ))} ) : ( {songUrl ? "Chargement des paroles..." : "Aucune parole disponible"} )} ) } export default MusicDetails const styles = StyleSheet.create({ img: { width: 160, height: 160, borderRadius: 24, alignSelf: 'center', }, title: { fontSize: 20, color: Palette.white, fontFamily: FONT_FAMILY.HelveticaNeueMedium, width: responsiveWidth(70), marginBottom: responsiveHeight(1), }, name: { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.HelveticaNeueRegular, }, karaokeWord: { color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, fontSize: 16, }, karaokeWordActive: { color: Palette.primary, fontFamily: FONT_FAMILY.InterBold, fontSize: 16, }, sectionContainer: { marginBottom: 16, }, sectionSpacing: { marginTop: 12, }, sectionTitle: { color: Palette.transparentWhite, fontFamily: FONT_FAMILY.InterSemiBold, fontSize: 12, letterSpacing: 0.4, textTransform: 'uppercase', marginBottom: 6, }, })