big refacto change all flows
This commit is contained in:
@@ -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="Qu’en 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;
|
||||
@@ -1,21 +1,20 @@
|
||||
import { View, Text, StyleSheet, Image } from "react-native";
|
||||
import React from "react";
|
||||
import { View, StyleSheet, Image } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import { ai, background } from "../../assets";
|
||||
import Page from "../../layouts/Page";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { Routes } from "../../navigation";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { gutters } from "../../styles";
|
||||
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
||||
|
||||
const Compose = () => {
|
||||
const route = useRoute();
|
||||
const projectId = route?.params?.projectId;
|
||||
const [showIntro, setShowIntro] = useState(true);
|
||||
return (
|
||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
|
||||
<MusicLandHeader showSkip onPressBack={goBack} progress={9} />
|
||||
<MusicLandHeader onPressBack={goBack} progress={9} />
|
||||
<View
|
||||
style={{ flex: 1, position: "relative", justifyContent: "flex-end" }}
|
||||
>
|
||||
@@ -28,10 +27,14 @@ const Compose = () => {
|
||||
>
|
||||
<GradientButton
|
||||
title="Composer ma chanson"
|
||||
onPress={() => navigate(Routes.ComposeSong, { projectId })}
|
||||
onPress={() => navigate(Routes.ComposeSong)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,9 +11,8 @@ import ChooseGenre from "./ChooseGenre";
|
||||
import CustomizeVoice from "./CustomizeVoice";
|
||||
import ChooseInstruments from "./ChooseInstruments";
|
||||
import ChooseRhythm from "./ChooseRhythm";
|
||||
import { useRoute } from "@react-navigation/native";
|
||||
import { projectsRef } from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { Routes } from "../../navigation";
|
||||
|
||||
const { width } = Dimensions.get("window");
|
||||
@@ -23,8 +22,7 @@ const ComposeSong = () => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [progress, setProgress] = useState(18);
|
||||
const [containerLayout, setContainerLayout] = useState(null);
|
||||
const route = useRoute();
|
||||
const projectId = route?.params?.projectId;
|
||||
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||
|
||||
// Selections state
|
||||
const [genres, setGenres] = useState([]);
|
||||
@@ -32,14 +30,6 @@ const ComposeSong = () => {
|
||||
const [instruments, setInstruments] = useState([]);
|
||||
const [rhythm, setRhythm] = useState(null);
|
||||
|
||||
// Fetch selected project to get title + lyrics
|
||||
const { data: project } = useDataFromRef({
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
});
|
||||
|
||||
const isStepValid = useMemo(() => {
|
||||
switch (selectedIndex) {
|
||||
case 0:
|
||||
@@ -57,31 +47,54 @@ const ComposeSong = () => {
|
||||
|
||||
const musicConfig = useMemo(() => {
|
||||
let lyricsArr = [];
|
||||
if (Array.isArray(project?.lyrics)) {
|
||||
lyricsArr = project.lyrics.map((s) => ({
|
||||
if (Array.isArray(selectedProject?.lyrics)) {
|
||||
lyricsArr = selectedProject.lyrics.map((s) => ({
|
||||
type: (s?.type || "").toLowerCase(),
|
||||
lyrics: s?.lyrics || "",
|
||||
}));
|
||||
} else {
|
||||
const c = project?.lyrics?.couplet;
|
||||
const r = project?.lyrics?.refrain;
|
||||
const c = selectedProject?.lyrics?.couplet;
|
||||
const r = selectedProject?.lyrics?.refrain;
|
||||
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
||||
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
||||
}
|
||||
return {
|
||||
title: project?.title || "",
|
||||
title: selectedProject?.title || "",
|
||||
lyrics: lyricsArr,
|
||||
genres: Array.isArray(genres) ? genres : [],
|
||||
voice: voice || undefined,
|
||||
instruments: Array.isArray(instruments) ? instruments : [],
|
||||
tempo: rhythm || undefined,
|
||||
projectId: projectId || undefined,
|
||||
projectId: selectedProjectId || undefined,
|
||||
};
|
||||
}, [project, genres, voice, instruments, rhythm, projectId]);
|
||||
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
|
||||
|
||||
const onPressNext = () => {
|
||||
const onPressNext = async () => {
|
||||
if (selectedIndex === 3) {
|
||||
navigate(Routes.GeneratingSong, { config: musicConfig, projectId });
|
||||
try {
|
||||
// Persist config on the selected selectedProject so GeneratingSong can pick it up
|
||||
if (selectedProjectId) {
|
||||
await updateProjectData({
|
||||
musicConfig: {
|
||||
title: musicConfig?.title || "",
|
||||
lyrics: Array.isArray(musicConfig?.lyrics)
|
||||
? musicConfig.lyrics
|
||||
: [],
|
||||
genres: Array.isArray(musicConfig?.genres)
|
||||
? musicConfig.genres
|
||||
: [],
|
||||
voice: musicConfig?.voice || "",
|
||||
instruments: Array.isArray(musicConfig?.instruments)
|
||||
? musicConfig.instruments
|
||||
: [],
|
||||
tempo: musicConfig?.tempo || "",
|
||||
},
|
||||
musicStatus: null,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
// Also pass the config to the screen to avoid any race condition
|
||||
navigate(Routes.GeneratingSong, { config: musicConfig });
|
||||
return;
|
||||
}
|
||||
setSelectedIndex(selectedIndex + 1);
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import ProgressBar from "../../components/ProgressBar";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { Routes } from "../../navigation";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import firebase, { projectsRef } from "../../config/firebase";
|
||||
@@ -21,6 +22,7 @@ const CreatingSong = ({ active, config }) => {
|
||||
const [musicStatus, setMusicStatus] = useState(null);
|
||||
const [generationStartAt, setGenerationStartAt] = useState(null);
|
||||
const progressTimerRef = React.useRef(null);
|
||||
const { selectedProjectId } = useUser();
|
||||
|
||||
// Abonnement au document projet pour suivre le statut et la date de début
|
||||
useEffect(() => {
|
||||
@@ -236,9 +238,7 @@ const CreatingSong = ({ active, config }) => {
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: config?.projectId })
|
||||
}
|
||||
onPress={() => navigate(Routes.SongReady)}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
@@ -1,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,
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Image, Platform, Text, View } from "react-native";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Alert, Image, Platform, Text, View } from "react-native";
|
||||
import Page from "../../layouts/Page";
|
||||
import { ai, background } from "../../assets";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
@@ -15,24 +15,22 @@ import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import moment from "moment";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import { useIsFocused } from "@react-navigation/native";
|
||||
|
||||
const GeneratingSong = ({ route }) => {
|
||||
const { config, projectId } = route.params;
|
||||
const GeneratingSong = () => {
|
||||
const { selectedProjectId, selectedProject } = useUser();
|
||||
const [progress, setProgress] = useState(0);
|
||||
const { setIsLoading } = useMinuit();
|
||||
const progressTimerRef = useRef(null);
|
||||
const navigatedRef = useRef(false);
|
||||
const isFocused = useIsFocused();
|
||||
const lastConfigKeyRef = useRef(null);
|
||||
const askedRef = useRef(false);
|
||||
|
||||
const { data: project } = useDataFromRef({
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
refreshArray: [projectId],
|
||||
});
|
||||
// project loaded from provider
|
||||
|
||||
// Progress based on 8 minutes cap or until status changes
|
||||
// Progress based on 8 minutes cap or until status becomes GENERATED
|
||||
useEffect(() => {
|
||||
const totalMs = 8 * 60 * 1000;
|
||||
const clearTimer = () => {
|
||||
@@ -41,15 +39,15 @@ const GeneratingSong = ({ route }) => {
|
||||
progressTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
if (project?.musicStatus !== "GENERATING") {
|
||||
if (selectedProject?.musicStatus === "GENERATED") {
|
||||
setProgress(100);
|
||||
clearTimer();
|
||||
return () => clearTimer();
|
||||
}
|
||||
const update = () => {
|
||||
const startDate = project?.generationStartAt?.toDate
|
||||
? project.generationStartAt.toDate()
|
||||
: new Date(project?.generationStartAt || Date.now());
|
||||
const startDate = selectedProject?.generationStartAt?.toDate
|
||||
? selectedProject.generationStartAt.toDate()
|
||||
: new Date(selectedProject?.generationStartAt || Date.now());
|
||||
const elapsed = moment().diff(moment(startDate));
|
||||
const raw = Math.floor((elapsed / totalMs) * 100);
|
||||
// While status is GENERATING, block visual progress at 99%
|
||||
@@ -60,16 +58,29 @@ const GeneratingSong = ({ route }) => {
|
||||
clearTimer();
|
||||
progressTimerRef.current = setInterval(update, 1000);
|
||||
return () => clearTimer();
|
||||
}, [project?.musicStatus]);
|
||||
}, [selectedProject?.musicStatus]);
|
||||
|
||||
// Auto navigate to SongReady when generation completed
|
||||
useEffect(() => {
|
||||
if (!config?.projectId) return;
|
||||
if (project?.musicStatus !== "GENERATING" && !navigatedRef.current) {
|
||||
if (!selectedProjectId) return;
|
||||
if (selectedProject?.musicStatus === "GENERATED" && !navigatedRef.current) {
|
||||
navigatedRef.current = true;
|
||||
navigate(Routes.SongReady, { projectId: config.projectId });
|
||||
navigate(Routes.SongReady);
|
||||
}
|
||||
}, [project?.musicStatus, config?.projectId]);
|
||||
}, [selectedProject?.musicStatus, selectedProjectId]);
|
||||
|
||||
const effectiveConfig = useMemo(() => {
|
||||
// Use selectedProject.musicConfig only
|
||||
const cfg = selectedProject?.musicConfig || {};
|
||||
return {
|
||||
title: cfg?.title || "",
|
||||
lyrics: Array.isArray(cfg?.lyrics) ? cfg.lyrics : [],
|
||||
genres: Array.isArray(cfg?.genres) ? cfg.genres : [],
|
||||
voice: cfg?.voice || "",
|
||||
instruments: Array.isArray(cfg?.instruments) ? cfg.instruments : [],
|
||||
tempo: cfg?.tempo || "",
|
||||
};
|
||||
}, [selectedProject?.musicConfig]);
|
||||
|
||||
async function startMusicGeneration() {
|
||||
try {
|
||||
@@ -78,39 +89,39 @@ const GeneratingSong = ({ route }) => {
|
||||
const callable = firebase
|
||||
.functions()
|
||||
.httpsCallable("music-generateMusic");
|
||||
const cfg = effectiveConfig || {};
|
||||
const { data } = await callable({
|
||||
title: config?.title,
|
||||
lyrics: config?.lyrics,
|
||||
genres: config?.genres,
|
||||
voice: config?.voice,
|
||||
instruments: config?.instruments,
|
||||
tempo: config?.tempo,
|
||||
projectId: config?.projectId,
|
||||
title: cfg?.title,
|
||||
lyrics: cfg?.lyrics,
|
||||
genres: cfg?.genres,
|
||||
voice: cfg?.voice,
|
||||
instruments: cfg?.instruments,
|
||||
tempo: cfg?.tempo,
|
||||
});
|
||||
const taskId =
|
||||
data?.response?.data?.taskId || data?.response?.data?.task_id;
|
||||
if (config?.projectId && taskId) {
|
||||
if (selectedProjectId && taskId) {
|
||||
const baseUpdate = {
|
||||
sunoTaskId: taskId,
|
||||
musicStatus: "GENERATING",
|
||||
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
};
|
||||
const updatePayload = project?.musicConfig
|
||||
const updatePayload = selectedProject?.musicConfig
|
||||
? baseUpdate
|
||||
: {
|
||||
...baseUpdate,
|
||||
musicConfig: {
|
||||
title: config?.title || "",
|
||||
lyrics: config?.lyrics || [],
|
||||
genres: config?.genres || [],
|
||||
voice: config?.voice || "",
|
||||
instruments: config?.instruments || [],
|
||||
tempo: config?.tempo || "",
|
||||
title: effectiveConfig?.title || "",
|
||||
lyrics: effectiveConfig?.lyrics || [],
|
||||
genres: effectiveConfig?.genres || [],
|
||||
voice: effectiveConfig?.voice || "",
|
||||
instruments: effectiveConfig?.instruments || [],
|
||||
tempo: effectiveConfig?.tempo || "",
|
||||
},
|
||||
};
|
||||
await projectsRef
|
||||
.doc(config.projectId)
|
||||
.doc(selectedProjectId)
|
||||
.set(updatePayload, { merge: true });
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -120,16 +131,44 @@ const GeneratingSong = ({ route }) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger generation only if not already GENERATING
|
||||
// Trigger generation only when focused; ask once per config
|
||||
useEffect(() => {
|
||||
if (!!project?.title && project?.musicStatus !== "GENERATING") {
|
||||
startMusicGeneration();
|
||||
if (!isFocused) return;
|
||||
|
||||
const key = JSON.stringify(effectiveConfig || {});
|
||||
if (lastConfigKeyRef.current !== key) {
|
||||
lastConfigKeyRef.current = key;
|
||||
askedRef.current = false;
|
||||
}
|
||||
}, [project]);
|
||||
|
||||
const canAsk =
|
||||
!!selectedProject?.title && selectedProject?.musicStatus !== "GENERATING";
|
||||
|
||||
if (canAsk && !askedRef.current) {
|
||||
askedRef.current = true;
|
||||
Alert.alert(
|
||||
"Attention",
|
||||
"Une génération va être lancée. Continuer ?",
|
||||
[
|
||||
{
|
||||
text: "Non",
|
||||
style: "cancel",
|
||||
},
|
||||
{
|
||||
text: "Oui",
|
||||
onPress: () => startMusicGeneration(),
|
||||
},
|
||||
],
|
||||
);
|
||||
}
|
||||
}, [isFocused, selectedProject?.title, selectedProject?.musicStatus, effectiveConfig]);
|
||||
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
<MusicLandHeader onPressBack={goBack} progress={63} />
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.FlowSelection)}
|
||||
progress={63}
|
||||
/>
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
<CreateLyricsHeader
|
||||
title="Ta musique est en cours de création !"
|
||||
@@ -206,14 +245,14 @@ const GeneratingSong = ({ route }) => {
|
||||
</Text>
|
||||
</View>
|
||||
<GradientButton
|
||||
title={"Création en cours..."}
|
||||
disabled={project?.musicStatus === "GENERATING"}
|
||||
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, {
|
||||
projectId: config?.projectId,
|
||||
})
|
||||
title={
|
||||
selectedProject?.musicStatus === "GENERATED"
|
||||
? "Découvrir ma musique"
|
||||
: "Création en cours..."
|
||||
}
|
||||
disabled={selectedProject?.musicStatus !== "GENERATED"}
|
||||
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||
onPress={() => navigate(Routes.SongReady)}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
@@ -1,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;
|
||||
@@ -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="Qu’en 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;
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import React, { useEffect, useRef, useState, useCallback } from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { background, icons } from "../../assets";
|
||||
@@ -9,16 +9,22 @@ import GradientButton from "../../components/GradientButton";
|
||||
import ValidateModal from "../../components/modal/ValidateModal";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import Slider from "../../components/Slider";
|
||||
import firebase, { projectsRef } from "../../config/firebase";
|
||||
import firebase from "../../config/firebase";
|
||||
import { useUser } from "../../providers/UserDataProvider";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
import { Style } from "../../styles";
|
||||
import { gutters, size } from "../../styles/Style";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
|
||||
const SongReady = ({ route }) => {
|
||||
const { projectId = null } = route?.params || {};
|
||||
const SongReady = () => {
|
||||
const {
|
||||
selectedProjectId: projectId,
|
||||
selectedProject,
|
||||
updateProjectData,
|
||||
} = useUser();
|
||||
const [showValidateModal, setShowValidateModal] = useState(false);
|
||||
const [musicUrls, setMusicUrls] = useState([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
@@ -28,29 +34,20 @@ const SongReady = ({ route }) => {
|
||||
1: { pos: 0, dur: 0 },
|
||||
});
|
||||
const player0 = useAudioPlayer(
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||
);
|
||||
const player1 = useAudioPlayer(
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
|
||||
);
|
||||
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
|
||||
|
||||
// Charger les URLs depuis le document projet
|
||||
// Sync URLs from provider's selectedProject
|
||||
useEffect(() => {
|
||||
if (!projectId) return;
|
||||
const unsub = firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.doc(projectId)
|
||||
.onSnapshot((doc) => {
|
||||
const data = doc.data() || {};
|
||||
const urls = Array.isArray(data?.musicUrls)
|
||||
? data.musicUrls.slice(0, 2)
|
||||
: [];
|
||||
setMusicUrls(urls);
|
||||
});
|
||||
return () => unsub?.();
|
||||
}, [projectId]);
|
||||
const urls = Array.isArray(selectedProject?.musicUrls)
|
||||
? selectedProject.musicUrls.slice(0, 2)
|
||||
: [];
|
||||
setMusicUrls(urls);
|
||||
}, [selectedProject?.musicUrls]);
|
||||
|
||||
// Sync progression depuis les players
|
||||
useEffect(() => {
|
||||
@@ -117,34 +114,50 @@ const SongReady = ({ route }) => {
|
||||
try {
|
||||
const url = musicUrls[selectedIndex];
|
||||
if (!projectId || !url) return;
|
||||
await projectsRef.doc(projectId).set(
|
||||
{
|
||||
songIndex: selectedIndex,
|
||||
songUrl: url,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
navigate(Routes.FlowSelection, { projectId });
|
||||
await updateProjectData({
|
||||
songIndex: selectedIndex,
|
||||
songUrl: url,
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
});
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
navigate(Routes.FlowSelection);
|
||||
} catch (e) {
|
||||
console.log("Validate error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const onPressRegenerate = async () => {
|
||||
try {
|
||||
navigate(Routes.GeneratingSong, { projectId });
|
||||
} catch (e) {
|
||||
console.log("Regenerate error", e?.message);
|
||||
} finally {
|
||||
goBack();
|
||||
}
|
||||
};
|
||||
// Pause audio when screen loses focus (navigate/reset) and on unmount
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return () => {
|
||||
try {
|
||||
player0?.pause?.();
|
||||
player1?.pause?.();
|
||||
} catch {}
|
||||
};
|
||||
}, [player0, player1]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
player0?.pause?.();
|
||||
player1?.pause?.();
|
||||
} catch {}
|
||||
};
|
||||
}, [player0, player1]);
|
||||
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigate(Routes.FlowSelection, { projectId })}
|
||||
onPressBack={async () => {
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
} catch {}
|
||||
navigate(Routes.FlowSelection);
|
||||
}}
|
||||
progress={63}
|
||||
/>
|
||||
<View style={{ flex: 1, marginTop: 16 }}>
|
||||
@@ -207,7 +220,7 @@ const SongReady = ({ route }) => {
|
||||
} catch (e) {
|
||||
console.log(
|
||||
"SongReady pause on seek start",
|
||||
e?.message
|
||||
e?.message,
|
||||
);
|
||||
}
|
||||
}}
|
||||
@@ -269,7 +282,13 @@ const SongReady = ({ route }) => {
|
||||
<BorderGradientButton
|
||||
title="Regénérer"
|
||||
icon={icons.stars}
|
||||
onPress={onPressRegenerate}
|
||||
onPress={async () => {
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
} catch {}
|
||||
navigate(Routes.ComposeSong);
|
||||
}}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Choisir ce morceau"
|
||||
|
||||
@@ -12,7 +12,7 @@ import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
const Studio = () => {
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const { userProjects: projects } = useUser();
|
||||
const { userProjects: projects, selectProject } = useUser();
|
||||
|
||||
return (
|
||||
<Page
|
||||
@@ -107,7 +107,8 @@ const Studio = () => {
|
||||
"La chanson est en cours de génération. Veuillez patienter.",
|
||||
);
|
||||
} else {
|
||||
navigate(Routes.Compose, { projectId: selected.id });
|
||||
selectProject(selected.id);
|
||||
navigate(Routes.Compose);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -118,18 +119,20 @@ const Studio = () => {
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: selected.id })
|
||||
}
|
||||
onPress={() => {
|
||||
selectProject(selected.id);
|
||||
navigate(Routes.SongReady);
|
||||
}}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Générer une pochette"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.PouchReady, { projectId: selected.id })
|
||||
}
|
||||
onPress={() => {
|
||||
selectProject(selected.id);
|
||||
navigate(Routes.PouchReady);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user