import React, { useMemo, useRef, useState } from "react"; import { Dimensions, Modal, Text, View } from "react-native"; import SwiperFlatList from "react-native-swiper-flatlist"; import { background, icons } 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 } = 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 [containerLayout, setContainerLayout] = useState(null); const { selectedProjectId, selectedProject, updateProjectData, currentUserData, currentUID, } = useUser(); // Selections state const [genres, setGenres] = useState([]); // voice: object keyed by category (e.g., { base: string|null, Sensibilité: string|null, Technique: string|null }) 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: // Must select at least one in base category 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 persistMusicConfig = 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 (e) {} }; const spendCoinsForGeneration = 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.", ); } }; const handleConfirmGeneration = 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); } }; const onPressNext = () => { if (selectedIndex === 3) { setIsConfirmVisible(true); return; } setSelectedIndex(selectedIndex + 1); setProgress(progress + 9); scrollRef.current.scrollToIndex({ index: selectedIndex + 1, animated: true, }); }; const onPressBack = () => { if (selectedIndex > 0) { setSelectedIndex(selectedIndex - 1); setProgress(progress - 9); scrollRef.current.scrollToIndex({ index: selectedIndex - 1, animated: true, }); } else { goBack(); } }; return ( setContainerLayout(event.nativeEvent.layout)} > {selectedIndex !== 4 && ( )} { 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;