/* 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