big refacto change all flows

This commit is contained in:
Thomas Demirdjian
2025-09-03 15:43:22 +02:00
parent 8f1c062b33
commit 60d1794c99
29 changed files with 841 additions and 344 deletions
-123
View File
@@ -1,123 +0,0 @@
import { View, Text, Image } from "react-native";
import React, { useState } from "react";
import MusicLandHeader from "../../components/MusicLandHeader";
import { background, icons, img } from "../../assets";
import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import { launchImageLibraryAsync } from "expo-image-picker";
import { Routes } from "../../navigation";
const AddPhotoCover = () => {
const [image, setImage] = useState(null);
const onPressAddImage = async () => {
const result = await launchImageLibraryAsync({
mediaTypes: ["images"],
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (!result.canceled) {
setImage(result.assets[0]?.uri);
}
};
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader onPressBack={goBack} progress={90} />
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta pochette est prête!"
subTitle="Quen penses-tu ?"
/>
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: "80%", position: "relative" }}>
<Image
source={img.placeholder}
style={{
width: "100%",
height: 300,
borderRadius: 20,
transform: [{ rotateY: "180deg" }],
}}
/>
<View
style={{ position: "absolute", width: "100%", height: "100%" }}
>
<Image
source={{ uri: image }}
style={{ width: "100%", height: "100%", borderRadius: 20 }}
/>
</View>
<View
style={{
position: "absolute",
alignSelf: "center",
alignItems: "center",
top: 10,
}}
>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
Lust for Life
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Lana del Rey
</Text>
</View>
<View
style={{
position: "absolute",
alignItems: "center",
bottom: 10,
width: "100%",
}}
>
<Image
source={icons.musicLandLogo}
style={{ width: "100%", height: 30 }}
resizeMode="contain"
/>
</View>
</View>
</View>
</View>
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
gap: 12,
}}
>
<BorderGradientButton
title="Ajouter une autre photo"
onPress={onPressAddImage}
/>
<GradientButton
title="Valider"
onPress={() => navigate(Routes.FinishCompose)}
/>
</View>
</Page>
);
};
export default AddPhotoCover;
+10 -7
View File
@@ -1,21 +1,20 @@
import { View, Text, StyleSheet, Image } from "react-native";
import React from "react";
import { View, StyleSheet, Image } from "react-native";
import React, { useState } from "react";
import { ai, background } from "../../assets";
import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useRoute } from "@react-navigation/native";
import MusicLandHeader from "../../components/MusicLandHeader";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import { gutters } from "../../styles";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
const Compose = () => {
const route = useRoute();
const projectId = route?.params?.projectId;
const [showIntro, setShowIntro] = useState(true);
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
<MusicLandHeader onPressBack={goBack} progress={9} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
@@ -28,10 +27,14 @@ const Compose = () => {
>
<GradientButton
title="Composer ma chanson"
onPress={() => navigate(Routes.ComposeSong, { projectId })}
onPress={() => navigate(Routes.ComposeSong)}
/>
</View>
</View>
<FullscreenIntroVideo
visible={showIntro}
onClose={() => setShowIntro(false)}
/>
</Page>
);
};
+34 -21
View File
@@ -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);
+3 -3
View File
@@ -7,6 +7,7 @@ import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import ProgressBar from "../../components/ProgressBar";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import firebase, { projectsRef } from "../../config/firebase";
@@ -21,6 +22,7 @@ const CreatingSong = ({ active, config }) => {
const [musicStatus, setMusicStatus] = useState(null);
const [generationStartAt, setGenerationStartAt] = useState(null);
const progressTimerRef = React.useRef(null);
const { selectedProjectId } = useUser();
// Abonnement au document projet pour suivre le statut et la date de début
useEffect(() => {
@@ -236,9 +238,7 @@ const CreatingSong = ({ active, config }) => {
width: "80%",
alignSelf: "center",
}}
onPress={() =>
navigate(Routes.SongReady, { projectId: config?.projectId })
}
onPress={() => navigate(Routes.SongReady)}
/>
</View>
</BlurView>
-52
View File
@@ -1,52 +0,0 @@
import { View, Image, StyleSheet } from "react-native";
import React from "react";
import { ai, background } from "../../assets";
import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService";
import MusicLandHeader from "../../components/MusicLandHeader";
import { gutters } from "../../styles";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
import BorderGradientButton from "../../components/BorderGradientButton";
const FinishCompose = () => {
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
<MusicLandHeader onPressBack={goBack} progress={100} />
<View
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
>
<View
style={{
paddingBottom: gutters * 2,
width: "70%",
alignSelf: "center",
gap: 12,
}}
>
<BorderGradientButton
title="Continuer plus tard"
onPress={() => navigate(Routes.Home)}
/>
<GradientButton
title="Continuer la création"
onPress={() => navigate(Routes.Home)}
/>
</View>
</View>
</Page>
);
};
export default FinishCompose;
const styles = StyleSheet.create({
img: {
width: "100%",
height: "70%",
position: "absolute",
bottom: -40,
right: -30,
},
});
+89 -50
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useRef, useState } from "react";
import { Image, Platform, Text, View } from "react-native";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Alert, Image, Platform, Text, View } from "react-native";
import Page from "../../layouts/Page";
import { ai, background } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
@@ -15,24 +15,22 @@ import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import moment from "moment";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { FONT_FAMILY } from "../../styles/Fonts";
import useDataFromRef from "../../hooks/useDataFromRef";
import { useUser } from "../../providers/UserDataProvider";
import { useIsFocused } from "@react-navigation/native";
const GeneratingSong = ({ route }) => {
const { config, projectId } = route.params;
const GeneratingSong = () => {
const { selectedProjectId, selectedProject } = useUser();
const [progress, setProgress] = useState(0);
const { setIsLoading } = useMinuit();
const progressTimerRef = useRef(null);
const navigatedRef = useRef(false);
const isFocused = useIsFocused();
const lastConfigKeyRef = useRef(null);
const askedRef = useRef(false);
const { data: project } = useDataFromRef({
ref: projectId ? projectsRef.doc(projectId) : null,
simpleRef: true,
listener: true,
condition: !!projectId,
refreshArray: [projectId],
});
// project loaded from provider
// Progress based on 8 minutes cap or until status changes
// Progress based on 8 minutes cap or until status becomes GENERATED
useEffect(() => {
const totalMs = 8 * 60 * 1000;
const clearTimer = () => {
@@ -41,15 +39,15 @@ const GeneratingSong = ({ route }) => {
progressTimerRef.current = null;
}
};
if (project?.musicStatus !== "GENERATING") {
if (selectedProject?.musicStatus === "GENERATED") {
setProgress(100);
clearTimer();
return () => clearTimer();
}
const update = () => {
const startDate = project?.generationStartAt?.toDate
? project.generationStartAt.toDate()
: new Date(project?.generationStartAt || Date.now());
const startDate = selectedProject?.generationStartAt?.toDate
? selectedProject.generationStartAt.toDate()
: new Date(selectedProject?.generationStartAt || Date.now());
const elapsed = moment().diff(moment(startDate));
const raw = Math.floor((elapsed / totalMs) * 100);
// While status is GENERATING, block visual progress at 99%
@@ -60,16 +58,29 @@ const GeneratingSong = ({ route }) => {
clearTimer();
progressTimerRef.current = setInterval(update, 1000);
return () => clearTimer();
}, [project?.musicStatus]);
}, [selectedProject?.musicStatus]);
// Auto navigate to SongReady when generation completed
useEffect(() => {
if (!config?.projectId) return;
if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) {
if (!selectedProjectId) return;
if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) {
navigatedRef.current = true;
navigate(Routes.SongReady, { projectId: config.projectId });
navigate(Routes.SongReady);
}
}, [project?.musicStatus, config?.projectId]);
}, [selectedProject?.musicStatus, selectedProjectId]);
const effectiveConfig = useMemo(() => {
// Use selectedProject.musicConfig only
const cfg = selectedProject?.musicConfig || {};
return {
title: cfg?.title || "",
lyrics: Array.isArray(cfg?.lyrics) ? cfg.lyrics : [],
genres: Array.isArray(cfg?.genres) ? cfg.genres : [],
voice: cfg?.voice || "",
instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [],
tempo: cfg?.tempo || "",
};
}, [selectedProject?.musicConfig]);
async function startMusicGeneration() {
try {
@@ -78,39 +89,39 @@ const GeneratingSong = ({ route }) => {
const callable = firebase
.functions()
.httpsCallable("music-generateMusic");
const cfg = effectiveConfig || {};
const { data } = await callable({
title: config?.title,
lyrics: config?.lyrics,
genres: config?.genres,
voice: config?.voice,
instruments: config?.instruments,
tempo: config?.tempo,
projectId: config?.projectId,
title: cfg?.title,
lyrics: cfg?.lyrics,
genres: cfg?.genres,
voice: cfg?.voice,
instruments: cfg?.instruments,
tempo: cfg?.tempo,
});
const taskId =
data?.response?.data?.taskId || data?.response?.data?.task_id;
if (config?.projectId && taskId) {
if (selectedProjectId && taskId) {
const baseUpdate = {
sunoTaskId: taskId,
musicStatus: "GENERATING",
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
};
const updatePayload = project?.musicConfig
const updatePayload = selectedProject?.musicConfig
? baseUpdate
: {
...baseUpdate,
musicConfig: {
title: config?.title || "",
lyrics: config?.lyrics || [],
genres: config?.genres || [],
voice: config?.voice || "",
instruments: config?.instruments || [],
tempo: config?.tempo || "",
title: effectiveConfig?.title || "",
lyrics: effectiveConfig?.lyrics || [],
genres: effectiveConfig?.genres || [],
voice: effectiveConfig?.voice || "",
instruments: effectiveConfig?.instruments || [],
tempo: effectiveConfig?.tempo || "",
},
};
await projectsRef
.doc(config.projectId)
.doc(selectedProjectId)
.set(updatePayload, { merge: true });
}
} catch (e) {
@@ -120,16 +131,44 @@ const GeneratingSong = ({ route }) => {
}
}
// Trigger generation only if not already GENERATING
// Trigger generation only when focused; ask once per config
useEffect(() => {
if (!!project?.title && project?.musicStatus !== "GENERATING") {
startMusicGeneration();
if (!isFocused) return;
const key = JSON.stringify(effectiveConfig || {});
if (lastConfigKeyRef.current !== key) {
lastConfigKeyRef.current = key;
askedRef.current = false;
}
}, [project]);
const canAsk =
!!selectedProject?.title && selectedProject?.musicStatus !== "GENERATING";
if (canAsk && !askedRef.current) {
askedRef.current = true;
Alert.alert(
"Attention",
"Une génération va être lancée. Continuer ?",
[
{
text: "Non",
style: "cancel",
},
{
text: "Oui",
onPress: () => startMusicGeneration(),
},
],
);
}
}, [isFocused, selectedProject?.title, selectedProject?.musicStatus, effectiveConfig]);
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader onPressBack={goBack} progress={63} />
<MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection)}
progress={63}
/>
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta musique est en cours de création !"
@@ -206,14 +245,14 @@ const GeneratingSong = ({ route }) => {
</Text>
</View>
<GradientButton
title={"Création en cours..."}
disabled={project?.musicStatus === "GENERATING"}
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() =>
navigate(Routes.SongReady, {
projectId: config?.projectId,
})
title={
selectedProject?.musicStatus === "GENERATED"
? "Découvrir ma musique"
: "Création en cours..."
}
disabled={selectedProject?.musicStatus !== "GENERATED"}
containerStyle={{ width: "80%", alignSelf: "center" }}
onPress={() => navigate(Routes.SongReady)}
/>
</View>
</BlurView>
-93
View File
@@ -1,93 +0,0 @@
import { View, Text, Image } from "react-native";
import React from "react";
import MusicLandHeader from "../../components/MusicLandHeader";
import { background, icons, img } from "../../assets";
import Page from "../../layouts/Page";
import { goBack, navigate } from "../../navigation/NavigationService";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { gutters, Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import BorderGradientButton from "../../components/BorderGradientButton";
import { Routes } from "../../navigation";
import GradientButton from "../../components/GradientButton";
const PhotoCover = () => {
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader onPressBack={goBack} progress={81} />
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader title="Souhaites-tu rajouter une photo de toi sur la pochette?" />
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: "80%", position: "relative" }}>
<Image
source={img.placeholder}
style={{
width: "100%",
height: 300,
borderRadius: 20,
transform: [{ rotateY: "180deg" }],
}}
/>
<View
style={{
position: "absolute",
alignSelf: "center",
alignItems: "center",
top: 10,
}}
>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
Lust for Life
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Lana del Rey
</Text>
</View>
<View
style={{
position: "absolute",
alignItems: "center",
bottom: 10,
width: "100%",
}}
>
<Image
source={icons.musicLandLogo}
style={{ width: "100%", height: 30 }}
resizeMode="contain"
/>
</View>
</View>
</View>
</View>
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
gap: 12,
}}
>
<BorderGradientButton title="Non, laisser la pochette telle quelle" />
<GradientButton
title="Oui"
onPress={() => navigate(Routes.AddPhotoCover)}
/>
</View>
</Page>
);
};
export default PhotoCover;
-183
View File
@@ -1,183 +0,0 @@
import { View, Text, Image, ActivityIndicator } from "react-native";
import { Image as ExpoImage } from "expo-image";
import React, { useEffect, useState } from "react";
import Page from "../../layouts/Page";
import { background, icons, img } from "../../assets";
import MusicLandHeader from "../../components/MusicLandHeader";
import { goBack, navigate } from "../../navigation/NavigationService";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { gutters, Palette, Style } from "../../styles";
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";
const PouchReady = () => {
const route = useRoute();
const projectId = route?.params?.projectId;
const [coverUrl, setCoverUrl] = useState(null);
const [loading, setLoading] = useState(false);
const [title, setTitle] = useState("");
const [coverStatus, setCoverStatus] = useState(null);
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 generateCover = async () => {
if (!projectId) return;
try {
setLoading(true);
// Marquer le projet en génération
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 firebase.firestore().collection("tasks").add({
type: "cover",
projectId,
status: "PENDING",
createdAt: firebase.firestore.FieldValue.serverTimestamp(),
});
} catch (e) {
console.log("Cover task error", e?.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (!projectId) return;
// Si pas de cover et pas déjà en génération, lancer une tâche
if (!coverUrl && !isGenerating && !loading) {
generateCover();
}
}, [projectId, coverUrl, isGenerating]);
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader onPressBack={goBack} progress={72} />
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title={
coverUrl ? "Ta pochette est prête!" : "Génération de la pochette"
}
subTitle="Quen penses-tu ?"
/>
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: "80%", position: "relative" }}>
{coverUrl ? (
<ExpoImage
source={{ uri: coverUrl }}
cachePolicy="memory-disk"
priority="high"
contentFit="cover"
transition={120}
style={{ width: "100%", height: 300, borderRadius: 20 }}
/>
) : (
<View
style={{
width: "100%",
height: 300,
borderRadius: 20,
backgroundColor: "#00000040",
...Style.containerCenter,
}}
>
{isGenerating || loading ? (
<ActivityIndicator color={Palette.white} />
) : (
<Image
source={img.placeholder}
style={{ width: 120, height: 120, opacity: 0.5 }}
/>
)}
</View>
)}
<View
style={{
position: "absolute",
alignSelf: "center",
alignItems: "center",
top: 10,
}}
>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
}}
>
{title || "Titre"}
</Text>
<Text
style={{
fontSize: 14,
color: Palette.gray,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
MusicLand
</Text>
</View>
<View
style={{
position: "absolute",
alignItems: "center",
bottom: 10,
width: "100%",
}}
>
<Image
source={icons.musicLandLogo}
style={{ width: "100%", height: 30 }}
resizeMode="contain"
/>
</View>
</View>
</View>
</View>
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
gap: 12,
}}
>
<BorderGradientButton
title={isGenerating ? "Génération en cours..." : "Regénérer"}
icon={icons.stars}
onPress={generateCover}
disabled={isGenerating}
/>
<GradientButton
title={isGenerating ? "Veuillez patienter..." : "Valider"}
disabled={isGenerating || !coverUrl}
onPress={() => navigate(Routes.PhotoCover)}
/>
</View>
</Page>
);
};
export default PouchReady;
+62 -43
View File
@@ -1,6 +1,6 @@
import { useAudioPlayer } from "expo-audio";
import { BlurView } from "expo-blur";
import React, { useEffect, useRef, useState } from "react";
import React, { useEffect, useRef, useState, useCallback } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
@@ -9,16 +9,22 @@ import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
import firebase, { projectsRef } from "../../config/firebase";
import firebase from "../../config/firebase";
import { useUser } from "../../providers/UserDataProvider";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { navigate } from "../../navigation/NavigationService";
import { Style } from "../../styles";
import { gutters, size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { useFocusEffect } from "@react-navigation/native";
const SongReady = ({ route }) => {
const { projectId = null } = route?.params || {};
const SongReady = () => {
const {
selectedProjectId: projectId,
selectedProject,
updateProjectData,
} = useUser();
const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
@@ -28,29 +34,20 @@ const SongReady = ({ route }) => {
1: { pos: 0, dur: 0 },
});
const player0 = useAudioPlayer(
musicUrls[0] ? { uri: musicUrls[0] } : undefined
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
);
const player1 = useAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
);
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
// Charger les URLs depuis le document projet
// Sync URLs from provider's selectedProject
useEffect(() => {
if (!projectId) return;
const unsub = firebase
.firestore()
.collection("projects")
.doc(projectId)
.onSnapshot((doc) => {
const data = doc.data() || {};
const urls = Array.isArray(data?.musicUrls)
? data.musicUrls.slice(0, 2)
: [];
setMusicUrls(urls);
});
return () => unsub?.();
}, [projectId]);
const urls = Array.isArray(selectedProject?.musicUrls)
? selectedProject.musicUrls.slice(0, 2)
: [];
setMusicUrls(urls);
}, [selectedProject?.musicUrls]);
// Sync progression depuis les players
useEffect(() => {
@@ -117,34 +114,50 @@ const SongReady = ({ route }) => {
try {
const url = musicUrls[selectedIndex];
if (!projectId || !url) return;
await projectsRef.doc(projectId).set(
{
songIndex: selectedIndex,
songUrl: url,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
navigate(Routes.FlowSelection, { projectId });
await updateProjectData({
songIndex: selectedIndex,
songUrl: url,
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
});
await player0?.pause?.();
await player1?.pause?.();
navigate(Routes.FlowSelection);
} catch (e) {
console.log("Validate error", e?.message);
}
};
const onPressRegenerate = async () => {
try {
navigate(Routes.GeneratingSong, { projectId });
} catch (e) {
console.log("Regenerate error", e?.message);
} finally {
goBack();
}
};
// Pause audio when screen loses focus (navigate/reset) and on unmount
useFocusEffect(
useCallback(() => {
return () => {
try {
player0?.pause?.();
player1?.pause?.();
} catch {}
};
}, [player0, player1]),
);
useEffect(() => {
return () => {
try {
player0?.pause?.();
player1?.pause?.();
} catch {}
};
}, [player0, player1]);
return (
<Page headerType="NONE" backgroundImg={background.studioBG2}>
<MusicLandHeader
onPressBack={() => navigate(Routes.FlowSelection, { projectId })}
onPressBack={async () => {
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.FlowSelection);
}}
progress={63}
/>
<View style={{ flex: 1, marginTop: 16 }}>
@@ -207,7 +220,7 @@ const SongReady = ({ route }) => {
} catch (e) {
console.log(
"SongReady pause on seek start",
e?.message
e?.message,
);
}
}}
@@ -269,7 +282,13 @@ const SongReady = ({ route }) => {
<BorderGradientButton
title="Regénérer"
icon={icons.stars}
onPress={onPressRegenerate}
onPress={async () => {
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.ComposeSong);
}}
/>
<GradientButton
title="Choisir ce morceau"
+11 -8
View File
@@ -12,7 +12,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
const Studio = () => {
const [selected, setSelected] = useState(null);
const { userProjects: projects } = useUser();
const { userProjects: projects, selectProject } = useUser();
return (
<Page
@@ -107,7 +107,8 @@ const Studio = () => {
"La chanson est en cours de génération. Veuillez patienter.",
);
} else {
navigate(Routes.Compose, { projectId: selected.id });
selectProject(selected.id);
navigate(Routes.Compose);
}
}}
/>
@@ -118,18 +119,20 @@ const Studio = () => {
containerStyle={{
marginTop: responsiveHeight(2),
}}
onPress={() =>
navigate(Routes.SongReady, { projectId: selected.id })
}
onPress={() => {
selectProject(selected.id);
navigate(Routes.SongReady);
}}
/>
<GradientButton
title="Générer une pochette"
containerStyle={{
marginTop: responsiveHeight(2),
}}
onPress={() =>
navigate(Routes.PouchReady, { projectId: selected.id })
}
onPress={() => {
selectProject(selected.id);
navigate(Routes.PouchReady);
}}
/>
</>
)}