Merge branch 'main' of gitlab.com:agenceminuit/musicland
This commit is contained in:
+10
-18
@@ -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"),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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.
@@ -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 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");
|
||||||
|
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -82,4 +82,5 @@ export const Routes = {
|
|||||||
Reels: "Reels",
|
Reels: "Reels",
|
||||||
Follows: "Follows",
|
Follows: "Follows",
|
||||||
FlowSelection: "FlowSelection",
|
FlowSelection: "FlowSelection",
|
||||||
|
ChooseCoverType: "ChooseCoverType",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 }}>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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")
|
setMusicUrls(urls);
|
||||||
.doc(projectId)
|
}, [selectedProject?.musicUrls]);
|
||||||
.onSnapshot((doc) => {
|
|
||||||
const data = doc.data() || {};
|
|
||||||
const urls = Array.isArray(data?.musicUrls)
|
|
||||||
? data.musicUrls.slice(0, 2)
|
|
||||||
: [];
|
|
||||||
setMusicUrls(urls);
|
|
||||||
});
|
|
||||||
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(),
|
});
|
||||||
},
|
await player0?.pause?.();
|
||||||
{ merge: true }
|
await player1?.pause?.();
|
||||||
);
|
navigate(Routes.FlowSelection);
|
||||||
navigate(Routes.FlowSelection, { projectId });
|
|
||||||
} 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
|
||||||
try {
|
useFocusEffect(
|
||||||
navigate(Routes.GeneratingSong, { projectId });
|
useCallback(() => {
|
||||||
} catch (e) {
|
return () => {
|
||||||
console.log("Regenerate error", e?.message);
|
try {
|
||||||
} finally {
|
player0?.pause?.();
|
||||||
goBack();
|
player1?.pause?.();
|
||||||
}
|
} catch {}
|
||||||
};
|
};
|
||||||
|
}, [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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<GradientButton
|
{!saved && (
|
||||||
title="Découvrir mon texte"
|
<GradientButton
|
||||||
containerStyle={{
|
title="Découvrir mon texte"
|
||||||
width: "80%",
|
containerStyle={{
|
||||||
alignSelf: "center",
|
width: "80%",
|
||||||
}}
|
alignSelf: "center",
|
||||||
onPress={() =>
|
}}
|
||||||
navigate(Routes.Lyrics, {
|
onPress={async () => {
|
||||||
lyricsData: result,
|
try {
|
||||||
config,
|
if (saved) return;
|
||||||
selections,
|
setSaved(true);
|
||||||
hasLyrics: false,
|
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>
|
</View>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
+111
-39
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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
|
setTitle(d?.title || "");
|
||||||
.firestore()
|
setCoverUrl(d?.coverUrl || null);
|
||||||
.collection("projects")
|
setCoverStatus(d?.coverStatus || null);
|
||||||
.doc(projectId)
|
}, [
|
||||||
.onSnapshot((doc) => {
|
selectedProject?.title,
|
||||||
const d = doc.data() || {};
|
selectedProject?.coverUrl,
|
||||||
setTitle(d?.title || "");
|
selectedProject?.coverStatus,
|
||||||
setCoverUrl(d?.coverUrl || null);
|
]);
|
||||||
setCoverStatus(d?.coverStatus || null);
|
|
||||||
});
|
|
||||||
return () => unsub?.();
|
|
||||||
}, [projectId]);
|
|
||||||
|
|
||||||
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">
|
||||||
Reference in New Issue
Block a user