big refacto change all flows
This commit is contained in:
+10
-18
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -230,6 +230,10 @@ export const ai = {
|
||||
john,
|
||||
};
|
||||
|
||||
export const videos = {
|
||||
test: require("./video/testVideo.mp4"),
|
||||
};
|
||||
|
||||
export const img = {
|
||||
placeholder,
|
||||
placeholder2,
|
||||
|
||||
Binary file not shown.
@@ -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;
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -82,4 +82,5 @@ export const Routes = {
|
||||
Reels: "Reels",
|
||||
Follows: "Follows",
|
||||
FlowSelection: "FlowSelection",
|
||||
ChooseCoverType: "ChooseCoverType",
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) => (
|
||||
<BorderGradientButton
|
||||
key={index}
|
||||
title={item}
|
||||
|
||||
+14
-10
@@ -17,7 +17,7 @@ import { projectsRef } from "../config/firebase";
|
||||
import { useGlobal } from "reactn";
|
||||
|
||||
const Home = () => {
|
||||
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 (
|
||||
<Page backgroundImg={background.homeBG} headerType="NONE">
|
||||
@@ -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 = () => {
|
||||
<GradientButton
|
||||
title="Créer une nouvelle musique"
|
||||
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||
onPress={() => navigate(Routes.FlowSelection)}
|
||||
onPress={() => {
|
||||
resetSelectedProject();
|
||||
navigate(Routes.FlowSelection);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -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 }) => {
|
||||
<Page
|
||||
backgroundImg={background.homeBG}
|
||||
headerType="NAVIGATION"
|
||||
title={!!currentProjet ? "Continuer la création" : "Nouvelle musique"}
|
||||
title={!!selectedProject ? "Continuer la création" : "Nouvelle musique"}
|
||||
scrollEnabled={true}
|
||||
>
|
||||
<View style={{ gap: 10 }}>
|
||||
|
||||
@@ -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 }) => {
|
||||
}}
|
||||
>
|
||||
<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" /> */}
|
||||
<GradientButton
|
||||
title="Enregistrer mon Playback"
|
||||
@@ -79,6 +84,10 @@ const Playback = ({ route }) => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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) => (
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => onPressMenu(item)}
|
||||
|
||||
@@ -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 (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
|
||||
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
|
||||
<MusicLandHeader onPressBack={goBack} progress={9} />
|
||||
<View
|
||||
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
|
||||
>
|
||||
@@ -28,10 +27,14 @@ const Compose = () => {
|
||||
>
|
||||
<GradientButton
|
||||
title="Composer ma chanson"
|
||||
onPress={() => navigate(Routes.ComposeSong, { projectId })}
|
||||
onPress={() => navigate(Routes.ComposeSong)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
@@ -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 (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
<MusicLandHeader onPressBack={goBack} progress={63} />
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.FlowSelection)}
|
||||
progress={63}
|
||||
/>
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Ta musique est en cours de création !"
|
||||
@@ -206,14 +245,14 @@ const GeneratingSong = ({ route }) => {
|
||||
</Text>
|
||||
</View>
|
||||
<GradientButton
|
||||
title={"Création en cours..."}
|
||||
disabled={project?.musicStatus === "GENERATING"}
|
||||
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||
onPress={() =>
|
||||
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)}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
@@ -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 (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.FlowSelection, { projectId })}
|
||||
onPressBack={async () => {
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
} catch {}
|
||||
navigate(Routes.FlowSelection);
|
||||
}}
|
||||
progress={63}
|
||||
/>
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
@@ -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 }) => {
|
||||
<BorderGradientButton
|
||||
title="Regénérer"
|
||||
icon={icons.stars}
|
||||
onPress={onPressRegenerate}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
} catch {}
|
||||
navigate(Routes.ComposeSong);
|
||||
}}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Choisir ce morceau"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
const Studio = () => {
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const { userProjects: projects } = useUser();
|
||||
const { userProjects: projects, selectProject } = useUser();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -107,7 +107,8 @@ const Studio = () => {
|
||||
"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);
|
||||
}}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Générer une pochette"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.PouchReady, { projectId: selected.id })
|
||||
}
|
||||
onPress={() => {
|
||||
selectProject(selected.id);
|
||||
navigate(Routes.PouchReady);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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 (
|
||||
<Page headerType="NONE">
|
||||
<MusicLandHeader
|
||||
onPressBack={onPressBack}
|
||||
progress={progress}
|
||||
showSkip={selectedIndex === 4}
|
||||
/>
|
||||
<MusicLandHeader onPressBack={onPressBack} progress={progress} />
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -250,7 +307,14 @@ const CreateLyricsWithAi = ({ route }) => {
|
||||
}}
|
||||
>
|
||||
<CustomizeSongStructure
|
||||
baseStructure={parsedStructure || []}
|
||||
baseStructure={
|
||||
parsedStructure ||
|
||||
(Array.isArray(customStructure)
|
||||
? customStructure
|
||||
: Array.isArray(selectedProject?.config?.structure)
|
||||
? selectedProject.config.structure
|
||||
: [])
|
||||
}
|
||||
onChange={setCustomStructure}
|
||||
/>
|
||||
</View>
|
||||
@@ -273,7 +337,6 @@ const CreateLyricsWithAi = ({ route }) => {
|
||||
<CreatingLyrics
|
||||
active={selectedIndex === 8}
|
||||
config={lyricsConfig}
|
||||
regenerateKey={regenerateKey}
|
||||
selections={{
|
||||
objective,
|
||||
otherObjective,
|
||||
@@ -292,7 +355,10 @@ const CreateLyricsWithAi = ({ route }) => {
|
||||
</SwiperFlatList>
|
||||
</View>
|
||||
{selectedIndex !== 8 && (
|
||||
<GradientButton title="Suivant" onPress={onPressNext} />
|
||||
<GradientButton
|
||||
title={selectedIndex === 7 ? "Générer" : "Suivant"}
|
||||
onPress={onPressNext}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
@@ -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 (
|
||||
<View
|
||||
@@ -121,19 +160,6 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
|
||||
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 }}>
|
||||
<Text
|
||||
style={{
|
||||
@@ -157,21 +183,37 @@ const CreatingLyrics = ({ active, config, regenerateKey, selections }) => {
|
||||
{progress}%
|
||||
</Text>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Découvrir mon texte"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.Lyrics, {
|
||||
lyricsData: result,
|
||||
config,
|
||||
selections,
|
||||
hasLyrics: false,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{!saved && (
|
||||
<GradientButton
|
||||
title="Découvrir mon texte"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={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("Save generated lyrics error", e?.message);
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</BlurView>
|
||||
</View>
|
||||
|
||||
+111
-39
@@ -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 (
|
||||
<Page headerType="NONE">
|
||||
<MusicLandHeader onPressBack={goBack} progress={95} />
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.FlowSelection)}
|
||||
progress={95}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
@@ -153,6 +223,8 @@ const Lyrics = ({ navigation }) => {
|
||||
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 && (
|
||||
<BorderGradientButton
|
||||
title="Générer d'autres paroles"
|
||||
onPress={regenerate}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Image, StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { ai } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
@@ -8,8 +8,39 @@ import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
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 { 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 (
|
||||
<Page headerType="NONE">
|
||||
<Image source={ai.nathalie} style={styles.img} resizeMode="contain" />
|
||||
@@ -25,18 +56,18 @@ const WritingLyrics = () => {
|
||||
}}
|
||||
>
|
||||
<BorderGradientButton
|
||||
onPress={() => {
|
||||
navigate(Routes.CreateLyricsWithAi, {
|
||||
hasLyrics: true,
|
||||
});
|
||||
}}
|
||||
onPress={() => createAndNavigate({ hasLyrics: true })}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Écrire des paroles avec une IA"
|
||||
onPress={() => navigate(Routes.CreateLyricsWithAi)}
|
||||
onPress={() => createAndNavigate({ hasLyrics: false })}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,8 @@ const CustomInput = ({
|
||||
value,
|
||||
setValue,
|
||||
height = 65,
|
||||
multiline = true,
|
||||
maxLength,
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ gap: 8 }}>
|
||||
@@ -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"}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -113,7 +113,7 @@ const AddPhotoCover = () => {
|
||||
/>
|
||||
<GradientButton
|
||||
title="Valider"
|
||||
onPress={() => navigate(Routes.FinishCompose)}
|
||||
onPress={() => navigate(Routes.FlowSelection)}
|
||||
/>
|
||||
</View>
|
||||
</Page>
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
Reference in New Issue
Block a user