import { useIsFocused } from "@react-navigation/native"; import { BlurView } from "expo-blur"; import moment from "moment"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { Image, Platform, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import { responsiveHeight } from "react-native-responsive-dimensions"; import { ai, background } from "../../assets"; import AppAlert from "../../components/Alert"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import ProgressBar from "../../components/ProgressBar"; import firebase, { projectsRef } from "../../config/firebase"; import { isWeb } from "../../hooks/useLayoutType"; import Page from "../../layouts/Page"; import { Routes } from "../../navigation"; import { navigate } from "../../navigation/NavigationService"; import { useUser } from "../../providers/UserDataProvider"; import { Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; const GeneratingSong = () => { const { selectedProjectId, selectedProject } = useUser(); const [progress, setProgress] = useState(0); const { setIsLoading } = useMinuit(); const progressTimerRef = useRef(null); const navigatedRef = useRef(false); const isFocused = useIsFocused(); const askedRef = useRef(false); const callingRef = useRef(false); const localStartRef = useRef(null); const errorAlertShownRef = useRef(false); const isFailed = selectedProject?.musicStatus === "FAILED"; const musicErrorMessage = selectedProject?.musicError?.message || selectedProject?.musicError?.status || "Une erreur est survenue lors de la génération."; const hasRefund = selectedProject?.musicCreditsRefunded === true || typeof selectedProject?.musicCreditsRefundOrderId === "string"; // project loaded from provider // Progress based on an 8 minute cap or until status becomes GENERATED useEffect(() => { const totalMs = 8 * 60 * 1000; const maxGeneratingProgress = 90; const clearTimer = () => { if (progressTimerRef.current) { global.clearInterval(progressTimerRef.current); progressTimerRef.current = null; } }; const status = selectedProject?.musicStatus; const readyUrls = Array.isArray(selectedProject?.musicUrls) ? selectedProject.musicUrls.filter(Boolean) : []; const hasReadySong = readyUrls.length > 0 || !!selectedProject?.songUrl; if (status !== "GENERATING") { // Reset local start when leaving generating state localStartRef.current = null; } if (status === "GENERATED") { clearTimer(); setProgress(hasReadySong ? 100 : maxGeneratingProgress); return () => clearTimer(); } if (status !== "GENERATING") { clearTimer(); setProgress(0); return () => clearTimer(); } const resolveStartDate = () => { const gs = selectedProject?.generationStartAt; if (gs?.toDate) return gs.toDate(); if (gs) return new Date(gs); if (status === "GENERATING") { if (!localStartRef.current) localStartRef.current = new Date(); return localStartRef.current; } return null; }; const update = () => { const startDate = resolveStartDate(); if (!startDate) { setProgress(0); return; } const elapsed = moment().diff(moment(startDate)); const raw = Math.floor((elapsed / totalMs) * 100); // While status is GENERATING, block visual progress at 90% const pct = Math.max(0, Math.min(maxGeneratingProgress, raw)); setProgress(pct); }; update(); clearTimer(); progressTimerRef.current = global.setInterval(update, 1000); return () => clearTimer(); }, [ selectedProject?.musicStatus, selectedProject?.generationStartAt, selectedProject?.musicUrls, selectedProject?.songUrl, ]); // Auto navigate to SongReady when generation completed useEffect(() => { if (!selectedProjectId) return; if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) { navigatedRef.current = true; navigate(Routes.SongReady); } }, [selectedProject?.musicStatus, selectedProjectId]); const effectiveConfig = useMemo(() => { // Use selectedProject.musicConfig only const cfg = selectedProject?.musicConfig || {}; return { title: cfg?.title || "", lyrics: Array.isArray(cfg?.lyrics) ? cfg.lyrics : [], genres: Array.isArray(cfg?.genres) ? cfg.genres : [], voice: Array.isArray(cfg?.voice) ? cfg.voice : [], instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [], tempo: cfg?.tempo || "", }; }, [selectedProject?.musicConfig]); async function startMusicGeneration() { try { console.log("startMusicGeneration"); await setIsLoading(true); const callable = firebase .functions() .httpsCallable("music-generateMusic"); const cfg = effectiveConfig || {}; const { data } = await callable({ title: cfg?.title, lyrics: cfg?.lyrics, genres: cfg?.genres, voice: cfg?.voice, instruments: cfg?.instruments, tempo: cfg?.tempo, projectId: selectedProjectId || selectedProject?.id || null, }); const taskId = data?.response?.data?.taskId || data?.response?.data?.task_id; if (selectedProjectId && taskId) { const baseUpdate = { sunoTaskId: taskId, musicStatus: "GENERATING", musicError: firebase.firestore.FieldValue.delete(), generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }; const updatePayload = selectedProject?.musicConfig ? baseUpdate : { ...baseUpdate, musicConfig: { title: effectiveConfig?.title || "", lyrics: effectiveConfig?.lyrics || [], genres: effectiveConfig?.genres || [], voice: effectiveConfig?.voice || "", instruments: effectiveConfig?.instruments || [], tempo: effectiveConfig?.tempo || "", }, }; await projectsRef .doc(selectedProjectId) .set(updatePayload, { merge: true }); } } catch (e) { console.log("GeneratingSong error", e?.message); const rawMessage = e?.message || ""; const sanitized = typeof rawMessage === "string" ? rawMessage.replace(/^INTERNAL:/i, "").trim() : ""; const fallbackMessage = e?.details?.message || e?.details?.status || "Le service Suno est indisponible. Veuillez réessayer plus tard."; AppAlert( "Impossible de générer la musique", sanitized || fallbackMessage, ); if (selectedProjectId) { try { await projectsRef.doc(selectedProjectId).set( { musicStatus: "FAILED", sunoTaskId: firebase.firestore.FieldValue.delete(), generationStartAt: firebase.firestore.FieldValue.delete(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), musicError: { source: "SUNO_API", message: sanitized || fallbackMessage, }, }, { merge: true }, ); } catch (firestoreError) { console.log( "GeneratingSong Firestore update error", firestoreError?.message, ); } } askedRef.current = false; } finally { await setIsLoading(false); } } // Ensure we never call generation more than once concurrently const startMusicGenerationOnce = React.useCallback(() => { if (callingRef.current) return; callingRef.current = true; startMusicGeneration() .catch(() => { }) .finally(() => { // Keep locked while status transitions to GENERATING; will be prevented by guards global.setTimeout(() => { callingRef.current = false; }, 500); }); }, [startMusicGeneration]); // Trigger generation only when focused; ask once while idle useEffect(() => { if (!isFocused) return; const status = selectedProject?.musicStatus; const titleOk = !!selectedProject?.title; // Idle if not actively generating or generated (ignore stale taskId) const isIdle = status == null || status === "" || status === "PENDING" || status === "READY" || status === "FAILED"; if (titleOk && isIdle && !askedRef.current) { askedRef.current = true; startMusicGenerationOnce(); } }, [ isFocused, selectedProject?.title, selectedProject?.musicStatus, startMusicGenerationOnce, ]); useEffect(() => { if (isFailed) { if (!errorAlertShownRef.current) { errorAlertShownRef.current = true; const refundNotice = hasRefund ? "\n\nTes crédits ont été automatiquement remboursés." : ""; AppAlert("Génération échouée", `${musicErrorMessage}${refundNotice}`); } } else if (selectedProject?.musicStatus === "GENERATING") { errorAlertShownRef.current = false; } }, [ hasRefund, isFailed, musicErrorMessage, selectedProject?.musicStatus, ]); const progressStatus = isFailed ? "error" : "default"; const progressLabel = isFailed ? "Erreur" : `${progress}%`; return ( navigate(Routes.Home)} progress={63} // logo={icons.musicLandStudio} /> Ta musique est{"\n"}en cours de création {progressLabel} {isFailed ? ( {musicErrorMessage} {hasRefund ? "\nTes crédits ont été remboursés automatiquement." : "\nTu peux revenir en arrière pour relancer la génération."} ) : null} navigate(Routes.SongReady)} /> ); }; export default GeneratingSong;