feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+178 -240
View File
@@ -1,213 +1,167 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useRoute } from "@react-navigation/native";
import {
FlatList,
Modal,
Text,
View,
useWindowDimensions,
} from "react-native";
import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import AppAlert from "../../components/Alert";
import CreditAmount from "../../components/CreditAmount";
import firebase, { getFunctionsClient } from "../../config/firebase";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { normalizeStructureType } from "../../utils/songStructure";
import { openCoinPackModal } from "../../utils/coinPackModal";
import ChooseGenre from "./ChooseGenre";
import ChooseInstruments from "./ChooseInstruments";
import ChooseRhythm from "./ChooseRhythm";
import CustomizeVoice from "./CustomizeVoice";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useRoute } from '@react-navigation/native'
import { FlatList, Modal, Text, View, useWindowDimensions } from 'react-native'
import { background } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppAlert from '../../components/Alert'
import CreditAmount from '../../components/CreditAmount'
import firebase, { getFunctionsClient } from '../../config/firebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { normalizeStructureType } from '../../utils/songStructure'
import { openCoinPackModal } from '../../utils/coinPackModal'
import ChooseGenre from './ChooseGenre'
import ChooseInstruments from './ChooseInstruments'
import ChooseRhythm from './ChooseRhythm'
import CustomizeVoice from './CustomizeVoice'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const MUSIC_GENERATION_COIN_COST = 8;
const CONFIRM_MODAL_MAX_WIDTH = 540;
const FUNCTIONS_REGION = "europe-west1";
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
const FIRST_VOICE_STEP_INDEX = 1;
const OPTIONAL_VOICE_CATEGORIES = new Set(["SENSIBILITÉ", "TECHNIQUE"]);
const PAGE_WIDTH_RATIO_WEB = 0.6; // keep in sync with Page default width on web
const PAGE_MAX_WIDTH = 1200;
const PAGE_HORIZONTAL_PADDING = gutters * 2;
const MUSIC_GENERATION_COIN_COST = 8
const CONFIRM_MODAL_MAX_WIDTH = 540
const FUNCTIONS_REGION = 'europe-west1'
const VOICE_SECTION_TITLES = ['BASE', 'SENSIBILITÉ', 'TECHNIQUE']
const FIRST_VOICE_STEP_INDEX = 1
const OPTIONAL_VOICE_CATEGORIES = new Set(['SENSIBILITÉ', 'TECHNIQUE'])
const PAGE_WIDTH_RATIO_WEB = 0.6 // keep in sync with Page default width on web
const PAGE_MAX_WIDTH = 1200
const PAGE_HORIZONTAL_PADDING = gutters * 2
const ComposeSong = () => {
const scrollRef = useRef(null);
const { width: windowWidth } = useWindowDimensions();
const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18);
const [parentLayout, setParentLayout] = useState(null);
const scrollRef = useRef(null)
const { width: windowWidth } = useWindowDimensions()
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(18)
const [parentLayout, setParentLayout] = useState(null)
const estimatedContainerWidth = useMemo(() => {
const safeWindowWidth =
typeof windowWidth === "number" && Number.isFinite(windowWidth)
? windowWidth
: 0;
const estimatedPageWidth = Math.min(
safeWindowWidth * PAGE_WIDTH_RATIO_WEB,
PAGE_MAX_WIDTH
);
const paddedWidth = estimatedPageWidth - PAGE_HORIZONTAL_PADDING;
return Math.max(1, paddedWidth);
}, [windowWidth]);
const containerWidth = parentLayout?.width || estimatedContainerWidth;
const route = useRoute();
const {
selectedProjectId,
selectedProject,
updateProjectData,
currentUserData,
currentUID,
} = useUser();
typeof windowWidth === 'number' && Number.isFinite(windowWidth) ? windowWidth : 0
const estimatedPageWidth = Math.min(safeWindowWidth * PAGE_WIDTH_RATIO_WEB, PAGE_MAX_WIDTH)
const paddedWidth = estimatedPageWidth - PAGE_HORIZONTAL_PADDING
return Math.max(1, paddedWidth)
}, [windowWidth])
const containerWidth = parentLayout?.width || estimatedContainerWidth
const route = useRoute()
const { selectedProjectId, selectedProject, updateProjectData, currentUserData, currentUID } =
useUser()
const [genres, setGenres] = useState([]);
const [voice, setVoice] = useState({});
const [instruments, setInstruments] = useState([]);
const [rhythm, setRhythm] = useState(null);
const [isConfirmVisible, setIsConfirmVisible] = useState(false);
const [isProcessingConfirmation, setIsProcessingConfirmation] =
useState(false);
const isRegenerationFlow = route?.params?.isRegeneration === true;
const [genres, setGenres] = useState([])
const [voice, setVoice] = useState({})
const [instruments, setInstruments] = useState([])
const [rhythm, setRhythm] = useState(null)
const [isConfirmVisible, setIsConfirmVisible] = useState(false)
const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false)
const isRegenerationFlow = route?.params?.isRegeneration === true
const coinBalance = useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
const value = currentUserData?.coins
if (typeof value === 'number' && Number.isFinite(value)) {
return value
}
if (typeof value === "string") {
const parsed = Number(value);
if (typeof value === 'string') {
const parsed = Number(value)
if (Number.isFinite(parsed)) {
return parsed;
return parsed
}
}
return 0;
}, [currentUserData?.coins]);
return 0
}, [currentUserData?.coins])
const steps = useMemo(
() => [
{
key: "genres",
key: 'genres',
render: () => <ChooseGenre selected={genres} setSelected={setGenres} />,
},
...VOICE_SECTION_TITLES.map((section) => ({
key: `voice-${section}`,
render: () => (
<CustomizeVoice
category={section}
selected={voice}
setSelected={setVoice}
/>
),
render: () => <CustomizeVoice category={section} selected={voice} setSelected={setVoice} />,
})),
{
key: "instruments",
render: () => (
<ChooseInstruments
selected={instruments}
setSelected={setInstruments}
/>
),
key: 'instruments',
render: () => <ChooseInstruments selected={instruments} setSelected={setInstruments} />,
},
{
key: "rhythm",
render: () => (
<ChooseRhythm selected={rhythm} setSelected={setRhythm} />
),
key: 'rhythm',
render: () => <ChooseRhythm selected={rhythm} setSelected={setRhythm} />,
},
],
[genres, voice, instruments, rhythm]
);
)
const totalSteps = steps.length;
const lastStepIndex = totalSteps - 1;
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0);
const totalSteps = steps.length
const lastStepIndex = totalSteps - 1
const instrumentStepIndex = Math.max(lastStepIndex - 1, 0)
const isStepValid = useMemo(() => {
if (selectedIndex === 0) {
return Array.isArray(genres) && genres.length > 0;
return Array.isArray(genres) && genres.length > 0
}
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX;
const isVoiceStep =
voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length;
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX
const isVoiceStep = voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length
if (isVoiceStep) {
const category = VOICE_SECTION_TITLES[voiceStepIndex];
const category = VOICE_SECTION_TITLES[voiceStepIndex]
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
return true;
return true
}
return !!(voice && typeof voice === "object" && voice[category]);
return !!(voice && typeof voice === 'object' && voice[category])
}
if (selectedIndex === instrumentStepIndex) {
return Array.isArray(instruments) && instruments.length > 0;
return Array.isArray(instruments) && instruments.length > 0
}
if (selectedIndex === lastStepIndex) {
return !!rhythm;
return !!rhythm
}
return true;
}, [
selectedIndex,
genres,
voice,
instruments,
rhythm,
instrumentStepIndex,
lastStepIndex,
]);
return true
}, [selectedIndex, genres, voice, instruments, rhythm, instrumentStepIndex, lastStepIndex])
const musicConfig = useMemo(() => {
let lyricsArr = [];
let lyricsArr = []
if (Array.isArray(selectedProject?.lyrics)) {
lyricsArr = selectedProject.lyrics.map((s) => ({
type: normalizeStructureType(s?.type),
lyrics: s?.lyrics || "",
}));
lyrics: s?.lyrics || '',
}))
} else {
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 });
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 })
}
const voiceArray = Object.entries(voice || {})
.filter(([, v]) => typeof v === "string" && v.trim())
.map(([category, value]) => ({ category, value }));
.filter(([, v]) => typeof v === 'string' && v.trim())
.map(([category, value]) => ({ category, value }))
return {
title: selectedProject?.title || "",
title: selectedProject?.title || '',
lyrics: lyricsArr,
genres: Array.isArray(genres) ? genres : [],
voice: voiceArray,
instruments: Array.isArray(instruments) ? instruments : [],
tempo: rhythm || undefined,
projectId: selectedProjectId || undefined,
};
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId]);
}
}, [selectedProject, genres, voice, instruments, rhythm, selectedProjectId])
useEffect(() => {
const nextProgress = 18 + selectedIndex * 9;
const nextProgress = 18 + selectedIndex * 9
if (nextProgress !== progress) {
setProgress(nextProgress);
setProgress(nextProgress)
}
try {
scrollRef.current?.scrollToIndex?.({
index: selectedIndex,
animated: true,
});
})
} catch (_) {}
}, [selectedIndex, progress, containerWidth]);
}, [selectedIndex, progress, containerWidth])
const getItemLayout = useCallback(
(_data, index) => ({
@@ -216,101 +170,88 @@ const ComposeSong = () => {
index,
}),
[containerWidth]
);
)
const handleCancelGeneration = () => {
setIsConfirmVisible(false);
navigate(Routes.SongReady);
};
setIsConfirmVisible(false)
navigate(Routes.SongReady)
}
const persistMusicConfig = useCallback(async () => {
try {
if (!selectedProjectId) return;
if (!selectedProjectId) return
const payload = {
musicConfig: {
title: musicConfig?.title || "",
title: musicConfig?.title || '',
lyrics: Array.isArray(musicConfig?.lyrics) ? musicConfig.lyrics : [],
genres: Array.isArray(musicConfig?.genres) ? musicConfig.genres : [],
voice: Array.isArray(musicConfig?.voice) ? musicConfig.voice : [],
instruments: Array.isArray(musicConfig?.instruments)
? musicConfig.instruments
: [],
tempo: musicConfig?.tempo || "",
instruments: Array.isArray(musicConfig?.instruments) ? musicConfig.instruments : [],
tempo: musicConfig?.tempo || '',
},
musicStatus: null,
sunoTaskId: firebase.firestore.FieldValue.delete(),
};
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete();
}
await updateProjectData(payload);
if (!isRegenerationFlow) {
payload.musicUrls = firebase.firestore.FieldValue.delete()
}
await updateProjectData(payload)
} catch (_error) {}
}, [
isRegenerationFlow,
musicConfig,
selectedProjectId,
updateProjectData,
]);
}, [isRegenerationFlow, musicConfig, selectedProjectId, updateProjectData])
const spendCoinsForGeneration = useCallback(async () => {
if (!currentUID) {
throw new Error("Utilisateur introuvable. Merci de réessayer.");
throw new Error('Utilisateur introuvable. Merci de réessayer.')
}
try {
const functionsClient = getFunctionsClient(FUNCTIONS_REGION);
const createSongOrder =
functionsClient.httpsCallable("orders-createSongOrder");
const functionsClient = getFunctionsClient(FUNCTIONS_REGION)
const createSongOrder = functionsClient.httpsCallable('orders-createSongOrder')
await createSongOrder({
amount: -MUSIC_GENERATION_COIN_COST,
songId: selectedProjectId || null,
source: "music_generation",
});
source: 'music_generation',
})
} catch (error) {
const message =
typeof error?.message === "string"
? error.message.replace(
/^functions\.https\.HttpsError:\s*/iu,
"",
)
: null;
typeof error?.message === 'string'
? error.message.replace(/^functions\.https\.HttpsError:\s*/iu, '')
: null
throw new Error(
message ||
"Une erreur est survenue lors de la création de la commande de crédits.",
);
message || 'Une erreur est survenue lors de la création de la commande de crédits.'
)
}
}, [currentUID, selectedProjectId]);
}, [currentUID, selectedProjectId])
const handleConfirmGeneration = useCallback(async () => {
if (isProcessingConfirmation) return;
setIsProcessingConfirmation(true);
if (isProcessingConfirmation) return
setIsProcessingConfirmation(true)
try {
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0;
const availableCoins = Number.isFinite(coinBalance) ? coinBalance : 0
if (availableCoins < MUSIC_GENERATION_COIN_COST) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
AppAlert(
"Crédits insuffisants",
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer.",
);
openCoinPackModal();
return;
'Crédits insuffisants',
"Tu n'as pas assez de pièces pour générer une musique. Recharge ton compte pour continuer."
)
openCoinPackModal()
return
}
await spendCoinsForGeneration();
await persistMusicConfig();
setIsConfirmVisible(false);
navigate(Routes.GeneratingSong, { config: musicConfig });
await spendCoinsForGeneration()
await persistMusicConfig()
setIsConfirmVisible(false)
navigate(Routes.GeneratingSong, { config: musicConfig })
} catch (error) {
setIsConfirmVisible(false);
setIsConfirmVisible(false)
const message =
error?.message ||
"Une erreur est survenue lors du lancement de la génération.";
AppAlert("Impossible de lancer la génération", message);
error?.message || 'Une erreur est survenue lors du lancement de la génération.'
AppAlert('Impossible de lancer la génération', message)
} finally {
setIsProcessingConfirmation(false);
setIsProcessingConfirmation(false)
}
}, [
coinBalance,
@@ -318,23 +259,23 @@ const ComposeSong = () => {
musicConfig,
persistMusicConfig,
spendCoinsForGeneration,
]);
])
const onPressNext = () => {
if (selectedIndex === steps.length - 1) {
setIsConfirmVisible(true);
return;
setIsConfirmVisible(true)
return
}
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1));
};
setSelectedIndex((prev) => Math.min(prev + 1, steps.length - 1))
}
const onPressBack = () => {
if (selectedIndex > 0) {
setSelectedIndex((prev) => Math.max(prev - 1, 0));
setSelectedIndex((prev) => Math.max(prev - 1, 0))
} else {
goBack();
goBack()
}
};
}
return (
<Page backgroundImg={background.studioBG2} headerType="NONE">
@@ -344,10 +285,7 @@ const ComposeSong = () => {
// logo={icons.musicLandStudio}
/>
<View style={{ flex: 1, paddingBottom: gutters, gap: 48 }}>
<View
style={{ flex: 1 }}
onLayout={(event) => setParentLayout(event.nativeEvent.layout)}
>
<View style={{ flex: 1 }} onLayout={(event) => setParentLayout(event.nativeEvent.layout)}>
<FlatList
ref={scrollRef}
data={steps}
@@ -381,7 +319,7 @@ const ComposeSong = () => {
/>
)}
<GradientButton
title={selectedIndex === steps.length - 1 ? "Générer" : "Suivant"}
title={selectedIndex === steps.length - 1 ? 'Générer' : 'Suivant'}
onPress={onPressNext}
disabled={!isStepValid}
/>
@@ -393,36 +331,36 @@ const ComposeSong = () => {
transparent
visible={isConfirmVisible}
onRequestClose={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: gutters,
paddingVertical: gutters * 1.5,
backgroundColor: "rgba(0, 0, 0, 0.6)",
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}}
>
<CreateLyricsHeader
containerStyle={{
width: "100%",
width: '100%',
maxWidth: CONFIRM_MODAL_MAX_WIDTH,
alignSelf: "center",
alignSelf: 'center',
paddingVertical: 24,
paddingHorizontal: 24,
gap: 24,
}}
>
<View style={{ gap: 30, alignItems: "center" }}>
<View style={{ gap: 30, alignItems: 'center' }}>
<View
style={{
paddingHorizontal: 15,
gap: 12,
alignItems: "center",
alignItems: 'center',
}}
>
<Text
@@ -430,18 +368,18 @@ const ComposeSong = () => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Générer la musique ?
</Text>
<View style={{ alignItems: "center", gap: 8 }}>
<View style={{ alignItems: 'center', gap: 8 }}>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -450,7 +388,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Cette action coûte
@@ -461,7 +399,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
iconSize={18}
/>
@@ -471,7 +409,7 @@ const ComposeSong = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Souhaites-tu les utiliser pour lancer la génération ?
@@ -479,9 +417,9 @@ const ComposeSong = () => {
</View>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
}}
>
@@ -490,7 +428,7 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
>
Solde disponible :
@@ -501,13 +439,13 @@ const ComposeSong = () => {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: "center",
textAlign: 'center',
}}
iconSize={16}
/>
</View>
</View>
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
<View style={{ width: '85%', alignSelf: 'center', gap: 15 }}>
<GradientButton
title="Confirmer"
onPress={handleConfirmGeneration}
@@ -516,8 +454,8 @@ const ComposeSong = () => {
<BorderGradientButton
title="Annuler"
onPress={() => {
if (isProcessingConfirmation) return;
setIsConfirmVisible(false);
if (isProcessingConfirmation) return
setIsConfirmVisible(false)
}}
disabled={isProcessingConfirmation}
/>
@@ -527,7 +465,7 @@ const ComposeSong = () => {
</View>
</Modal>
</Page>
);
};
)
}
export default ComposeSong;
export default ComposeSong