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
+228 -254
View File
@@ -1,7 +1,7 @@
/* global setInterval, clearInterval */
import { useFocusEffect } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useFocusEffect } from '@react-navigation/native'
import { BlurView } from 'expo-blur'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import {
FlatList,
Image,
@@ -11,30 +11,30 @@ import {
Text,
View,
useWindowDimensions,
} from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import alert from "../../components/Alert";
import BorderGradientButton from "../../components/BorderGradientButton";
import CreditAmount from "../../components/CreditAmount";
import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader";
import ProgressSlider from "../../components/player/ProgressSlider";
import { isWeb } from "../../hooks/useLayoutType";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate, reset } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles";
import { gutters, size } from "../../styles/Style";
import { FONT_FAMILY } from "../../styles/Fonts";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
} from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets'
import alert from '../../components/Alert'
import BorderGradientButton from '../../components/BorderGradientButton'
import CreditAmount from '../../components/CreditAmount'
import GradientButton from '../../components/GradientButton'
import ValidateModal from '../../components/modal/ValidateModal'
import MusicLandHeader from '../../components/MusicLandHeader'
import ProgressSlider from '../../components/player/ProgressSlider'
import { isWeb } from '../../hooks/useLayoutType'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { navigate, reset } from '../../navigation/NavigationService'
import { useUser } from '../../providers/UserDataProvider'
import { Palette, Style } from '../../styles'
import { gutters, size } from '../../styles/Style'
import { FONT_FAMILY } from '../../styles/Fonts'
import useSharedAudioPlayer from '../../hooks/useSharedAudioPlayer'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const MUSIC_GENERATION_COIN_COST = 8;
const SONG_OPTIONS_PER_GENERATION = 2;
const REGENERATE_MODAL_MAX_WIDTH = 540;
const MUSIC_GENERATION_COIN_COST = 8
const SONG_OPTIONS_PER_GENERATION = 2
const REGENERATE_MODAL_MAX_WIDTH = 540
const SongReady = () => {
const {
@@ -42,81 +42,79 @@ const SongReady = () => {
selectedProject,
updateProjectData,
updateUserData,
} = useUser();
const [showValidateModal, setShowValidateModal] = useState(false);
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const playerRefs = useRef({});
} = useUser()
const [showValidateModal, setShowValidateModal] = useState(false)
const [showRegenerateModal, setShowRegenerateModal] = useState(false)
const [musicUrls, setMusicUrls] = useState([])
const [selectedIndex, setSelectedIndex] = useState(0)
const playerRefs = useRef({})
const registerPlayer = useCallback((index, player) => {
if (player) {
playerRefs.current[index] = player;
playerRefs.current[index] = player
} else {
delete playerRefs.current[index];
delete playerRefs.current[index]
}
}, []);
}, [])
const pauseAllExcept = useCallback(async (keepIndex = null) => {
const tasks = Object.entries(playerRefs.current).map(
async ([key, player]) => {
const idx = Number(key);
if (!player || idx === keepIndex) return;
try {
await player.pause?.();
} catch (error) {
console.log("SongReady pause error", error?.message);
}
},
);
await Promise.all(tasks);
}, []);
const tasks = Object.entries(playerRefs.current).map(async ([key, player]) => {
const idx = Number(key)
if (!player || idx === keepIndex) return
try {
await player.pause?.()
} catch (error) {
console.log('SongReady pause error', error?.message)
}
})
await Promise.all(tasks)
}, [])
const pauseAllPlayers = useCallback(async () => {
await pauseAllExcept(null);
}, [pauseAllExcept]);
await pauseAllExcept(null)
}, [pauseAllExcept])
const handleTogglePlayback = useCallback(
async (index, isPlayingNow) => {
const player = playerRefs.current[index];
if (!player) return;
const player = playerRefs.current[index]
if (!player) return
if (isPlayingNow) {
try {
await player.pause?.();
await player.pause?.()
} catch (error) {
console.log("SongReady pause toggle", error?.message);
console.log('SongReady pause toggle', error?.message)
}
return;
return
}
await pauseAllExcept(index);
await pauseAllExcept(index)
try {
await player.play?.();
await player.play?.()
} catch (error) {
console.log("SongReady play error", error?.message);
console.log('SongReady play error', error?.message)
}
},
[pauseAllExcept],
);
[pauseAllExcept]
)
// Sync URLs from provider's selectedProject
useEffect(() => {
const urls = Array.isArray(selectedProject?.musicUrls)
? selectedProject.musicUrls
.filter((url) => typeof url === "string" && url.trim())
.map((url) => url.trim())
: [];
setMusicUrls(urls);
.filter((url) => typeof url === 'string' && url.trim())
.map((url) => url.trim())
: []
setMusicUrls(urls)
setSelectedIndex((prev) => {
if (!urls.length) return 0;
return Math.min(prev, urls.length - 1);
});
}, [selectedProject?.musicUrls]);
if (!urls.length) return 0
return Math.min(prev, urls.length - 1)
})
}, [selectedProject?.musicUrls])
const validateSelection = async () => {
try {
const url = musicUrls[selectedIndex];
const url = musicUrls[selectedIndex]
if (!projectId || !url) return;
if (!projectId || !url) return
await updateProjectData({
songIndex: selectedIndex,
songUrl: url,
@@ -126,74 +124,74 @@ const SongReady = () => {
playbackUrl: null,
playbackStatus: null,
playbackGenerating: null,
});
})
// Incrémenter le compteur de chansons générées
const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0;
const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0
await updateUserData({
data: {
generatedSongs: currentGeneratedSongs + 1,
},
shouldSetTooltip: false,
});
})
await pauseAllPlayers();
await pauseAllPlayers()
reset({
index: 1,
routes: [{ name: Routes.BottomTab }, { name: Routes.ChooseCoverType }],
});
})
} catch (e) {
console.log("Validate error", e?.message);
console.log('Validate error', e?.message)
}
};
}
// Pause audio when screen loses focus (navigate/reset) and on unmount
useFocusEffect(
useCallback(() => {
return () => {
pauseAllPlayers();
};
}, [pauseAllPlayers]),
);
pauseAllPlayers()
}
}, [pauseAllPlayers])
)
useEffect(() => {
return () => {
pauseAllPlayers();
};
}, [pauseAllPlayers]);
pauseAllPlayers()
}
}, [pauseAllPlayers])
const handleChooseTrack = () => {
if (isWeb) {
alert(
"Attention !",
"Lorsque tu clique sur valider, tu ne pourra plus changer ni le texte ni la mélodie.",
'Attention !',
'Lorsque tu clique sur valider, tu ne pourra plus changer ni le texte ni la mélodie.',
[
{
text: "Retour",
style: "cancel",
text: 'Retour',
style: 'cancel',
},
{
text: "Valider",
style: "confirm",
text: 'Valider',
style: 'confirm',
onPress: () => {
validateSelection();
validateSelection()
},
},
],
{ cancelable: false },
);
return;
{ cancelable: false }
)
return
}
setShowValidateModal(true);
};
setShowValidateModal(true)
}
const handleConfirmRegenerate = async () => {
setShowRegenerateModal(false);
setShowRegenerateModal(false)
try {
await pauseAllPlayers();
} catch { }
navigate(Routes.Lyrics);
};
await pauseAllPlayers()
} catch {}
navigate(Routes.Lyrics)
}
const handleRegeneratePress = () => {
if (isWeb) {
@@ -201,19 +199,19 @@ const SongReady = () => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
};
textAlign: 'center',
}
const amountTextStyle = {
...descriptionTextStyle,
fontFamily: FONT_FAMILY.InterSemiBold,
};
}
alert(
"Re-générer le morceau",
'Re-générer le morceau',
<View
style={{
width: "100%",
alignItems: "center",
width: '100%',
alignItems: 'center',
gap: 12,
}}
>
@@ -222,10 +220,10 @@ const SongReady = () => {
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -242,30 +240,30 @@ const SongReady = () => {
</View>,
[
{
text: "Annuler",
style: "cancel",
text: 'Annuler',
style: 'cancel',
},
{
text: "Oui",
style: "confirm",
text: 'Oui',
style: 'confirm',
onPress: () => handleConfirmRegenerate(),
},
],
{ cancelable: true },
);
return;
{ cancelable: true }
)
return
}
setShowRegenerateModal(true);
};
setShowRegenerateModal(true)
}
return (
<Page headerType="NONE" backgroundImg={background.studioBG2} maxWidth={800}>
<MusicLandHeader
onPressBack={async () => {
try {
await pauseAllPlayers();
} catch { }
navigate(Routes.Home);
await pauseAllPlayers()
} catch {}
navigate(Routes.Home)
}}
progress={63}
/>
@@ -280,7 +278,7 @@ const SongReady = () => {
<FlatList
style={{ flex: 1 }}
data={musicUrls}
keyExtractor={(item, idx) => `${item || "song"}-${idx}`}
keyExtractor={(item, idx) => `${item || 'song'}-${idx}`}
renderItem={({ item, index }) => (
<SongOptionCard
index={index}
@@ -301,8 +299,8 @@ const SongReady = () => {
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
width: '80%',
alignSelf: 'center',
gap: 12,
}}
>
@@ -311,10 +309,7 @@ const SongReady = () => {
onPress={handleChooseTrack}
disabled={!musicUrls?.length}
/>
<BorderGradientButton
title="Re-générer le morceau"
onPress={handleRegeneratePress}
/>
<BorderGradientButton title="Re-générer le morceau" onPress={handleRegeneratePress} />
</View>
{!isWeb && (
<>
@@ -331,8 +326,8 @@ const SongReady = () => {
</>
)}
</Page>
);
};
)
}
const SongOptionCard = ({
index,
@@ -345,11 +340,9 @@ const SongOptionCard = ({
selectedProject,
}) => {
const trackTitle =
(Array.isArray(selectedProject?.musicTitles)
? selectedProject.musicTitles?.[index]
: null) ||
(Array.isArray(selectedProject?.musicTitles) ? selectedProject.musicTitles?.[index] : null) ||
selectedProject?.title ||
`Morceau ${index + 1}`;
`Morceau ${index + 1}`
const player = useSharedAudioPlayer(url ? { uri: url } : undefined, {
id: url ? `songready-${url}` : undefined,
@@ -357,130 +350,124 @@ const SongOptionCard = ({
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index, projectId },
});
})
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
const [isPlaying, setIsPlaying] = useState(false);
const shouldResumeAfterSeekRef = useRef(false);
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 })
const [isPlaying, setIsPlaying] = useState(false)
const shouldResumeAfterSeekRef = useRef(false)
useEffect(() => {
registerPlayer(index, player);
return () => registerPlayer(index, null);
}, [index, player, registerPlayer]);
registerPlayer(index, player)
return () => registerPlayer(index, null)
}, [index, player, registerPlayer])
useEffect(() => {
if (!player || !url) return;
if (!player || !url) return
const preload = async () => {
try {
await player.load?.({ startPositionMs: 0 });
await player.load?.({ startPositionMs: 0 })
} catch (error) {
console.log("SongReady preload error", error?.message);
console.log('SongReady preload error', error?.message)
}
};
preload();
}, [player, url]);
}
preload()
}, [player, url])
useEffect(() => {
if (!player) {
setIsPlaying(false);
setProgressInfo({ pos: 0, dur: 0 });
return undefined;
setIsPlaying(false)
setProgressInfo({ pos: 0, dur: 0 })
return undefined
}
const id = setInterval(() => {
const durationMs = Math.max(
0,
Math.round((Number(player.duration) || 0) * 1000),
);
const positionMs = Math.max(
0,
Math.round((Number(player.currentTime) || 0) * 1000),
);
const durationMs = Math.max(0, Math.round((Number(player.duration) || 0) * 1000))
const positionMs = Math.max(0, Math.round((Number(player.currentTime) || 0) * 1000))
setProgressInfo((prev) => {
if (
Math.abs((prev?.dur || 0) - durationMs) < 5 &&
Math.abs((prev?.pos || 0) - positionMs) < 5
) {
return prev;
return prev
}
return { pos: positionMs, dur: durationMs };
});
return { pos: positionMs, dur: durationMs }
})
setIsPlaying((prev) => {
const next = !!player.playing;
const next = !!player.playing
if (prev !== next) {
return next;
return next
}
return prev;
});
}, 300);
return () => clearInterval(id);
}, [player]);
return prev
})
}, 300)
return () => clearInterval(id)
}, [player])
const hasCapturedSeekStateRef = useRef(false);
const hasCapturedSeekStateRef = useRef(false)
const resumePlaybackIfNeeded = useCallback(async () => {
if (!player) return;
const shouldResume = shouldResumeAfterSeekRef.current;
shouldResumeAfterSeekRef.current = false;
hasCapturedSeekStateRef.current = false; // Reset capture flag
if (!player) return
const shouldResume = shouldResumeAfterSeekRef.current
shouldResumeAfterSeekRef.current = false
hasCapturedSeekStateRef.current = false // Reset capture flag
if (!shouldResume) return;
if (!shouldResume) return
try {
if (player.resume) {
await player.resume?.();
await player.resume?.()
} else {
await player.play?.();
await player.play?.()
}
} catch (error) {
console.log("SongReady resume after seek", error?.message);
console.log('SongReady resume after seek', error?.message)
}
}, [player]);
}, [player])
const handleSeek = useCallback(
async (targetMs) => {
if (!player || !progressInfo?.dur) {
return;
return
}
const dur = progressInfo.dur || 0;
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)));
const dur = progressInfo.dur || 0
const pos = Math.max(0, Math.min(dur, Math.floor(targetMs)))
// Mise à jour visuelle immédiate
setProgressInfo((prev) => ({ ...prev, pos }));
setProgressInfo((prev) => ({ ...prev, pos }))
try {
await player.seekTo?.(Math.floor(pos / 1000));
await player.seekTo?.(Math.floor(pos / 1000))
} catch (error) {
console.log("SongReady seek error", error?.message);
console.log('SongReady seek error', error?.message)
}
},
[player, progressInfo?.dur],
);
[player, progressInfo?.dur]
)
const handleSeekStart = useCallback(async () => {
onSelect(index);
if (!player) return;
onSelect(index)
if (!player) return
const isCurrentlyPlaying = !!player.playing || isPlaying;
const isCurrentlyPlaying = !!player.playing || isPlaying
// Only capture state if we haven't already for this drag interaction
if (!hasCapturedSeekStateRef.current) {
hasCapturedSeekStateRef.current = true;
shouldResumeAfterSeekRef.current = isCurrentlyPlaying;
hasCapturedSeekStateRef.current = true
shouldResumeAfterSeekRef.current = isCurrentlyPlaying
}
try {
if (isCurrentlyPlaying) {
await player.pause?.();
await player.pause?.()
}
} catch (error) {
console.log("SongReady pause on seek start", error?.message);
console.log('SongReady pause on seek start', error?.message)
}
}, [index, isPlaying, onSelect, player]);
}, [index, isPlaying, onSelect, player])
const handleToggle = () => {
onSelect(index);
onTogglePlayback?.(index, isPlaying);
};
onSelect(index)
onTogglePlayback?.(index, isPlaying)
}
return (
<Pressable
@@ -490,7 +477,7 @@ const SongOptionCard = ({
borderRadius: 20,
borderWidth: isSelected ? 2 : 1,
borderColor: isSelected ? Palette.primary : Palette.ultraLightWhite,
overflow: "hidden",
overflow: 'hidden',
},
pressed && { opacity: 0.96 },
]}
@@ -500,15 +487,15 @@ const SongOptionCard = ({
tint="dark"
style={{
borderRadius: 18,
overflow: "hidden",
overflow: 'hidden',
padding: 12,
backgroundColor: "#FFFFFF0A",
backgroundColor: '#FFFFFF0A',
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 12,
}}
>
@@ -524,7 +511,7 @@ const SongOptionCard = ({
<View style={{ flex: 1 }}>
<Text
style={{
color: "white",
color: 'white',
marginBottom: responsiveHeight(1),
}}
>{`Morceau ${index + 1}`}</Text>
@@ -551,9 +538,9 @@ const SongOptionCard = ({
height: 18,
borderRadius: 9,
borderWidth: 2,
borderColor: isSelected ? Palette.primary : "white",
alignItems: "center",
justifyContent: "center",
borderColor: isSelected ? Palette.primary : 'white',
alignItems: 'center',
justifyContent: 'center',
}}
>
{isSelected && (
@@ -571,44 +558,34 @@ const SongOptionCard = ({
</View>
</BlurView>
</Pressable>
);
};
)
}
const RegenerateModal = ({ visible, onClose, onConfirm }) => {
const { width, height } = useWindowDimensions();
const isCompactWidth = width < 420;
const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12);
const verticalPadding = Math.max(
isCompactWidth ? gutters : gutters * 1.5,
12,
);
const availableWidth = Math.max(width - horizontalPadding * 2, 0);
const { width, height } = useWindowDimensions()
const isCompactWidth = width < 420
const horizontalPadding = Math.max(isCompactWidth ? 16 : gutters, 12)
const verticalPadding = Math.max(isCompactWidth ? gutters : gutters * 1.5, 12)
const availableWidth = Math.max(width - horizontalPadding * 2, 0)
const contentWidth =
availableWidth > 0
? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth)
: undefined;
const contentGap = isCompactWidth ? 18 : 24;
const buttonGap = isCompactWidth ? 12 : 15;
availableWidth > 0 ? Math.min(REGENERATE_MODAL_MAX_WIDTH, availableWidth) : undefined
const contentGap = isCompactWidth ? 18 : 24
const buttonGap = isCompactWidth ? 12 : 15
return (
<Modal
animationType="fade"
transparent
visible={visible}
onRequestClose={onClose}
>
<Modal animationType="fade" transparent visible={visible} onRequestClose={onClose}>
<View
style={{
flex: 1,
backgroundColor: "rgba(0, 0, 0, 0.6)",
backgroundColor: 'rgba(0, 0, 0, 0.6)',
}}
>
<ScrollView
bounces={false}
contentContainerStyle={{
flexGrow: 1,
justifyContent: "center",
alignItems: "center",
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: horizontalPadding,
paddingVertical: verticalPadding,
minHeight: height,
@@ -618,20 +595,20 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
>
<CreateLyricsHeader
containerStyle={{
width: contentWidth ?? "100%",
width: contentWidth ?? '100%',
maxWidth: REGENERATE_MODAL_MAX_WIDTH,
alignSelf: "center",
alignSelf: 'center',
paddingVertical: contentGap,
paddingHorizontal: contentGap,
gap: contentGap,
flexShrink: 1,
}}
>
<View style={{ gap: contentGap, alignItems: "center" }}>
<View style={{ gap: contentGap, alignItems: 'center' }}>
<View
style={{
gap: isCompactWidth ? 10 : 12,
alignItems: "center",
alignItems: 'center',
paddingHorizontal: isCompactWidth ? 4 : 12,
}}
>
@@ -640,7 +617,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
>
Re-générer le morceau ?
@@ -650,17 +627,17 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
{`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`}
</Text>
<View
style={{
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
flexWrap: "wrap",
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
flexWrap: 'wrap',
gap: 6,
}}
>
@@ -669,7 +646,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Cette action coûte
@@ -681,7 +658,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
textAlign: 'center',
}}
/>
</View>
@@ -690,7 +667,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
textAlign: 'center',
}}
>
Les crédits seront utilisés lors de l'étape de génération.
@@ -698,15 +675,12 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
</View>
<View
style={{
width: isCompactWidth ? "100%" : "85%",
alignSelf: "center",
width: isCompactWidth ? '100%' : '85%',
alignSelf: 'center',
gap: buttonGap,
}}
>
<GradientButton
title="Oui, modifier mes choix"
onPress={onConfirm}
/>
<GradientButton title="Oui, modifier mes choix" onPress={onConfirm} />
<BorderGradientButton title="Retour" onPress={onClose} />
</View>
</View>
@@ -714,7 +688,7 @@ const RegenerateModal = ({ visible, onClose, onConfirm }) => {
</ScrollView>
</View>
</Modal>
);
};
)
}
export default SongReady;
export default SongReady