diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js
index 849cc3d..35091a8 100644
--- a/functions/src/lyrics.js
+++ b/functions/src/lyrics.js
@@ -5,21 +5,13 @@ const { generateAI } = require("../index");
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
try {
const {
- objective = "Célébrer l'agence Minuit et mettre en avant son " +
- "expertise digitale, son esprit d'équipe et sa créativité.",
- context = "L'agence Minuit accompagne les startups et entreprises " +
- "innovantes dans la création de produits digitaux, du prototype " +
- "à la version de financement, jusqu'à l'optimisation et la mise " +
- "à l'échelle. Spécialisée dans le développement sur-mesure " +
- "d'applications mobiles, elle valorise l'humain, le design et " +
- "l'accompagnement personnalisé. Esprit nocturne, équipe passionnée.",
- emotion = "La Joie : expose un bonheur profond, l'émerveillement, " +
- "la gratitude, satisfaction intense, énergie positive.",
- style = "Upbeat : Pour une ambiance joyeuse et rythmée.",
- audience = "L'équipe Minuit et ses clients fidèles, startups " +
- "ambitieuses et partenaires visionnaires.",
+ objective = "",
+ context = "",
+ style = "",
+ audience = "",
+ emotion = "",
structure = ["couplet", "refrain", "couplet", "refrain"],
- rhymes = "Avec rimes",
+ rhymes = "",
} = data;
console.log("Function generate lyrics start with data", data);
@@ -69,19 +61,19 @@ Pour chaque ` +
.string()
.describe(
'Type de section : "couplet" ou "refrain" ' +
- "selon la structure"
+ "selon la structure",
),
lyrics: z
.string()
.describe(
"Paroles de la section, chaque ligne séparée " +
- "par un retour à la ligne"
+ "par un retour à la ligne",
),
- })
+ }),
)
.describe(
"Paroles de la chanson sous forme de tableau de sections " +
- "structurées."
+ "structurées.",
),
success: z.boolean().describe("Indique si la génération a réussi"),
});
diff --git a/src/assets/index.js b/src/assets/index.js
index 0657576..119c57d 100644
--- a/src/assets/index.js
+++ b/src/assets/index.js
@@ -230,6 +230,10 @@ export const ai = {
john,
};
+export const videos = {
+ test: require("./video/testVideo.mp4"),
+};
+
export const img = {
placeholder,
placeholder2,
diff --git a/src/assets/video/testVideo.mp4 b/src/assets/video/testVideo.mp4
new file mode 100644
index 0000000..5b5d54e
Binary files /dev/null and b/src/assets/video/testVideo.mp4 differ
diff --git a/src/components/FullscreenIntroVideo.js b/src/components/FullscreenIntroVideo.js
new file mode 100644
index 0000000..852b583
--- /dev/null
+++ b/src/components/FullscreenIntroVideo.js
@@ -0,0 +1,88 @@
+import React, { useEffect } from "react";
+import { View, Pressable, Text } from "react-native";
+import { VideoView, useVideoPlayer } from "expo-video";
+import { videos } from "../assets";
+import { Portal } from "@gorhom/portal";
+
+// Fullscreen vertical video overlay without controls
+// Props:
+// - url?: string | number (require), source of the video. Defaults to videos.test
+// - visible?: boolean, when false returns null
+// - onClose: () => void, called when user skips or when video ends
+const FullscreenIntroVideo = ({ url, visible = true, onClose }) => {
+ if (!visible) return null;
+ const source = url
+ ? typeof url === "string"
+ ? { uri: url }
+ : url
+ : videos.test;
+
+ const player = useVideoPlayer(source, (p) => {
+ p.loop = false;
+ p.timeUpdateEventInterval = 0.25;
+ });
+
+ useEffect(() => {
+ try {
+ player?.play?.();
+ } catch (e) {}
+ }, [player]);
+
+ useEffect(() => {
+ if (!player) return;
+ const sub = player.addListener?.("playToEnd", () => {
+ try {
+ onClose?.();
+ } catch (e) {}
+ });
+ return () => {
+ try {
+ sub?.remove?.();
+ } catch (e) {}
+ };
+ }, [player, onClose]);
+
+ return (
+
+
+
+ onClose?.()}
+ style={{
+ position: "absolute",
+ top: 50,
+ right: 20,
+ backgroundColor: "#00000080",
+ paddingVertical: 10,
+ paddingHorizontal: 14,
+ borderRadius: 20,
+ borderWidth: 1,
+ borderColor: "#FFFFFF55",
+ }}
+ >
+ Passer la vidéo
+
+
+
+ );
+};
+
+export default FullscreenIntroVideo;
diff --git a/src/config/firebase.js b/src/config/firebase.js
index d6ec54a..0b3ba34 100644
--- a/src/config/firebase.js
+++ b/src/config/firebase.js
@@ -48,6 +48,7 @@ try {
export const usersRef = firestore.collection("users");
export const projectsRef = firestore.collection("projects");
+export const tasksRef = firestore.collection("tasks");
export const playlistsRef = firestore.collection("playlists");
export const chatsRef = firestore.collection("chats");
diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js
index c28162d..17c519e 100644
--- a/src/navigation/MainStack.js
+++ b/src/navigation/MainStack.js
@@ -26,10 +26,10 @@ import Compose from "../screens/Studio/Compose";
import CustomizeVoice from "../screens/Studio/CustomizeVoice";
import SongReady from "../screens/Studio/SongReady";
import Regenerate from "../screens/Studio/Regenerate";
-import PouchReady from "../screens/Studio/PouchReady";
-import PhotoCover from "../screens/Studio/PhotoCover";
-import AddPhotoCover from "../screens/Studio/AddPhotoCover";
-import FinishCompose from "../screens/Studio/FinishCompose";
+import PouchReady from "../screens/cover/PouchReady";
+import PhotoCover from "../screens/cover/PhotoCover";
+import AddPhotoCover from "../screens/cover/AddPhotoCover";
+import FinishCompose from "../screens/cover/FinishCompose";
import Production from "../screens/Production/Production";
import ProductionOnboarding from "../screens/Production/ProductionOnboarding";
import DownloadSongs from "../screens/Production/DownloadSongs";
@@ -64,6 +64,7 @@ import NewMusicOptions from "../screens/NewMusicOptions";
import Register from "../screens/Register";
import CreatePassword from "../screens/CreatePassword";
import CreatePseudo from "../screens/CreatePseudo";
+import ChooseCoverType from "../screens/cover/ChooseCoverType";
const screenOptions = {
headerShown: false,
@@ -302,6 +303,10 @@ const screens = [
name: Routes.CreatePseudo,
component: CreatePseudo,
},
+ {
+ name: Routes.ChooseCoverType,
+ component: ChooseCoverType,
+ },
];
export default function Main() {
diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js
index 9eb66de..6936f2b 100644
--- a/src/navigation/Routes.js
+++ b/src/navigation/Routes.js
@@ -82,4 +82,5 @@ export const Routes = {
Reels: "Reels",
Follows: "Follows",
FlowSelection: "FlowSelection",
+ ChooseCoverType: "ChooseCoverType",
};
diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js
index a18f19b..1cd0dec 100644
--- a/src/providers/UserDataProvider.js
+++ b/src/providers/UserDataProvider.js
@@ -90,6 +90,62 @@ export default ({ children }) => {
...newData,
}));
};
+
+ // Selected project state and helpers
+ const [selectedProjectId, setSelectedProjectId] = useState(null);
+ const { data: selectedProject = null, setData: setSelectedProject } =
+ useDataFromRef({
+ ref: selectedProjectId ? projectsRef.doc(selectedProjectId) : null,
+ simpleRef: true,
+ listener: true,
+ condition: !!selectedProjectId,
+ refreshArray: [selectedProjectId],
+ });
+
+ const resetSelectedProject = () => {
+ setSelectedProjectId(null);
+ setSelectedProject(null);
+ };
+ const selectProject = (projectId) => setSelectedProjectId(projectId || null);
+
+ // Ancienne variante de création de projet supprimée pour éviter les doublons.
+
+ const updateProjectData = async (partial = {}, options = { merge: true }) => {
+ if (!selectedProjectId) return;
+ try {
+ await projectsRef.doc(selectedProjectId).set(
+ {
+ ...partial,
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ },
+ options,
+ );
+ } catch (e) {
+ console.log("updateProjectData error", e?.message);
+ throw e;
+ }
+ };
+
+ const createNewProject = async ({ hasLyrics = false } = {}) => {
+ try {
+ await setIsLoading(true);
+ const user = firebase.auth().currentUser;
+ const payload = {
+ userId: user ? user.uid : currentUID || null,
+ hasLyrics: !!hasLyrics,
+ createdAt: firebase.firestore.FieldValue.serverTimestamp(),
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ };
+ const { id } = await projectsRef.add(payload);
+ setSelectedProjectId(id);
+ return id;
+ } catch (e) {
+ console.log("createNewProject error", e?.message);
+ throw e;
+ } finally {
+ await setIsLoading(false);
+ }
+ };
const followUser = async (userId) => {
try {
if (currentUID && userId) {
@@ -98,7 +154,7 @@ export default ({ children }) => {
followedBy: arrayUnion(currentUID),
lastFollowersUpdateAt: new Date(),
},
- { merge: true }
+ { merge: true },
);
setTooltip({ type: "success", text: "Abonnement mis à jour" });
}
@@ -116,7 +172,7 @@ export default ({ children }) => {
followedBy: arrayRemove(currentUID),
lastFollowersUpdateAt: new Date(),
},
- { merge: true }
+ { merge: true },
);
setTooltip({
type: "success",
@@ -248,6 +304,8 @@ export default ({ children }) => {
userPlaybacks,
userLikedProjects,
userPlaylists,
+ selectedProjectId,
+ selectedProject,
followingCount: userFollowing?.length || 0,
setCurrentUserData,
@@ -260,6 +318,11 @@ export default ({ children }) => {
updateUserData,
followUser,
unfollowUser,
+ resetSelectedProject,
+ selectProject,
+ setSelectedProjectId,
+ updateProjectData,
+ createNewProject,
}}
>
{children}
diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js
index efe683a..4d779fc 100644
--- a/src/screens/HitParade/HitParade.js
+++ b/src/screens/HitParade/HitParade.js
@@ -68,7 +68,8 @@ const HitParade = () => {
justifyContent: "space-between",
}}
>
- {["Chansons", "Playbacks", "Clips"].map((item, index) => (
+ {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
+ {["Chansons", "Playbacks"].map((item, index) => (
{
- const { userProjects = [] } = useUser();
+ const { userProjects = [], resetSelectedProject, selectProject } = useUser();
const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []),
[userProjects],
@@ -25,7 +25,7 @@ const Home = () => {
const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false);
- const [selectedProjectId, setSelectedProjectId] = useState(null);
+ const [menuProjectId, setMenuProjectId] = useState(null);
return (
@@ -77,11 +77,12 @@ const Home = () => {
imageUri={item?.coverUrl || null}
projectId={item?.id}
likedBy={item?.likedBy || []}
- onPress={() =>
- navigate(Routes.FlowSelection, { projectId: item.id })
- }
+ onPress={() => {
+ selectProject(item.id);
+ navigate(Routes.FlowSelection);
+ }}
onPressMore={(posTop) => {
- setSelectedProjectId(item.id);
+ setMenuProjectId(item.id);
setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop);
}}
@@ -93,7 +94,7 @@ const Home = () => {
top={menuTop}
onClose={() => setShowMenu(false)}
inPlaylist={false}
- projectId={selectedProjectId}
+ projectId={menuProjectId}
extraItems={[
{
label: "Supprimer",
@@ -108,9 +109,9 @@ const Home = () => {
style: "destructive",
onPress: async () => {
try {
- if (!selectedProjectId) return;
+ if (!menuProjectId) return;
await projectsRef
- .doc(selectedProjectId)
+ .doc(menuProjectId)
.delete();
setTooltip({
type: "success",
@@ -137,7 +138,10 @@ const Home = () => {
navigate(Routes.FlowSelection)}
+ onPress={() => {
+ resetSelectedProject();
+ navigate(Routes.FlowSelection);
+ }}
/>
diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js
index 1669d84..ceacd1f 100644
--- a/src/screens/NewMusicOptions.js
+++ b/src/screens/NewMusicOptions.js
@@ -1,6 +1,6 @@
import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BlurView } from "expo-blur";
-import React, { useMemo } from "react";
+import React from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { ai, background } from "../assets";
import Page from "../layouts/Page";
@@ -42,69 +42,62 @@ const CREATE_DATA = [
},
];
-const NewMusicOptions = ({ route }) => {
- const { userProjects } = useUserData();
- const { projectId = null } = route?.params || {};
-
- const currentProjet = useMemo(() => {
- if (!projectId) {
- return null;
- }
- return userProjects?.find((p) => p.id === projectId) || null;
- }, [projectId, userProjects]);
-
- const hasProject = !!currentProjet;
- const hasLyrics = Array.isArray(currentProjet?.lyrics)
- ? currentProjet.lyrics.length > 0
- : !!currentProjet?.lyrics;
- const hasCover = !!currentProjet?.coverUrl;
- console.log("current projet cover", currentProjet?.coverUrl);
+const NewMusicOptions = () => {
+ const { selectedProject } = useUserData();
+ // Lock rules per option index:
+ // 0 (Songwriter): allowed if no project OR no songUrl
+ // 1 (Beatmaker): allowed only if lyrics exist and no songUrl
+ // 2 (Designer): allowed only if songUrl exists and no coverUrl
+ // 3 (Director): allowed only if coverUrl exists
const isLocked = (index) => {
- // 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
- // Rules:
- // - If no project: only Songwriter (0) is available.
- // - If project exists: Songwriter (0) is always available.
- // - If lyrics exist: Beatmaker (1) becomes available.
- // - If cover exists: Producer (2) and Director (3) become available.
- if (!hasProject) return index !== 0;
+ const songUrl = selectedProject?.songUrl || null;
+ const coverUrl = selectedProject?.coverUrl || null;
+ const playbackUrl = selectedProject?.songUrl || null;
+ const lyricsLen = Array.isArray(selectedProject?.lyrics)
+ ? selectedProject.lyrics.length
+ : 0;
- const allowed = new Set([0]); // songwriter always allowed when project exists
- if (hasLyrics) {
- allowed.add(1);
+ switch (index) {
+ case 0: // Songwriter
+ return !!songUrl; // locked if a song already exists
+ case 1: // Beatmaker
+ return !(lyricsLen > 0 && !songUrl);
+ case 2: // Designer
+ return !(!!songUrl && !!playbackUrl);
+ case 3: // Director
+ return !!!coverUrl;
+ default:
+ return true;
}
- if (hasCover) {
- allowed.add(2);
- allowed.add(3);
- }
-
- return !allowed.has(index);
};
const onPressOption = (index, item) => {
if (isLocked(index)) return;
switch (index) {
case 0:
- navigate(Routes.WritingLyrics, {
- projectId: currentProjet?.id || null,
- });
+ if (selectedProject?.lyrics?.length) {
+ navigate(Routes.Lyrics);
+ } else {
+ navigate(Routes.WritingLyrics);
+ }
break;
case 1:
- if (currentProjet?.musicStatus === "GENERATING") {
- navigate(Routes.GeneratingSong, { projectId: currentProjet.id });
- } else if (currentProjet?.musicStatus === "GENERATED") {
- navigate(Routes.SongReady, { projectId: currentProjet.id });
+ if (selectedProject?.musicStatus === "GENERATING") {
+ navigate(Routes.GeneratingSong);
+ } else if (selectedProject?.musicStatus === "GENERATED") {
+ navigate(Routes.SongReady);
} else {
- navigate(Routes.Compose, { projectId: currentProjet.id });
+ navigate(Routes.Compose);
}
break;
case 2:
- navigate(Routes.PouchReady, { projectId: currentProjet.id });
+ navigate(Routes.ChooseCoverType);
break;
case 3:
console.log("test");
navigate(Routes.Playback, {
- project: currentProjet,
+ project: selectedProject,
});
default:
break;
@@ -115,7 +108,7 @@ const NewMusicOptions = ({ route }) => {
diff --git a/src/screens/Playback/Playback.js b/src/screens/Playback/Playback.js
index 21638df..c785460 100644
--- a/src/screens/Playback/Playback.js
+++ b/src/screens/Playback/Playback.js
@@ -1,4 +1,4 @@
-import React, { useCallback } from "react";
+import React, { useCallback, useState } from "react";
import { Alert, Image, StyleSheet, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, background } from "../../assets";
@@ -10,6 +10,7 @@ import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
+import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const Playback = ({ route }) => {
const { project } = route.params;
@@ -17,6 +18,7 @@ const Playback = ({ route }) => {
const songIndex = project?.songIndex;
const sunoTaskId = project?.sunoTaskId;
const { setIsLoading } = useMinuit();
+ const [showIntro, setShowIntro] = useState(false);
const onPressRecord = useCallback(async () => {
try {
@@ -43,7 +45,7 @@ const Playback = ({ route }) => {
console.log("getSunoTimestamps failed", data?.error || data);
Alert.alert(
"Timestamps",
- "Impossible de récupérer les timestamps pour ce playback pour le moment. Tu peux quand même continuer."
+ "Impossible de récupérer les timestamps pour ce playback pour le moment. Tu peux quand même continuer.",
);
}
@@ -53,7 +55,7 @@ const Playback = ({ route }) => {
console.log("getSunoTimestamps error", e?.message || e);
Alert.alert(
"Timestamps",
- "Une erreur est survenue lors de la génération des timestamps. Tu peux quand même continuer."
+ "Une erreur est survenue lors de la génération des timestamps. Tu peux quand même continuer.",
);
await setIsLoading(false);
}
@@ -71,7 +73,10 @@ const Playback = ({ route }) => {
}}
>
-
+ setShowIntro(true)}
+ />
{/* */}
{
/>
+ setShowIntro(false)}
+ />
);
};
diff --git a/src/screens/Profile/Profile.js b/src/screens/Profile/Profile.js
index 7a6ff07..481b163 100644
--- a/src/screens/Profile/Profile.js
+++ b/src/screens/Profile/Profile.js
@@ -65,7 +65,7 @@ const Profile = () => {
setFollowers(
Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy.length
- : 0
+ : 0,
);
return;
}
@@ -367,7 +367,8 @@ const Profile = () => {
gap: 6,
}}
>
- {["Chansons", "Playbacks", "Clips"].map((item, index) => (
+ {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
+ {["Chansons", "Playbacks"].map((item, index) => (
onPressMenu(item)}
diff --git a/src/screens/Studio/Compose.js b/src/screens/Studio/Compose.js
index fd79684..cf6d7f9 100644
--- a/src/screens/Studio/Compose.js
+++ b/src/screens/Studio/Compose.js
@@ -1,21 +1,20 @@
-import { View, Text, StyleSheet, Image } from "react-native";
-import React from "react";
+import { View, StyleSheet, Image } from "react-native";
+import React, { useState } from "react";
import { ai, background } from "../../assets";
import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService";
-import { useRoute } from "@react-navigation/native";
import MusicLandHeader from "../../components/MusicLandHeader";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles";
+import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const Compose = () => {
- const route = useRoute();
- const projectId = route?.params?.projectId;
+ const [showIntro, setShowIntro] = useState(true);
return (
-
+
@@ -28,10 +27,14 @@ const Compose = () => {
>
navigate(Routes.ComposeSong, { projectId })}
+ onPress={() => navigate(Routes.ComposeSong)}
/>
+ setShowIntro(false)}
+ />
);
};
diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js
index edb41e9..2038d03 100644
--- a/src/screens/Studio/ComposeSong.js
+++ b/src/screens/Studio/ComposeSong.js
@@ -11,9 +11,8 @@ import ChooseGenre from "./ChooseGenre";
import CustomizeVoice from "./CustomizeVoice";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
-import { useRoute } from "@react-navigation/native";
import { projectsRef } from "../../config/firebase";
-import useDataFromRef from "../../hooks/useDataFromRef";
+import { useUser } from "../../providers/UserDataProvider";
import { Routes } from "../../navigation";
const { width } = Dimensions.get("window");
@@ -23,8 +22,7 @@ const ComposeSong = () => {
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18);
const [containerLayout, setContainerLayout] = useState(null);
- const route = useRoute();
- const projectId = route?.params?.projectId;
+ const { selectedProjectId, selectedProject, updateProjectData } = useUser();
// Selections state
const [genres, setGenres] = useState([]);
@@ -32,14 +30,6 @@ const ComposeSong = () => {
const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null);
- // Fetch selected project to get title + lyrics
- const { data: project } = useDataFromRef({
- ref: projectId ? projectsRef.doc(projectId) : null,
- simpleRef: true,
- listener: true,
- condition: !!projectId,
- });
-
const isStepValid = useMemo(() => {
switch (selectedIndex) {
case 0:
@@ -57,31 +47,54 @@ const ComposeSong = () => {
const musicConfig = useMemo(() => {
let lyricsArr = [];
- if (Array.isArray(project?.lyrics)) {
- lyricsArr = project.lyrics.map((s) => ({
+ if (Array.isArray(selectedProject?.lyrics)) {
+ lyricsArr = selectedProject.lyrics.map((s) => ({
type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "",
}));
} else {
- const c = project?.lyrics?.couplet;
- const r = project?.lyrics?.refrain;
+ 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 });
}
return {
- title: project?.title || "",
+ title: selectedProject?.title || "",
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voice || undefined,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
- projectId: projectId || undefined,
+ projectId: selectedProjectId || undefined,
};
- }, [project, genres, voice, instruments, rhythm, projectId]);
+ }, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
- const onPressNext = () => {
+ const onPressNext = async () => {
if (selectedIndex === 3) {
- navigate(Routes.GeneratingSong, { config: musicConfig, projectId });
+ try {
+ // Persist config on the selected selectedProject so GeneratingSong can pick it up
+ if (selectedProjectId) {
+ await updateProjectData({
+ musicConfig: {
+ title: musicConfig?.title || "",
+ lyrics: Array.isArray(musicConfig?.lyrics)
+ ? musicConfig.lyrics
+ : [],
+ genres: Array.isArray(musicConfig?.genres)
+ ? musicConfig.genres
+ : [],
+ voice: musicConfig?.voice || "",
+ instruments: Array.isArray(musicConfig?.instruments)
+ ? musicConfig.instruments
+ : [],
+ tempo: musicConfig?.tempo || "",
+ },
+ musicStatus: null,
+ });
+ }
+ } catch (e) {}
+ // Also pass the config to the screen to avoid any race condition
+ navigate(Routes.GeneratingSong, { config: musicConfig });
return;
}
setSelectedIndex(selectedIndex + 1);
diff --git a/src/screens/Studio/CreatingSong.js b/src/screens/Studio/CreatingSong.js
index 78dfd50..37a0848 100644
--- a/src/screens/Studio/CreatingSong.js
+++ b/src/screens/Studio/CreatingSong.js
@@ -7,6 +7,7 @@ import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ProgressBar from "../../components/ProgressBar";
import { goBack, navigate } from "../../navigation/NavigationService";
+import { useUser } from "../../providers/UserDataProvider";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import firebase, { projectsRef } from "../../config/firebase";
@@ -21,6 +22,7 @@ const CreatingSong = ({ active, config }) => {
const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null);
const progressTimerRef = React.useRef(null);
+ const { selectedProjectId } = useUser();
// Abonnement au document projet pour suivre le statut et la date de début
useEffect(() => {
@@ -236,9 +238,7 @@ const CreatingSong = ({ active, config }) => {
width: "80%",
alignSelf: "center",
}}
- onPress={() =>
- navigate(Routes.SongReady, { projectId: config?.projectId })
- }
+ onPress={() => navigate(Routes.SongReady)}
/>
diff --git a/src/screens/Studio/GeneratingSong.js b/src/screens/Studio/GeneratingSong.js
index ab4d4ad..49dad27 100644
--- a/src/screens/Studio/GeneratingSong.js
+++ b/src/screens/Studio/GeneratingSong.js
@@ -1,5 +1,5 @@
-import React, { useEffect, useRef, useState } from "react";
-import { Image, Platform, Text, View } from "react-native";
+import React, { useEffect, useMemo, useRef, useState } from "react";
+import { Alert, Image, Platform, Text, View } from "react-native";
import Page from "../../layouts/Page";
import { ai, background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -15,24 +15,22 @@ 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";
+import { useUser } from "../../providers/UserDataProvider";
+import { useIsFocused } from "@react-navigation/native";
-const GeneratingSong = ({ route }) => {
- const { config, projectId } = route.params;
+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 lastConfigKeyRef = useRef(null);
+ const askedRef = useRef(false);
- const { data: project } = useDataFromRef({
- ref: projectId ? projectsRef.doc(projectId) : null,
- simpleRef: true,
- listener: true,
- condition: !!projectId,
- refreshArray: [projectId],
- });
+ // project loaded from provider
- // Progress based on 8 minutes cap or until status changes
+ // Progress based on 8 minutes cap or until status becomes GENERATED
useEffect(() => {
const totalMs = 8 * 60 * 1000;
const clearTimer = () => {
@@ -41,15 +39,15 @@ const GeneratingSong = ({ route }) => {
progressTimerRef.current = null;
}
};
- if (project?.musicStatus !== "GENERATING") {
+ if (selectedProject?.musicStatus === "GENERATED") {
setProgress(100);
clearTimer();
return () => clearTimer();
}
const update = () => {
- const startDate = project?.generationStartAt?.toDate
- ? project.generationStartAt.toDate()
- : new Date(project?.generationStartAt || Date.now());
+ const startDate = selectedProject?.generationStartAt?.toDate
+ ? selectedProject.generationStartAt.toDate()
+ : new Date(selectedProject?.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%
@@ -60,16 +58,29 @@ const GeneratingSong = ({ route }) => {
clearTimer();
progressTimerRef.current = setInterval(update, 1000);
return () => clearTimer();
- }, [project?.musicStatus]);
+ }, [selectedProject?.musicStatus]);
// Auto navigate to SongReady when generation completed
useEffect(() => {
- if (!config?.projectId) return;
- if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) {
+ if (!selectedProjectId) return;
+ if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) {
navigatedRef.current = true;
- navigate(Routes.SongReady, { projectId: config.projectId });
+ navigate(Routes.SongReady);
}
- }, [project?.musicStatus, config?.projectId]);
+ }, [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: cfg?.voice || "",
+ instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [],
+ tempo: cfg?.tempo || "",
+ };
+ }, [selectedProject?.musicConfig]);
async function startMusicGeneration() {
try {
@@ -78,39 +89,39 @@ const GeneratingSong = ({ route }) => {
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
+ const cfg = effectiveConfig || {};
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,
+ title: cfg?.title,
+ lyrics: cfg?.lyrics,
+ genres: cfg?.genres,
+ voice: cfg?.voice,
+ instruments: cfg?.instruments,
+ tempo: cfg?.tempo,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
- if (config?.projectId && taskId) {
+ if (selectedProjectId && taskId) {
const baseUpdate = {
sunoTaskId: taskId,
musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
};
- const updatePayload = project?.musicConfig
+ const updatePayload = selectedProject?.musicConfig
? baseUpdate
: {
...baseUpdate,
musicConfig: {
- title: config?.title || "",
- lyrics: config?.lyrics || [],
- genres: config?.genres || [],
- voice: config?.voice || "",
- instruments: config?.instruments || [],
- tempo: config?.tempo || "",
+ title: effectiveConfig?.title || "",
+ lyrics: effectiveConfig?.lyrics || [],
+ genres: effectiveConfig?.genres || [],
+ voice: effectiveConfig?.voice || "",
+ instruments: effectiveConfig?.instruments || [],
+ tempo: effectiveConfig?.tempo || "",
},
};
await projectsRef
- .doc(config.projectId)
+ .doc(selectedProjectId)
.set(updatePayload, { merge: true });
}
} catch (e) {
@@ -120,16 +131,44 @@ const GeneratingSong = ({ route }) => {
}
}
- // Trigger generation only if not already GENERATING
+ // Trigger generation only when focused; ask once per config
useEffect(() => {
- if (!!project?.title && project?.musicStatus !== "GENERATING") {
- startMusicGeneration();
+ if (!isFocused) return;
+
+ const key = JSON.stringify(effectiveConfig || {});
+ if (lastConfigKeyRef.current !== key) {
+ lastConfigKeyRef.current = key;
+ askedRef.current = false;
}
- }, [project]);
+
+ const canAsk =
+ !!selectedProject?.title && selectedProject?.musicStatus !== "GENERATING";
+
+ if (canAsk && !askedRef.current) {
+ askedRef.current = true;
+ Alert.alert(
+ "Attention",
+ "Une génération va être lancée. Continuer ?",
+ [
+ {
+ text: "Non",
+ style: "cancel",
+ },
+ {
+ text: "Oui",
+ onPress: () => startMusicGeneration(),
+ },
+ ],
+ );
+ }
+ }, [isFocused, selectedProject?.title, selectedProject?.musicStatus, effectiveConfig]);
return (
-
+ navigate(Routes.FlowSelection)}
+ progress={63}
+ />
{
- navigate(Routes.SongReady, {
- projectId: config?.projectId,
- })
+ 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)}
/>
diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js
index 584e7db..eeaadea 100644
--- a/src/screens/Studio/SongReady.js
+++ b/src/screens/Studio/SongReady.js
@@ -1,6 +1,6 @@
import { useAudioPlayer } from "expo-audio";
import { BlurView } from "expo-blur";
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState, useCallback } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
@@ -9,16 +9,22 @@ import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
-import firebase, { projectsRef } from "../../config/firebase";
+import firebase from "../../config/firebase";
+import { useUser } from "../../providers/UserDataProvider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
-import { goBack, navigate } from "../../navigation/NavigationService";
+import { navigate } from "../../navigation/NavigationService";
import { Style } from "../../styles";
import { gutters, size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
+import { useFocusEffect } from "@react-navigation/native";
-const SongReady = ({ route }) => {
- const { projectId = null } = route?.params || {};
+const SongReady = () => {
+ const {
+ selectedProjectId: projectId,
+ selectedProject,
+ updateProjectData,
+ } = useUser();
const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -28,29 +34,20 @@ const SongReady = ({ route }) => {
1: { pos: 0, dur: 0 },
});
const player0 = useAudioPlayer(
- musicUrls[0] ? { uri: musicUrls[0] } : undefined
+ musicUrls[0] ? { uri: musicUrls[0] } : undefined,
);
const player1 = useAudioPlayer(
- musicUrls[1] ? { uri: musicUrls[1] } : undefined
+ musicUrls[1] ? { uri: musicUrls[1] } : undefined,
);
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
- // Charger les URLs depuis le document projet
+ // Sync URLs from provider's selectedProject
useEffect(() => {
- if (!projectId) return;
- const unsub = firebase
- .firestore()
- .collection("projects")
- .doc(projectId)
- .onSnapshot((doc) => {
- const data = doc.data() || {};
- const urls = Array.isArray(data?.musicUrls)
- ? data.musicUrls.slice(0, 2)
- : [];
- setMusicUrls(urls);
- });
- return () => unsub?.();
- }, [projectId]);
+ const urls = Array.isArray(selectedProject?.musicUrls)
+ ? selectedProject.musicUrls.slice(0, 2)
+ : [];
+ setMusicUrls(urls);
+ }, [selectedProject?.musicUrls]);
// Sync progression depuis les players
useEffect(() => {
@@ -117,34 +114,50 @@ const SongReady = ({ route }) => {
try {
const url = musicUrls[selectedIndex];
if (!projectId || !url) return;
- await projectsRef.doc(projectId).set(
- {
- songIndex: selectedIndex,
- songUrl: url,
- updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
- },
- { merge: true }
- );
- navigate(Routes.FlowSelection, { projectId });
+ await updateProjectData({
+ songIndex: selectedIndex,
+ songUrl: url,
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ });
+ await player0?.pause?.();
+ await player1?.pause?.();
+ navigate(Routes.FlowSelection);
} catch (e) {
console.log("Validate error", e?.message);
}
};
- const onPressRegenerate = async () => {
- try {
- navigate(Routes.GeneratingSong, { projectId });
- } catch (e) {
- console.log("Regenerate error", e?.message);
- } finally {
- goBack();
- }
- };
+ // Pause audio when screen loses focus (navigate/reset) and on unmount
+ useFocusEffect(
+ useCallback(() => {
+ return () => {
+ try {
+ player0?.pause?.();
+ player1?.pause?.();
+ } catch {}
+ };
+ }, [player0, player1]),
+ );
+
+ useEffect(() => {
+ return () => {
+ try {
+ player0?.pause?.();
+ player1?.pause?.();
+ } catch {}
+ };
+ }, [player0, player1]);
return (
navigate(Routes.FlowSelection, { projectId })}
+ onPressBack={async () => {
+ try {
+ await player0?.pause?.();
+ await player1?.pause?.();
+ } catch {}
+ navigate(Routes.FlowSelection);
+ }}
progress={63}
/>
@@ -207,7 +220,7 @@ const SongReady = ({ route }) => {
} catch (e) {
console.log(
"SongReady pause on seek start",
- e?.message
+ e?.message,
);
}
}}
@@ -269,7 +282,13 @@ const SongReady = ({ route }) => {
{
+ try {
+ await player0?.pause?.();
+ await player1?.pause?.();
+ } catch {}
+ navigate(Routes.ComposeSong);
+ }}
/>
{
const [selected, setSelected] = useState(null);
- const { userProjects: projects } = useUser();
+ const { userProjects: projects, selectProject } = useUser();
return (
{
"La chanson est en cours de génération. Veuillez patienter.",
);
} else {
- navigate(Routes.Compose, { projectId: selected.id });
+ selectProject(selected.id);
+ navigate(Routes.Compose);
}
}}
/>
@@ -118,18 +119,20 @@ const Studio = () => {
containerStyle={{
marginTop: responsiveHeight(2),
}}
- onPress={() =>
- navigate(Routes.SongReady, { projectId: selected.id })
- }
+ onPress={() => {
+ selectProject(selected.id);
+ navigate(Routes.SongReady);
+ }}
/>
- navigate(Routes.PouchReady, { projectId: selected.id })
- }
+ onPress={() => {
+ selectProject(selected.id);
+ navigate(Routes.PouchReady);
+ }}
/>
>
)}
diff --git a/src/screens/Writing/CreateLyricsWithAi.js b/src/screens/Writing/CreateLyricsWithAi.js
index bfddb2d..ae8d8a4 100644
--- a/src/screens/Writing/CreateLyricsWithAi.js
+++ b/src/screens/Writing/CreateLyricsWithAi.js
@@ -17,13 +17,14 @@ import CustomizeSongStructure from "./CustomizeSongStructure";
import Rhymes from "./Rhymes";
import CreatingLyrics from "./CreatingLyrics";
import { responsiveHeight } from "react-native-responsive-dimensions";
+import { useUser } from "../../providers/UserDataProvider";
const { width } = Dimensions.get("window");
-const CreateLyricsWithAi = ({ route }) => {
- const { hasLyrics = false, regenerateKey } = route?.params || {};
+const CreateLyricsWithAi = () => {
+ const { selectedProject, updateProjectData } = useUser();
+ const hasLyrics = selectedProject?.hasLyrics === true;
const scrollRef = useRef(null);
- // If user already has lyrics, start at SongStructure (index 5)
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null);
@@ -40,6 +41,58 @@ const CreateLyricsWithAi = ({ route }) => {
const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain']
+ // Pré-remplir les états depuis le projet sélectionné si disponibles
+ React.useEffect(() => {
+ if (!selectedProject) return;
+ const sel = selectedProject?.selections || {};
+ const cfg = selectedProject?.config || {};
+
+ // Text inputs / choix simples
+ if (objective == null && typeof sel.objective === "string" && sel.objective)
+ setObjective(sel.objective);
+ if (!otherObjective && typeof sel.otherObjective === "string")
+ setOtherObjective(sel.otherObjective);
+ if (!context && typeof sel.context === "string") setContext(sel.context);
+
+ // Emotion: accepter objet {title, description} ou string "Titre : description"
+ if (emotion == null && sel.emotion) {
+ if (typeof sel.emotion === "object" && sel.emotion.title) {
+ setEmotion({
+ title: sel.emotion.title,
+ description: sel.emotion.description || "",
+ });
+ } else if (typeof sel.emotion === "string") {
+ const [t, d] = sel.emotion.split(":");
+ const title = (t || "").trim();
+ const description = (d || "").trim();
+ if (title) setEmotion({ title, description });
+ }
+ }
+
+ if (style == null && typeof sel.style === "string" && sel.style)
+ setStyle(sel.style);
+ if (!otherStyle && typeof sel.otherStyle === "string")
+ setOtherStyle(sel.otherStyle);
+ if (!audience && typeof sel.audience === "string") setAudience(sel.audience);
+
+ // Structure choisie (string) si déjà enregistrée dans selections
+ if (structure == null && typeof sel.structure === "string" && sel.structure)
+ setStructure(sel.structure);
+
+ // Rimes
+ if (rhymes == null && typeof sel.rhymes === "string" && sel.rhymes)
+ setRhymes(sel.rhymes);
+
+ // Structure personnalisée: prioriser selections.customStructure puis config.structure
+ const savedCustom = Array.isArray(sel.customStructure)
+ ? sel.customStructure
+ : null;
+ const cfgStructure = Array.isArray(cfg.structure) ? cfg.structure : null;
+ if (!Array.isArray(customStructure) && (savedCustom || cfgStructure)) {
+ setCustomStructure(savedCustom || cfgStructure);
+ }
+ }, [selectedProject]);
+
const parsedStructure = useMemo(() => {
// Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain"
try {
@@ -65,6 +118,13 @@ const CreateLyricsWithAi = ({ route }) => {
}
}, [structure]);
+ // Si déjà des paroles, forcer l'accès à partir de l'étape 5 et ignorer 0-4
+ React.useEffect(() => {
+ if (hasLyrics && selectedIndex < 5) {
+ setSelectedIndex(5);
+ }
+ }, [hasLyrics]);
+
const lyricsConfig = useMemo(() => {
return {
objective: otherObjective?.trim()
@@ -98,9 +158,9 @@ const CreateLyricsWithAi = ({ route }) => {
customStructure,
]);
- const onPressNext = () => {
- // If user already has lyrics and just finished CustomizeSongStructure (index 6),
- // skip AI generation and go straight to Lyrics editor
+ const onPressNext = async () => {
+ // Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
+ // sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
if (hasLyrics && selectedIndex === 6) {
const chosenStructure =
(customStructure &&
@@ -108,7 +168,7 @@ const CreateLyricsWithAi = ({ route }) => {
customStructure.length === parsedStructure.length
? customStructure
: parsedStructure) || [];
- navigate(Routes.Lyrics, {
+ await updateProjectData({
config: { structure: chosenStructure },
selections: {
objective,
@@ -125,6 +185,7 @@ const CreateLyricsWithAi = ({ route }) => {
},
hasLyrics: true,
});
+ navigate(Routes.Lyrics);
return;
}
setSelectedIndex((idx) => idx + 1);
@@ -156,11 +217,7 @@ const CreateLyricsWithAi = ({ route }) => {
return (
-
+
{
}}
>
@@ -273,7 +337,6 @@ const CreateLyricsWithAi = ({ route }) => {
{
{selectedIndex !== 8 && (
-
+
)}
diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js
index 4120ea1..512fe2b 100644
--- a/src/screens/Writing/CreatingLyrics.js
+++ b/src/screens/Writing/CreatingLyrics.js
@@ -11,21 +11,25 @@ import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation";
import firebase from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
+import { useUser } from "../../providers/UserDataProvider";
-const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
+const CreatingLyrics = ({ active, config, selections }) => {
const [progress, setProgress] = useState(0);
const [called, setCalled] = useState(false);
const [result, setResult] = useState(null);
+ const [saved, setSaved] = useState(false);
const { setIsLoading } = useMinuit();
+ const { updateProjectData } = useUser();
- // When asked to regenerate, reset flags so effect runs again
+ // Reset when becomes active
useEffect(() => {
if (active) {
setCalled(false);
setResult(null);
setProgress(0);
+ setSaved(false);
}
- }, [regenerateKey, active]);
+ }, [active]);
useEffect(() => {
if (active) {
@@ -74,7 +78,42 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
}
}, [active, called, config, setIsLoading]);
- console.log(result);
+ // Lorsque la génération est terminée, enregistrer et naviguer automatiquement vers Lyrics
+ useEffect(() => {
+ const autoSaveAndGo = async () => {
+ try {
+ if (saved) return;
+ setSaved(true);
+ await setIsLoading(true);
+ await updateProjectData({
+ title: result?.title || "",
+ titleLower: (result?.title || "").toLowerCase(),
+ lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
+ config: config || null,
+ selections: selections || null,
+ hasLyrics: false,
+ });
+ navigate(Routes.Lyrics);
+ } catch (e) {
+ console.log("Auto save generated lyrics error", e?.message);
+ } finally {
+ await setIsLoading(false);
+ }
+ };
+
+ if (active && result && progress >= 100 && !saved) {
+ autoSaveAndGo();
+ }
+ }, [
+ active,
+ result,
+ progress,
+ saved,
+ setIsLoading,
+ updateProjectData,
+ config,
+ selections,
+ ]);
return (
{
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
}
>
-
-
-
{
{progress}%
-
- navigate(Routes.Lyrics, {
- lyricsData: result,
- config,
- selections,
- hasLyrics: false,
- })
- }
- />
+ {!saved && (
+ {
+ try {
+ if (saved) return;
+ setSaved(true);
+ await setIsLoading(true);
+ await updateProjectData({
+ title: result?.title || "",
+ titleLower: (result?.title || "").toLowerCase(),
+ lyrics: Array.isArray(result?.lyrics)
+ ? result.lyrics
+ : [],
+ config: config || null,
+ selections: selections || null,
+ hasLyrics: false,
+ });
+ navigate(Routes.Lyrics);
+ } catch (e) {
+ console.log("Save generated lyrics error", e?.message);
+ } finally {
+ await setIsLoading(false);
+ }
+ }}
+ />
+ )}
diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js
index d8520ff..bfd97e6 100644
--- a/src/screens/Writing/Lyrics.js
+++ b/src/screens/Writing/Lyrics.js
@@ -1,41 +1,44 @@
import { View, ScrollView, Alert } from "react-native";
-import React, { useMemo, useState, useCallback } from "react";
+import React, { useMemo, useState, useCallback, useEffect, useRef } from "react";
import Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader";
-import { goBack, navigate } from "../../navigation/NavigationService";
+import { navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import CustomInput from "./components/CustomInput";
import ItemContainer from "../../components/ItemContainer/ItemContainer";
-import { useRoute } from "@react-navigation/native";
import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
+import { useUser } from "../../providers/UserDataProvider";
const Lyrics = ({ navigation }) => {
const [containerLayout, setContainerLayout] = useState(null);
- const route = useRoute();
- const lyricsData = route?.params?.lyricsData;
- const config = route?.params?.config;
- const selections = route?.params?.selections;
- const hasLyrics = !!route?.params?.hasLyrics;
const { setIsLoading } = useMinuit();
+ const { selectedProjectId, selectedProject } = useUser();
+
+ // Effective sources from provider only
+ const projectTitle = selectedProject?.title || "";
+ const projectLyrics = Array.isArray(selectedProject?.lyrics)
+ ? selectedProject.lyrics
+ : [];
+ const projectConfig = selectedProject?.config || null;
+ const projectSelections = selectedProject?.selections || null;
+ const projectHasLyrics = !!selectedProject?.hasLyrics;
const initial = useMemo(() => {
- const title = lyricsData?.title || "";
- const aiSections = Array.isArray(lyricsData?.lyrics)
- ? lyricsData.lyrics
- : [];
+ const title = projectTitle || "";
+ const aiSections = Array.isArray(projectLyrics) ? projectLyrics : [];
// Respecter l'ordre de la structure choisie si disponible
- const targetStructure = Array.isArray(config?.structure)
- ? config.structure.map((t) => (t || "").toLowerCase())
+ const targetStructure = Array.isArray(projectConfig?.structure)
+ ? projectConfig.structure.map((t) => (t || "").toLowerCase())
: null;
- if (
- aiSections.length &&
- targetStructure &&
- aiSections.length === targetStructure.length
- ) {
+
+ // 1) Si des paroles existent déjà, les utiliser en priorité
+ if (aiSections.length) {
+ // Optionnel: si une structure cible de même longueur existe, garder l'ordre courant
+ // et harmoniser les types en minuscule.
return {
title,
sections: aiSections.map((s) => ({
@@ -44,18 +47,51 @@ const Lyrics = ({ navigation }) => {
})),
};
}
- // Sinon, créer à partir de la structure
+
+ // 2) Sinon, créer à partir de la structure si fournie
if (targetStructure && targetStructure.length) {
return {
title,
sections: targetStructure.map((t) => ({ type: t, lyrics: "" })),
};
}
- // Fallback vide
+
+ // 3) Fallback vide
return { title, sections: [] };
- }, [lyricsData, config]);
+ }, [projectTitle, projectLyrics, projectConfig]);
const [titleValue, setTitleValue] = useState(initial.title || "");
+ const titleSaveTimer = useRef(null);
+
+ // Auto-save du titre lorsqu'il est modifié (si non vide)
+ useEffect(() => {
+ const newTitle = (titleValue || "").trim();
+ // Annuler tout timer précédent
+ if (titleSaveTimer.current) clearTimeout(titleSaveTimer.current);
+ // Ne rien faire si inchangé vs projet courant
+ const currentTitle = (selectedProject?.title || "").trim();
+ if (!newTitle || newTitle === currentTitle) return;
+
+ titleSaveTimer.current = setTimeout(async () => {
+ try {
+ await projectsRef.doc(selectedProjectId).set(
+ {
+ title: newTitle,
+ titleLower: newTitle.toLowerCase(),
+ updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
+ },
+ { merge: true },
+ );
+ } catch (e) {
+ // silencieux: l'utilisateur pourra tjs valider plus tard
+ console.log("Auto-save titre échoué", e?.message);
+ }
+ }, 500);
+
+ return () => {
+ if (titleSaveTimer.current) clearTimeout(titleSaveTimer.current);
+ };
+ }, [titleValue, selectedProjectId, selectedProject]);
const [sections, setSections] = useState(initial.sections || []);
const setSectionAt = (index, value) => {
setSections((prev) => {
@@ -67,7 +103,7 @@ const Lyrics = ({ navigation }) => {
// Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente
const regenerate = useCallback(() => {
- navigate(Routes.CreateLyricsWithAi, { regenerateKey: Date.now() });
+ navigate(Routes.CreateLyricsWithAi);
}, []);
const sanitize = (obj) => {
@@ -102,34 +138,68 @@ const Lyrics = ({ navigation }) => {
);
return;
}
- const user = firebase.auth().currentUser;
- const payload = {
+ const normalizedNewLyrics = (sections || []).map((s) => ({
+ type: (s?.type || "").toLowerCase(),
+ lyrics: (s?.lyrics || "").trim(),
+ }));
+ const normalizedOldLyrics = Array.isArray(projectLyrics)
+ ? projectLyrics.map((s) => ({
+ type: (s?.type || "").toLowerCase(),
+ lyrics: (s?.lyrics || "").trim(),
+ }))
+ : [];
+ const sameLength =
+ normalizedOldLyrics.length === normalizedNewLyrics.length;
+ const isSame =
+ sameLength &&
+ normalizedOldLyrics.every(
+ (s, i) =>
+ s.type === normalizedNewLyrics[i]?.type &&
+ s.lyrics === normalizedNewLyrics[i]?.lyrics,
+ );
+
+ const baseData = {
title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(),
- lyrics: (sections || []).map((s) => ({
- type: (s?.type || "").toLowerCase(),
- lyrics: s?.lyrics || "",
- })),
- config: sanitize(config),
- selections: sanitize(selections),
- userId: user ? user.uid : null,
- createdAt: firebase.firestore.FieldValue.serverTimestamp(),
+ lyrics: normalizedNewLyrics,
+ config: sanitize(projectConfig),
+ selections: sanitize(projectSelections),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
- hasLyrics,
+ hasLyrics: projectHasLyrics,
};
- const { id: projectId } = await projectsRef.add(payload);
- navigate(Routes.FlowSelection, { projectId });
+
+ const updateData = { ...baseData };
+ if (!isSame) {
+ updateData.musicUrls = firebase.firestore.FieldValue.delete();
+ updateData.musicStatus = firebase.firestore.FieldValue.delete();
+ await projectsRef
+ .doc(selectedProjectId)
+ .set(updateData, { merge: true });
+ }
+ navigate(Routes.FlowSelection);
} catch (e) {
console.log(e);
Alert.alert("Erreur", "Échec de l'enregistrement dans le projet.");
} finally {
await setIsLoading(false);
}
- }, [titleValue, sections, config, selections, setIsLoading]);
+ }, [
+ titleValue,
+ sections,
+ projectConfig,
+ projectSelections,
+ setIsLoading,
+ selectedProjectId,
+ projectHasLyrics,
+ projectLyrics,
+ ]);
return (
-
+ navigate(Routes.FlowSelection)}
+ progress={95}
+ />
{
height={45}
value={titleValue}
setValue={setTitleValue}
+ multiline={false}
+ maxLength={60}
/>
{sections.map((s, idx) => {
// Calculer l'index humain par type
@@ -184,7 +256,7 @@ const Lyrics = ({ navigation }) => {
gap: 12,
}}
>
- {!hasLyrics && (
+ {!projectHasLyrics && (
{
+ const { createNewProject, selectedProject, updateProjectData } =
+ useUserData();
+ const { setIsLoading } = useMinuit();
+
+ const [showIntro, setShowIntro] = useState(false);
+
+ useEffect(() => {
+ if (selectedProject === null) {
+ setShowIntro(true);
+ }
+ }, [selectedProject]);
+
+ async function createAndNavigate({ hasLyrics = false }) {
+ try {
+ await setIsLoading(true);
+ if (selectedProject) {
+ updateProjectData({ hasLyrics });
+ } else {
+ await createNewProject({ hasLyrics });
+ }
+ setTimeout(() => navigate(Routes.CreateLyricsWithAi), 500);
+ } catch (e) {
+ console.log(e);
+ } finally {
+ await setIsLoading(false);
+ }
+ }
+
return (
@@ -25,18 +56,18 @@ const WritingLyrics = () => {
}}
>
{
- navigate(Routes.CreateLyricsWithAi, {
- hasLyrics: true,
- });
- }}
+ onPress={() => createAndNavigate({ hasLyrics: true })}
/>
navigate(Routes.CreateLyricsWithAi)}
+ onPress={() => createAndNavigate({ hasLyrics: false })}
/>
+ setShowIntro(false)}
+ />
);
};
diff --git a/src/screens/Writing/components/CustomInput.js b/src/screens/Writing/components/CustomInput.js
index 99099ce..8aa0d82 100644
--- a/src/screens/Writing/components/CustomInput.js
+++ b/src/screens/Writing/components/CustomInput.js
@@ -9,6 +9,8 @@ const CustomInput = ({
value,
setValue,
height = 65,
+ multiline = true,
+ maxLength,
}) => {
return (
@@ -42,8 +44,10 @@ const CustomInput = ({
color: Palette.black,
fontFamily: FONT_FAMILY.InterRegularItalic,
}}
- multiline
- textAlignVertical="top"
+ multiline={multiline}
+ numberOfLines={multiline ? undefined : 1}
+ maxLength={maxLength}
+ textAlignVertical={multiline ? "top" : "center"}
/>
diff --git a/src/screens/Studio/AddPhotoCover.js b/src/screens/cover/AddPhotoCover.js
similarity index 98%
rename from src/screens/Studio/AddPhotoCover.js
rename to src/screens/cover/AddPhotoCover.js
index 4db53cf..1320c62 100644
--- a/src/screens/Studio/AddPhotoCover.js
+++ b/src/screens/cover/AddPhotoCover.js
@@ -113,7 +113,7 @@ const AddPhotoCover = () => {
/>
navigate(Routes.FinishCompose)}
+ onPress={() => navigate(Routes.FlowSelection)}
/>
diff --git a/src/screens/cover/ChooseCoverType.js b/src/screens/cover/ChooseCoverType.js
new file mode 100644
index 0000000..bbd0f80
--- /dev/null
+++ b/src/screens/cover/ChooseCoverType.js
@@ -0,0 +1,54 @@
+import { View, StyleSheet, Image } from "react-native";
+import React, { useState } from "react";
+import { ai, background } from "../../assets";
+import Page from "../../layouts/Page";
+import { goBack, navigate } from "../../navigation/NavigationService";
+import MusicLandHeader from "../../components/MusicLandHeader";
+import { Routes } from "../../navigation";
+import GradientButton from "../../components/GradientButton";
+import { gutters } from "../../styles";
+import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
+import BorderGradientButton from "../../components/BorderGradientButton";
+
+const ChooseCoverType = () => {
+ const [showIntro, setShowIntro] = useState(true);
+ return (
+
+
+
+
+
+
+ navigate(Routes.PouchReady)}
+ />
+
+
+ setShowIntro(false)}
+ />
+
+ );
+};
+
+export default ChooseCoverType;
+
+const styles = StyleSheet.create({
+ img: {
+ width: "100%",
+ height: "70%",
+ position: "absolute",
+ bottom: -40,
+ right: -30,
+ },
+});
diff --git a/src/screens/Studio/FinishCompose.js b/src/screens/cover/FinishCompose.js
similarity index 99%
rename from src/screens/Studio/FinishCompose.js
rename to src/screens/cover/FinishCompose.js
index bb7e993..b8f79c4 100644
--- a/src/screens/Studio/FinishCompose.js
+++ b/src/screens/cover/FinishCompose.js
@@ -50,3 +50,4 @@ const styles = StyleSheet.create({
right: -30,
},
});
+
diff --git a/src/screens/Studio/PhotoCover.js b/src/screens/cover/PhotoCover.js
similarity index 100%
rename from src/screens/Studio/PhotoCover.js
rename to src/screens/cover/PhotoCover.js
diff --git a/src/screens/Studio/PouchReady.js b/src/screens/cover/PouchReady.js
similarity index 81%
rename from src/screens/Studio/PouchReady.js
rename to src/screens/cover/PouchReady.js
index 93683a2..efe0598 100644
--- a/src/screens/Studio/PouchReady.js
+++ b/src/screens/cover/PouchReady.js
@@ -11,12 +11,11 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation";
import { FONT_FAMILY } from "../../styles/Fonts";
-import { useRoute } from "@react-navigation/native";
-import firebase from "../../config/firebase";
+import firebase, { tasksRef } from "../../config/firebase";
+import { useUser } from "../../providers/UserDataProvider";
const PouchReady = () => {
- const route = useRoute();
- const projectId = route?.params?.projectId;
+ const { selectedProjectId, selectedProject, updateProjectData } = useUser();
const [coverUrl, setCoverUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [title, setTitle] = useState("");
@@ -24,35 +23,25 @@ const PouchReady = () => {
const isGenerating = coverStatus === "GENERATING";
useEffect(() => {
- if (!projectId) return;
- const unsub = firebase
- .firestore()
- .collection("projects")
- .doc(projectId)
- .onSnapshot((doc) => {
- const d = doc.data() || {};
- setTitle(d?.title || "");
- setCoverUrl(d?.coverUrl || null);
- setCoverStatus(d?.coverStatus || null);
- });
- return () => unsub?.();
- }, [projectId]);
+ const d = selectedProject || {};
+ setTitle(d?.title || "");
+ setCoverUrl(d?.coverUrl || null);
+ setCoverStatus(d?.coverStatus || null);
+ }, [
+ selectedProject?.title,
+ selectedProject?.coverUrl,
+ selectedProject?.coverStatus,
+ ]);
const generateCover = async () => {
- if (!projectId) return;
+ if (!selectedProjectId) return;
try {
setLoading(true);
- // Marquer le projet en génération
- await firebase
- .firestore()
- .collection("projects")
- .doc(projectId)
- .set({ coverStatus: "GENERATING" }, { merge: true });
+ await updateProjectData({ coverStatus: "GENERATING" });
- // Créer une tâche pour déclencher la Cloud Function onCreate
- await firebase.firestore().collection("tasks").add({
+ await tasksRef.add({
type: "cover",
- projectId,
+ projectId: selectedProjectId,
status: "PENDING",
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
});
@@ -64,12 +53,11 @@ const PouchReady = () => {
};
useEffect(() => {
- if (!projectId) return;
- // Si pas de cover et pas déjà en génération, lancer une tâche
+ if (!selectedProjectId) return;
if (!coverUrl && !isGenerating && !loading) {
generateCover();
}
- }, [projectId, coverUrl, isGenerating]);
+ }, [selectedProjectId, coverUrl, isGenerating]);
return (