Files
musicland/src/screens/Studio/GeneratingSong.js
T
2025-12-10 14:55:57 +01:00

391 lines
13 KiB
JavaScript

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 (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader
onPressBack={() => navigate(Routes.Home)}
progress={63}
// logo={icons.musicLandStudio}
/>
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta musique est en cours de création !"
subTitle="Encore un peu de patience"
containerStyle={{ marginBottom: responsiveHeight(2) }}
/>
<View
style={{
marginTop: responsiveHeight(10),
gap: 16,
flex: 1, // Added flex: 1 to ensure full height usage
}}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
style={{
flex: 1,
borderRadius: 16,
overflow: "hidden",
padding: 12,
backgroundColor: "#FFFFFF0A",
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ flex: 1 }}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
style={{ flex: 1, padding: 10, paddingBottom: 20 }}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
>
Ta musique est{"\n"}en cours de création
</Text>
<View style={{ alignItems: "center", gap: 16 }}>
<ProgressBar
gradient
progress={progress}
status={progressStatus}
/>
<Text
style={{
fontSize: 14,
color: isFailed ? Palette.red : Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
{progressLabel}
</Text>
</View>
{isFailed ? (
<Text
style={{
fontSize: 13,
color: Palette.red,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
lineHeight: 20,
}}
>
{musicErrorMessage}
{hasRefund
? "\nTes crédits ont été remboursés automatiquement."
: "\nTu peux revenir en arrière pour relancer la génération."}
</Text>
) : null}
<GradientButton
title={
selectedProject?.musicStatus === "GENERATED"
? "Découvrir ma musique"
: "Création en cours..."
}
disabled={selectedProject?.musicStatus !== "GENERATED"}
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.SongReady)}
/>
</View>
</BlurView>
</View>
</BlurView>
</View>
</View>
</Page>
);
};
export default GeneratingSong;