player, song generation again, home and design fixes

This commit is contained in:
Thomas Demirdjian
2025-11-14 13:51:53 +01:00
parent 5457030d4a
commit cd31d42c0c
14 changed files with 367 additions and 115 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove"/> <uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+2 -2
View File
@@ -73,7 +73,7 @@
{ {
"photosPermission": "Nous avons besoin d'accéder à votre galerie pour vous permettre d'ajouter des photos à vos tâches.", "photosPermission": "Nous avons besoin d'accéder à votre galerie pour vous permettre d'ajouter des photos à vos tâches.",
"cameraPermission": "Nous avons besoin d'accéder à votre appareil photo pour vous permettre de prendre des photos de vos tâches.", "cameraPermission": "Nous avons besoin d'accéder à votre appareil photo pour vous permettre de prendre des photos de vos tâches.",
"microphonePermission": false "microphonePermission": "MusicLand uses the microphone so you can capture audio when recording videos and musical ideas."
} }
], ],
[ [
@@ -101,7 +101,7 @@
"expo-camera", "expo-camera",
{ {
"cameraPermission": "Allow $(PRODUCT_NAME) to access your camera", "cameraPermission": "Allow $(PRODUCT_NAME) to access your camera",
"microphonePermission": false, "microphonePermission": "MusicLand uses the microphone so you can record vocals, instruments, and other audio content inside the app.",
"recordAudioAndroid": false "recordAudioAndroid": false
} }
], ],
+35 -2
View File
@@ -18,6 +18,8 @@ const {
SUNO_STATUS_PATH, SUNO_STATUS_PATH,
} = require("../config/suno"); } = require("../config/suno");
const MAX_STORED_MUSIC_TRACKS = 4;
/** /**
* Marque un projet comme échoué suite à une erreur Suno * Marque un projet comme échoué suite à une erreur Suno
* @param {string} projectId - Identifiant du projet * @param {string} projectId - Identifiant du projet
@@ -673,8 +675,39 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
// 4) Mettre à jour le statut du projet // 4) Mettre à jour le statut du projet
try { try {
const musicUrls = [p1?.url, p2?.url].filter(Boolean); const newMusicUrls = [p1?.url, p2?.url]
await refList.projects.doc(projectId).set( .filter((url) => typeof url === "string" && url.trim())
.map((url) => url.trim());
const projectRef = refList.projects.doc(projectId);
let existingMusicUrls = [];
try {
const projectSnap = await projectRef.get();
const projectData = projectSnap?.data() || {};
if (Array.isArray(projectData.musicUrls)) {
existingMusicUrls = projectData.musicUrls
.filter((url) => typeof url === "string" && url.trim())
.map((url) => url.trim());
}
} catch (readError) {
console.error(
"⚠️ [SunoCallback] Impossible de récupérer les anciennes musiques:",
readError,
);
}
const mergedMusicUrls = [...existingMusicUrls, ...newMusicUrls];
const uniqueMusicUrls = mergedMusicUrls.filter(
(url, index) => mergedMusicUrls.indexOf(url) === index,
);
const musicUrls =
uniqueMusicUrls.length > MAX_STORED_MUSIC_TRACKS
? uniqueMusicUrls.slice(
uniqueMusicUrls.length - MAX_STORED_MUSIC_TRACKS,
)
: uniqueMusicUrls;
await projectRef.set(
{ {
musicStatus: "GENERATED", musicStatus: "GENERATED",
musicUrls, musicUrls,
+8 -6
View File
@@ -13,6 +13,7 @@ import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette } from "../../styles"; import { gutters, Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { ensureAuthenticated } from "../../utils/authRedirect"; import { ensureAuthenticated } from "../../utils/authRedirect";
import { responsiveHeight } from "react-native-responsive-dimensions";
const formatDuration = (ms) => { const formatDuration = (ms) => {
const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000)); const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000));
@@ -103,13 +104,13 @@ const GlobalAudioPlayer = () => {
{ {
likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID), likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID),
}, },
{ merge: true } { merge: true },
); );
} catch (_error) { } catch (_error) {
setPendingLike(null); setPendingLike(null);
} }
}, },
[currentUID, effectiveIsLiked, isLikeProcessing, projectId] [currentUID, effectiveIsLiked, isLikeProcessing, projectId],
); );
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
@@ -138,7 +139,7 @@ const GlobalAudioPlayer = () => {
await resume(); await resume();
} }
}, },
[isPlaying, pause, resume] [isPlaying, pause, resume],
); );
const handleOpenDetails = useCallback(() => { const handleOpenDetails = useCallback(() => {
@@ -169,7 +170,9 @@ const GlobalAudioPlayer = () => {
}); });
}, [currentTrack, navigateToMusicDetails, projectId]); }, [currentTrack, navigateToMusicDetails, projectId]);
const TAB_BAR_HEIGHT = 64; const TAB_BAR_HEIGHT = isWeb
? 64
: (insets.bottom < 20 ? 30 : insets.bottom) + 60;
const GAP_WITH_TAB = 8; const GAP_WITH_TAB = 8;
const bottomOffset = (isWeb ? gutters : 5) + TAB_BAR_HEIGHT + GAP_WITH_TAB; const bottomOffset = (isWeb ? gutters : 5) + TAB_BAR_HEIGHT + GAP_WITH_TAB;
@@ -216,8 +219,7 @@ const GlobalAudioPlayer = () => {
style={[ style={[
styles.actionButton, styles.actionButton,
styles.likeButton, styles.likeButton,
(!projectId || isLikeProcessing) && (!projectId || isLikeProcessing) && styles.disabledAction,
styles.disabledAction,
]} ]}
> >
<Image <Image
+2 -2
View File
@@ -12,7 +12,7 @@ import { Platform } from "react-native";
const functionsInstances = {}; const functionsInstances = {};
const emulatorConfigured = {}; const emulatorConfigured = {};
const emulatorHost = Platform.OS === "web" ? "localhost" : "192.168.1.146"; const emulatorHost = Platform.OS === "web" ? "localhost" : "192.168.1.62";
const configureFunctionsEmulator = (instance, regionKey = "us-central1") => { const configureFunctionsEmulator = (instance, regionKey = "us-central1") => {
if (!__DEV__ || !instance?.useEmulator || emulatorConfigured[regionKey]) { if (!__DEV__ || !instance?.useEmulator || emulatorConfigured[regionKey]) {
@@ -25,7 +25,7 @@ const configureFunctionsEmulator = (instance, regionKey = "us-central1") => {
} catch (error) { } catch (error) {
console.warn( console.warn(
`[firebase] Unable to set functions emulator for region ${regionKey}`, `[firebase] Unable to set functions emulator for region ${regionKey}`,
error?.message error?.message,
); );
} }
}; };
+1
View File
@@ -54,6 +54,7 @@ const HIDDEN_ROUTE_NAMES = new Set([
Routes.ChangePassword, Routes.ChangePassword,
Routes.Notifications, Routes.Notifications,
Routes.Language, Routes.Language,
Routes.Payments,
Routes.Login, Routes.Login,
Routes.ResetPassword, Routes.ResetPassword,
Routes.Register, Routes.Register,
-7
View File
@@ -228,13 +228,6 @@ const HitParade = () => {
/> />
<View style={{ flex: 1, gap: 24 }}> <View style={{ flex: 1, gap: 24 }}>
<View style={{ gap: 11 }}> <View style={{ gap: 11 }}>
<Pressable style={{ position: "absolute", right: 8, top: 20 }}>
<Image source={icons.calendar} />
</Pressable>
<Image
source={icons.hitParadeLogo}
style={{ alignSelf: "center" }}
/>
<Pressable onPress={handlePlayRandomSong}> <Pressable onPress={handlePlayRandomSong}>
<Image <Image
source={icons.play} source={icons.play}
+96 -26
View File
@@ -10,6 +10,8 @@ import { Image as ExpoImage } from "expo-image";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background, cardsImg, icons } from "../../assets"; import { background, cardsImg, icons } from "../../assets";
import Alert from "../../components/Alert"; import Alert from "../../components/Alert";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import { LinearGradient } from "../../components/LinearGradient/LinearGradient";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo"; import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu"; import MoreMenu from "../../components/MoreMenu";
@@ -474,21 +476,23 @@ const Home = ({ navigation, route }) => {
</View> </View>
); );
const topBar = (
<View style={styles.topBar}>
<ProjectDropDown
style={styles.projectDropDown}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
</View>
);
const journeyContent = ( const journeyContent = (
<> <>
<View style={styles.topBar}>
<ProjectDropDown
style={styles.projectDropDown}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
</View>
<MoreMenu <MoreMenu
visible={menuVisible} visible={menuVisible}
top={menuAnchor?.top ?? 0} top={menuAnchor?.top ?? 0}
@@ -499,7 +503,34 @@ const Home = ({ navigation, route }) => {
extraItems={moreMenuItems} extraItems={moreMenuItems}
/> />
<Text style={styles.subtitle}>5 espaces à découvrir</Text> <View style={styles.subtitleWrapper}>
{isWeb ? (
<LinearGradient
colors={["#F94697", "#7023F7"]}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={[styles.subtitleGradient, styles.subtitleGradientWeb]}
>
<View style={[styles.subtitleInner, styles.subtitleInnerWeb]}>
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
</View>
</LinearGradient>
) : (
<BorderGradient
style={[styles.subtitleGradient, styles.subtitleBorder]}
gradientProps={{
colors: ["#F94697", "#7023F7"],
start: { x: 0, y: 0.5 },
end: { x: 1, y: 0.5 },
locations: [0, 1],
}}
>
<View style={styles.subtitleInner}>
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
</View>
</BorderGradient>
)}
</View>
{stageCardsList} {stageCardsList}
@@ -530,15 +561,21 @@ const Home = ({ navigation, route }) => {
style={styles.centerImage} style={styles.centerImage}
/> />
{isWeb ? ( {isWeb ? (
<View style={styles.contentOverlay}>{journeyContent}</View> <View style={styles.contentOverlay}>
) : ( {topBar}
<ScrollView
style={[styles.mobileScrollView, styles.contentOverlay]}
contentContainerStyle={styles.mobileScrollContent}
showsVerticalScrollIndicator={false}
>
{journeyContent} {journeyContent}
</ScrollView> </View>
) : (
<View style={[styles.contentOverlay, styles.mobileContent]}>
{topBar}
<ScrollView
style={styles.mobileScrollView}
contentContainerStyle={styles.mobileScrollContent}
showsVerticalScrollIndicator={false}
>
{journeyContent}
</ScrollView>
</View>
)} )}
</View> </View>
</Page> </Page>
@@ -629,6 +666,7 @@ const styles = StyleSheet.create({
gap: 12, gap: 12,
marginTop: isWeb ? 24 : 0, marginTop: isWeb ? 24 : 0,
marginBottom: isWeb ? 8 : 16, marginBottom: isWeb ? 8 : 16,
paddingHorizontal: isWeb ? 0 : gutters,
zIndex: 10, zIndex: 10,
}, },
projectDropDown: { projectDropDown: {
@@ -636,6 +674,10 @@ const styles = StyleSheet.create({
maxWidth: 420, maxWidth: 420,
flexGrow: 1, flexGrow: 1,
}, },
mobileContent: {
flex: 1,
width: "100%",
},
mobileScrollView: { mobileScrollView: {
flex: 1, flex: 1,
width: "100%", width: "100%",
@@ -645,16 +687,44 @@ const styles = StyleSheet.create({
paddingTop: 24, paddingTop: 24,
paddingBottom: gutters * 6, paddingBottom: gutters * 6,
}, },
subtitle: { subtitleWrapper: {
width: "100%", width: "100%",
fontFamily: FONT_FAMILY.InterSemiBold, marginBottom: 16,
marginTop: 15,
alignItems: "center",
},
subtitleGradient: {
width: "100%",
maxWidth: 420,
borderRadius: 999,
alignSelf: "center",
},
subtitleBorder: {
borderWidth: 1,
},
subtitleGradientWeb: {
padding: 2,
},
subtitleInner: {
width: "100%",
paddingHorizontal: 24,
paddingVertical: 10,
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
backgroundColor: Palette.glass,
},
subtitleInnerWeb: {
paddingVertical: 12,
backgroundColor: Palette.lightPurple,
},
subtitle: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 18, fontSize: 18,
color: Palette.white, color: Palette.white,
textAlign: "center", textAlign: "center",
textTransform: "uppercase", textTransform: "uppercase",
letterSpacing: 1, letterSpacing: 1,
marginBottom: 16,
marginTop: 15,
}, },
cardsGrid: { cardsGrid: {
width: "100%", width: "100%",
+21 -5
View File
@@ -1,4 +1,5 @@
import React, { useMemo, useRef, useState } from "react"; import React, { useMemo, useRef, useState } from "react";
import { useRoute } from "@react-navigation/native";
import { Dimensions, Modal, Text, View } from "react-native"; import { Dimensions, Modal, Text, View } from "react-native";
import SwiperFlatList from "react-native-swiper-flatlist"; import SwiperFlatList from "react-native-swiper-flatlist";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
@@ -32,6 +33,7 @@ const ComposeSong = () => {
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const [progress, setProgress] = useState(18); const [progress, setProgress] = useState(18);
const [containerLayout, setContainerLayout] = useState(null); const [containerLayout, setContainerLayout] = useState(null);
const route = useRoute();
const { const {
selectedProjectId, selectedProjectId,
selectedProject, selectedProject,
@@ -50,6 +52,7 @@ const ComposeSong = () => {
const [isConfirmVisible, setIsConfirmVisible] = useState(false); const [isConfirmVisible, setIsConfirmVisible] = useState(false);
const [isProcessingConfirmation, setIsProcessingConfirmation] = const [isProcessingConfirmation, setIsProcessingConfirmation] =
useState(false); useState(false);
const isRegenerationFlow = route?.params?.isRegeneration === true;
const coinBalance = useMemo(() => { const coinBalance = useMemo(() => {
const value = currentUserData?.coins; const value = currentUserData?.coins;
@@ -216,6 +219,11 @@ const ComposeSong = () => {
} }
}; };
const handleCancelGeneration = () => {
setIsConfirmVisible(false);
navigate(Routes.SongReady);
};
return ( return (
<Page backgroundImg={background.studioBG2} headerType="NONE"> <Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader <MusicLandHeader
@@ -275,11 +283,19 @@ const ComposeSong = () => {
</SwiperFlatList> </SwiperFlatList>
</View> </View>
{selectedIndex !== 4 && ( {selectedIndex !== 4 && (
<GradientButton <View style={{ gap: 12 }}>
title={selectedIndex === 3 ? "Générer" : "Suivant"} {selectedIndex === 3 && isRegenerationFlow && (
onPress={onPressNext} <BorderGradientButton
disabled={!isStepValid} title="Annuler la nouvelle génération"
/> onPress={handleCancelGeneration}
/>
)}
<GradientButton
title={selectedIndex === 3 ? "Générer" : "Suivant"}
onPress={onPressNext}
disabled={!isStepValid}
/>
</View>
)} )}
</View> </View>
<Modal <Modal
+21 -5
View File
@@ -5,6 +5,7 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { useRoute } from "@react-navigation/native";
import { Dimensions, FlatList, Modal, Text, View } from "react-native"; import { Dimensions, FlatList, Modal, Text, View } from "react-native";
import { background } from "../../assets"; import { background } from "../../assets";
import BorderGradientButton from "../../components/BorderGradientButton"; import BorderGradientButton from "../../components/BorderGradientButton";
@@ -38,6 +39,7 @@ const ComposeSong = () => {
const [progress, setProgress] = useState(18); const [progress, setProgress] = useState(18);
const [parentLayout, setParentLayout] = useState(null); const [parentLayout, setParentLayout] = useState(null);
const containerWidth = parentLayout?.width || windowWidth || 1; const containerWidth = parentLayout?.width || windowWidth || 1;
const route = useRoute();
const { const {
selectedProjectId, selectedProjectId,
selectedProject, selectedProject,
@@ -53,6 +55,7 @@ const ComposeSong = () => {
const [isConfirmVisible, setIsConfirmVisible] = useState(false); const [isConfirmVisible, setIsConfirmVisible] = useState(false);
const [isProcessingConfirmation, setIsProcessingConfirmation] = const [isProcessingConfirmation, setIsProcessingConfirmation] =
useState(false); useState(false);
const isRegenerationFlow = route?.params?.isRegeneration === true;
const coinBalance = useMemo(() => { const coinBalance = useMemo(() => {
const value = currentUserData?.coins; const value = currentUserData?.coins;
@@ -164,6 +167,11 @@ const ComposeSong = () => {
[containerWidth] [containerWidth]
); );
const handleCancelGeneration = () => {
setIsConfirmVisible(false);
navigate(Routes.SongReady);
};
const persistMusicConfig = useCallback(async () => { const persistMusicConfig = useCallback(async () => {
try { try {
if (!selectedProjectId) return; if (!selectedProjectId) return;
@@ -304,11 +312,19 @@ const ComposeSong = () => {
/> />
</View> </View>
{selectedIndex !== steps.length && ( {selectedIndex !== steps.length && (
<GradientButton <View style={{ gap: 12 }}>
title={selectedIndex === steps.length - 1 ? "Générer" : "Suivant"} {selectedIndex === steps.length - 1 && isRegenerationFlow && (
onPress={onPressNext} <BorderGradientButton
disabled={!isStepValid} title="Annuler la nouvelle génération"
/> onPress={handleCancelGeneration}
/>
)}
<GradientButton
title={selectedIndex === steps.length - 1 ? "Générer" : "Suivant"}
onPress={onPressNext}
disabled={!isStepValid}
/>
</View>
)} )}
</View> </View>
<Modal <Modal
+168 -6
View File
@@ -2,14 +2,16 @@
import { useFocusEffect } from "@react-navigation/native"; import { useFocusEffect } from "@react-navigation/native";
import { BlurView } from "expo-blur"; import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import { Image, Pressable, Text, View } from "react-native"; import { Image, Modal, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions"; import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
import alert from "../../components/Alert"; import alert from "../../components/Alert";
import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal"; import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider"; import Slider from "../../components/Slider";
import CreditAmount from "../../components/CreditAmount";
import { isWeb } from "../../hooks/useLayoutType"; import { isWeb } from "../../hooks/useLayoutType";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer"; import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import Page from "../../layouts/Page"; import Page from "../../layouts/Page";
@@ -18,8 +20,13 @@ import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles"; import { Palette, Style } from "../../styles";
import { gutters, size } from "../../styles/Style"; import { gutters, size } from "../../styles/Style";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; 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 SongReady = () => { const SongReady = () => {
const { const {
selectedProjectId: projectId, selectedProjectId: projectId,
@@ -28,6 +35,7 @@ const SongReady = () => {
updateUserData, updateUserData,
} = useUser(); } = useUser();
const [showValidateModal, setShowValidateModal] = useState(false); const [showValidateModal, setShowValidateModal] = useState(false);
const [showRegenerateModal, setShowRegenerateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]); const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false }); const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false });
@@ -323,6 +331,38 @@ const SongReady = () => {
setShowValidateModal(true); setShowValidateModal(true);
}; };
const handleConfirmRegenerate = async () => {
setShowRegenerateModal(false);
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.ComposeSong, { isRegeneration: true });
};
const handleRegeneratePress = () => {
if (isWeb) {
alert(
"Re-générer le morceau",
`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux pour ${MUSIC_GENERATION_COIN_COST} crédits.\n\nLes crédits seront utilisés lors de l'étape de génération.`,
[
{
text: "Annuler",
style: "cancel",
},
{
text: "Oui",
style: "confirm",
onPress: () => handleConfirmRegenerate(),
},
],
{ cancelable: true },
);
return;
}
setShowRegenerateModal(true);
};
return ( return (
<Page headerType="NONE" backgroundImg={background.studioBG2} maxWidth={800}> <Page headerType="NONE" backgroundImg={background.studioBG2} maxWidth={800}>
<MusicLandHeader <MusicLandHeader
@@ -499,16 +539,138 @@ const SongReady = () => {
onPress={handleChooseTrack} onPress={handleChooseTrack}
disabled={!musicUrls?.length} disabled={!musicUrls?.length}
/> />
<BorderGradientButton
title="Re-générer le morceau"
onPress={handleRegeneratePress}
/>
</View> </View>
{!isWeb && ( {!isWeb && (
<ValidateModal <>
visible={showValidateModal} <ValidateModal
onClose={() => setShowValidateModal(false)} visible={showValidateModal}
onPressValidate={validateSelection} onClose={() => setShowValidateModal(false)}
/> onPressValidate={validateSelection}
/>
<RegenerateModal
visible={showRegenerateModal}
onClose={() => setShowRegenerateModal(false)}
onConfirm={handleConfirmRegenerate}
/>
</>
)} )}
</Page> </Page>
); );
}; };
const RegenerateModal = ({ visible, onClose, onConfirm }) => {
return (
<Modal
animationType="fade"
transparent
visible={visible}
onRequestClose={onClose}
>
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingHorizontal: gutters,
paddingVertical: gutters * 1.5,
backgroundColor: "rgba(0, 0, 0, 0.6)",
}}
>
<CreateLyricsHeader
containerStyle={{
width: "100%",
maxWidth: REGENERATE_MODAL_MAX_WIDTH,
alignSelf: "center",
paddingVertical: 24,
paddingHorizontal: 24,
gap: 24,
}}
>
<View style={{ gap: 24, alignItems: "center" }}>
<View
style={{
gap: 12,
alignItems: "center",
paddingHorizontal: 12,
}}
>
<Text
style={{
fontSize: 22,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
>
Re-générer le morceau ?
</Text>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
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",
gap: 6,
}}
>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
}}
>
Cette action coûte
</Text>
<CreditAmount
value={MUSIC_GENERATION_COIN_COST}
iconSize={18}
textStyle={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: "center",
}}
/>
</View>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
textAlign: "center",
}}
>
Les crédits seront utilisés lors de l'étape de génération.
</Text>
</View>
<View style={{ width: "85%", alignSelf: "center", gap: 15 }}>
<GradientButton
title="Oui, modifier mes choix"
onPress={onConfirm}
/>
<BorderGradientButton title="Retour" onPress={onClose} />
</View>
</View>
</CreateLyricsHeader>
</View>
</Modal>
);
};
export default SongReady; export default SongReady;
+4 -11
View File
@@ -147,9 +147,7 @@ const CreateLyricsWithAi = () => {
Array.isArray(customStructure) && Array.isArray(customStructure) &&
fallbackStructure?.length && fallbackStructure?.length &&
(customStructure.length !== fallbackStructure.length || (customStructure.length !== fallbackStructure.length ||
customStructure.some( customStructure.some((value, idx) => value !== fallbackStructure[idx]))
(value, idx) => value !== fallbackStructure[idx]
))
) { ) {
setCustomStructure(fallbackStructure); setCustomStructure(fallbackStructure);
} }
@@ -218,10 +216,7 @@ const CreateLyricsWithAi = () => {
fallbackProjectStructure, fallbackProjectStructure,
]); ]);
const progress = useMemo( const progress = useMemo(() => 16 + selectedIndex * 9, [selectedIndex]);
() => 16 + selectedIndex * 9,
[selectedIndex]
);
const lyricsConfig = useMemo(() => { const lyricsConfig = useMemo(() => {
const resolvedObjective = shouldUseOtherObjective const resolvedObjective = shouldUseOtherObjective
@@ -428,7 +423,7 @@ const CreateLyricsWithAi = () => {
<MusicLandHeader <MusicLandHeader
onPressBack={onPressBack} onPressBack={onPressBack}
progress={progress} progress={progress}
logo={icons.musicLandWriting} // logo={icons.musicLandWriting}
/> />
<View <View
style={{ style={{
@@ -437,9 +432,7 @@ const CreateLyricsWithAi = () => {
gap: responsiveHeight(5), gap: responsiveHeight(5),
}} }}
> >
<View <View style={{ flex: 1 }}>
style={{ flex: 1 }}
>
{currentStep ? ( {currentStep ? (
<View <View
key={currentStep.key} key={currentStep.key}
+5 -21
View File
@@ -251,27 +251,11 @@ const PouchReady = () => {
}; };
const playbackStage = getStageAction("director", projectForStage); const playbackStage = getStageAction("director", projectForStage);
await setLoading(false); await setLoading(false);
alert( const targetRoute = playbackStage?.route || Routes.Playback;
"Playback prêt", const params = playbackStage?.params || {
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.", project: projectForStage,
[ };
{ navigate(targetRoute, params);
text: "Retour à l'accueil",
style: "cancel",
onPress: () => navigate(Routes.Home),
},
{
text: "Continuer avec Theo",
onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback;
const params = playbackStage?.params || {
project: projectForStage,
};
navigate(targetRoute, params);
},
},
],
);
} catch (e) { } catch (e) {
console.log("PouchReady: unable to validate cover", e?.message); console.log("PouchReady: unable to validate cover", e?.message);
} finally { } finally {
+3 -21
View File
@@ -3,7 +3,6 @@ import React, { useCallback, useMemo, useState } from "react";
import { Pressable, Text, View } from "react-native"; import { Pressable, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background } from "../../assets"; import { background } from "../../assets";
import alert from "../../components/Alert";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import { isWeb } from "../../hooks/useLayoutType"; import { isWeb } from "../../hooks/useLayoutType";
@@ -116,26 +115,9 @@ const ValidateCover = () => {
}; };
const playbackStage = getStageAction("director", projectForStage); const playbackStage = getStageAction("director", projectForStage);
await setIsLoading(false); await setIsLoading(false);
alert( const targetRoute = playbackStage?.route || Routes.Playback;
"Playback prêt", const params = playbackStage?.params || { project: projectForStage };
"Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.", navigate(targetRoute, params);
[
{
text: "Retour à l'accueil",
style: "cancel",
onPress: () => navigate(Routes.Home),
},
{
text: "Continuer avec Theo",
onPress: () => {
const targetRoute = playbackStage?.route || Routes.Playback;
const params =
playbackStage?.params || { project: projectForStage };
navigate(targetRoute, params);
},
},
]
);
return; return;
} catch (e) { } catch (e) {
console.log(e); console.log(e);