/* global setInterval, clearInterval */ import { useFocusEffect } from '@react-navigation/native' import { BlurView } from 'expo-blur' import React, { useCallback, useEffect, useRef, useState } from 'react' import { FlatList, Image, Modal, Pressable, ScrollView, Text, View, useWindowDimensions, } from 'react-native' import { responsiveHeight } from 'react-native-responsive-dimensions' import { background, icons } from '../../assets' import alert from '../../components/Alert' import BorderGradientButton from '../../components/BorderGradientButton' import CreditAmount from '../../components/CreditAmount' import GradientButton from '../../components/GradientButton' import ValidateModal from '../../components/modal/ValidateModal' import MusicLandHeader from '../../components/MusicLandHeader' import ProgressSlider from '../../components/player/ProgressSlider' import { isDesktopWeb, isWeb } from '../../hooks/useLayoutType' import Page from '../../layouts/Page' import { Routes } from '../../navigation' import { navigate, reset } from '../../navigation/NavigationService' import { useUser } from '../../providers/UserDataProvider' import { Palette, Style } from '../../styles' import { gutters, size } from '../../styles/Style' import { FONT_FAMILY } from '../../styles/Fonts' import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer' import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader' const MUSIC_GENERATION_COIN_COST = 8 const SONG_OPTIONS_PER_GENERATION = 2 const REGENERATE_MODAL_MAX_WIDTH = 540 const SongReady = () => { const { selectedProjectId: projectId, selectedProject, updateProjectData, updateUserData, } = useUser() const [showValidateModal, setShowValidateModal] = useState(false) const [showRegenerateModal, setShowRegenerateModal] = useState(false) const [musicUrls, setMusicUrls] = useState([]) const [selectedIndex, setSelectedIndex] = useState(0) const playerRefs = useRef({}) const registerPlayer = useCallback((index, player) => { if (player) { playerRefs.current[index] = player } else { delete playerRefs.current[index] } }, []) const pauseAllExcept = useCallback(async (keepIndex = null) => { const tasks = Object.entries(playerRefs.current).map(async ([key, player]) => { const idx = Number(key) if (!player || idx === keepIndex) return try { await player.pause?.() } catch (error) { console.log('SongReady pause error', error?.message) } }) await Promise.all(tasks) }, []) const pauseAllPlayers = useCallback(async () => { await pauseAllExcept(null) }, [pauseAllExcept]) const handleTogglePlayback = useCallback( async (index, isPlayingNow) => { const player = playerRefs.current[index] if (!player) return if (isPlayingNow) { try { await player.pause?.() } catch (error) { console.log('SongReady pause toggle', error?.message) } return } await pauseAllExcept(index) try { await player.play?.() } catch (error) { console.log('SongReady play error', error?.message) } }, [pauseAllExcept] ) // Sync URLs from provider's selectedProject useEffect(() => { const urls = Array.isArray(selectedProject?.musicUrls) ? selectedProject.musicUrls .filter((url) => typeof url === 'string' && url.trim()) .map((url) => url.trim()) : [] setMusicUrls(urls) setSelectedIndex((prev) => { if (!urls.length) return 0 return Math.min(prev, urls.length - 1) }) }, [selectedProject?.musicUrls]) const validateSelection = async () => { try { const url = musicUrls[selectedIndex] if (!projectId || !url) return await updateProjectData({ songIndex: selectedIndex, songUrl: url, sunoAudioId: selectedProject?.musicAudioIds?.[selectedIndex] || null, songSunoTaskId: selectedProject?.musicTaskIds?.[selectedIndex] || null, lyricsSyncStatus: 'PENDING', lyricsSyncError: null, cover: null, coverUrl: null, coverStatus: null, playbackUrl: null, playbackStatus: null, playbackGenerating: null, songPublishedOnMusicLand: false, songPublishedOnMusicLandAt: null, playbackPublishedOnMusicLand: false, playbackPublishedOnMusicLandAt: null, }) // Incrémenter le compteur de chansons générées const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0 await updateUserData({ data: { generatedSongs: currentGeneratedSongs + 1, }, shouldSetTooltip: false, }) await pauseAllPlayers() reset({ index: 1, routes: [{ name: Routes.BottomTab }, { name: Routes.ChooseCoverType }], }) } catch (e) { console.log('Validate error', e?.message) } } // Pause audio when screen loses focus (navigate/reset) and on unmount useFocusEffect( useCallback(() => { return () => { pauseAllPlayers() } }, [pauseAllPlayers]) ) useEffect(() => { return () => { pauseAllPlayers() } }, [pauseAllPlayers]) const handleChooseTrack = () => { if (isWeb) { alert( 'Attention !', 'Lorsque tu cliques sur valider, tu ne pourras plus changer ni le texte ni la mélodie.', [ { text: 'Retour', style: 'cancel', }, { text: 'Valider', style: 'confirm', onPress: () => { validateSelection() }, }, ], { cancelable: false } ) return } setShowValidateModal(true) } const handleConfirmRegenerate = async () => { setShowRegenerateModal(false) try { await pauseAllPlayers() } catch {} navigate(Routes.Lyrics) } const handleRegeneratePress = () => { if (isWeb) { const descriptionTextStyle = { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, textAlign: 'center', } const amountTextStyle = { ...descriptionTextStyle, fontFamily: FONT_FAMILY.InterSemiBold, } alert( 'Re-générer le morceau', {`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`} Cette action coûte Les crédits seront utilisés lors de l'étape de génération. , [ { text: 'Annuler', style: 'cancel', }, { text: 'Oui', style: 'confirm', onPress: () => handleConfirmRegenerate(), }, ], { cancelable: true } ) return } setShowRegenerateModal(true) } return ( { try { await pauseAllPlayers() } catch {} navigate(Routes.Home) }} progress={63} /> `${item || 'song'}-${idx}`} renderItem={({ item, index }) => ( )} contentContainerStyle={{ gap: 16, paddingBottom: gutters }} showsVerticalScrollIndicator={false} extraData={selectedIndex} /> {!isWeb && ( <> setShowValidateModal(false)} onPressValidate={validateSelection} /> setShowRegenerateModal(false)} onConfirm={handleConfirmRegenerate} /> )} ) } const SongOptionCard = ({ index, url, isSelected, onSelect, registerPlayer, onTogglePlayback, projectId, selectedProject, fallbackDurationS, }) => { const trackTitle = (Array.isArray(selectedProject?.musicTitles) ? selectedProject.musicTitles?.[index] : null) || selectedProject?.title || `Morceau ${index + 1}` const player = useSharedAudioPlayer(url ? { uri: url } : undefined, { id: url ? `songready-${url}` : undefined, title: trackTitle, artwork: selectedProject?.coverUrl || null, coverUrl: selectedProject?.coverUrl || null, metadata: { index, projectId }, }) const fallbackDurationMs = Math.max(0, Math.round((Number(fallbackDurationS) || 0) * 1000)) const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: fallbackDurationMs, }) const [isPlaying, setIsPlaying] = useState(false) useEffect(() => { if (!fallbackDurationMs) return setProgressInfo((current) => current.dur ? current : { ...current, dur: fallbackDurationMs } ) }, [fallbackDurationMs]) useEffect(() => { registerPlayer(index, player) return () => registerPlayer(index, null) }, [index, player, registerPlayer]) useEffect(() => { if (!player || !url) return const preload = async () => { try { await player.load?.({ startPositionMs: 0 }) } catch (error) { console.log('SongReady preload error', error?.message) } } preload() }, [player, url]) useEffect(() => { if (!player) { setIsPlaying(false) setProgressInfo({ pos: 0, dur: 0 }) return undefined } const id = setInterval(() => { const playerDurationMs = Math.max(0, Math.round((Number(player.duration) || 0) * 1000)) const durationMs = playerDurationMs || fallbackDurationMs const positionMs = Math.max(0, Math.round((Number(player.currentTime) || 0) * 1000)) setProgressInfo((prev) => { if ( Math.abs((prev?.dur || 0) - durationMs) < 5 && Math.abs((prev?.pos || 0) - positionMs) < 5 ) { return prev } return { pos: positionMs, dur: durationMs } }) setIsPlaying((prev) => { const next = !!player.playing if (prev !== next) { return next } return prev }) }, 300) return () => clearInterval(id) }, [fallbackDurationMs, player]) const handleSeek = useCallback( async (targetMs) => { if (!player || !progressInfo?.dur) { return } const dur = progressInfo.dur || 0 const pos = Math.max(0, Math.min(dur, Math.floor(targetMs))) // Mise à jour visuelle immédiate setProgressInfo((prev) => ({ ...prev, pos })) try { await player.seekTo?.(Math.floor(pos / 1000)) } catch (error) { console.log('SongReady seek error', error?.message) } }, [player, progressInfo?.dur] ) const handleSeekStart = useCallback(() => { onSelect(index) }, [index, onSelect]) const handlePause = useCallback(async () => { if (!player) return try { await player.pause?.() } catch (error) { console.log('SongReady pause on seek', error?.message) } }, [player]) const handlePlay = useCallback(async () => { if (!player) return try { if (player.resume) { await player.resume?.() } else { await player.play?.() } } catch (error) { console.log('SongReady resume on seek', error?.message) } }, [player]) const handleToggle = () => { onSelect(index) onTogglePlayback?.(index, isPlaying) } return ( onSelect(index)} style={({ pressed }) => [ { borderRadius: 20, borderWidth: isSelected ? 2 : 1, borderColor: isSelected ? Palette.primary : Palette.ultraLightWhite, overflow: 'hidden', }, pressed && { opacity: 0.96 }, ]} > {`Morceau ${index + 1}`} onSelect(index)} style={{ ...Style.containerCenter, ...size({ size: 24 }), }} > {isSelected && ( )} ) } const RegenerateModal = ({ visible, onClose, onConfirm }) => { const { width, height } = useWindowDimensions() const isCompactWidth = width < 420 const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12) const verticalPadding = Math.max(isCompactWidth ? gutters : gutters * 1.5, 12) const availableWidth = Math.max(width - horizontalPadding * 2, 0) const contentWidth = availableWidth > 0 ? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth) : undefined const contentGap = isCompactWidth ? 18 : 24 const buttonGap = isCompactWidth ? 12 : 15 return ( Re-générer le morceau ? {`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`} Cette action coûte Les crédits seront utilisés lors de l'étape de génération. ) } export default SongReady