From cd31d42c0c43572c8e70388f991cdbe002c663f7 Mon Sep 17 00:00:00 2001 From: Thomas Demirdjian Date: Fri, 14 Nov 2025 13:51:53 +0100 Subject: [PATCH] player, song generation again, home and design fixes --- android/app/src/main/AndroidManifest.xml | 2 +- app.json | 4 +- functions/src/music.js | 37 +++- src/components/player/GlobalAudioPlayer.js | 14 +- src/config/firebase.js | 4 +- src/providers/PlayerProvider.js | 1 + src/screens/HitParade/HitParade.js | 7 - src/screens/Home/Home.js | 122 +++++++++--- src/screens/Studio/ComposeSong.js | 26 ++- src/screens/Studio/ComposeSong.web.js | 26 ++- src/screens/Studio/SongReady.js | 174 +++++++++++++++++- src/screens/Writing/CreateLyricsWithAi.web.js | 15 +- src/screens/cover/PouchReady.js | 26 +-- src/screens/cover/ValidateCover.js | 24 +-- 14 files changed, 367 insertions(+), 115 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7fffd0f..ff5db11 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,7 +6,7 @@ - + diff --git a/app.json b/app.json index 360deb8..66cd5a8 100644 --- a/app.json +++ b/app.json @@ -73,7 +73,7 @@ { "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.", - "microphonePermission": false + "microphonePermission": "MusicLand uses the microphone so you can capture audio when recording videos and musical ideas." } ], [ @@ -101,7 +101,7 @@ "expo-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 } ], diff --git a/functions/src/music.js b/functions/src/music.js index 9750b98..01085af 100644 --- a/functions/src/music.js +++ b/functions/src/music.js @@ -18,6 +18,8 @@ const { SUNO_STATUS_PATH, } = require("../config/suno"); +const MAX_STORED_MUSIC_TRACKS = 4; + /** * Marque un projet comme échoué suite à une erreur Suno * @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 try { - const musicUrls = [p1?.url, p2?.url].filter(Boolean); - await refList.projects.doc(projectId).set( + const newMusicUrls = [p1?.url, p2?.url] + .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", musicUrls, diff --git a/src/components/player/GlobalAudioPlayer.js b/src/components/player/GlobalAudioPlayer.js index ab5545b..c62d4bf 100644 --- a/src/components/player/GlobalAudioPlayer.js +++ b/src/components/player/GlobalAudioPlayer.js @@ -13,6 +13,7 @@ import { useUser } from "../../providers/UserDataProvider"; import { gutters, Palette } from "../../styles"; import { FONT_FAMILY } from "../../styles/Fonts"; import { ensureAuthenticated } from "../../utils/authRedirect"; +import { responsiveHeight } from "react-native-responsive-dimensions"; const formatDuration = (ms) => { const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000)); @@ -103,13 +104,13 @@ const GlobalAudioPlayer = () => { { likedBy: next ? arrayUnion(currentUID) : arrayRemove(currentUID), }, - { merge: true } + { merge: true }, ); } catch (_error) { setPendingLike(null); } }, - [currentUID, effectiveIsLiked, isLikeProcessing, projectId] + [currentUID, effectiveIsLiked, isLikeProcessing, projectId], ); const insets = useSafeAreaInsets(); @@ -138,7 +139,7 @@ const GlobalAudioPlayer = () => { await resume(); } }, - [isPlaying, pause, resume] + [isPlaying, pause, resume], ); const handleOpenDetails = useCallback(() => { @@ -169,7 +170,9 @@ const GlobalAudioPlayer = () => { }); }, [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 bottomOffset = (isWeb ? gutters : 5) + TAB_BAR_HEIGHT + GAP_WITH_TAB; @@ -216,8 +219,7 @@ const GlobalAudioPlayer = () => { style={[ styles.actionButton, styles.likeButton, - (!projectId || isLikeProcessing) && - styles.disabledAction, + (!projectId || isLikeProcessing) && styles.disabledAction, ]} > { if (!__DEV__ || !instance?.useEmulator || emulatorConfigured[regionKey]) { @@ -25,7 +25,7 @@ const configureFunctionsEmulator = (instance, regionKey = "us-central1") => { } catch (error) { console.warn( `[firebase] Unable to set functions emulator for region ${regionKey}`, - error?.message + error?.message, ); } }; diff --git a/src/providers/PlayerProvider.js b/src/providers/PlayerProvider.js index fb28361..14f69ba 100644 --- a/src/providers/PlayerProvider.js +++ b/src/providers/PlayerProvider.js @@ -54,6 +54,7 @@ const HIDDEN_ROUTE_NAMES = new Set([ Routes.ChangePassword, Routes.Notifications, Routes.Language, + Routes.Payments, Routes.Login, Routes.ResetPassword, Routes.Register, diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index 6c32d98..5064ddf 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -228,13 +228,6 @@ const HitParade = () => { /> - - - - { ); + const topBar = ( + + + + ); + const journeyContent = ( <> - - - - { extraItems={moreMenuItems} /> - 5 espaces à découvrir + + {isWeb ? ( + + + 5 espaces à découvrir + + + ) : ( + + + 5 espaces à découvrir + + + )} + {stageCardsList} @@ -530,15 +561,21 @@ const Home = ({ navigation, route }) => { style={styles.centerImage} /> {isWeb ? ( - {journeyContent} - ) : ( - + + {topBar} {journeyContent} - + + ) : ( + + {topBar} + + {journeyContent} + + )} @@ -629,6 +666,7 @@ const styles = StyleSheet.create({ gap: 12, marginTop: isWeb ? 24 : 0, marginBottom: isWeb ? 8 : 16, + paddingHorizontal: isWeb ? 0 : gutters, zIndex: 10, }, projectDropDown: { @@ -636,6 +674,10 @@ const styles = StyleSheet.create({ maxWidth: 420, flexGrow: 1, }, + mobileContent: { + flex: 1, + width: "100%", + }, mobileScrollView: { flex: 1, width: "100%", @@ -645,16 +687,44 @@ const styles = StyleSheet.create({ paddingTop: 24, paddingBottom: gutters * 6, }, - subtitle: { + subtitleWrapper: { 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, color: Palette.white, textAlign: "center", textTransform: "uppercase", letterSpacing: 1, - marginBottom: 16, - marginTop: 15, }, cardsGrid: { width: "100%", diff --git a/src/screens/Studio/ComposeSong.js b/src/screens/Studio/ComposeSong.js index 56d8d64..ec380ad 100644 --- a/src/screens/Studio/ComposeSong.js +++ b/src/screens/Studio/ComposeSong.js @@ -1,4 +1,5 @@ import React, { useMemo, useRef, useState } from "react"; +import { useRoute } from "@react-navigation/native"; import { Dimensions, Modal, Text, View } from "react-native"; import SwiperFlatList from "react-native-swiper-flatlist"; import { background, icons } from "../../assets"; @@ -32,6 +33,7 @@ const ComposeSong = () => { const [selectedIndex, setSelectedIndex] = useState(0); const [progress, setProgress] = useState(18); const [containerLayout, setContainerLayout] = useState(null); + const route = useRoute(); const { selectedProjectId, selectedProject, @@ -50,6 +52,7 @@ const ComposeSong = () => { const [isConfirmVisible, setIsConfirmVisible] = useState(false); const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false); + const isRegenerationFlow = route?.params?.isRegeneration === true; const coinBalance = useMemo(() => { const value = currentUserData?.coins; @@ -216,6 +219,11 @@ const ComposeSong = () => { } }; + const handleCancelGeneration = () => { + setIsConfirmVisible(false); + navigate(Routes.SongReady); + }; + return ( { {selectedIndex !== 4 && ( - + + {selectedIndex === 3 && isRegenerationFlow && ( + + )} + + )} { const [progress, setProgress] = useState(18); const [parentLayout, setParentLayout] = useState(null); const containerWidth = parentLayout?.width || windowWidth || 1; + const route = useRoute(); const { selectedProjectId, selectedProject, @@ -53,6 +55,7 @@ const ComposeSong = () => { const [isConfirmVisible, setIsConfirmVisible] = useState(false); const [isProcessingConfirmation, setIsProcessingConfirmation] = useState(false); + const isRegenerationFlow = route?.params?.isRegeneration === true; const coinBalance = useMemo(() => { const value = currentUserData?.coins; @@ -164,6 +167,11 @@ const ComposeSong = () => { [containerWidth] ); + const handleCancelGeneration = () => { + setIsConfirmVisible(false); + navigate(Routes.SongReady); + }; + const persistMusicConfig = useCallback(async () => { try { if (!selectedProjectId) return; @@ -304,11 +312,19 @@ const ComposeSong = () => { /> {selectedIndex !== steps.length && ( - + + {selectedIndex === steps.length - 1 && isRegenerationFlow && ( + + )} + + )} { const { selectedProjectId: projectId, @@ -28,6 +35,7 @@ const SongReady = () => { updateUserData, } = useUser(); const [showValidateModal, setShowValidateModal] = useState(false); + const [showRegenerateModal, setShowRegenerateModal] = useState(false); const [musicUrls, setMusicUrls] = useState([]); const [selectedIndex, setSelectedIndex] = useState(0); const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false }); @@ -323,6 +331,38 @@ const SongReady = () => { 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 ( { onPress={handleChooseTrack} disabled={!musicUrls?.length} /> + {!isWeb && ( - setShowValidateModal(false)} - onPressValidate={validateSelection} - /> + <> + setShowValidateModal(false)} + onPressValidate={validateSelection} + /> + setShowRegenerateModal(false)} + onConfirm={handleConfirmRegenerate} + /> + )} ); }; +const RegenerateModal = ({ visible, onClose, onConfirm }) => { + return ( + + + + + + + Re-générer le morceau ? + + + {`Tu vas pouvoir modifier tes choix pour re-générer ${SONG_OPTIONS_PER_GENERATION} nouveaux morceaux.`} + + + + Cette action coûte + + + + + Les crédits seront utilisés lors de l'étape de génération. + + + + + + + + + + + ); +}; + export default SongReady; diff --git a/src/screens/Writing/CreateLyricsWithAi.web.js b/src/screens/Writing/CreateLyricsWithAi.web.js index 72db5df..1205cad 100644 --- a/src/screens/Writing/CreateLyricsWithAi.web.js +++ b/src/screens/Writing/CreateLyricsWithAi.web.js @@ -147,9 +147,7 @@ const CreateLyricsWithAi = () => { Array.isArray(customStructure) && fallbackStructure?.length && (customStructure.length !== fallbackStructure.length || - customStructure.some( - (value, idx) => value !== fallbackStructure[idx] - )) + customStructure.some((value, idx) => value !== fallbackStructure[idx])) ) { setCustomStructure(fallbackStructure); } @@ -218,10 +216,7 @@ const CreateLyricsWithAi = () => { fallbackProjectStructure, ]); - const progress = useMemo( - () => 16 + selectedIndex * 9, - [selectedIndex] - ); + const progress = useMemo(() => 16 + selectedIndex * 9, [selectedIndex]); const lyricsConfig = useMemo(() => { const resolvedObjective = shouldUseOtherObjective @@ -428,7 +423,7 @@ const CreateLyricsWithAi = () => { { gap: responsiveHeight(5), }} > - + {currentStep ? ( { }; const playbackStage = getStageAction("director", projectForStage); await setLoading(false); - alert( - "Playback prêt", - "Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.", - [ - { - 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); - }, - }, - ], - ); + const targetRoute = playbackStage?.route || Routes.Playback; + const params = playbackStage?.params || { + project: projectForStage, + }; + navigate(targetRoute, params); } catch (e) { console.log("PouchReady: unable to validate cover", e?.message); } finally { diff --git a/src/screens/cover/ValidateCover.js b/src/screens/cover/ValidateCover.js index ef93323..a99b5de 100644 --- a/src/screens/cover/ValidateCover.js +++ b/src/screens/cover/ValidateCover.js @@ -3,7 +3,6 @@ import React, { useCallback, useMemo, useState } from "react"; import { Pressable, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import { background } from "../../assets"; -import alert from "../../components/Alert"; import GradientButton from "../../components/GradientButton"; import MusicLandHeader from "../../components/MusicLandHeader"; import { isWeb } from "../../hooks/useLayoutType"; @@ -116,26 +115,9 @@ const ValidateCover = () => { }; const playbackStage = getStageAction("director", projectForStage); await setIsLoading(false); - alert( - "Playback prêt", - "Super ! Ta pochette est validée. Tu peux retourner à l'accueil ou continuer avec Theo pour produire ton playback.", - [ - { - 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); - }, - }, - ] - ); + const targetRoute = playbackStage?.route || Routes.Playback; + const params = playbackStage?.params || { project: projectForStage }; + navigate(targetRoute, params); return; } catch (e) { console.log(e);