diff --git a/dragNdropScreen.png b/dragNdropScreen.png
deleted file mode 100644
index 7cd09c7..0000000
Binary files a/dragNdropScreen.png and /dev/null differ
diff --git a/src/components/modal/ValidateModal.js b/src/components/modal/ValidateModal.js
index 11228dd..0d13b5e 100644
--- a/src/components/modal/ValidateModal.js
+++ b/src/components/modal/ValidateModal.js
@@ -44,22 +44,13 @@ const ValidateModal = ({ visible, onClose, onPressValidate }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
+ textAlign: "center",
}}
>
Lorsque tu clique sur valider, tu ne pourra plus changer ni le
- texte ni la mélodie.{" "}
+ texte ni la mélodie.
-
- Valider
-
diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js
index 237670d..0c02dfb 100644
--- a/src/navigation/MainStack.js
+++ b/src/navigation/MainStack.js
@@ -45,6 +45,7 @@ import RecordedPlayback from "../screens/Playback/RecordedPlayback";
import ChooseDecor from "../screens/Playback/ChooseDecor";
import CreatingDecor from "../screens/Playback/CreatingDecor";
import VideoFinalize from "../screens/Playback/VideoFinalize";
+import GeneratingSong from "../screens/Studio/GeneratingSong";
import AllMyMusic from "../screens/Library/AllMyMusic";
import AllMyPlaylist from "../screens/Library/AllMyPlaylist";
import AllMyLikedMusic from "../screens/Library/AllMyLikedMusic";
@@ -185,6 +186,10 @@ const screens = [
name: Routes.StreamSong,
component: StreamSong,
},
+ {
+ name: Routes.GeneratingSong,
+ component: GeneratingSong,
+ },
{
name: Routes.SongRelease,
component: SongRelease,
diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js
index c653db7..9eb66de 100644
--- a/src/navigation/Routes.js
+++ b/src/navigation/Routes.js
@@ -49,6 +49,7 @@ export const Routes = {
StreamSong: "StreamSong",
SongRelease: "SongRelease",
PlaybackExample: "PlaybackExample",
+ GeneratingSong: "GeneratingSong",
PlaybackOnboarding: "PlaybackOnboarding",
Playback: "Playback",
diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js
index 647cbaa..213cf0e 100644
--- a/src/screens/Library/MusicDetails.js
+++ b/src/screens/Library/MusicDetails.js
@@ -174,17 +174,22 @@ const MusicDetails = () => {
}
};
const description = useMemo(() => {
- // Try to build a readable text from lyrics if present
+ // Build a readable text from lyrics with section labels
if (Array.isArray(project?.lyrics)) {
return project.lyrics
- .map((s) => (s?.lyrics || "").trim())
+ .map((s) => {
+ const body = (s?.lyrics || "").trim();
+ if (!body) return null;
+ const t = (s?.type || "").toLowerCase();
+ const label = t === "refrain" ? "Refrain" : "Couplet";
+ return `[${label}]\n${body}`;
+ })
.filter(Boolean)
- .slice(0, 4)
.join("\n\n");
}
const c = project?.lyrics?.couplet;
const r = project?.lyrics?.refrain;
- const parts = [c, r].filter(Boolean);
+ const parts = [c ? `[Couplet]\n${c}` : null, r ? `[Refrain]\n${r}` : null].filter(Boolean);
return parts.length ? parts.join("\n\n") : "";
}, [project]);
diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js
index be81f4a..a1654d0 100644
--- a/src/screens/NewMusicOptions.js
+++ b/src/screens/NewMusicOptions.js
@@ -10,6 +10,7 @@ import { navigate } from "../navigation/NavigationService";
import { useUserData } from "../providers/UserDataProvider";
import FontAwesome from "@expo/vector-icons/FontAwesome";
import palette from "../styles/Palette";
+import GeneratingSong from "./Studio/GeneratingSong";
const CREATE_DATA = [
{
@@ -77,13 +78,19 @@ const NewMusicOptions = ({ route }) => {
});
break;
case 1:
- navigate(Routes.Compose, { projectId: currentProjet.id });
+ if (currentProjet?.musicStatus === "GENERATING") {
+ navigate(Routes.GeneratingSong, { projectId: currentProjet.id });
+ } else if (currentProjet?.musicStatus === "GENERATED") {
+ navigate(Routes.SongReady, { projectId: currentProjet.id });
+ } else {
+ navigate(Routes.Compose, { projectId: currentProjet.id });
+ }
break;
case 2:
navigate(Routes.PouchReady, { projectId: currentProjet.id });
break;
- default:
- navigate(Routes.PouchReady, { projectId: currentProjet.id });
+ case 3:
+ navigate(Routes.Playback, { projectId: currentProjet.id });
break;
}
};
diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js
index 8a52c20..edb41e9 100644
--- a/src/screens/Studio/ComposeSong.js
+++ b/src/screens/Studio/ComposeSong.js
@@ -4,17 +4,17 @@ import Page from "../../layouts/Page";
import { background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import GradientButton from "../../components/GradientButton";
-import { goBack } from "../../navigation/NavigationService";
+import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
import SwiperFlatList from "react-native-swiper-flatlist";
import ChooseGenre from "./ChooseGenre";
import CustomizeVoice from "./CustomizeVoice";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
-import CreatingSong from "./CreatingSong";
import { useRoute } from "@react-navigation/native";
-import firebase from "../../config/firebase";
+import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
+import { Routes } from "../../navigation";
const { width } = Dimensions.get("window");
@@ -27,20 +27,14 @@ const ComposeSong = () => {
const projectId = route?.params?.projectId;
// Selections state
- const [genres, setGenres] = useState(__DEV__ ? ["Hip Hop/Rap", "Punk"] : []);
- const [voice, setVoice] = useState(
- __DEV__ ? "Deux voix pour interpreter ta chanson" : null,
- );
- const [instruments, setInstruments] = useState(
- __DEV__ ? ["Synthétiseur", "Trompette", "Piano classique"] : [],
- );
- const [rhythm, setRhythm] = useState(__DEV__ ? "Rapide" : null);
+ const [genres, setGenres] = useState([]);
+ const [voice, setVoice] = useState(null);
+ const [instruments, setInstruments] = useState([]);
+ const [rhythm, setRhythm] = useState(null);
// Fetch selected project to get title + lyrics
const { data: project } = useDataFromRef({
- ref: projectId
- ? firebase.firestore().collection("projects").doc(projectId)
- : null,
+ ref: projectId ? projectsRef.doc(projectId) : null,
simpleRef: true,
listener: true,
condition: !!projectId,
@@ -86,6 +80,10 @@ const ComposeSong = () => {
}, [project, genres, voice, instruments, rhythm, projectId]);
const onPressNext = () => {
+ if (selectedIndex === 3) {
+ navigate(Routes.GeneratingSong, { config: musicConfig, projectId });
+ return;
+ }
setSelectedIndex(selectedIndex + 1);
setProgress(progress + 9);
scrollRef.current.scrollToIndex({
@@ -159,15 +157,6 @@ const ComposeSong = () => {
>
-
-
-
{selectedIndex !== 4 && (
diff --git a/src/screens/Studio/CreatingSong.js b/src/screens/Studio/CreatingSong.js
index 7725ded..78dfd50 100644
--- a/src/screens/Studio/CreatingSong.js
+++ b/src/screens/Studio/CreatingSong.js
@@ -9,14 +9,14 @@ import ProgressBar from "../../components/ProgressBar";
import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
-import firebase from "../../config/firebase";
+import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import moment from "moment";
const CreatingSong = ({ active, config }) => {
const [progress, setProgress] = useState(0);
const [called, setCalled] = useState(false);
- const [result, setResult] = useState(null);
+ const [, setResult] = useState(null);
const { setIsLoading } = useMinuit();
const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null);
@@ -110,10 +110,7 @@ const CreatingSong = ({ active, config }) => {
// Si un projectId et un taskId existent, mettre à jour le projet
if (config?.projectId && taskId) {
- const projectRef = firebase
- .firestore()
- .collection("projects")
- .doc(config.projectId);
+ const projectRef = projectsRef.doc(config.projectId);
await projectRef.set(
{
diff --git a/src/screens/Studio/GeneratingSong.js b/src/screens/Studio/GeneratingSong.js
new file mode 100644
index 0000000..ab4d4ad
--- /dev/null
+++ b/src/screens/Studio/GeneratingSong.js
@@ -0,0 +1,228 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Image, Platform, Text, View } from "react-native";
+import Page from "../../layouts/Page";
+import { ai, background } from "../../assets";
+import MusicLandHeader from "../../components/MusicLandHeader";
+import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
+import { BlurView } from "expo-blur";
+import { Palette } from "../../styles";
+import ProgressBar from "../../components/ProgressBar";
+import GradientButton from "../../components/GradientButton";
+import { goBack, navigate } from "../../navigation/NavigationService";
+import { Routes } from "../../navigation";
+import firebase, { projectsRef } from "../../config/firebase";
+import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
+import moment from "moment";
+import { responsiveHeight } from "react-native-responsive-dimensions";
+import { FONT_FAMILY } from "../../styles/Fonts";
+import useDataFromRef from "../../hooks/useDataFromRef";
+
+const GeneratingSong = ({ route }) => {
+ const { config, projectId } = route.params;
+ const [progress, setProgress] = useState(0);
+ const { setIsLoading } = useMinuit();
+ const progressTimerRef = useRef(null);
+ const navigatedRef = useRef(false);
+
+ const { data: project } = useDataFromRef({
+ ref: projectId ? projectsRef.doc(projectId) : null,
+ simpleRef: true,
+ listener: true,
+ condition: !!projectId,
+ refreshArray: [projectId],
+ });
+
+ // Progress based on 8 minutes cap or until status changes
+ useEffect(() => {
+ const totalMs = 8 * 60 * 1000;
+ const clearTimer = () => {
+ if (progressTimerRef.current) {
+ clearInterval(progressTimerRef.current);
+ progressTimerRef.current = null;
+ }
+ };
+ if (project?.musicStatus !== "GENERATING") {
+ setProgress(100);
+ clearTimer();
+ return () => clearTimer();
+ }
+ const update = () => {
+ const startDate = project?.generationStartAt?.toDate
+ ? project.generationStartAt.toDate()
+ : new Date(project?.generationStartAt || Date.now());
+ const elapsed = moment().diff(moment(startDate));
+ const raw = Math.floor((elapsed / totalMs) * 100);
+ // While status is GENERATING, block visual progress at 99%
+ const pct = Math.max(0, Math.min(99, raw));
+ setProgress(pct);
+ };
+ update();
+ clearTimer();
+ progressTimerRef.current = setInterval(update, 1000);
+ return () => clearTimer();
+ }, [project?.musicStatus]);
+
+ // Auto navigate to SongReady when generation completed
+ useEffect(() => {
+ if (!config?.projectId) return;
+ if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) {
+ navigatedRef.current = true;
+ navigate(Routes.SongReady, { projectId: config.projectId });
+ }
+ }, [project?.musicStatus, config?.projectId]);
+
+ async function startMusicGeneration() {
+ try {
+ console.log("startMusicGeneration");
+ await setIsLoading(true);
+ const callable = firebase
+ .functions()
+ .httpsCallable("music-generateMusic");
+ const { data } = await callable({
+ title: config?.title,
+ lyrics: config?.lyrics,
+ genres: config?.genres,
+ voice: config?.voice,
+ instruments: config?.instruments,
+ tempo: config?.tempo,
+ projectId: config?.projectId,
+ });
+ const taskId =
+ data?.response?.data?.taskId || data?.response?.data?.task_id;
+ if (config?.projectId && taskId) {
+ const baseUpdate = {
+ sunoTaskId: taskId,
+ musicStatus: "GENERATING",
+ generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ };
+ const updatePayload = project?.musicConfig
+ ? baseUpdate
+ : {
+ ...baseUpdate,
+ musicConfig: {
+ title: config?.title || "",
+ lyrics: config?.lyrics || [],
+ genres: config?.genres || [],
+ voice: config?.voice || "",
+ instruments: config?.instruments || [],
+ tempo: config?.tempo || "",
+ },
+ };
+ await projectsRef
+ .doc(config.projectId)
+ .set(updatePayload, { merge: true });
+ }
+ } catch (e) {
+ console.log("GeneratingSong error", e?.message);
+ } finally {
+ await setIsLoading(false);
+ }
+ }
+
+ // Trigger generation only if not already GENERATING
+ useEffect(() => {
+ if (!!project?.title && project?.musicStatus !== "GENERATING") {
+ startMusicGeneration();
+ }
+ }, [project]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Ta musique est{"\n"}en cours de création
+
+
+
+
+ {progress}%
+
+
+
+ navigate(Routes.SongReady, {
+ projectId: config?.projectId,
+ })
+ }
+ />
+
+
+
+
+
+
+
+ );
+};
+
+export default GeneratingSong;
diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js
index b22102d..9988aa4 100644
--- a/src/screens/Studio/SongReady.js
+++ b/src/screens/Studio/SongReady.js
@@ -13,14 +13,12 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import ValidateModal from "../../components/modal/ValidateModal";
-import { useRoute } from "@react-navigation/core";
-import firebase from "../../config/firebase";
+import firebase, { projectsRef } from "../../config/firebase";
import { useAudioPlayer } from "expo-audio";
import { responsiveHeight } from "react-native-responsive-dimensions";
-const SongReady = () => {
- const params = useRoute().params || {};
- const projectId = params?.projectId;
+const SongReady = ({ route }) => {
+ const { projectId = null } = route?.params || {};
const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -35,6 +33,7 @@ const SongReady = () => {
const player1 = useAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
);
+ const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
// Charger les URLs depuis le document projet
useEffect(() => {
@@ -118,18 +117,14 @@ const SongReady = () => {
try {
const url = musicUrls[selectedIndex];
if (!projectId || !url) return;
- await firebase
- .firestore()
- .collection("projects")
- .doc(projectId)
- .set(
- {
- songUrl: url,
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
- },
- { merge: true },
- );
- navigate(Routes.PouchReady, { projectId });
+ await projectsRef.doc(projectId).set(
+ {
+ songUrl: url,
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ },
+ { merge: true },
+ );
+ navigate(Routes.FlowSelection, { projectId });
} catch (e) {
console.log("Validate error", e?.message);
}
@@ -137,36 +132,7 @@ const SongReady = () => {
const onPressRegenerate = async () => {
try {
- if (!projectId) return goBack();
- const doc = await firebase
- .firestore()
- .collection("projects")
- .doc(projectId)
- .get();
- const project = doc.data() || {};
- const musicConfig = project?.musicConfig || {};
-
- const callable = firebase
- .functions()
- .httpsCallable("music-generateMusic");
- const { data } = await callable({
- ...musicConfig,
- projectId,
- });
-
- const taskId =
- data?.response?.data?.taskId || data?.response?.data?.task_id;
- if (taskId) {
- await firebase.firestore().collection("projects").doc(projectId).set(
- {
- sunoTaskId: taskId,
- musicStatus: "GENERATING",
- generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
- },
- { merge: true },
- );
- }
+ navigate(Routes.GeneratingSong, { projectId });
} catch (e) {
console.log("Regenerate error", e?.message);
} finally {
@@ -176,7 +142,10 @@ const SongReady = () => {
return (
-
+ navigate(Routes.FlowSelection, { projectId })}
+ progress={63}
+ />
{
: 0
}
seekEnabled={!!musicUrls[idx]}
+ onSeekStart={async () => {
+ try {
+ const player = idx === 0 ? player0 : player1;
+ wasPlayingBeforeSeek.current[idx] = !!player?.playing;
+ if (player?.playing) {
+ await player.pause?.();
+ setIsPlaying((p) => ({ ...p, [idx]: false }));
+ }
+ } catch (e) {
+ console.log(
+ "SongReady pause on seek start",
+ e?.message,
+ );
+ }
+ }}
onSeek={(ratio) => onSeek(idx, ratio)}
+ onSeekEnd={async () => {
+ try {
+ const player = idx === 0 ? player0 : player1;
+ if (player && wasPlayingBeforeSeek.current[idx]) {
+ await player.play?.();
+ setIsPlaying((p) => ({ ...p, [idx]: true }));
+ }
+ wasPlayingBeforeSeek.current[idx] = false;
+ } catch (e) {
+ console.log("SongReady resume after seek", e?.message);
+ }
+ }}
/>