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);