big refacto change all flows

This commit is contained in:
Thomas Demirdjian
2025-09-03 15:43:22 +02:00
parent 8f1c062b33
commit 60d1794c99
29 changed files with 841 additions and 344 deletions
+10 -18
View File
@@ -5,21 +5,13 @@ const { generateAI } = require("../index");
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
try { try {
const { const {
objective = "Célébrer l'agence Minuit et mettre en avant son " + objective = "",
"expertise digitale, son esprit d'équipe et sa créativité.", context = "",
context = "L'agence Minuit accompagne les startups et entreprises " + style = "",
"innovantes dans la création de produits digitaux, du prototype " + audience = "",
"à la version de financement, jusqu'à l'optimisation et la mise " + emotion = "",
"à 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.",
structure = ["couplet", "refrain", "couplet", "refrain"], structure = ["couplet", "refrain", "couplet", "refrain"],
rhymes = "Avec rimes", rhymes = "",
} = data; } = data;
console.log("Function generate lyrics start with data", data); console.log("Function generate lyrics start with data", data);
@@ -69,19 +61,19 @@ Pour chaque ` +
.string() .string()
.describe( .describe(
'Type de section : "couplet" ou "refrain" ' + 'Type de section : "couplet" ou "refrain" ' +
"selon la structure" "selon la structure",
), ),
lyrics: z lyrics: z
.string() .string()
.describe( .describe(
"Paroles de la section, chaque ligne séparée " + "Paroles de la section, chaque ligne séparée " +
"par un retour à la ligne" "par un retour à la ligne",
), ),
}) }),
) )
.describe( .describe(
"Paroles de la chanson sous forme de tableau de sections " + "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"), success: z.boolean().describe("Indique si la génération a réussi"),
}); });
+4
View File
@@ -230,6 +230,10 @@ export const ai = {
john, john,
}; };
export const videos = {
test: require("./video/testVideo.mp4"),
};
export const img = { export const img = {
placeholder, placeholder,
placeholder2, placeholder2,
Binary file not shown.
+88
View File
@@ -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 (
<Portal>
<View
pointerEvents="box-none"
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "black",
justifyContent: "center",
alignItems: "center",
zIndex: 9999,
}}
>
<VideoView
player={player}
nativeControls={false}
contentFit="cover"
style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 }}
/>
<Pressable
onPress={() => onClose?.()}
style={{
position: "absolute",
top: 50,
right: 20,
backgroundColor: "#00000080",
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 20,
borderWidth: 1,
borderColor: "#FFFFFF55",
}}
>
<Text style={{ color: "#FFF", fontSize: 14 }}>Passer la vidéo</Text>
</Pressable>
</View>
</Portal>
);
};
export default FullscreenIntroVideo;
+1
View File
@@ -48,6 +48,7 @@ try {
export const usersRef = firestore.collection("users"); export const usersRef = firestore.collection("users");
export const projectsRef = firestore.collection("projects"); export const projectsRef = firestore.collection("projects");
export const tasksRef = firestore.collection("tasks");
export const playlistsRef = firestore.collection("playlists"); export const playlistsRef = firestore.collection("playlists");
export const chatsRef = firestore.collection("chats"); export const chatsRef = firestore.collection("chats");
+9 -4
View File
@@ -26,10 +26,10 @@ import Compose from "../screens/Studio/Compose";
import CustomizeVoice from "../screens/Studio/CustomizeVoice"; import CustomizeVoice from "../screens/Studio/CustomizeVoice";
import SongReady from "../screens/Studio/SongReady"; import SongReady from "../screens/Studio/SongReady";
import Regenerate from "../screens/Studio/Regenerate"; import Regenerate from "../screens/Studio/Regenerate";
import PouchReady from "../screens/Studio/PouchReady"; import PouchReady from "../screens/cover/PouchReady";
import PhotoCover from "../screens/Studio/PhotoCover"; import PhotoCover from "../screens/cover/PhotoCover";
import AddPhotoCover from "../screens/Studio/AddPhotoCover"; import AddPhotoCover from "../screens/cover/AddPhotoCover";
import FinishCompose from "../screens/Studio/FinishCompose"; import FinishCompose from "../screens/cover/FinishCompose";
import Production from "../screens/Production/Production"; import Production from "../screens/Production/Production";
import ProductionOnboarding from "../screens/Production/ProductionOnboarding"; import ProductionOnboarding from "../screens/Production/ProductionOnboarding";
import DownloadSongs from "../screens/Production/DownloadSongs"; import DownloadSongs from "../screens/Production/DownloadSongs";
@@ -64,6 +64,7 @@ import NewMusicOptions from "../screens/NewMusicOptions";
import Register from "../screens/Register"; import Register from "../screens/Register";
import CreatePassword from "../screens/CreatePassword"; import CreatePassword from "../screens/CreatePassword";
import CreatePseudo from "../screens/CreatePseudo"; import CreatePseudo from "../screens/CreatePseudo";
import ChooseCoverType from "../screens/cover/ChooseCoverType";
const screenOptions = { const screenOptions = {
headerShown: false, headerShown: false,
@@ -302,6 +303,10 @@ const screens = [
name: Routes.CreatePseudo, name: Routes.CreatePseudo,
component: CreatePseudo, component: CreatePseudo,
}, },
{
name: Routes.ChooseCoverType,
component: ChooseCoverType,
},
]; ];
export default function Main() { export default function Main() {
+1
View File
@@ -82,4 +82,5 @@ export const Routes = {
Reels: "Reels", Reels: "Reels",
Follows: "Follows", Follows: "Follows",
FlowSelection: "FlowSelection", FlowSelection: "FlowSelection",
ChooseCoverType: "ChooseCoverType",
}; };
+65 -2
View File
@@ -90,6 +90,62 @@ export default ({ children }) => {
...newData, ...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) => { const followUser = async (userId) => {
try { try {
if (currentUID && userId) { if (currentUID && userId) {
@@ -98,7 +154,7 @@ export default ({ children }) => {
followedBy: arrayUnion(currentUID), followedBy: arrayUnion(currentUID),
lastFollowersUpdateAt: new Date(), lastFollowersUpdateAt: new Date(),
}, },
{ merge: true } { merge: true },
); );
setTooltip({ type: "success", text: "Abonnement mis à jour" }); setTooltip({ type: "success", text: "Abonnement mis à jour" });
} }
@@ -116,7 +172,7 @@ export default ({ children }) => {
followedBy: arrayRemove(currentUID), followedBy: arrayRemove(currentUID),
lastFollowersUpdateAt: new Date(), lastFollowersUpdateAt: new Date(),
}, },
{ merge: true } { merge: true },
); );
setTooltip({ setTooltip({
type: "success", type: "success",
@@ -248,6 +304,8 @@ export default ({ children }) => {
userPlaybacks, userPlaybacks,
userLikedProjects, userLikedProjects,
userPlaylists, userPlaylists,
selectedProjectId,
selectedProject,
followingCount: userFollowing?.length || 0, followingCount: userFollowing?.length || 0,
setCurrentUserData, setCurrentUserData,
@@ -260,6 +318,11 @@ export default ({ children }) => {
updateUserData, updateUserData,
followUser, followUser,
unfollowUser, unfollowUser,
resetSelectedProject,
selectProject,
setSelectedProjectId,
updateProjectData,
createNewProject,
}} }}
> >
{children} {children}
+2 -1
View File
@@ -68,7 +68,8 @@ const HitParade = () => {
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
{["Chansons", "Playbacks", "Clips"].map((item, index) => ( {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
{["Chansons", "Playbacks"].map((item, index) => (
<BorderGradientButton <BorderGradientButton
key={index} key={index}
title={item} title={item}
+14 -10
View File
@@ -17,7 +17,7 @@ import { projectsRef } from "../config/firebase";
import { useGlobal } from "reactn"; import { useGlobal } from "reactn";
const Home = () => { const Home = () => {
const { userProjects = [] } = useUser(); const { userProjects = [], resetSelectedProject, selectProject } = useUser();
const projects = useMemo( const projects = useMemo(
() => (Array.isArray(userProjects) ? userProjects : []), () => (Array.isArray(userProjects) ? userProjects : []),
[userProjects], [userProjects],
@@ -25,7 +25,7 @@ const Home = () => {
const [, setTooltip] = useGlobal("_tooltip"); const [, setTooltip] = useGlobal("_tooltip");
const [menuTop, setMenuTop] = useState(0); const [menuTop, setMenuTop] = useState(0);
const [showMenu, setShowMenu] = useState(false); const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null); const [menuProjectId, setMenuProjectId] = useState(null);
return ( return (
<Page backgroundImg={background.homeBG} headerType="NONE"> <Page backgroundImg={background.homeBG} headerType="NONE">
@@ -77,11 +77,12 @@ const Home = () => {
imageUri={item?.coverUrl || null} imageUri={item?.coverUrl || null}
projectId={item?.id} projectId={item?.id}
likedBy={item?.likedBy || []} likedBy={item?.likedBy || []}
onPress={() => onPress={() => {
navigate(Routes.FlowSelection, { projectId: item.id }) selectProject(item.id);
} navigate(Routes.FlowSelection);
}}
onPressMore={(posTop) => { onPressMore={(posTop) => {
setSelectedProjectId(item.id); setMenuProjectId(item.id);
setMenuTop(posTop); setMenuTop(posTop);
setShowMenu((prev) => !prev || posTop !== menuTop); setShowMenu((prev) => !prev || posTop !== menuTop);
}} }}
@@ -93,7 +94,7 @@ const Home = () => {
top={menuTop} top={menuTop}
onClose={() => setShowMenu(false)} onClose={() => setShowMenu(false)}
inPlaylist={false} inPlaylist={false}
projectId={selectedProjectId} projectId={menuProjectId}
extraItems={[ extraItems={[
{ {
label: "Supprimer", label: "Supprimer",
@@ -108,9 +109,9 @@ const Home = () => {
style: "destructive", style: "destructive",
onPress: async () => { onPress: async () => {
try { try {
if (!selectedProjectId) return; if (!menuProjectId) return;
await projectsRef await projectsRef
.doc(selectedProjectId) .doc(menuProjectId)
.delete(); .delete();
setTooltip({ setTooltip({
type: "success", type: "success",
@@ -137,7 +138,10 @@ const Home = () => {
<GradientButton <GradientButton
title="Créer une nouvelle musique" title="Créer une nouvelle musique"
containerStyle={{ width: "80%", alignSelf: "center" }} containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.FlowSelection)} onPress={() => {
resetSelectedProject();
navigate(Routes.FlowSelection);
}}
/> />
</View> </View>
</View> </View>
+38 -45
View File
@@ -1,6 +1,6 @@
import FontAwesome from "@expo/vector-icons/FontAwesome"; import FontAwesome from "@expo/vector-icons/FontAwesome";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React, { useMemo } from "react"; import React from "react";
import { Image, Platform, Pressable, Text, View } from "react-native"; import { Image, Platform, Pressable, Text, View } from "react-native";
import { ai, background } from "../assets"; import { ai, background } from "../assets";
import Page from "../layouts/Page"; import Page from "../layouts/Page";
@@ -42,69 +42,62 @@ const CREATE_DATA = [
}, },
]; ];
const NewMusicOptions = ({ route }) => { const NewMusicOptions = () => {
const { userProjects } = useUserData(); const { selectedProject } = 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);
// 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) => { const isLocked = (index) => {
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director const songUrl = selectedProject?.songUrl || null;
// Rules: const coverUrl = selectedProject?.coverUrl || null;
// - If no project: only Songwriter (0) is available. const playbackUrl = selectedProject?.songUrl || null;
// - If project exists: Songwriter (0) is always available. const lyricsLen = Array.isArray(selectedProject?.lyrics)
// - If lyrics exist: Beatmaker (1) becomes available. ? selectedProject.lyrics.length
// - If cover exists: Producer (2) and Director (3) become available. : 0;
if (!hasProject) return index !== 0;
const allowed = new Set([0]); // songwriter always allowed when project exists switch (index) {
if (hasLyrics) { case 0: // Songwriter
allowed.add(1); 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) => { const onPressOption = (index, item) => {
if (isLocked(index)) return; if (isLocked(index)) return;
switch (index) { switch (index) {
case 0: case 0:
navigate(Routes.WritingLyrics, { if (selectedProject?.lyrics?.length) {
projectId: currentProjet?.id || null, navigate(Routes.Lyrics);
}); } else {
navigate(Routes.WritingLyrics);
}
break; break;
case 1: case 1:
if (currentProjet?.musicStatus === "GENERATING") { if (selectedProject?.musicStatus === "GENERATING") {
navigate(Routes.GeneratingSong, { projectId: currentProjet.id }); navigate(Routes.GeneratingSong);
} else if (currentProjet?.musicStatus === "GENERATED") { } else if (selectedProject?.musicStatus === "GENERATED") {
navigate(Routes.SongReady, { projectId: currentProjet.id }); navigate(Routes.SongReady);
} else { } else {
navigate(Routes.Compose, { projectId: currentProjet.id }); navigate(Routes.Compose);
} }
break; break;
case 2: case 2:
navigate(Routes.PouchReady, { projectId: currentProjet.id }); navigate(Routes.ChooseCoverType);
break; break;
case 3: case 3:
console.log("test"); console.log("test");
navigate(Routes.Playback, { navigate(Routes.Playback, {
project: currentProjet, project: selectedProject,
}); });
default: default:
break; break;
@@ -115,7 +108,7 @@ const NewMusicOptions = ({ route }) => {
<Page <Page
backgroundImg={background.homeBG} backgroundImg={background.homeBG}
headerType="NAVIGATION" headerType="NAVIGATION"
title={!!currentProjet ? "Continuer la création" : "Nouvelle musique"} title={!!selectedProject ? "Continuer la création" : "Nouvelle musique"}
scrollEnabled={true} scrollEnabled={true}
> >
<View style={{ gap: 10 }}> <View style={{ gap: 10 }}>
+13 -4
View File
@@ -1,4 +1,4 @@
import React, { useCallback } from "react"; import React, { useCallback, useState } from "react";
import { Alert, Image, StyleSheet, View } from "react-native"; import { Alert, Image, StyleSheet, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { ai, background } from "../../assets"; import { ai, background } from "../../assets";
@@ -10,6 +10,7 @@ import Page from "../../layouts/Page";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const Playback = ({ route }) => { const Playback = ({ route }) => {
const { project } = route.params; const { project } = route.params;
@@ -17,6 +18,7 @@ const Playback = ({ route }) => {
const songIndex = project?.songIndex; const songIndex = project?.songIndex;
const sunoTaskId = project?.sunoTaskId; const sunoTaskId = project?.sunoTaskId;
const { setIsLoading } = useMinuit(); const { setIsLoading } = useMinuit();
const [showIntro, setShowIntro] = useState(false);
const onPressRecord = useCallback(async () => { const onPressRecord = useCallback(async () => {
try { try {
@@ -43,7 +45,7 @@ const Playback = ({ route }) => {
console.log("getSunoTimestamps failed", data?.error || data); console.log("getSunoTimestamps failed", data?.error || data);
Alert.alert( Alert.alert(
"Timestamps", "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); console.log("getSunoTimestamps error", e?.message || e);
Alert.alert( Alert.alert(
"Timestamps", "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); await setIsLoading(false);
} }
@@ -71,7 +73,10 @@ const Playback = ({ route }) => {
}} }}
> >
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}> <View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
<BorderGradientButton title="Guide du Playbacker" /> <BorderGradientButton
title="Guide du Playbacker"
onPress={() => setShowIntro(true)}
/>
{/* <BorderGradientButton title="Importer une vidéo" /> */} {/* <BorderGradientButton title="Importer une vidéo" /> */}
<GradientButton <GradientButton
title="Enregistrer mon Playback" title="Enregistrer mon Playback"
@@ -79,6 +84,10 @@ const Playback = ({ route }) => {
/> />
</View> </View>
</View> </View>
<FullscreenIntroVideo
visible={showIntro}
onClose={() => setShowIntro(false)}
/>
</Page> </Page>
); );
}; };
+3 -2
View File
@@ -65,7 +65,7 @@ const Profile = () => {
setFollowers( setFollowers(
Array.isArray(currentUserData?.followedBy) Array.isArray(currentUserData?.followedBy)
? currentUserData.followedBy.length ? currentUserData.followedBy.length
: 0 : 0,
); );
return; return;
} }
@@ -367,7 +367,8 @@ const Profile = () => {
gap: 6, gap: 6,
}} }}
> >
{["Chansons", "Playbacks", "Clips"].map((item, index) => ( {/*{["Chansons", "Playbacks", "Clips"].map((item, index) => (*/}
{["Chansons", "Playbacks"].map((item, index) => (
<Pressable <Pressable
key={index} key={index}
onPress={() => onPressMenu(item)} onPress={() => onPressMenu(item)}
+10 -7
View File
@@ -1,21 +1,20 @@
import { View, Text, StyleSheet, Image } from "react-native"; import { View, StyleSheet, Image } from "react-native";
import React from "react"; import React, { useState } from "react";
import { ai, background } from "../../assets"; import { ai, background } from "../../assets";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { useRoute } from "@react-navigation/native";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const Compose = () => { const Compose = () => {
const route = useRoute(); const [showIntro, setShowIntro] = useState(true);
const projectId = route?.params?.projectId;
return ( return (
<Page backgroundImg={background.studioBG2} headerType="NONE"> <Page backgroundImg={background.studioBG2} headerType="NONE">
<Image source={ai.theo} style={styles.img} resizeMode="contain" /> <Image source={ai.theo} style={styles.img} resizeMode="contain" />
<MusicLandHeader showSkip onPressBack={goBack} progress={9} /> <MusicLandHeader onPressBack={goBack} progress={9} />
<View <View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }} style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
> >
@@ -28,10 +27,14 @@ const Compose = () => {
> >
<GradientButton <GradientButton
title="Composer ma chanson" title="Composer ma chanson"
onPress={() => navigate(Routes.ComposeSong, { projectId })} onPress={() => navigate(Routes.ComposeSong)}
/> />
</View> </View>
</View> </View>
<FullscreenIntroVideo
visible={showIntro}
onClose={() => setShowIntro(false)}
/>
</Page> </Page>
); );
}; };
+34 -21
View File
@@ -11,9 +11,8 @@ import ChooseGenre from "./ChooseGenre";
import CustomizeVoice from "./CustomizeVoice"; import CustomizeVoice from "./CustomizeVoice";
import ChooseInstruments from "./ChooseInstruments"; import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm"; import ChooseRhythm from "./ChooseRhythm";
import { useRoute } from "@react-navigation/native";
import { projectsRef } from "../../config/firebase"; import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef"; import { useUser } from "../../providers/UserDataProvider";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
const { width } = Dimensions.get("window"); const { width } = Dimensions.get("window");
@@ -23,8 +22,7 @@ const ComposeSong = () => {
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18); const [progress, setProgress] = useState(18);
const [containerLayout, setContainerLayout] = useState(null); const [containerLayout, setContainerLayout] = useState(null);
const route = useRoute(); const { selectedProjectId, selectedProject, updateProjectData } = useUser();
const projectId = route?.params?.projectId;
// Selections state // Selections state
const [genres, setGenres] = useState([]); const [genres, setGenres] = useState([]);
@@ -32,14 +30,6 @@ const ComposeSong = () => {
const [instruments, setInstruments] = useState([]); const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null); 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(() => { const isStepValid = useMemo(() => {
switch (selectedIndex) { switch (selectedIndex) {
case 0: case 0:
@@ -57,31 +47,54 @@ const ComposeSong = () => {
const musicConfig = useMemo(() => { const musicConfig = useMemo(() => {
let lyricsArr = []; let lyricsArr = [];
if (Array.isArray(project?.lyrics)) { if (Array.isArray(selectedProject?.lyrics)) {
lyricsArr = project.lyrics.map((s) => ({ lyricsArr = selectedProject.lyrics.map((s) => ({
type: (s?.type || "").toLowerCase(), type: (s?.type || "").toLowerCase(),
lyrics: s?.lyrics || "", lyrics: s?.lyrics || "",
})); }));
} else { } else {
const c = project?.lyrics?.couplet; const c = selectedProject?.lyrics?.couplet;
const r = project?.lyrics?.refrain; const r = selectedProject?.lyrics?.refrain;
if (c) lyricsArr.push({ type: "couplet", lyrics: c }); if (c) lyricsArr.push({ type: "couplet", lyrics: c });
if (r) lyricsArr.push({ type: "refrain", lyrics: r }); if (r) lyricsArr.push({ type: "refrain", lyrics: r });
} }
return { return {
title: project?.title || "", title: selectedProject?.title || "",
lyrics: lyricsArr, lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [], genres: Array.isArray(genres) ? genres : [],
voice: voice || undefined, voice: voice || undefined,
instruments: Array.isArray(instruments) ? instruments : [], instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined, 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) { 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; return;
} }
setSelectedIndex(selectedIndex + 1); setSelectedIndex(selectedIndex + 1);
+3 -3
View File
@@ -7,6 +7,7 @@ import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import ProgressBar from "../../components/ProgressBar"; import ProgressBar from "../../components/ProgressBar";
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import firebase, { projectsRef } from "../../config/firebase"; import firebase, { projectsRef } from "../../config/firebase";
@@ -21,6 +22,7 @@ const CreatingSong = ({ active, config }) => {
const [musicStatus, setMusicStatus] = useState(null); const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null); const [generationStartAt, setGenerationStartAt] = useState(null);
const progressTimerRef = React.useRef(null); const progressTimerRef = React.useRef(null);
const { selectedProjectId } = useUser();
// Abonnement au document projet pour suivre le statut et la date de début // Abonnement au document projet pour suivre le statut et la date de début
useEffect(() => { useEffect(() => {
@@ -236,9 +238,7 @@ const CreatingSong = ({ active, config }) => {
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={() => onPress={() => navigate(Routes.SongReady)}
navigate(Routes.SongReady, { projectId: config?.projectId })
}
/> />
</View> </View>
</BlurView> </BlurView>
+89 -50
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useMemo, useRef, useState } from "react";
import { Image, Platform, Text, View } from "react-native"; import { Alert, Image, Platform, Text, View } from "react-native";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { ai, background } from "../../assets"; import { ai, background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
@@ -15,24 +15,22 @@ import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import moment from "moment"; import moment from "moment";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { FONT_FAMILY } from "../../styles/Fonts"; 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 GeneratingSong = () => {
const { config, projectId } = route.params; const { selectedProjectId, selectedProject } = useUser();
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const { setIsLoading } = useMinuit(); const { setIsLoading } = useMinuit();
const progressTimerRef = useRef(null); const progressTimerRef = useRef(null);
const navigatedRef = useRef(false); const navigatedRef = useRef(false);
const isFocused = useIsFocused();
const lastConfigKeyRef = useRef(null);
const askedRef = useRef(false);
const { data: project } = useDataFromRef({ // project loaded from provider
ref: projectId ? projectsRef.doc(projectId) : null,
simpleRef: true,
listener: true,
condition: !!projectId,
refreshArray: [projectId],
});
// Progress based on 8 minutes cap or until status changes // Progress based on 8 minutes cap or until status becomes GENERATED
useEffect(() => { useEffect(() => {
const totalMs = 8 * 60 * 1000; const totalMs = 8 * 60 * 1000;
const clearTimer = () => { const clearTimer = () => {
@@ -41,15 +39,15 @@ const GeneratingSong = ({ route }) => {
progressTimerRef.current = null; progressTimerRef.current = null;
} }
}; };
if (project?.musicStatus !== "GENERATING") { if (selectedProject?.musicStatus === "GENERATED") {
setProgress(100); setProgress(100);
clearTimer(); clearTimer();
return () => clearTimer(); return () => clearTimer();
} }
const update = () => { const update = () => {
const startDate = project?.generationStartAt?.toDate const startDate = selectedProject?.generationStartAt?.toDate
? project.generationStartAt.toDate() ? selectedProject.generationStartAt.toDate()
: new Date(project?.generationStartAt || Date.now()); : new Date(selectedProject?.generationStartAt || Date.now());
const elapsed = moment().diff(moment(startDate)); const elapsed = moment().diff(moment(startDate));
const raw = Math.floor((elapsed / totalMs) * 100); const raw = Math.floor((elapsed / totalMs) * 100);
// While status is GENERATING, block visual progress at 99% // While status is GENERATING, block visual progress at 99%
@@ -60,16 +58,29 @@ const GeneratingSong = ({ route }) => {
clearTimer(); clearTimer();
progressTimerRef.current = setInterval(update, 1000); progressTimerRef.current = setInterval(update, 1000);
return () => clearTimer(); return () => clearTimer();
}, [project?.musicStatus]); }, [selectedProject?.musicStatus]);
// Auto navigate to SongReady when generation completed // Auto navigate to SongReady when generation completed
useEffect(() => { useEffect(() => {
if (!config?.projectId) return; if (!selectedProjectId) return;
if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) { if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) {
navigatedRef.current = true; 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() { async function startMusicGeneration() {
try { try {
@@ -78,39 +89,39 @@ const GeneratingSong = ({ route }) => {
const callable = firebase const callable = firebase
.functions() .functions()
.httpsCallable("music-generateMusic"); .httpsCallable("music-generateMusic");
const cfg = effectiveConfig || {};
const { data } = await callable({ const { data } = await callable({
title: config?.title, title: cfg?.title,
lyrics: config?.lyrics, lyrics: cfg?.lyrics,
genres: config?.genres, genres: cfg?.genres,
voice: config?.voice, voice: cfg?.voice,
instruments: config?.instruments, instruments: cfg?.instruments,
tempo: config?.tempo, tempo: cfg?.tempo,
projectId: config?.projectId,
}); });
const taskId = const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id; data?.response?.data?.taskId || data?.response?.data?.task_id;
if (config?.projectId && taskId) { if (selectedProjectId && taskId) {
const baseUpdate = { const baseUpdate = {
sunoTaskId: taskId, sunoTaskId: taskId,
musicStatus: "GENERATING", musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
}; };
const updatePayload = project?.musicConfig const updatePayload = selectedProject?.musicConfig
? baseUpdate ? baseUpdate
: { : {
...baseUpdate, ...baseUpdate,
musicConfig: { musicConfig: {
title: config?.title || "", title: effectiveConfig?.title || "",
lyrics: config?.lyrics || [], lyrics: effectiveConfig?.lyrics || [],
genres: config?.genres || [], genres: effectiveConfig?.genres || [],
voice: config?.voice || "", voice: effectiveConfig?.voice || "",
instruments: config?.instruments || [], instruments: effectiveConfig?.instruments || [],
tempo: config?.tempo || "", tempo: effectiveConfig?.tempo || "",
}, },
}; };
await projectsRef await projectsRef
.doc(config.projectId) .doc(selectedProjectId)
.set(updatePayload, { merge: true }); .set(updatePayload, { merge: true });
} }
} catch (e) { } 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(() => { useEffect(() => {
if (!!project?.title && project?.musicStatus !== "GENERATING") { if (!isFocused) return;
startMusicGeneration();
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 ( return (
<Page headerType="NONE" backgroundImg={background.studioBG2}> <Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader onPressBack={goBack} progress={63} /> <MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection)}
progress={63}
/>
<View style={{ flex: 1, marginTop: 16 }}> <View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader <CreateLyricsHeader
title="Ta musique est en cours de création !" title="Ta musique est en cours de création !"
@@ -206,14 +245,14 @@ const GeneratingSong = ({ route }) => {
</Text> </Text>
</View> </View>
<GradientButton <GradientButton
title={"Création en cours..."} title={
disabled={project?.musicStatus === "GENERATING"} selectedProject?.musicStatus === "GENERATED"
containerStyle={{ width: "80%", alignSelf: "center" }} ? "Découvrir ma musique"
onPress={() => : "Création en cours..."
navigate(Routes.SongReady, {
projectId: config?.projectId,
})
} }
disabled={selectedProject?.musicStatus !== "GENERATED"}
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.SongReady)}
/> />
</View> </View>
</BlurView> </BlurView>
+55 -36
View File
@@ -1,6 +1,6 @@
import { useAudioPlayer } from "expo-audio"; import { useAudioPlayer } from "expo-audio";
import { BlurView } from "expo-blur"; 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 { Image, Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
@@ -9,16 +9,22 @@ import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal"; import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider"; 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 Page from "../../layouts/Page";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { Style } from "../../styles"; import { Style } from "../../styles";
import { gutters, size } from "../../styles/Style"; import { gutters, size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { useFocusEffect } from "@react-navigation/native";
const SongReady = ({ route }) => { const SongReady = () => {
const { projectId = null } = route?.params || {}; const {
selectedProjectId: projectId,
selectedProject,
updateProjectData,
} = useUser();
const [showValidateModal, setShowValidateModal] = useState(false); const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]); const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
@@ -28,29 +34,20 @@ const SongReady = ({ route }) => {
1: { pos: 0, dur: 0 }, 1: { pos: 0, dur: 0 },
}); });
const player0 = useAudioPlayer( const player0 = useAudioPlayer(
musicUrls[0] ? { uri: musicUrls[0] } : undefined musicUrls[0] ? { uri: musicUrls[0] } : undefined,
); );
const player1 = useAudioPlayer( const player1 = useAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined musicUrls[1] ? { uri: musicUrls[1] } : undefined,
); );
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false }); const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
// Charger les URLs depuis le document projet // Sync URLs from provider's selectedProject
useEffect(() => { useEffect(() => {
if (!projectId) return; const urls = Array.isArray(selectedProject?.musicUrls)
const unsub = firebase ? selectedProject.musicUrls.slice(0, 2)
.firestore()
.collection("projects")
.doc(projectId)
.onSnapshot((doc) => {
const data = doc.data() || {};
const urls = Array.isArray(data?.musicUrls)
? data.musicUrls.slice(0, 2)
: []; : [];
setMusicUrls(urls); setMusicUrls(urls);
}); }, [selectedProject?.musicUrls]);
return () => unsub?.();
}, [projectId]);
// Sync progression depuis les players // Sync progression depuis les players
useEffect(() => { useEffect(() => {
@@ -117,34 +114,50 @@ const SongReady = ({ route }) => {
try { try {
const url = musicUrls[selectedIndex]; const url = musicUrls[selectedIndex];
if (!projectId || !url) return; if (!projectId || !url) return;
await projectsRef.doc(projectId).set( await updateProjectData({
{
songIndex: selectedIndex, songIndex: selectedIndex,
songUrl: url, songUrl: url,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
}, });
{ merge: true } await player0?.pause?.();
); await player1?.pause?.();
navigate(Routes.FlowSelection, { projectId }); navigate(Routes.FlowSelection);
} catch (e) { } catch (e) {
console.log("Validate error", e?.message); console.log("Validate error", e?.message);
} }
}; };
const onPressRegenerate = async () => { // Pause audio when screen loses focus (navigate/reset) and on unmount
useFocusEffect(
useCallback(() => {
return () => {
try { try {
navigate(Routes.GeneratingSong, { projectId }); player0?.pause?.();
} catch (e) { player1?.pause?.();
console.log("Regenerate error", e?.message); } catch {}
} finally {
goBack();
}
}; };
}, [player0, player1]),
);
useEffect(() => {
return () => {
try {
player0?.pause?.();
player1?.pause?.();
} catch {}
};
}, [player0, player1]);
return ( return (
<Page headerType="NONE" backgroundImg={background.studioBG2}> <Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader <MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection, { projectId })} onPressBack={async () => {
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.FlowSelection);
}}
progress={63} progress={63}
/> />
<View style={{ flex: 1, marginTop: 16 }}> <View style={{ flex: 1, marginTop: 16 }}>
@@ -207,7 +220,7 @@ const SongReady = ({ route }) => {
} catch (e) { } catch (e) {
console.log( console.log(
"SongReady pause on seek start", "SongReady pause on seek start",
e?.message e?.message,
); );
} }
}} }}
@@ -269,7 +282,13 @@ const SongReady = ({ route }) => {
<BorderGradientButton <BorderGradientButton
title="Regénérer" title="Regénérer"
icon={icons.stars} icon={icons.stars}
onPress={onPressRegenerate} onPress={async () => {
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.ComposeSong);
}}
/> />
<GradientButton <GradientButton
title="Choisir ce morceau" title="Choisir ce morceau"
+11 -8
View File
@@ -12,7 +12,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
const Studio = () => { const Studio = () => {
const [selected, setSelected] = useState(null); const [selected, setSelected] = useState(null);
const { userProjects: projects } = useUser(); const { userProjects: projects, selectProject } = useUser();
return ( return (
<Page <Page
@@ -107,7 +107,8 @@ const Studio = () => {
"La chanson est en cours de génération. Veuillez patienter.", "La chanson est en cours de génération. Veuillez patienter.",
); );
} else { } else {
navigate(Routes.Compose, { projectId: selected.id }); selectProject(selected.id);
navigate(Routes.Compose);
} }
}} }}
/> />
@@ -118,18 +119,20 @@ const Studio = () => {
containerStyle={{ containerStyle={{
marginTop: responsiveHeight(2), marginTop: responsiveHeight(2),
}} }}
onPress={() => onPress={() => {
navigate(Routes.SongReady, { projectId: selected.id }) selectProject(selected.id);
} navigate(Routes.SongReady);
}}
/> />
<GradientButton <GradientButton
title="Générer une pochette" title="Générer une pochette"
containerStyle={{ containerStyle={{
marginTop: responsiveHeight(2), marginTop: responsiveHeight(2),
}} }}
onPress={() => onPress={() => {
navigate(Routes.PouchReady, { projectId: selected.id }) selectProject(selected.id);
} navigate(Routes.PouchReady);
}}
/> />
</> </>
)} )}
+81 -15
View File
@@ -17,13 +17,14 @@ import CustomizeSongStructure from "./CustomizeSongStructure";
import Rhymes from "./Rhymes"; import Rhymes from "./Rhymes";
import CreatingLyrics from "./CreatingLyrics"; import CreatingLyrics from "./CreatingLyrics";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { useUser } from "../../providers/UserDataProvider";
const { width } = Dimensions.get("window"); const { width } = Dimensions.get("window");
const CreateLyricsWithAi = ({ route }) => { const CreateLyricsWithAi = () => {
const { hasLyrics = false, regenerateKey } = route?.params || {}; const { selectedProject, updateProjectData } = useUser();
const hasLyrics = selectedProject?.hasLyrics === true;
const scrollRef = useRef(null); const scrollRef = useRef(null);
// If user already has lyrics, start at SongStructure (index 5)
const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0); const [selectedIndex, setSelectedIndex] = useState(hasLyrics ? 5 : 0);
const [progress, setProgress] = useState(16); const [progress, setProgress] = useState(16);
const [parentLayout, setparentLayout] = useState(null); const [parentLayout, setparentLayout] = useState(null);
@@ -40,6 +41,58 @@ const CreateLyricsWithAi = ({ route }) => {
const [rhymes, setRhymes] = useState(null); const [rhymes, setRhymes] = useState(null);
const [customStructure, setCustomStructure] = useState(null); // array like ['couplet','refrain'] 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(() => { const parsedStructure = useMemo(() => {
// Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain" // Parses strings like "1 couplet, 1 refrain, 1 couplet, 1 refrain"
try { try {
@@ -65,6 +118,13 @@ const CreateLyricsWithAi = ({ route }) => {
} }
}, [structure]); }, [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(() => { const lyricsConfig = useMemo(() => {
return { return {
objective: otherObjective?.trim() objective: otherObjective?.trim()
@@ -98,9 +158,9 @@ const CreateLyricsWithAi = ({ route }) => {
customStructure, customStructure,
]); ]);
const onPressNext = () => { const onPressNext = async () => {
// If user already has lyrics and just finished CustomizeSongStructure (index 6), // Si l'utilisateur a déjà des paroles et vient de finir CustomizeSongStructure (index 6),
// skip AI generation and go straight to Lyrics editor // sauter Rhymes et CreatingLyrics et aller directement sur Lyrics
if (hasLyrics && selectedIndex === 6) { if (hasLyrics && selectedIndex === 6) {
const chosenStructure = const chosenStructure =
(customStructure && (customStructure &&
@@ -108,7 +168,7 @@ const CreateLyricsWithAi = ({ route }) => {
customStructure.length === parsedStructure.length customStructure.length === parsedStructure.length
? customStructure ? customStructure
: parsedStructure) || []; : parsedStructure) || [];
navigate(Routes.Lyrics, { await updateProjectData({
config: { structure: chosenStructure }, config: { structure: chosenStructure },
selections: { selections: {
objective, objective,
@@ -125,6 +185,7 @@ const CreateLyricsWithAi = ({ route }) => {
}, },
hasLyrics: true, hasLyrics: true,
}); });
navigate(Routes.Lyrics);
return; return;
} }
setSelectedIndex((idx) => idx + 1); setSelectedIndex((idx) => idx + 1);
@@ -156,11 +217,7 @@ const CreateLyricsWithAi = ({ route }) => {
return ( return (
<Page headerType="NONE"> <Page headerType="NONE">
<MusicLandHeader <MusicLandHeader onPressBack={onPressBack} progress={progress} />
onPressBack={onPressBack}
progress={progress}
showSkip={selectedIndex === 4}
/>
<View <View
style={{ style={{
flex: 1, flex: 1,
@@ -250,7 +307,14 @@ const CreateLyricsWithAi = ({ route }) => {
}} }}
> >
<CustomizeSongStructure <CustomizeSongStructure
baseStructure={parsedStructure || []} baseStructure={
parsedStructure ||
(Array.isArray(customStructure)
? customStructure
: Array.isArray(selectedProject?.config?.structure)
? selectedProject.config.structure
: [])
}
onChange={setCustomStructure} onChange={setCustomStructure}
/> />
</View> </View>
@@ -273,7 +337,6 @@ const CreateLyricsWithAi = ({ route }) => {
<CreatingLyrics <CreatingLyrics
active={selectedIndex === 8} active={selectedIndex === 8}
config={lyricsConfig} config={lyricsConfig}
regenerateKey={regenerateKey}
selections={{ selections={{
objective, objective,
otherObjective, otherObjective,
@@ -292,7 +355,10 @@ const CreateLyricsWithAi = ({ route }) => {
</SwiperFlatList> </SwiperFlatList>
</View> </View>
{selectedIndex !== 8 && ( {selectedIndex !== 8 && (
<GradientButton title="Suivant" onPress={onPressNext} /> <GradientButton
title={selectedIndex === 7 ? "Générer" : "Suivant"}
onPress={onPressNext}
/>
)} )}
</View> </View>
</Page> </Page>
+65 -23
View File
@@ -11,21 +11,25 @@ import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import firebase from "../../config/firebase"; import firebase from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; 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 [progress, setProgress] = useState(0);
const [called, setCalled] = useState(false); const [called, setCalled] = useState(false);
const [result, setResult] = useState(null); const [result, setResult] = useState(null);
const [saved, setSaved] = useState(false);
const { setIsLoading } = useMinuit(); const { setIsLoading } = useMinuit();
const { updateProjectData } = useUser();
// When asked to regenerate, reset flags so effect runs again // Reset when becomes active
useEffect(() => { useEffect(() => {
if (active) { if (active) {
setCalled(false); setCalled(false);
setResult(null); setResult(null);
setProgress(0); setProgress(0);
setSaved(false);
} }
}, [regenerateKey, active]); }, [active]);
useEffect(() => { useEffect(() => {
if (active) { if (active) {
@@ -74,7 +78,42 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
} }
}, [active, called, config, setIsLoading]); }, [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 ( return (
<View <View
@@ -121,19 +160,6 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
Platform.OS !== "ios" ? "dimezisBlurView" : "none" Platform.OS !== "ios" ? "dimezisBlurView" : "none"
} }
> >
<Pressable
style={{
...size({ size: 24 }),
...Style.containerCenter,
position: "absolute",
top: 10,
left: 10,
zIndex: 3,
}}
onPress={goBack}
>
<Image source={icons.close} style={size({ size: 11 })} />
</Pressable>
<View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}> <View style={{ flex: 1, justifyContent: "flex-end", gap: 20 }}>
<Text <Text
style={{ style={{
@@ -157,21 +183,37 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
{progress}% {progress}%
</Text> </Text>
</View> </View>
{!saved && (
<GradientButton <GradientButton
title="Découvrir mon texte" title="Découvrir mon texte"
containerStyle={{ containerStyle={{
width: "80%", width: "80%",
alignSelf: "center", alignSelf: "center",
}} }}
onPress={() => onPress={async () => {
navigate(Routes.Lyrics, { try {
lyricsData: result, if (saved) return;
config, setSaved(true);
selections, 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, hasLyrics: false,
}) });
navigate(Routes.Lyrics);
} catch (e) {
console.log("Save generated lyrics error", e?.message);
} finally {
await setIsLoading(false);
} }
}}
/> />
)}
</View> </View>
</BlurView> </BlurView>
</View> </View>
+111 -39
View File
@@ -1,41 +1,44 @@
import { View, ScrollView, Alert } from "react-native"; 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 Page from "../../layouts/Page";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService"; import { navigate } from "../../navigation/NavigationService";
import { gutters } from "../../styles"; import { gutters } from "../../styles";
import BorderGradientButton from "../../components/BorderGradientButton"; import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import CustomInput from "./components/CustomInput"; import CustomInput from "./components/CustomInput";
import ItemContainer from "../../components/ItemContainer/ItemContainer"; import ItemContainer from "../../components/ItemContainer/ItemContainer";
import { useRoute } from "@react-navigation/native";
import firebase, { projectsRef } from "../../config/firebase"; import firebase, { projectsRef } from "../../config/firebase";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { useUser } from "../../providers/UserDataProvider";
const Lyrics = ({ navigation }) => { const Lyrics = ({ navigation }) => {
const [containerLayout, setContainerLayout] = useState(null); 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 { 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 initial = useMemo(() => {
const title = lyricsData?.title || ""; const title = projectTitle || "";
const aiSections = Array.isArray(lyricsData?.lyrics) const aiSections = Array.isArray(projectLyrics) ? projectLyrics : [];
? lyricsData.lyrics
: [];
// Respecter l'ordre de la structure choisie si disponible // Respecter l'ordre de la structure choisie si disponible
const targetStructure = Array.isArray(config?.structure) const targetStructure = Array.isArray(projectConfig?.structure)
? config.structure.map((t) => (t || "").toLowerCase()) ? projectConfig.structure.map((t) => (t || "").toLowerCase())
: null; : null;
if (
aiSections.length && // 1) Si des paroles existent déjà, les utiliser en priorité
targetStructure && if (aiSections.length) {
aiSections.length === targetStructure.length // Optionnel: si une structure cible de même longueur existe, garder l'ordre courant
) { // et harmoniser les types en minuscule.
return { return {
title, title,
sections: aiSections.map((s) => ({ 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) { if (targetStructure && targetStructure.length) {
return { return {
title, title,
sections: targetStructure.map((t) => ({ type: t, lyrics: "" })), sections: targetStructure.map((t) => ({ type: t, lyrics: "" })),
}; };
} }
// Fallback vide
// 3) Fallback vide
return { title, sections: [] }; return { title, sections: [] };
}, [lyricsData, config]); }, [projectTitle, projectLyrics, projectConfig]);
const [titleValue, setTitleValue] = useState(initial.title || ""); 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 [sections, setSections] = useState(initial.sections || []);
const setSectionAt = (index, value) => { const setSectionAt = (index, value) => {
setSections((prev) => { setSections((prev) => {
@@ -67,7 +103,7 @@ const Lyrics = ({ navigation }) => {
// Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente // Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente
const regenerate = useCallback(() => { const regenerate = useCallback(() => {
navigate(Routes.CreateLyricsWithAi, { regenerateKey: Date.now() }); navigate(Routes.CreateLyricsWithAi);
}, []); }, []);
const sanitize = (obj) => { const sanitize = (obj) => {
@@ -102,34 +138,68 @@ const Lyrics = ({ navigation }) => {
); );
return; return;
} }
const user = firebase.auth().currentUser; const normalizedNewLyrics = (sections || []).map((s) => ({
const payload = { 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, title: titleTrimmed,
titleLower: titleTrimmed.toLowerCase(), titleLower: titleTrimmed.toLowerCase(),
lyrics: (sections || []).map((s) => ({ lyrics: normalizedNewLyrics,
type: (s?.type || "").toLowerCase(), config: sanitize(projectConfig),
lyrics: s?.lyrics || "", selections: sanitize(projectSelections),
})),
config: sanitize(config),
selections: sanitize(selections),
userId: user ? user.uid : null,
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(), 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) { } catch (e) {
console.log(e); console.log(e);
Alert.alert("Erreur", "Échec de l'enregistrement dans le projet."); Alert.alert("Erreur", "Échec de l'enregistrement dans le projet.");
} finally { } finally {
await setIsLoading(false); await setIsLoading(false);
} }
}, [titleValue, sections, config, selections, setIsLoading]); }, [
titleValue,
sections,
projectConfig,
projectSelections,
setIsLoading,
selectedProjectId,
projectHasLyrics,
projectLyrics,
]);
return ( return (
<Page headerType="NONE"> <Page headerType="NONE">
<MusicLandHeader onPressBack={goBack} progress={95} /> <MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection)}
progress={95}
/>
<View <View
style={{ style={{
flex: 1, flex: 1,
@@ -153,6 +223,8 @@ const Lyrics = ({ navigation }) => {
height={45} height={45}
value={titleValue} value={titleValue}
setValue={setTitleValue} setValue={setTitleValue}
multiline={false}
maxLength={60}
/> />
{sections.map((s, idx) => { {sections.map((s, idx) => {
// Calculer l'index humain par type // Calculer l'index humain par type
@@ -184,7 +256,7 @@ const Lyrics = ({ navigation }) => {
gap: 12, gap: 12,
}} }}
> >
{!hasLyrics && ( {!projectHasLyrics && (
<BorderGradientButton <BorderGradientButton
title="Générer d'autres paroles" title="Générer d'autres paroles"
onPress={regenerate} onPress={regenerate}
+38 -7
View File
@@ -1,5 +1,5 @@
import { View, Image, StyleSheet } from "react-native"; import { View, Image, StyleSheet } from "react-native";
import React from "react"; import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
import { ai } from "../../assets"; import { ai } from "../../assets";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
@@ -8,8 +8,39 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService"; import { goBack, navigate } from "../../navigation/NavigationService";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { useUserData } from "../../providers/UserDataProvider";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const WritingLyrics = () => { const WritingLyrics = () => {
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 ( return (
<Page headerType="NONE"> <Page headerType="NONE">
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" /> <Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
@@ -25,18 +56,18 @@ const WritingLyrics = () => {
}} }}
> >
<BorderGradientButton <BorderGradientButton
onPress={() => { onPress={() => createAndNavigate({ hasLyrics: true })}
navigate(Routes.CreateLyricsWithAi, {
hasLyrics: true,
});
}}
/> />
<GradientButton <GradientButton
title="Écrire des paroles avec une IA" title="Écrire des paroles avec une IA"
onPress={() => navigate(Routes.CreateLyricsWithAi)} onPress={() => createAndNavigate({ hasLyrics: false })}
/> />
</View> </View>
</View> </View>
<FullscreenIntroVideo
visible={showIntro}
onClose={() => setShowIntro(false)}
/>
</Page> </Page>
); );
}; };
@@ -9,6 +9,8 @@ const CustomInput = ({
value, value,
setValue, setValue,
height = 65, height = 65,
multiline = true,
maxLength,
}) => { }) => {
return ( return (
<View style={{ gap: 8 }}> <View style={{ gap: 8 }}>
@@ -42,8 +44,10 @@ const CustomInput = ({
color: Palette.black, color: Palette.black,
fontFamily: FONT_FAMILY.InterRegularItalic, fontFamily: FONT_FAMILY.InterRegularItalic,
}} }}
multiline multiline={multiline}
textAlignVertical="top" numberOfLines={multiline ? undefined : 1}
maxLength={maxLength}
textAlignVertical={multiline ? "top" : "center"}
/> />
</View> </View>
</View> </View>
@@ -113,7 +113,7 @@ const AddPhotoCover = () => {
/> />
<GradientButton <GradientButton
title="Valider" title="Valider"
onPress={() => navigate(Routes.FinishCompose)} onPress={() => navigate(Routes.FlowSelection)}
/> />
</View> </View>
</Page> </Page>
+54
View File
@@ -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 (
<Page backgroundImg={background.productionBG2} headerType="NONE">
<Image source={ai.bena} style={styles.img} resizeMode="contain" />
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
<View
style={{
paddingBottom: gutters * 2,
paddingHorizontal: gutters,
gap: 12,
}}
>
<BorderGradientButton title="Choisir une image" />
<GradientButton
title="Générer une pochette"
onPress={() => navigate(Routes.PouchReady)}
/>
</View>
</View>
<FullscreenIntroVideo
visible={showIntro}
onClose={() => setShowIntro(false)}
/>
</Page>
);
};
export default ChooseCoverType;
const styles = StyleSheet.create({
img: {
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
},
});
@@ -50,3 +50,4 @@ const styles = StyleSheet.create({
right: -30, right: -30,
}, },
}); });
@@ -11,12 +11,11 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import { Routes } from "../../navigation"; import { Routes } from "../../navigation";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { useRoute } from "@react-navigation/native"; import firebase, { tasksRef } from "../../config/firebase";
import firebase from "../../config/firebase"; import { useUser } from "../../providers/UserDataProvider";
const PouchReady = () => { const PouchReady = () => {
const route = useRoute(); const { selectedProjectId, selectedProject, updateProjectData } = useUser();
const projectId = route?.params?.projectId;
const [coverUrl, setCoverUrl] = useState(null); const [coverUrl, setCoverUrl] = useState(null);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [title, setTitle] = useState(""); const [title, setTitle] = useState("");
@@ -24,35 +23,25 @@ const PouchReady = () => {
const isGenerating = coverStatus === "GENERATING"; const isGenerating = coverStatus === "GENERATING";
useEffect(() => { useEffect(() => {
if (!projectId) return; const d = selectedProject || {};
const unsub = firebase
.firestore()
.collection("projects")
.doc(projectId)
.onSnapshot((doc) => {
const d = doc.data() || {};
setTitle(d?.title || ""); setTitle(d?.title || "");
setCoverUrl(d?.coverUrl || null); setCoverUrl(d?.coverUrl || null);
setCoverStatus(d?.coverStatus || null); setCoverStatus(d?.coverStatus || null);
}); }, [
return () => unsub?.(); selectedProject?.title,
}, [projectId]); selectedProject?.coverUrl,
selectedProject?.coverStatus,
]);
const generateCover = async () => { const generateCover = async () => {
if (!projectId) return; if (!selectedProjectId) return;
try { try {
setLoading(true); setLoading(true);
// Marquer le projet en génération await updateProjectData({ coverStatus: "GENERATING" });
await firebase
.firestore()
.collection("projects")
.doc(projectId)
.set({ coverStatus: "GENERATING" }, { merge: true });
// Créer une tâche pour déclencher la Cloud Function onCreate await tasksRef.add({
await firebase.firestore().collection("tasks").add({
type: "cover", type: "cover",
projectId, projectId: selectedProjectId,
status: "PENDING", status: "PENDING",
createdAt: firebase.firestore.FieldValue.serverTimestamp(), createdAt: firebase.firestore.FieldValue.serverTimestamp(),
}); });
@@ -64,12 +53,11 @@ const PouchReady = () => {
}; };
useEffect(() => { useEffect(() => {
if (!projectId) return; if (!selectedProjectId) return;
// Si pas de cover et pas déjà en génération, lancer une tâche
if (!coverUrl && !isGenerating && !loading) { if (!coverUrl && !isGenerating && !loading) {
generateCover(); generateCover();
} }
}, [projectId, coverUrl, isGenerating]); }, [selectedProjectId, coverUrl, isGenerating]);
return ( return (
<Page backgroundImg={background.studioBG2} headerType="NONE"> <Page backgroundImg={background.studioBG2} headerType="NONE">