end of lyricks and misic generation
This commit is contained in:
+35
-14
@@ -1,9 +1,10 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { StyleSheet, Text, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
runOnJS,
|
||||
} from "react-native-reanimated";
|
||||
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
||||
import { Palette, Style } from "../styles";
|
||||
@@ -11,7 +12,7 @@ import { FONT_FAMILY } from "../styles/Fonts";
|
||||
|
||||
const INITIAL_BOX_SIZE = 6;
|
||||
|
||||
export default ({ value, maxValue }) => {
|
||||
export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
|
||||
const offset = useSharedValue(0);
|
||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
||||
const [layout, setLayout] = useState(null);
|
||||
@@ -19,19 +20,39 @@ export default ({ value, maxValue }) => {
|
||||
const SLIDER_WIDTH = layout?.width;
|
||||
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
|
||||
|
||||
const pan = Gesture.Pan().onChange((event) => {
|
||||
offset.value =
|
||||
Math.abs(offset.value) <= MAX_VALUE
|
||||
? offset.value + event.changeX <= 0
|
||||
? 0
|
||||
: offset.value + event.changeX >= MAX_VALUE
|
||||
? MAX_VALUE
|
||||
: offset.value + event.changeX
|
||||
: offset.value;
|
||||
const pan = Gesture.Pan()
|
||||
.enabled(seekEnabled)
|
||||
.onChange((event) => {
|
||||
offset.value =
|
||||
Math.abs(offset.value) <= MAX_VALUE
|
||||
? offset.value + event.changeX <= 0
|
||||
? 0
|
||||
: offset.value + event.changeX >= MAX_VALUE
|
||||
? MAX_VALUE
|
||||
: offset.value + event.changeX
|
||||
: offset.value;
|
||||
|
||||
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
||||
boxWidth.value = newWidth;
|
||||
});
|
||||
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
||||
boxWidth.value = newWidth;
|
||||
})
|
||||
.onEnd(() => {
|
||||
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
|
||||
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
||||
// Reanimated -> JS thread bridge
|
||||
runOnJS(onSeek)(ratio);
|
||||
});
|
||||
|
||||
// Reflect external progress into the slider UI
|
||||
useEffect(() => {
|
||||
if (typeof progress === "number" && layout?.width) {
|
||||
const max = layout.width - INITIAL_BOX_SIZE;
|
||||
const clamped = Math.max(0, Math.min(1, progress));
|
||||
const newOffset = clamped * max;
|
||||
offset.value = newOffset;
|
||||
boxWidth.value = INITIAL_BOX_SIZE + newOffset;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [progress, layout?.width]);
|
||||
|
||||
const boxStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
|
||||
@@ -62,11 +62,18 @@ const ComposeSong = () => {
|
||||
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
||||
|
||||
const musicConfig = useMemo(() => {
|
||||
const lyricsArr = [];
|
||||
const c = project?.lyrics?.couplet;
|
||||
const r = project?.lyrics?.refrain;
|
||||
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
||||
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
||||
let lyricsArr = [];
|
||||
if (Array.isArray(project?.lyrics)) {
|
||||
lyricsArr = project.lyrics.map((s) => ({
|
||||
type: (s?.type || "").toLowerCase(),
|
||||
lyrics: s?.lyrics || "",
|
||||
}));
|
||||
} else {
|
||||
const c = project?.lyrics?.couplet;
|
||||
const r = project?.lyrics?.refrain;
|
||||
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
||||
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
||||
}
|
||||
return {
|
||||
title: project?.title || "",
|
||||
lyrics: lyricsArr,
|
||||
|
||||
@@ -119,8 +119,15 @@ const CreatingSong = ({ active, config }) => {
|
||||
{
|
||||
sunoTaskId: taskId,
|
||||
musicStatus: "GENERATING",
|
||||
generationStartAt:
|
||||
firebase.firestore.FieldValue.serverTimestamp(),
|
||||
musicConfig: {
|
||||
title: config?.title || "",
|
||||
lyrics: config?.lyrics || [],
|
||||
genres: config?.genres || [],
|
||||
voice: config?.voice || "",
|
||||
instruments: config?.instruments || [],
|
||||
tempo: config?.tempo || "",
|
||||
},
|
||||
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
@@ -221,12 +228,19 @@ const CreatingSong = ({ active, config }) => {
|
||||
</Text>
|
||||
</View>
|
||||
<GradientButton
|
||||
title="Découvrir ma musique"
|
||||
title={
|
||||
musicStatus === "GENERATING"
|
||||
? "Création en cours..."
|
||||
: "Découvrir ma musique"
|
||||
}
|
||||
disabled={musicStatus === "GENERATING"}
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.SongReady, { result })}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: config?.projectId })
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
</BlurView>
|
||||
|
||||
+240
-41
@@ -1,22 +1,178 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background, icons } from "../../assets";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import Slider from "../../components/Slider";
|
||||
import { Image, Platform, Pressable, View } from "react-native";
|
||||
import { Image, Platform, Pressable, View, Text } from "react-native";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Style } from "../../styles";
|
||||
import { responsiveWidth } from "react-native-responsive-dimensions";
|
||||
import { gutters, size } from "../../styles/Style";
|
||||
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 { useAudioPlayer } from "expo-audio";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
|
||||
const SongReady = () => {
|
||||
const params = useRoute().params || {};
|
||||
const projectId = params?.projectId;
|
||||
const [showValidateModal, setShowValidateModal] = useState(false);
|
||||
const [musicUrls, setMusicUrls] = useState([]);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false });
|
||||
const [progressInfo, setProgressInfo] = useState({
|
||||
0: { pos: 0, dur: 0 },
|
||||
1: { pos: 0, dur: 0 },
|
||||
});
|
||||
const player0 = useAudioPlayer(
|
||||
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||
);
|
||||
const player1 = useAudioPlayer(
|
||||
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
|
||||
);
|
||||
|
||||
// Charger les URLs depuis le document projet
|
||||
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]);
|
||||
|
||||
// Sync progression depuis les players
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => {
|
||||
const d0 = (player0?.duration || 0) * 1000;
|
||||
const p0 = (player0?.currentTime || 0) * 1000;
|
||||
const d1 = (player1?.duration || 0) * 1000;
|
||||
const p1 = (player1?.currentTime || 0) * 1000;
|
||||
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
|
||||
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
|
||||
}, 300);
|
||||
return () => clearInterval(id);
|
||||
}, [player0, player1]);
|
||||
|
||||
const togglePlay = async (idx) => {
|
||||
const url = musicUrls[idx];
|
||||
if (!url) return;
|
||||
try {
|
||||
// Pause l'autre piste si elle joue
|
||||
const other = idx === 0 ? 1 : 0;
|
||||
if (isPlaying[other]) {
|
||||
if (other === 0) await player0?.pause?.();
|
||||
else await player1?.pause?.();
|
||||
setIsPlaying((p) => ({ ...p, [other]: false }));
|
||||
}
|
||||
const player = idx === 0 ? player0 : player1;
|
||||
if (!player) return;
|
||||
if (player.playing) {
|
||||
await player.pause?.();
|
||||
setIsPlaying((p) => ({ ...p, [idx]: false }));
|
||||
} else {
|
||||
await player.play?.();
|
||||
setIsPlaying((p) => ({ ...p, [idx]: true }));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Audio error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const fmt = (ms) => {
|
||||
const total = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||
const m = Math.floor(total / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const s = (total % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
const onSeek = async (idx, ratio) => {
|
||||
try {
|
||||
const info = progressInfo[idx] || {};
|
||||
const dur = info.dur || 0;
|
||||
const pos = Math.floor(dur * ratio);
|
||||
const player = idx === 0 ? player0 : player1;
|
||||
if (player && dur > 0) {
|
||||
await player.seekTo?.(Math.floor((pos || 0) / 1000));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Seek error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const validateSelection = async () => {
|
||||
try {
|
||||
const url = musicUrls[selectedIndex];
|
||||
if (!projectId || !url) return;
|
||||
await firebase
|
||||
.firestore()
|
||||
.collection("projects")
|
||||
.doc(projectId)
|
||||
.set(
|
||||
{
|
||||
song: { index: selectedIndex, url },
|
||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
navigate(Routes.PouchReady);
|
||||
} catch (e) {
|
||||
console.log("Validate error", e?.message);
|
||||
}
|
||||
};
|
||||
|
||||
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 },
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Regenerate error", e?.message);
|
||||
} finally {
|
||||
goBack();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||
@@ -25,38 +181,84 @@ const SongReady = () => {
|
||||
<CreateLyricsHeader
|
||||
title="Ta chanson est prête !"
|
||||
subTitle="Qu’en penses-tu ?"
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
...size({ size: responsiveWidth(80) }),
|
||||
alignSelf: "center",
|
||||
borderRadius: 1000,
|
||||
overflow: "hidden",
|
||||
marginTop: 16,
|
||||
containerStyle={{
|
||||
marginBottom: responsiveHeight(2),
|
||||
}}
|
||||
>
|
||||
<BlurView
|
||||
intensity={40}
|
||||
tint="dark"
|
||||
style={{ ...Style.containerCenter, flex: 1 }}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Image source={icons.disk} />
|
||||
</BlurView>
|
||||
</View>
|
||||
<View style={{ marginTop: 49 }}>
|
||||
<Slider value="0" maxValue="2:11" />
|
||||
<Pressable
|
||||
style={{
|
||||
alignSelf: "center",
|
||||
...size({ size: 48 }),
|
||||
...Style.containerCenter,
|
||||
}}
|
||||
>
|
||||
<Image source={icons.play} />
|
||||
</Pressable>
|
||||
/>
|
||||
<View style={{ gap: 16 }}>
|
||||
{[0, 1].map((idx) => (
|
||||
<BlurView
|
||||
key={idx}
|
||||
intensity={40}
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
padding: 12,
|
||||
backgroundColor: "#FFFFFF0A",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<View
|
||||
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
||||
>
|
||||
<Pressable
|
||||
onPress={() => togglePlay(idx)}
|
||||
style={{ ...Style.containerCenter, ...size({ size: 48 }) }}
|
||||
>
|
||||
<Image source={isPlaying[idx] ? icons.pause : icons.play} />
|
||||
</Pressable>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
marginBottom: responsiveHeight(1),
|
||||
}}
|
||||
>{`Morceau ${idx + 1}`}</Text>
|
||||
<Slider
|
||||
value={fmt(progressInfo[idx]?.pos)}
|
||||
maxValue={fmt(progressInfo[idx]?.dur)}
|
||||
progress={
|
||||
progressInfo[idx]?.dur
|
||||
? (progressInfo[idx].pos || 0) / progressInfo[idx].dur
|
||||
: 0
|
||||
}
|
||||
seekEnabled={!!musicUrls[idx]}
|
||||
onSeek={(ratio) => onSeek(idx, ratio)}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={() => setSelectedIndex(idx)}
|
||||
style={{ ...Style.containerCenter, ...size({ size: 24 }) }}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 18,
|
||||
height: 18,
|
||||
borderRadius: 9,
|
||||
borderWidth: 2,
|
||||
borderColor: "white",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
{selectedIndex === idx && (
|
||||
<View
|
||||
style={{
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: "white",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</View>
|
||||
</BlurView>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -70,21 +272,18 @@ const SongReady = () => {
|
||||
<BorderGradientButton
|
||||
title="Regénérer"
|
||||
icon={icons.stars}
|
||||
onPress={() =>
|
||||
navigate(Routes.Regenerate, {
|
||||
progress: 63,
|
||||
})
|
||||
}
|
||||
onPress={onPressRegenerate}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Valider"
|
||||
title="Choisir ce morceau"
|
||||
onPress={() => setShowValidateModal(true)}
|
||||
disabled={!musicUrls?.length}
|
||||
/>
|
||||
</View>
|
||||
<ValidateModal
|
||||
visible={showValidateModal}
|
||||
onClose={() => setShowValidateModal(false)}
|
||||
onPressValidate={() => navigate(Routes.PouchReady)}
|
||||
onPressValidate={validateSelection}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { View, Text, ScrollView, Pressable } from "react-native";
|
||||
import React, { useEffect, useState, useMemo } from "react";
|
||||
import { View, Text, ScrollView, Pressable, Alert } from "react-native";
|
||||
import React, { useState } from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background } from "../../assets";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
@@ -9,11 +9,11 @@ import { gutters } from "../../styles";
|
||||
import firebase from "../../config/firebase";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
|
||||
const Studio = () => {
|
||||
const { setIsLoading } = useMinuit();
|
||||
const [selectedId, setSelectedId] = useState(null);
|
||||
const isDisabled = useMemo(() => !selectedId, [selectedId]);
|
||||
const [selected, setSelected] = useState(null);
|
||||
|
||||
const user = firebase.auth().currentUser;
|
||||
const { data: projects } = useDataFromRef({
|
||||
@@ -131,14 +131,11 @@ const Studio = () => {
|
||||
>
|
||||
Derniers projets générés
|
||||
</Text>
|
||||
<ScrollView
|
||||
style={{ maxHeight: 260 }}
|
||||
contentContainerStyle={{ gap: 10, paddingRight: 6 }}
|
||||
>
|
||||
<ScrollView contentContainerStyle={{ gap: 10, paddingRight: 6 }}>
|
||||
{projects.map((p) => {
|
||||
const couplet = p?.lyrics?.couplet || "";
|
||||
const preview = couplet.split("\n").slice(0, 2).join(" ");
|
||||
const selected = selectedId === p.id;
|
||||
const isSelected = selected === p;
|
||||
const createdAt = p?.createdAt?.toDate
|
||||
? p.createdAt.toDate()
|
||||
: p?.createdAt
|
||||
@@ -151,13 +148,13 @@ const Studio = () => {
|
||||
return (
|
||||
<Pressable
|
||||
key={p.id}
|
||||
onPress={() => setSelectedId(p.id)}
|
||||
onPress={() => setSelected(p)}
|
||||
style={{
|
||||
backgroundColor: "#0F0C1933",
|
||||
borderRadius: 12,
|
||||
padding: 12,
|
||||
borderWidth: selected ? 2 : 0,
|
||||
borderColor: selected ? "#F94697" : "transparent",
|
||||
borderWidth: isSelected ? 2 : 0,
|
||||
borderColor: isSelected ? "#F94697" : "transparent",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
@@ -197,9 +194,29 @@ const Studio = () => {
|
||||
<View style={{ justifyContent: "flex-end" }}>
|
||||
<GradientButton
|
||||
title="Commencer"
|
||||
disabled={isDisabled}
|
||||
onPress={() => navigate(Routes.Compose, { projectId: selectedId })}
|
||||
disabled={!selected}
|
||||
onPress={() => {
|
||||
if (selected.musicStatus === "GENERATING") {
|
||||
Alert.alert(
|
||||
"Attention",
|
||||
"La chanson est en cours de génération. Veuillez patienter.",
|
||||
);
|
||||
} else {
|
||||
navigate(Routes.Compose, { projectId: selected.id });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{selected?.musicUrls?.length > 0 && (
|
||||
<GradientButton
|
||||
title="Ecouter les audios"
|
||||
containerStyle={{
|
||||
marginTop: responsiveHeight(2),
|
||||
}}
|
||||
onPress={() =>
|
||||
navigate(Routes.SongReady, { projectId: selected.id })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -23,25 +23,35 @@ const Lyrics = ({ navigation }) => {
|
||||
const { setIsLoading } = useMinuit();
|
||||
|
||||
const initial = useMemo(() => {
|
||||
if (!lyricsData || !lyricsData?.success) return {};
|
||||
const title = lyricsData?.title || "";
|
||||
const sections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
|
||||
const couplets = sections
|
||||
.filter((s) => (s?.type || "").toLowerCase().includes("couplet"))
|
||||
.map((s) => s?.lyrics || "");
|
||||
const refrains = sections
|
||||
.filter((s) => (s?.type || "").toLowerCase().includes("refrain"))
|
||||
.map((s) => s?.lyrics || "");
|
||||
return {
|
||||
title,
|
||||
couplet: couplets.join("\n\n"),
|
||||
refrain: refrains.join("\n\n"),
|
||||
};
|
||||
}, [lyricsData]);
|
||||
const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
|
||||
// Respecter l'ordre de la structure choisie si disponible
|
||||
const targetStructure = Array.isArray(config?.structure)
|
||||
? config.structure.map((t) => (t || "").toLowerCase())
|
||||
: null;
|
||||
if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) {
|
||||
return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) };
|
||||
}
|
||||
// Sinon, créer à partir de la structure
|
||||
if (targetStructure && targetStructure.length) {
|
||||
return {
|
||||
title,
|
||||
sections: targetStructure.map((t) => ({ type: t, lyrics: "" })),
|
||||
};
|
||||
}
|
||||
// Fallback vide
|
||||
return { title, sections: [] };
|
||||
}, [lyricsData, config]);
|
||||
|
||||
const [titleValue, setTitleValue] = useState(initial.title || "");
|
||||
const [coupletValue, setCoupletValue] = useState(initial.couplet || "");
|
||||
const [refrainValue, setRefrainValue] = useState(initial.refrain || "");
|
||||
const [sections, setSections] = useState(initial.sections || []);
|
||||
const setSectionAt = (index, value) => {
|
||||
setSections((prev) => {
|
||||
const next = [...prev];
|
||||
if (next[index]) next[index] = { ...next[index], lyrics: value };
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
// Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente
|
||||
const regenerate = useCallback(() => {
|
||||
@@ -70,10 +80,10 @@ const Lyrics = ({ navigation }) => {
|
||||
const user = firebase.auth().currentUser;
|
||||
const payload = {
|
||||
title: titleValue?.trim() || "",
|
||||
lyrics: {
|
||||
couplet: coupletValue || "",
|
||||
refrain: refrainValue || "",
|
||||
},
|
||||
lyrics: (sections || []).map((s) => ({
|
||||
type: (s?.type || "").toLowerCase(),
|
||||
lyrics: s?.lyrics || "",
|
||||
})),
|
||||
config: sanitize(config),
|
||||
selections: sanitize(selections),
|
||||
userId: user ? user.uid : null,
|
||||
@@ -88,14 +98,7 @@ const Lyrics = ({ navigation }) => {
|
||||
} finally {
|
||||
await setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
titleValue,
|
||||
coupletValue,
|
||||
refrainValue,
|
||||
config,
|
||||
selections,
|
||||
setIsLoading,
|
||||
]);
|
||||
}, [titleValue, sections, config, selections, setIsLoading]);
|
||||
|
||||
return (
|
||||
<Page headerType="NONE">
|
||||
@@ -124,40 +127,25 @@ const Lyrics = ({ navigation }) => {
|
||||
value={titleValue}
|
||||
setValue={setTitleValue}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
height: 45,
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#00000080",
|
||||
borderRadius: 14,
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 20,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
Introduction instrumentale longue
|
||||
</Text>
|
||||
</View>
|
||||
<CustomInput
|
||||
label="Couplet"
|
||||
placeholder="Couplet"
|
||||
height={225}
|
||||
value={coupletValue}
|
||||
setValue={setCoupletValue}
|
||||
/>
|
||||
<CustomInput
|
||||
label="Refrain"
|
||||
placeholder="Refrain"
|
||||
height={170}
|
||||
value={refrainValue}
|
||||
setValue={setRefrainValue}
|
||||
/>
|
||||
{sections.map((s, idx) => {
|
||||
// Calculer l'index humain par type
|
||||
const type = (s?.type || "").toLowerCase();
|
||||
const countBefore = sections
|
||||
.slice(0, idx)
|
||||
.filter((x) => (x?.type || "").toLowerCase() === type).length;
|
||||
const labelBase = type === "refrain" ? "Refrain" : "Couplet";
|
||||
const label = `${labelBase} ${countBefore + 1}`;
|
||||
return (
|
||||
<CustomInput
|
||||
key={idx}
|
||||
label={label}
|
||||
placeholder={labelBase}
|
||||
height={type === "refrain" ? 170 : 225}
|
||||
value={s?.lyrics || ""}
|
||||
setValue={(val) => setSectionAt(idx, val)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</ItemContainer>
|
||||
</View>
|
||||
|
||||
Reference in New Issue
Block a user