player, song generation again, home and design fixes
This commit is contained in:
@@ -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"/>
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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%",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,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);
|
||||||
|
|||||||
Reference in New Issue
Block a user