diff --git a/dragNdropScreen.png b/dragNdropScreen.png deleted file mode 100644 index 7cd09c7..0000000 Binary files a/dragNdropScreen.png and /dev/null differ diff --git a/src/components/modal/ValidateModal.js b/src/components/modal/ValidateModal.js index 11228dd..0d13b5e 100644 --- a/src/components/modal/ValidateModal.js +++ b/src/components/modal/ValidateModal.js @@ -44,22 +44,13 @@ const ValidateModal = ({ visible, onClose, onPressValidate }) => { fontSize: 16, color: Palette.white, fontFamily: FONT_FAMILY.InterRegular, + textAlign: "center", }} > Lorsque tu clique sur valider, tu ne pourra plus changer ni le - texte ni la mélodie.{" "} + texte ni la mélodie. - - Valider - diff --git a/src/navigation/MainStack.js b/src/navigation/MainStack.js index 237670d..0c02dfb 100644 --- a/src/navigation/MainStack.js +++ b/src/navigation/MainStack.js @@ -45,6 +45,7 @@ import RecordedPlayback from "../screens/Playback/RecordedPlayback"; import ChooseDecor from "../screens/Playback/ChooseDecor"; import CreatingDecor from "../screens/Playback/CreatingDecor"; import VideoFinalize from "../screens/Playback/VideoFinalize"; +import GeneratingSong from "../screens/Studio/GeneratingSong"; import AllMyMusic from "../screens/Library/AllMyMusic"; import AllMyPlaylist from "../screens/Library/AllMyPlaylist"; import AllMyLikedMusic from "../screens/Library/AllMyLikedMusic"; @@ -185,6 +186,10 @@ const screens = [ name: Routes.StreamSong, component: StreamSong, }, + { + name: Routes.GeneratingSong, + component: GeneratingSong, + }, { name: Routes.SongRelease, component: SongRelease, diff --git a/src/navigation/Routes.js b/src/navigation/Routes.js index c653db7..9eb66de 100644 --- a/src/navigation/Routes.js +++ b/src/navigation/Routes.js @@ -49,6 +49,7 @@ export const Routes = { StreamSong: "StreamSong", SongRelease: "SongRelease", PlaybackExample: "PlaybackExample", + GeneratingSong: "GeneratingSong", PlaybackOnboarding: "PlaybackOnboarding", Playback: "Playback", diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js index 647cbaa..213cf0e 100644 --- a/src/screens/Library/MusicDetails.js +++ b/src/screens/Library/MusicDetails.js @@ -174,17 +174,22 @@ const MusicDetails = () => { } }; const description = useMemo(() => { - // Try to build a readable text from lyrics if present + // Build a readable text from lyrics with section labels if (Array.isArray(project?.lyrics)) { return project.lyrics - .map((s) => (s?.lyrics || "").trim()) + .map((s) => { + const body = (s?.lyrics || "").trim(); + if (!body) return null; + const t = (s?.type || "").toLowerCase(); + const label = t === "refrain" ? "Refrain" : "Couplet"; + return `[${label}]\n${body}`; + }) .filter(Boolean) - .slice(0, 4) .join("\n\n"); } const c = project?.lyrics?.couplet; const r = project?.lyrics?.refrain; - const parts = [c, r].filter(Boolean); + const parts = [c ? `[Couplet]\n${c}` : null, r ? `[Refrain]\n${r}` : null].filter(Boolean); return parts.length ? parts.join("\n\n") : ""; }, [project]); diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js index be81f4a..a1654d0 100644 --- a/src/screens/NewMusicOptions.js +++ b/src/screens/NewMusicOptions.js @@ -10,6 +10,7 @@ import { navigate } from "../navigation/NavigationService"; import { useUserData } from "../providers/UserDataProvider"; import FontAwesome from "@expo/vector-icons/FontAwesome"; import palette from "../styles/Palette"; +import GeneratingSong from "./Studio/GeneratingSong"; const CREATE_DATA = [ { @@ -77,13 +78,19 @@ const NewMusicOptions = ({ route }) => { }); break; case 1: - navigate(Routes.Compose, { projectId: currentProjet.id }); + if (currentProjet?.musicStatus === "GENERATING") { + navigate(Routes.GeneratingSong, { projectId: currentProjet.id }); + } else if (currentProjet?.musicStatus === "GENERATED") { + navigate(Routes.SongReady, { projectId: currentProjet.id }); + } else { + navigate(Routes.Compose, { projectId: currentProjet.id }); + } break; case 2: navigate(Routes.PouchReady, { projectId: currentProjet.id }); break; - default: - navigate(Routes.PouchReady, { projectId: currentProjet.id }); + case 3: + navigate(Routes.Playback, { projectId: currentProjet.id }); break; } }; diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js index 8a52c20..edb41e9 100644 --- a/src/screens/Studio/ComposeSong.js +++ b/src/screens/Studio/ComposeSong.js @@ -4,17 +4,17 @@ import Page from "../../layouts/Page"; import { background } from "../../assets"; import MusicLandHeader from "../../components/MusicLandHeader"; import GradientButton from "../../components/GradientButton"; -import { goBack } from "../../navigation/NavigationService"; +import { goBack, navigate } from "../../navigation/NavigationService"; import { gutters } from "../../styles"; import SwiperFlatList from "react-native-swiper-flatlist"; import ChooseGenre from "./ChooseGenre"; import CustomizeVoice from "./CustomizeVoice"; import ChooseInstruments from "./ChooseInstruments"; import ChooseRhythm from "./ChooseRhythm"; -import CreatingSong from "./CreatingSong"; import { useRoute } from "@react-navigation/native"; -import firebase from "../../config/firebase"; +import { projectsRef } from "../../config/firebase"; import useDataFromRef from "../../hooks/useDataFromRef"; +import { Routes } from "../../navigation"; const { width } = Dimensions.get("window"); @@ -27,20 +27,14 @@ const ComposeSong = () => { const projectId = route?.params?.projectId; // Selections state - const [genres, setGenres] = useState(__DEV__ ? ["Hip Hop/Rap", "Punk"] : []); - const [voice, setVoice] = useState( - __DEV__ ? "Deux voix pour interpreter ta chanson" : null, - ); - const [instruments, setInstruments] = useState( - __DEV__ ? ["Synthétiseur", "Trompette", "Piano classique"] : [], - ); - const [rhythm, setRhythm] = useState(__DEV__ ? "Rapide" : null); + const [genres, setGenres] = useState([]); + const [voice, setVoice] = useState(null); + const [instruments, setInstruments] = useState([]); + const [rhythm, setRhythm] = useState(null); // Fetch selected project to get title + lyrics const { data: project } = useDataFromRef({ - ref: projectId - ? firebase.firestore().collection("projects").doc(projectId) - : null, + ref: projectId ? projectsRef.doc(projectId) : null, simpleRef: true, listener: true, condition: !!projectId, @@ -86,6 +80,10 @@ const ComposeSong = () => { }, [project, genres, voice, instruments, rhythm, projectId]); const onPressNext = () => { + if (selectedIndex === 3) { + navigate(Routes.GeneratingSong, { config: musicConfig, projectId }); + return; + } setSelectedIndex(selectedIndex + 1); setProgress(progress + 9); scrollRef.current.scrollToIndex({ @@ -159,15 +157,6 @@ const ComposeSong = () => { > - - - {selectedIndex !== 4 && ( diff --git a/src/screens/Studio/CreatingSong.js b/src/screens/Studio/CreatingSong.js index 7725ded..78dfd50 100644 --- a/src/screens/Studio/CreatingSong.js +++ b/src/screens/Studio/CreatingSong.js @@ -9,14 +9,14 @@ import ProgressBar from "../../components/ProgressBar"; import { goBack, navigate } from "../../navigation/NavigationService"; import { Routes } from "../../navigation"; import GradientButton from "../../components/GradientButton"; -import firebase from "../../config/firebase"; +import firebase, { projectsRef } from "../../config/firebase"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import moment from "moment"; const CreatingSong = ({ active, config }) => { const [progress, setProgress] = useState(0); const [called, setCalled] = useState(false); - const [result, setResult] = useState(null); + const [, setResult] = useState(null); const { setIsLoading } = useMinuit(); const [musicStatus, setMusicStatus] = useState(null); const [generationStartAt, setGenerationStartAt] = useState(null); @@ -110,10 +110,7 @@ const CreatingSong = ({ active, config }) => { // Si un projectId et un taskId existent, mettre à jour le projet if (config?.projectId && taskId) { - const projectRef = firebase - .firestore() - .collection("projects") - .doc(config.projectId); + const projectRef = projectsRef.doc(config.projectId); await projectRef.set( { diff --git a/src/screens/Studio/GeneratingSong.js b/src/screens/Studio/GeneratingSong.js new file mode 100644 index 0000000..ab4d4ad --- /dev/null +++ b/src/screens/Studio/GeneratingSong.js @@ -0,0 +1,228 @@ +import React, { useEffect, useRef, useState } from "react"; +import { Image, Platform, Text, View } from "react-native"; +import Page from "../../layouts/Page"; +import { ai, background } from "../../assets"; +import MusicLandHeader from "../../components/MusicLandHeader"; +import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; +import { BlurView } from "expo-blur"; +import { Palette } from "../../styles"; +import ProgressBar from "../../components/ProgressBar"; +import GradientButton from "../../components/GradientButton"; +import { goBack, navigate } from "../../navigation/NavigationService"; +import { Routes } from "../../navigation"; +import firebase, { projectsRef } from "../../config/firebase"; +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"; + +const GeneratingSong = ({ route }) => { + const { config, projectId } = route.params; + const [progress, setProgress] = useState(0); + const { setIsLoading } = useMinuit(); + const progressTimerRef = useRef(null); + const navigatedRef = useRef(false); + + const { data: project } = useDataFromRef({ + ref: projectId ? projectsRef.doc(projectId) : null, + simpleRef: true, + listener: true, + condition: !!projectId, + refreshArray: [projectId], + }); + + // Progress based on 8 minutes cap or until status changes + useEffect(() => { + const totalMs = 8 * 60 * 1000; + const clearTimer = () => { + if (progressTimerRef.current) { + clearInterval(progressTimerRef.current); + progressTimerRef.current = null; + } + }; + if (project?.musicStatus !== "GENERATING") { + setProgress(100); + clearTimer(); + return () => clearTimer(); + } + const update = () => { + const startDate = project?.generationStartAt?.toDate + ? project.generationStartAt.toDate() + : new Date(project?.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% + const pct = Math.max(0, Math.min(99, raw)); + setProgress(pct); + }; + update(); + clearTimer(); + progressTimerRef.current = setInterval(update, 1000); + return () => clearTimer(); + }, [project?.musicStatus]); + + // Auto navigate to SongReady when generation completed + useEffect(() => { + if (!config?.projectId) return; + if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) { + navigatedRef.current = true; + navigate(Routes.SongReady, { projectId: config.projectId }); + } + }, [project?.musicStatus, config?.projectId]); + + async function startMusicGeneration() { + try { + console.log("startMusicGeneration"); + await setIsLoading(true); + const callable = firebase + .functions() + .httpsCallable("music-generateMusic"); + 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, + }); + const taskId = + data?.response?.data?.taskId || data?.response?.data?.task_id; + if (config?.projectId && taskId) { + const baseUpdate = { + sunoTaskId: taskId, + musicStatus: "GENERATING", + generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }; + const updatePayload = project?.musicConfig + ? baseUpdate + : { + ...baseUpdate, + musicConfig: { + title: config?.title || "", + lyrics: config?.lyrics || [], + genres: config?.genres || [], + voice: config?.voice || "", + instruments: config?.instruments || [], + tempo: config?.tempo || "", + }, + }; + await projectsRef + .doc(config.projectId) + .set(updatePayload, { merge: true }); + } + } catch (e) { + console.log("GeneratingSong error", e?.message); + } finally { + await setIsLoading(false); + } + } + + // Trigger generation only if not already GENERATING + useEffect(() => { + if (!!project?.title && project?.musicStatus !== "GENERATING") { + startMusicGeneration(); + } + }, [project]); + + return ( + + + + + + + + + + + + + + Ta musique est{"\n"}en cours de création + + + + + {progress}% + + + + navigate(Routes.SongReady, { + projectId: config?.projectId, + }) + } + /> + + + + + + + + ); +}; + +export default GeneratingSong; diff --git a/src/screens/Studio/SongReady.js b/src/screens/Studio/SongReady.js index b22102d..9988aa4 100644 --- a/src/screens/Studio/SongReady.js +++ b/src/screens/Studio/SongReady.js @@ -13,14 +13,12 @@ import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import { Routes } from "../../navigation"; import ValidateModal from "../../components/modal/ValidateModal"; -import { useRoute } from "@react-navigation/core"; -import firebase from "../../config/firebase"; +import firebase, { projectsRef } from "../../config/firebase"; import { useAudioPlayer } from "expo-audio"; import { responsiveHeight } from "react-native-responsive-dimensions"; -const SongReady = () => { - const params = useRoute().params || {}; - const projectId = params?.projectId; +const SongReady = ({ route }) => { + const { projectId = null } = route?.params || {}; const [showValidateModal, setShowValidateModal] = useState(false); const [musicUrls, setMusicUrls] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); @@ -35,6 +33,7 @@ const SongReady = () => { const player1 = useAudioPlayer( musicUrls[1] ? { uri: musicUrls[1] } : undefined, ); + const wasPlayingBeforeSeek = useRef({ 0: false, 1: false }); // Charger les URLs depuis le document projet useEffect(() => { @@ -118,18 +117,14 @@ const SongReady = () => { try { const url = musicUrls[selectedIndex]; if (!projectId || !url) return; - await firebase - .firestore() - .collection("projects") - .doc(projectId) - .set( - { - songUrl: url, - updatedAt: firebase.firestore.FieldValue.serverTimestamp(), - }, - { merge: true }, - ); - navigate(Routes.PouchReady, { projectId }); + await projectsRef.doc(projectId).set( + { + songUrl: url, + updatedAt: firebase.firestore.FieldValue.serverTimestamp(), + }, + { merge: true }, + ); + navigate(Routes.FlowSelection, { projectId }); } catch (e) { console.log("Validate error", e?.message); } @@ -137,36 +132,7 @@ const SongReady = () => { const onPressRegenerate = async () => { try { - if (!projectId) return goBack(); - const doc = await firebase - .firestore() - .collection("projects") - .doc(projectId) - .get(); - const project = doc.data() || {}; - const musicConfig = project?.musicConfig || {}; - - const callable = firebase - .functions() - .httpsCallable("music-generateMusic"); - const { data } = await callable({ - ...musicConfig, - projectId, - }); - - const taskId = - data?.response?.data?.taskId || data?.response?.data?.task_id; - if (taskId) { - await firebase.firestore().collection("projects").doc(projectId).set( - { - sunoTaskId: taskId, - musicStatus: "GENERATING", - generationStartAt: firebase.firestore.FieldValue.serverTimestamp(), - updatedAt: firebase.firestore.FieldValue.serverTimestamp(), - }, - { merge: true }, - ); - } + navigate(Routes.GeneratingSong, { projectId }); } catch (e) { console.log("Regenerate error", e?.message); } finally { @@ -176,7 +142,10 @@ const SongReady = () => { return ( - + navigate(Routes.FlowSelection, { projectId })} + progress={63} + /> { : 0 } seekEnabled={!!musicUrls[idx]} + onSeekStart={async () => { + try { + const player = idx === 0 ? player0 : player1; + wasPlayingBeforeSeek.current[idx] = !!player?.playing; + if (player?.playing) { + await player.pause?.(); + setIsPlaying((p) => ({ ...p, [idx]: false })); + } + } catch (e) { + console.log( + "SongReady pause on seek start", + e?.message, + ); + } + }} onSeek={(ratio) => onSeek(idx, ratio)} + onSeekEnd={async () => { + try { + const player = idx === 0 ? player0 : player1; + if (player && wasPlayingBeforeSeek.current[idx]) { + await player.play?.(); + setIsPlaying((p) => ({ ...p, [idx]: true })); + } + wasPlayingBeforeSeek.current[idx] = false; + } catch (e) { + console.log("SongReady resume after seek", e?.message); + } + }} />