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