import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { Dimensions, FlatList, Modal, Text, View } from "react-native"; import { background } from "../../assets"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import AppAlert from "../../components/Alert"; import firebase, { getFunctionsClient } from "../../config/firebase"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; import { goBack, navigate } from "../../navigation/NavigationService"; import { useUser } from "../../providers/UserDataProvider"; import { gutters, Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { normalizeStructureType } from "../../utils/songStructure"; import { openCoinPackModal } from "../../utils/coinPackModal"; import ChooseGenre from "./ChooseGenre"; import ChooseInstruments from "./ChooseInstruments"; import ChooseRhythm from "./ChooseRhythm"; import CustomizeVoice from "./CustomizeVoice"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; const { width: windowWidth } = Dimensions.get("window"); const MUSIC_GENERATION_COIN_COST = 8; const CONFIRM_MODAL_MAX_WIDTH = 540; const ComposeSong = () => { const scrollRef = useRef(null); const [selectedIndex, setSelectedIndex] = useState(0); const [progress, setProgress] = useState(18); const [parentLayout, setParentLayout] = useState(null); const containerWidth = parentLayout?.width || windowWidth || 1; const { selectedProjectId, selectedProject, updateProjectData, currentUserData, currentUID, } = useUser(); const [genres, setGenres] = useState([]); const [voice, setVoice] = useState({}); const [instruments, setInstruments] = useState([]); const [rhythm, setRhythm] = useState(null); const [isConfirmVisible, setIsConfirmVisible] = useState(false); const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false); const coinBalance = useMemo(() => { const value = currentUserData?.coins; if (typeof value === "number" && Number.isFinite(value)) { return value; } if (typeof value === "string") { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return 0; }, [currentUserData?.coins]); const formattedCoinBalance = useMemo(() => { try { return new Intl.NumberFormat("fr-FR", { maximumFractionDigits: 0, }).format(coinBalance); } catch (_error) { return `${coinBalance}`; } }, [coinBalance]); const isStepValid = useMemo(() => { switch (selectedIndex) { case 0: return Array.isArray(genres) && genres.length > 0; case 1: return !!(voice && typeof voice === "object" && voice.BASE); case 2: return Array.isArray(instruments) && instruments.length > 0; case 3: return !!rhythm; default: return true; } }, [selectedIndex, genres, voice, instruments, rhythm]); const musicConfig = useMemo(() => { let lyricsArr = []; if (Array.isArray(selectedProject?.lyrics)) { lyricsArr = selectedProject.lyrics.map((s) => ({ type: normalizeStructureType(s?.type), lyrics: s?.lyrics || "", })); } else { const c = selectedProject?.lyrics?.couplet; const r = selectedProject?.lyrics?.refrain; if (c) lyricsArr.push({ type: "couplet", lyrics: c }); if (r) lyricsArr.push({ type: "refrain", lyrics: r }); } const voiceArray = Object.entries(voice || {}) .filter(([, v]) => typeof v === "string" && v.trim()) .map(([category, value]) => ({ category, value })); return { title: selectedProject?.title || "", lyrics: lyricsArr, genres: Array.isArray(genres) ? genres : [], voice: voiceArray, instruments: Array.isArray(instruments) ? instruments : [], tempo: rhythm || undefined, projectId: selectedProjectId || undefined, }; }, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]); const steps = useMemo( () => [ { key: "genres", render: () => , }, { key: "voice", render: () => ( ), }, { key: "instruments", render: () => ( ), }, { key: "rhythm", render: () => ( ), }, ], [genres, voice, instruments, rhythm] ); useEffect(() => { const nextProgress = 18 + selectedIndex * 9; if (nextProgress !== progress) { setProgress(nextProgress); } try { scrollRef.current?.scrollToIndex?.({ index: selectedIndex, animated: true, }); } catch (_) {} }, [selectedIndex, progress, containerWidth]); const getItemLayout = useCallback( (_data, index) => ({ length: containerWidth, offset: containerWidth * index, index, }), [containerWidth] ); const persistMusicConfig = useCallback(async () => { try { if (!selectedProjectId) return; await updateProjectData({ musicConfig: { title: musicConfig?.title || "", lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [], genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [], voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [], instruments: Array.isArray(musicConfig?.instruments) ? musicConfig.instruments : [], tempo: musicConfig?.tempo || "", }, musicStatus: null, sunoTaskId: firebase.firestore.FieldValue.delete(), musicUrls: firebase.firestore.FieldValue.delete(), }); } catch (_error) {} }, [musicConfig, selectedProjectId, updateProjectData]); const spendCoinsForGeneration = useCallback(async () => { if (!currentUID) { throw new Error("Utilisateur introuvable. Merci de réessayer."); } try { const functionsClient = getFunctionsClient(); const createSongOrder = functionsClient.httpsCallable("orders-createSongOrder"); await createSongOrder({ amount: -MUSIC_GENERATION_COIN_COST, songId: selectedProjectId || null, source: "music_generation", }); } catch (error) { const message = typeof error?.message === "string" ? error.message.replace( /^functions\.https\.HttpsError:\s*/iu, "", ) : null; throw new Error( message || "Une erreur est survenue lors de la création de la commande de crédits.", ); } }, [currentUID, selectedProjectId]); const handleConfirmGeneration = useCallback(async () => { if (isProcessingConfirmation) return; setIsProcessingConfirmation(true); try { const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0; if (availableCoins < MUSIC_GENERATION_COIN_COST) { setIsConfirmVisible(false); AppAlert( "Crédits insuffisants", "Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.", ); openCoinPackModal(); return; } await spendCoinsForGeneration(); await persistMusicConfig(); setIsConfirmVisible(false); navigate(Routes.GeneratingSong, { config: musicConfig }); } catch (error) { setIsConfirmVisible(false); const message = error?.message || "Une erreur est survenue lors du lancement de la génération."; AppAlert("Impossible de lancer la génération", message); } finally { setIsProcessingConfirmation(false); } }, [ coinBalance, isProcessingConfirmation, musicConfig, persistMusicConfig, spendCoinsForGeneration, ]); const onPressNext = () => { if (selectedIndex === steps.length - 1) { setIsConfirmVisible(true); return; } setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1)); }; const onPressBack = () => { if (selectedIndex > 0) { setSelectedIndex((prev) => Math.max(prev - 1, 0)); } else { goBack(); } }; return ( setParentLayout(event.nativeEvent.layout)} > item.key} horizontal pagingEnabled scrollEnabled={false} showsHorizontalScrollIndicator={false} initialScrollIndex={selectedIndex} getItemLayout={getItemLayout} style={{ width: containerWidth }} renderItem={({ item }) => ( {item.render()} )} /> {selectedIndex !== steps.length && ( )} { if (isProcessingConfirmation) return; setIsConfirmVisible(false); }} > Générer la musique ? Cette action coûte {MUSIC_GENERATION_COIN_COST} pièces. {"\n"}Souhaites-tu les utiliser pour lancer la génération ? Solde disponible : {formattedCoinBalance} pièces { if (isProcessingConfirmation) return; setIsConfirmVisible(false); }} disabled={isProcessingConfirmation} /> ); }; export default ComposeSong;