end of lyricks and misic generation
This commit is contained in:
@@ -16,7 +16,14 @@
|
|||||||
"fallbackToCacheTimeout": 0
|
"fallbackToCacheTimeout": 0
|
||||||
},
|
},
|
||||||
"packagerOpts": {
|
"packagerOpts": {
|
||||||
"sourceExts": ["js", "json", "ts", "tsx", "jsx", "vue"]
|
"sourceExts": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts",
|
||||||
|
"tsx",
|
||||||
|
"jsx",
|
||||||
|
"vue"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"splash": {
|
"splash": {
|
||||||
"image": "./assets/splash.png",
|
"image": "./assets/splash.png",
|
||||||
@@ -29,7 +36,9 @@
|
|||||||
"supportsTablet": true,
|
"supportsTablet": true,
|
||||||
"requireFullScreen": true,
|
"requireFullScreen": true,
|
||||||
"userInterfaceStyle": "dark",
|
"userInterfaceStyle": "dark",
|
||||||
"associatedDomains": ["applinks:minuit.starter"],
|
"associatedDomains": [
|
||||||
|
"applinks:minuit.starter"
|
||||||
|
],
|
||||||
"bundleIdentifier": "com.minuit.starter",
|
"bundleIdentifier": "com.minuit.starter",
|
||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"UISupportedInterfaceOrientations": [
|
"UISupportedInterfaceOrientations": [
|
||||||
@@ -40,7 +49,10 @@
|
|||||||
"UIInterfaceOrientationLandscapeLeft",
|
"UIInterfaceOrientationLandscapeLeft",
|
||||||
"UIInterfaceOrientationLandscapeRight"
|
"UIInterfaceOrientationLandscapeRight"
|
||||||
],
|
],
|
||||||
"LSApplicationQueriesSchemes": ["itms-apps", "minuit"]
|
"LSApplicationQueriesSchemes": [
|
||||||
|
"itms-apps",
|
||||||
|
"minuit"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"usesNonExemptEncryption": false
|
"usesNonExemptEncryption": false
|
||||||
@@ -57,7 +69,9 @@
|
|||||||
"backgroundColor": "#FFFFFF"
|
"backgroundColor": "#FFFFFF"
|
||||||
},
|
},
|
||||||
"package": "com.minuit.starter",
|
"package": "com.minuit.starter",
|
||||||
"permissions": ["android.permission.RECORD_AUDIO"]
|
"permissions": [
|
||||||
|
"android.permission.RECORD_AUDIO"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"web": {
|
"web": {
|
||||||
"bundler": "metro"
|
"bundler": "metro"
|
||||||
@@ -105,7 +119,8 @@
|
|||||||
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
|
"microphonePermission": "Allow $(PRODUCT_NAME) to access your microphone",
|
||||||
"recordAudioAndroid": true
|
"recordAudioAndroid": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-audio"
|
||||||
],
|
],
|
||||||
"extra": {
|
"extra": {
|
||||||
"eas": {
|
"eas": {
|
||||||
|
|||||||
Generated
+5
-4
@@ -10,7 +10,8 @@
|
|||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"firebase-admin": "^12.1.0",
|
"firebase-admin": "^12.1.0",
|
||||||
"firebase-functions": "^5.0.0",
|
"firebase-functions": "^5.0.0",
|
||||||
"genkit": "^1.16.0"
|
"genkit": "^1.16.0",
|
||||||
|
"zod": "3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.15.0",
|
||||||
@@ -4749,9 +4750,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.23.8",
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
|||||||
@@ -18,11 +18,15 @@
|
|||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"firebase-admin": "^12.1.0",
|
"firebase-admin": "^12.1.0",
|
||||||
"firebase-functions": "^5.0.0",
|
"firebase-functions": "^5.0.0",
|
||||||
"genkit": "^1.16.0"
|
"genkit": "^1.16.0",
|
||||||
|
"zod": "3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint": "^8.15.0",
|
"eslint": "^8.15.0",
|
||||||
"eslint-config-google": "^0.14.0"
|
"eslint-config-google": "^0.14.0"
|
||||||
},
|
},
|
||||||
|
"overrides": {
|
||||||
|
"zod": "3.23.8"
|
||||||
|
},
|
||||||
"private": true
|
"private": true
|
||||||
}
|
}
|
||||||
|
|||||||
+163
-169
@@ -329,174 +329,168 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
|
|||||||
* Cette fonction est appelée par l'API Suno lorsque la génération
|
* Cette fonction est appelée par l'API Suno lorsque la génération
|
||||||
* de musique est terminée
|
* de musique est terminée
|
||||||
*/
|
*/
|
||||||
exports.sunoCallback = onRequest(
|
exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
|
||||||
{
|
try {
|
||||||
methods: ["POST"],
|
console.log("🎵 [SunoCallback] Reçu:", JSON.stringify(req.body));
|
||||||
},
|
|
||||||
async (req, res) => {
|
|
||||||
try {
|
|
||||||
console.log("🎵 Received music generation callback body:", req.body);
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
console.warn("⚠️ Méthode non autorisée:", req.method);
|
|
||||||
return res.status(405).json({ error: "Méthode non autorisée" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, msg, data } = req.body || {};
|
if (req.method !== "POST") {
|
||||||
console.log("🎵 Callback details:", { code, msg, data });
|
console.warn("⚠️ [SunoCallback] Méthode non autorisée:", req.method);
|
||||||
|
return res.status(405).json({ error: "Méthode non autorisée" });
|
||||||
// Extraire les données du callback - peut être dans data ou directement dans req.body
|
|
||||||
const callbackData = data || req.body || {};
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
taskId,
|
|
||||||
status,
|
|
||||||
audio_url: audioUrl,
|
|
||||||
video_url: videoUrl,
|
|
||||||
image_url: imageUrl,
|
|
||||||
lyric,
|
|
||||||
title,
|
|
||||||
tags,
|
|
||||||
duration,
|
|
||||||
error_message: errorMessage,
|
|
||||||
} = callbackData;
|
|
||||||
|
|
||||||
// Normaliser le statut global et le taskId
|
|
||||||
const overallStatus = status || code || callbackData?.status || null;
|
|
||||||
const overallTaskId = callbackData?.taskId || callbackData?.task_id || null;
|
|
||||||
|
|
||||||
// Déterminer les éléments piste(s) à traiter
|
|
||||||
const items = Array.isArray(callbackData?.data)
|
|
||||||
? callbackData.data
|
|
||||||
: Array.isArray(callbackData?.response?.data)
|
|
||||||
? callbackData.response.data
|
|
||||||
: Array.isArray(callbackData?.sunoData)
|
|
||||||
? callbackData.sunoData
|
|
||||||
: id || audioUrl || imageUrl || title || tags || duration
|
|
||||||
? [callbackData]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (!items.length) {
|
|
||||||
// Aucun contenu piste à mettre à jour: acquitter le callback sans erreur
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Callback reçu (aucune piste à mettre à jour)",
|
|
||||||
taskId: overallTaskId,
|
|
||||||
status: overallStatus,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrouver l'association taskId -> projectId via le champ sunoTaskId du projet
|
|
||||||
let mappedProjectId = null;
|
|
||||||
try {
|
|
||||||
if (overallTaskId) {
|
|
||||||
const projSnap = await db
|
|
||||||
.collection("projects")
|
|
||||||
.where("sunoTaskId", "==", overallTaskId)
|
|
||||||
.limit(1)
|
|
||||||
.get();
|
|
||||||
if (!projSnap.empty) {
|
|
||||||
mappedProjectId = projSnap.docs[0].id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore mapping error, we'll proceed without projectId
|
|
||||||
}
|
|
||||||
|
|
||||||
const updates = [];
|
|
||||||
|
|
||||||
for (const item of items) {
|
|
||||||
const trackId = item.id || item.musicId || item.audioId || item.audio_id;
|
|
||||||
|
|
||||||
if (!trackId) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Chercher le document correspondant dans Firestore
|
|
||||||
const musicRef = db.collection("music").doc(trackId);
|
|
||||||
const musicDoc = await musicRef.get();
|
|
||||||
|
|
||||||
const updateData = {
|
|
||||||
status: item.status || overallStatus,
|
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
||||||
};
|
|
||||||
|
|
||||||
const audioU = item.audio_url || item.audioUrl || item.streamAudioUrl;
|
|
||||||
const videoU = item.video_url || item.videoUrl;
|
|
||||||
const imageU = item.image_url || item.imageUrl;
|
|
||||||
const lyricText = item.lyric || item.prompt;
|
|
||||||
const titleText = item.title;
|
|
||||||
const tagsText = item.tags;
|
|
||||||
const durationVal = item.duration;
|
|
||||||
const errMsg = item.error_message || item.errorMessage;
|
|
||||||
|
|
||||||
if (audioU) updateData.audioUrl = audioU;
|
|
||||||
if (videoU) updateData.videoUrl = videoU;
|
|
||||||
if (imageU) updateData.imageUrl = imageU;
|
|
||||||
if (lyricText) updateData.lyrics = lyricText;
|
|
||||||
if (titleText) updateData.title = titleText;
|
|
||||||
if (tagsText) updateData.style = tagsText;
|
|
||||||
if (durationVal) updateData.duration = durationVal;
|
|
||||||
if (errMsg) updateData.errorMessage = errMsg;
|
|
||||||
if (overallTaskId) updateData.taskId = overallTaskId;
|
|
||||||
if (mappedProjectId) updateData.projectId = mappedProjectId;
|
|
||||||
|
|
||||||
if (!musicDoc.exists) {
|
|
||||||
updates.push(
|
|
||||||
musicRef
|
|
||||||
.set(
|
|
||||||
{
|
|
||||||
...updateData,
|
|
||||||
createdAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
||||||
},
|
|
||||||
{ merge: true },
|
|
||||||
)
|
|
||||||
.then(() => ({ id: trackId, ok: true }))
|
|
||||||
.catch(() => ({ id: trackId, ok: false })),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
updates.push(
|
|
||||||
musicRef
|
|
||||||
.update(updateData)
|
|
||||||
.then(() => ({ id: trackId, ok: true }))
|
|
||||||
.catch(() => ({ id: trackId, ok: false })),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await Promise.all(updates);
|
|
||||||
const updated = results.filter((r) => r.ok).map((r) => r.id);
|
|
||||||
|
|
||||||
// Mettre à jour le statut du projet si on connaît le projectId
|
|
||||||
if (
|
|
||||||
mappedProjectId &&
|
|
||||||
(overallStatus === 200 ||
|
|
||||||
overallStatus === "SUCCESS" ||
|
|
||||||
callbackData?.callbackType === "complete")
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
await db.collection("projects").doc(mappedProjectId).set(
|
|
||||||
{
|
|
||||||
musicStatus: "GENERATED",
|
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
|
||||||
},
|
|
||||||
{ merge: true },
|
|
||||||
);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
message: "Callback traité avec succès",
|
|
||||||
updated,
|
|
||||||
taskId: overallTaskId,
|
|
||||||
status: overallStatus,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("❌ Erreur lors du traitement du callback:", error);
|
|
||||||
res.status(500).json({
|
|
||||||
error: "Erreur interne du serveur",
|
|
||||||
message: error.message,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
);
|
const body = req.body || {};
|
||||||
|
const code = body.code ?? body.statusCode ?? null;
|
||||||
|
const callbackType = (body?.data?.callbackType || "").toString().toLowerCase();
|
||||||
|
const status = (body.status || body.state || callbackType)
|
||||||
|
.toString()
|
||||||
|
.toLowerCase();
|
||||||
|
const taskId =
|
||||||
|
body.taskId ||
|
||||||
|
body.task_id ||
|
||||||
|
body?.data?.taskId ||
|
||||||
|
body?.data?.task_id ||
|
||||||
|
null;
|
||||||
|
const tracks = Array.isArray(body?.data?.data)
|
||||||
|
? body.data.data
|
||||||
|
: Array.isArray(body.data)
|
||||||
|
? body.data
|
||||||
|
: [];
|
||||||
|
|
||||||
|
console.log("🎯 [SunoCallback] Détails:", {
|
||||||
|
code,
|
||||||
|
status,
|
||||||
|
callbackType,
|
||||||
|
taskId,
|
||||||
|
count: tracks.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// On n'agit que si code === 200 et status === "complete"
|
||||||
|
if (code !== 200 || status !== "complete") {
|
||||||
|
console.log("ℹ️ [SunoCallback] Callback ignoré (code/status)", {
|
||||||
|
code,
|
||||||
|
status,
|
||||||
|
});
|
||||||
|
return res.status(200).json({ success: true, ignored: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskId) {
|
||||||
|
console.warn("⚠️ [SunoCallback] taskId manquant dans le callback");
|
||||||
|
return res.status(200).json({ success: true, ignored: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Récupérer le projectId associé au taskId
|
||||||
|
let projectId = null;
|
||||||
|
try {
|
||||||
|
const projSnap = await db
|
||||||
|
.collection("projects")
|
||||||
|
.where("sunoTaskId", "==", taskId)
|
||||||
|
.limit(1)
|
||||||
|
.get();
|
||||||
|
if (!projSnap.empty) {
|
||||||
|
projectId = projSnap.docs[0].id;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ [SunoCallback] Erreur lookup project par taskId:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!projectId) {
|
||||||
|
console.warn("⚠️ [SunoCallback] Aucun projet trouvé pour", { taskId });
|
||||||
|
return res.status(200).json({ success: true, ignored: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Extraire jusqu'à 2 URLs audio
|
||||||
|
const audioUrls = tracks
|
||||||
|
.map(
|
||||||
|
(t) =>
|
||||||
|
t.audio_url ||
|
||||||
|
t.audioUrl ||
|
||||||
|
t.stream_audio_url ||
|
||||||
|
t.streamAudioUrl,
|
||||||
|
)
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2);
|
||||||
|
|
||||||
|
if (audioUrls.length < 2) {
|
||||||
|
console.warn(
|
||||||
|
"⚠️ [SunoCallback] Moins de 2 pistes audio dans le callback",
|
||||||
|
{
|
||||||
|
found: audioUrls.length,
|
||||||
|
tracksSample: tracks.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
has_audio_url: !!t.audio_url,
|
||||||
|
has_stream_audio_url: !!t.stream_audio_url,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) Télécharger et sauvegarder dans Cloud Storage + récupérer download URLs
|
||||||
|
const bucket = admin.storage().bucket();
|
||||||
|
console.log("🪣 [SunoCallback] Bucket:", bucket.name);
|
||||||
|
const saveOne = async (url, index) => {
|
||||||
|
if (!url) return null;
|
||||||
|
try {
|
||||||
|
console.log(`⬇️ [SunoCallback] Téléchargement piste ${index + 1}`);
|
||||||
|
const resp = await axios.get(url, { responseType: "arraybuffer" });
|
||||||
|
const buffer = Buffer.from(resp.data);
|
||||||
|
const path = `musics/${projectId}/sound${index + 1}.mp3`;
|
||||||
|
const token = require("crypto").randomUUID();
|
||||||
|
const file = bucket.file(path);
|
||||||
|
await file.save(buffer, {
|
||||||
|
resumable: false,
|
||||||
|
metadata: {
|
||||||
|
contentType: "audio/mpeg",
|
||||||
|
cacheControl: "public, max-age=31536000",
|
||||||
|
metadata: { firebaseStorageDownloadTokens: token },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
|
||||||
|
path,
|
||||||
|
)}?alt=media&token=${token}`;
|
||||||
|
console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl);
|
||||||
|
return { path, url: downloadUrl };
|
||||||
|
} catch (e) {
|
||||||
|
console.error(
|
||||||
|
`❌ [SunoCallback] Échec save piste ${index + 1}:`,
|
||||||
|
e.message,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const [p1, p2] = await Promise.all([
|
||||||
|
saveOne(audioUrls[0], 0),
|
||||||
|
saveOne(audioUrls[1], 1),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 4) Mettre à jour le statut du projet
|
||||||
|
try {
|
||||||
|
const musicUrls = [p1?.url, p2?.url].filter(Boolean);
|
||||||
|
await db.collection("projects").doc(projectId).set(
|
||||||
|
{
|
||||||
|
musicStatus: "GENERATED",
|
||||||
|
musicUrls,
|
||||||
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
console.log("🏷️ [SunoCallback] Projet marqué GENERATED", {
|
||||||
|
projectId,
|
||||||
|
musicUrlsCount: musicUrls.length,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("❌ [SunoCallback] Erreur maj projet:", e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
projectId,
|
||||||
|
saved: [p1, p2].filter(Boolean),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error("❌ [SunoCallback] Erreur interne:", error);
|
||||||
|
res
|
||||||
|
.status(500)
|
||||||
|
.json({ error: "Erreur interne du serveur", message: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -40,6 +40,7 @@
|
|||||||
"add": "^2.0.6",
|
"add": "^2.0.6",
|
||||||
"deprecated-react-native-prop-types": "^3.0.1",
|
"deprecated-react-native-prop-types": "^3.0.1",
|
||||||
"expo": "~52.0.47",
|
"expo": "~52.0.47",
|
||||||
|
"expo-audio": "~0.3.5",
|
||||||
"expo-av": "~15.0.2",
|
"expo-av": "~15.0.2",
|
||||||
"expo-blur": "~14.0.3",
|
"expo-blur": "~14.0.3",
|
||||||
"expo-build-properties": "~0.13.3",
|
"expo-build-properties": "~0.13.3",
|
||||||
|
|||||||
+35
-14
@@ -1,9 +1,10 @@
|
|||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { StyleSheet, Text, View } from "react-native";
|
import { StyleSheet, Text, View } from "react-native";
|
||||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||||
import Animated, {
|
import Animated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
|
runOnJS,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
import { LinearGradient } from "./LinearGradient/LinearGradient";
|
||||||
import { Palette, Style } from "../styles";
|
import { Palette, Style } from "../styles";
|
||||||
@@ -11,7 +12,7 @@ import { FONT_FAMILY } from "../styles/Fonts";
|
|||||||
|
|
||||||
const INITIAL_BOX_SIZE = 6;
|
const INITIAL_BOX_SIZE = 6;
|
||||||
|
|
||||||
export default ({ value, maxValue }) => {
|
export default ({ value, maxValue, progress, onSeek, seekEnabled = false }) => {
|
||||||
const offset = useSharedValue(0);
|
const offset = useSharedValue(0);
|
||||||
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
const boxWidth = useSharedValue(INITIAL_BOX_SIZE);
|
||||||
const [layout, setLayout] = useState(null);
|
const [layout, setLayout] = useState(null);
|
||||||
@@ -19,19 +20,39 @@ export default ({ value, maxValue }) => {
|
|||||||
const SLIDER_WIDTH = layout?.width;
|
const SLIDER_WIDTH = layout?.width;
|
||||||
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
|
const MAX_VALUE = SLIDER_WIDTH - INITIAL_BOX_SIZE;
|
||||||
|
|
||||||
const pan = Gesture.Pan().onChange((event) => {
|
const pan = Gesture.Pan()
|
||||||
offset.value =
|
.enabled(seekEnabled)
|
||||||
Math.abs(offset.value) <= MAX_VALUE
|
.onChange((event) => {
|
||||||
? offset.value + event.changeX <= 0
|
offset.value =
|
||||||
? 0
|
Math.abs(offset.value) <= MAX_VALUE
|
||||||
: offset.value + event.changeX >= MAX_VALUE
|
? offset.value + event.changeX <= 0
|
||||||
? MAX_VALUE
|
? 0
|
||||||
: offset.value + event.changeX
|
: offset.value + event.changeX >= MAX_VALUE
|
||||||
: offset.value;
|
? MAX_VALUE
|
||||||
|
: offset.value + event.changeX
|
||||||
|
: offset.value;
|
||||||
|
|
||||||
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
const newWidth = INITIAL_BOX_SIZE + offset.value;
|
||||||
boxWidth.value = newWidth;
|
boxWidth.value = newWidth;
|
||||||
});
|
})
|
||||||
|
.onEnd(() => {
|
||||||
|
if (!seekEnabled || !onSeek || !MAX_VALUE) return;
|
||||||
|
const ratio = MAX_VALUE > 0 ? Math.min(1, Math.max(0, offset.value / MAX_VALUE)) : 0;
|
||||||
|
// Reanimated -> JS thread bridge
|
||||||
|
runOnJS(onSeek)(ratio);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reflect external progress into the slider UI
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof progress === "number" && layout?.width) {
|
||||||
|
const max = layout.width - INITIAL_BOX_SIZE;
|
||||||
|
const clamped = Math.max(0, Math.min(1, progress));
|
||||||
|
const newOffset = clamped * max;
|
||||||
|
offset.value = newOffset;
|
||||||
|
boxWidth.value = INITIAL_BOX_SIZE + newOffset;
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [progress, layout?.width]);
|
||||||
|
|
||||||
const boxStyle = useAnimatedStyle(() => {
|
const boxStyle = useAnimatedStyle(() => {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -62,11 +62,18 @@ const ComposeSong = () => {
|
|||||||
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
}, [selectedIndex, genres, voice, instruments, rhythm]);
|
||||||
|
|
||||||
const musicConfig = useMemo(() => {
|
const musicConfig = useMemo(() => {
|
||||||
const lyricsArr = [];
|
let lyricsArr = [];
|
||||||
const c = project?.lyrics?.couplet;
|
if (Array.isArray(project?.lyrics)) {
|
||||||
const r = project?.lyrics?.refrain;
|
lyricsArr = project.lyrics.map((s) => ({
|
||||||
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
type: (s?.type || "").toLowerCase(),
|
||||||
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
lyrics: s?.lyrics || "",
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const c = project?.lyrics?.couplet;
|
||||||
|
const r = project?.lyrics?.refrain;
|
||||||
|
if (c) lyricsArr.push({ type: "couplet", lyrics: c });
|
||||||
|
if (r) lyricsArr.push({ type: "refrain", lyrics: r });
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
title: project?.title || "",
|
title: project?.title || "",
|
||||||
lyrics: lyricsArr,
|
lyrics: lyricsArr,
|
||||||
|
|||||||
@@ -119,8 +119,15 @@ const CreatingSong = ({ active, config }) => {
|
|||||||
{
|
{
|
||||||
sunoTaskId: taskId,
|
sunoTaskId: taskId,
|
||||||
musicStatus: "GENERATING",
|
musicStatus: "GENERATING",
|
||||||
generationStartAt:
|
musicConfig: {
|
||||||
firebase.firestore.FieldValue.serverTimestamp(),
|
title: config?.title || "",
|
||||||
|
lyrics: config?.lyrics || [],
|
||||||
|
genres: config?.genres || [],
|
||||||
|
voice: config?.voice || "",
|
||||||
|
instruments: config?.instruments || [],
|
||||||
|
tempo: config?.tempo || "",
|
||||||
|
},
|
||||||
|
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||||
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||||
},
|
},
|
||||||
{ merge: true }
|
{ merge: true }
|
||||||
@@ -221,12 +228,19 @@ const CreatingSong = ({ active, config }) => {
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Découvrir ma musique"
|
title={
|
||||||
|
musicStatus === "GENERATING"
|
||||||
|
? "Création en cours..."
|
||||||
|
: "Découvrir ma musique"
|
||||||
|
}
|
||||||
|
disabled={musicStatus === "GENERATING"}
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
width: "80%",
|
width: "80%",
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
}}
|
}}
|
||||||
onPress={() => navigate(Routes.SongReady, { result })}
|
onPress={() =>
|
||||||
|
navigate(Routes.SongReady, { projectId: config?.projectId })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
|
|||||||
+240
-41
@@ -1,22 +1,178 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { background, icons } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import Slider from "../../components/Slider";
|
import Slider from "../../components/Slider";
|
||||||
import { Image, Platform, Pressable, View } from "react-native";
|
import { Image, Platform, Pressable, View, Text } from "react-native";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
import { BlurView } from "expo-blur";
|
import { BlurView } from "expo-blur";
|
||||||
import { Style } from "../../styles";
|
import { Style } from "../../styles";
|
||||||
import { responsiveWidth } from "react-native-responsive-dimensions";
|
|
||||||
import { gutters, size } from "../../styles/Style";
|
import { gutters, size } from "../../styles/Style";
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import ValidateModal from "../../components/modal/ValidateModal";
|
import ValidateModal from "../../components/modal/ValidateModal";
|
||||||
|
import { useRoute } from "@react-navigation/core";
|
||||||
|
import firebase from "../../config/firebase";
|
||||||
|
import { useAudioPlayer } from "expo-audio";
|
||||||
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
|
|
||||||
const SongReady = () => {
|
const SongReady = () => {
|
||||||
|
const params = useRoute().params || {};
|
||||||
|
const projectId = params?.projectId;
|
||||||
const [showValidateModal, setShowValidateModal] = useState(false);
|
const [showValidateModal, setShowValidateModal] = useState(false);
|
||||||
|
const [musicUrls, setMusicUrls] = useState([]);
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false });
|
||||||
|
const [progressInfo, setProgressInfo] = useState({
|
||||||
|
0: { pos: 0, dur: 0 },
|
||||||
|
1: { pos: 0, dur: 0 },
|
||||||
|
});
|
||||||
|
const player0 = useAudioPlayer(
|
||||||
|
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
|
||||||
|
);
|
||||||
|
const player1 = useAudioPlayer(
|
||||||
|
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Charger les URLs depuis le document projet
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projectId) return;
|
||||||
|
const unsub = firebase
|
||||||
|
.firestore()
|
||||||
|
.collection("projects")
|
||||||
|
.doc(projectId)
|
||||||
|
.onSnapshot((doc) => {
|
||||||
|
const data = doc.data() || {};
|
||||||
|
const urls = Array.isArray(data?.musicUrls)
|
||||||
|
? data.musicUrls.slice(0, 2)
|
||||||
|
: [];
|
||||||
|
setMusicUrls(urls);
|
||||||
|
});
|
||||||
|
return () => unsub?.();
|
||||||
|
}, [projectId]);
|
||||||
|
|
||||||
|
// Sync progression depuis les players
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => {
|
||||||
|
const d0 = (player0?.duration || 0) * 1000;
|
||||||
|
const p0 = (player0?.currentTime || 0) * 1000;
|
||||||
|
const d1 = (player1?.duration || 0) * 1000;
|
||||||
|
const p1 = (player1?.currentTime || 0) * 1000;
|
||||||
|
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
|
||||||
|
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
|
||||||
|
}, 300);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, [player0, player1]);
|
||||||
|
|
||||||
|
const togglePlay = async (idx) => {
|
||||||
|
const url = musicUrls[idx];
|
||||||
|
if (!url) return;
|
||||||
|
try {
|
||||||
|
// Pause l'autre piste si elle joue
|
||||||
|
const other = idx === 0 ? 1 : 0;
|
||||||
|
if (isPlaying[other]) {
|
||||||
|
if (other === 0) await player0?.pause?.();
|
||||||
|
else await player1?.pause?.();
|
||||||
|
setIsPlaying((p) => ({ ...p, [other]: false }));
|
||||||
|
}
|
||||||
|
const player = idx === 0 ? player0 : player1;
|
||||||
|
if (!player) return;
|
||||||
|
if (player.playing) {
|
||||||
|
await player.pause?.();
|
||||||
|
setIsPlaying((p) => ({ ...p, [idx]: false }));
|
||||||
|
} else {
|
||||||
|
await player.play?.();
|
||||||
|
setIsPlaying((p) => ({ ...p, [idx]: true }));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Audio error", e?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fmt = (ms) => {
|
||||||
|
const total = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||||
|
const m = Math.floor(total / 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(1, "0");
|
||||||
|
const s = (total % 60).toString().padStart(2, "0");
|
||||||
|
return `${m}:${s}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSeek = async (idx, ratio) => {
|
||||||
|
try {
|
||||||
|
const info = progressInfo[idx] || {};
|
||||||
|
const dur = info.dur || 0;
|
||||||
|
const pos = Math.floor(dur * ratio);
|
||||||
|
const player = idx === 0 ? player0 : player1;
|
||||||
|
if (player && dur > 0) {
|
||||||
|
await player.seekTo?.(Math.floor((pos || 0) / 1000));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Seek error", e?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSelection = async () => {
|
||||||
|
try {
|
||||||
|
const url = musicUrls[selectedIndex];
|
||||||
|
if (!projectId || !url) return;
|
||||||
|
await firebase
|
||||||
|
.firestore()
|
||||||
|
.collection("projects")
|
||||||
|
.doc(projectId)
|
||||||
|
.set(
|
||||||
|
{
|
||||||
|
song: { index: selectedIndex, url },
|
||||||
|
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
navigate(Routes.PouchReady);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Validate error", e?.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onPressRegenerate = async () => {
|
||||||
|
try {
|
||||||
|
if (!projectId) return goBack();
|
||||||
|
const doc = await firebase
|
||||||
|
.firestore()
|
||||||
|
.collection("projects")
|
||||||
|
.doc(projectId)
|
||||||
|
.get();
|
||||||
|
const project = doc.data() || {};
|
||||||
|
const musicConfig = project?.musicConfig || {};
|
||||||
|
|
||||||
|
const callable = firebase
|
||||||
|
.functions()
|
||||||
|
.httpsCallable("music-generateMusic");
|
||||||
|
const { data } = await callable({
|
||||||
|
...musicConfig,
|
||||||
|
projectId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const taskId =
|
||||||
|
data?.response?.data?.taskId || data?.response?.data?.task_id;
|
||||||
|
if (taskId) {
|
||||||
|
await firebase.firestore().collection("projects").doc(projectId).set(
|
||||||
|
{
|
||||||
|
sunoTaskId: taskId,
|
||||||
|
musicStatus: "GENERATING",
|
||||||
|
generationStartAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||||
|
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
|
||||||
|
},
|
||||||
|
{ merge: true },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Regenerate error", e?.message);
|
||||||
|
} finally {
|
||||||
|
goBack();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
<Page headerType="NONE" backgroundImg={background.studioBG2}>
|
||||||
@@ -25,38 +181,84 @@ const SongReady = () => {
|
|||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
title="Ta chanson est prête !"
|
title="Ta chanson est prête !"
|
||||||
subTitle="Qu’en penses-tu ?"
|
subTitle="Qu’en penses-tu ?"
|
||||||
/>
|
containerStyle={{
|
||||||
<View
|
marginBottom: responsiveHeight(2),
|
||||||
style={{
|
|
||||||
...size({ size: responsiveWidth(80) }),
|
|
||||||
alignSelf: "center",
|
|
||||||
borderRadius: 1000,
|
|
||||||
overflow: "hidden",
|
|
||||||
marginTop: 16,
|
|
||||||
}}
|
}}
|
||||||
>
|
/>
|
||||||
<BlurView
|
<View style={{ gap: 16 }}>
|
||||||
intensity={40}
|
{[0, 1].map((idx) => (
|
||||||
tint="dark"
|
<BlurView
|
||||||
style={{ ...Style.containerCenter, flex: 1 }}
|
key={idx}
|
||||||
experimentalBlurMethod={
|
intensity={40}
|
||||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
tint="dark"
|
||||||
}
|
style={{
|
||||||
>
|
borderRadius: 16,
|
||||||
<Image source={icons.disk} />
|
overflow: "hidden",
|
||||||
</BlurView>
|
padding: 12,
|
||||||
</View>
|
backgroundColor: "#FFFFFF0A",
|
||||||
<View style={{ marginTop: 49 }}>
|
}}
|
||||||
<Slider value="0" maxValue="2:11" />
|
experimentalBlurMethod={
|
||||||
<Pressable
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||||
style={{
|
}
|
||||||
alignSelf: "center",
|
>
|
||||||
...size({ size: 48 }),
|
<View
|
||||||
...Style.containerCenter,
|
style={{ flexDirection: "row", alignItems: "center", gap: 12 }}
|
||||||
}}
|
>
|
||||||
>
|
<Pressable
|
||||||
<Image source={icons.play} />
|
onPress={() => togglePlay(idx)}
|
||||||
</Pressable>
|
style={{ ...Style.containerCenter, ...size({ size: 48 }) }}
|
||||||
|
>
|
||||||
|
<Image source={isPlaying[idx] ? icons.pause : icons.play} />
|
||||||
|
</Pressable>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: "white",
|
||||||
|
marginBottom: responsiveHeight(1),
|
||||||
|
}}
|
||||||
|
>{`Morceau ${idx + 1}`}</Text>
|
||||||
|
<Slider
|
||||||
|
value={fmt(progressInfo[idx]?.pos)}
|
||||||
|
maxValue={fmt(progressInfo[idx]?.dur)}
|
||||||
|
progress={
|
||||||
|
progressInfo[idx]?.dur
|
||||||
|
? (progressInfo[idx].pos || 0) / progressInfo[idx].dur
|
||||||
|
: 0
|
||||||
|
}
|
||||||
|
seekEnabled={!!musicUrls[idx]}
|
||||||
|
onSeek={(ratio) => onSeek(idx, ratio)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setSelectedIndex(idx)}
|
||||||
|
style={{ ...Style.containerCenter, ...size({ size: 24 }) }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
borderRadius: 9,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: "white",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedIndex === idx && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: "white",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
</BlurView>
|
||||||
|
))}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
@@ -70,21 +272,18 @@ const SongReady = () => {
|
|||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
title="Regénérer"
|
title="Regénérer"
|
||||||
icon={icons.stars}
|
icon={icons.stars}
|
||||||
onPress={() =>
|
onPress={onPressRegenerate}
|
||||||
navigate(Routes.Regenerate, {
|
|
||||||
progress: 63,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Valider"
|
title="Choisir ce morceau"
|
||||||
onPress={() => setShowValidateModal(true)}
|
onPress={() => setShowValidateModal(true)}
|
||||||
|
disabled={!musicUrls?.length}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<ValidateModal
|
<ValidateModal
|
||||||
visible={showValidateModal}
|
visible={showValidateModal}
|
||||||
onClose={() => setShowValidateModal(false)}
|
onClose={() => setShowValidateModal(false)}
|
||||||
onPressValidate={() => navigate(Routes.PouchReady)}
|
onPressValidate={validateSelection}
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { View, Text, ScrollView, Pressable } from "react-native";
|
import { View, Text, ScrollView, Pressable, Alert } from "react-native";
|
||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { useState } from "react";
|
||||||
import Page from "../../layouts/Page";
|
import Page from "../../layouts/Page";
|
||||||
import { background } from "../../assets";
|
import { background } from "../../assets";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
@@ -9,11 +9,11 @@ import { gutters } from "../../styles";
|
|||||||
import firebase from "../../config/firebase";
|
import firebase from "../../config/firebase";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||||
|
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
|
|
||||||
const Studio = () => {
|
const Studio = () => {
|
||||||
const { setIsLoading } = useMinuit();
|
const { setIsLoading } = useMinuit();
|
||||||
const [selectedId, setSelectedId] = useState(null);
|
const [selected, setSelected] = useState(null);
|
||||||
const isDisabled = useMemo(() => !selectedId, [selectedId]);
|
|
||||||
|
|
||||||
const user = firebase.auth().currentUser;
|
const user = firebase.auth().currentUser;
|
||||||
const { data: projects } = useDataFromRef({
|
const { data: projects } = useDataFromRef({
|
||||||
@@ -131,14 +131,11 @@ const Studio = () => {
|
|||||||
>
|
>
|
||||||
Derniers projets générés
|
Derniers projets générés
|
||||||
</Text>
|
</Text>
|
||||||
<ScrollView
|
<ScrollView contentContainerStyle={{ gap: 10, paddingRight: 6 }}>
|
||||||
style={{ maxHeight: 260 }}
|
|
||||||
contentContainerStyle={{ gap: 10, paddingRight: 6 }}
|
|
||||||
>
|
|
||||||
{projects.map((p) => {
|
{projects.map((p) => {
|
||||||
const couplet = p?.lyrics?.couplet || "";
|
const couplet = p?.lyrics?.couplet || "";
|
||||||
const preview = couplet.split("\n").slice(0, 2).join(" ");
|
const preview = couplet.split("\n").slice(0, 2).join(" ");
|
||||||
const selected = selectedId === p.id;
|
const isSelected = selected === p;
|
||||||
const createdAt = p?.createdAt?.toDate
|
const createdAt = p?.createdAt?.toDate
|
||||||
? p.createdAt.toDate()
|
? p.createdAt.toDate()
|
||||||
: p?.createdAt
|
: p?.createdAt
|
||||||
@@ -151,13 +148,13 @@ const Studio = () => {
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
key={p.id}
|
key={p.id}
|
||||||
onPress={() => setSelectedId(p.id)}
|
onPress={() => setSelected(p)}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "#0F0C1933",
|
backgroundColor: "#0F0C1933",
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
padding: 12,
|
padding: 12,
|
||||||
borderWidth: selected ? 2 : 0,
|
borderWidth: isSelected ? 2 : 0,
|
||||||
borderColor: selected ? "#F94697" : "transparent",
|
borderColor: isSelected ? "#F94697" : "transparent",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
@@ -197,9 +194,29 @@ const Studio = () => {
|
|||||||
<View style={{ justifyContent: "flex-end" }}>
|
<View style={{ justifyContent: "flex-end" }}>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Commencer"
|
title="Commencer"
|
||||||
disabled={isDisabled}
|
disabled={!selected}
|
||||||
onPress={() => navigate(Routes.Compose, { projectId: selectedId })}
|
onPress={() => {
|
||||||
|
if (selected.musicStatus === "GENERATING") {
|
||||||
|
Alert.alert(
|
||||||
|
"Attention",
|
||||||
|
"La chanson est en cours de génération. Veuillez patienter.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
navigate(Routes.Compose, { projectId: selected.id });
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
{selected?.musicUrls?.length > 0 && (
|
||||||
|
<GradientButton
|
||||||
|
title="Ecouter les audios"
|
||||||
|
containerStyle={{
|
||||||
|
marginTop: responsiveHeight(2),
|
||||||
|
}}
|
||||||
|
onPress={() =>
|
||||||
|
navigate(Routes.SongReady, { projectId: selected.id })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,25 +23,35 @@ const Lyrics = ({ navigation }) => {
|
|||||||
const { setIsLoading } = useMinuit();
|
const { setIsLoading } = useMinuit();
|
||||||
|
|
||||||
const initial = useMemo(() => {
|
const initial = useMemo(() => {
|
||||||
if (!lyricsData || !lyricsData?.success) return {};
|
|
||||||
const title = lyricsData?.title || "";
|
const title = lyricsData?.title || "";
|
||||||
const sections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
|
const aiSections = Array.isArray(lyricsData?.lyrics) ? lyricsData.lyrics : [];
|
||||||
const couplets = sections
|
// Respecter l'ordre de la structure choisie si disponible
|
||||||
.filter((s) => (s?.type || "").toLowerCase().includes("couplet"))
|
const targetStructure = Array.isArray(config?.structure)
|
||||||
.map((s) => s?.lyrics || "");
|
? config.structure.map((t) => (t || "").toLowerCase())
|
||||||
const refrains = sections
|
: null;
|
||||||
.filter((s) => (s?.type || "").toLowerCase().includes("refrain"))
|
if (aiSections.length && targetStructure && aiSections.length === targetStructure.length) {
|
||||||
.map((s) => s?.lyrics || "");
|
return { title, sections: aiSections.map((s) => ({ type: (s?.type || "").toLowerCase(), lyrics: s?.lyrics || "" })) };
|
||||||
return {
|
}
|
||||||
title,
|
// Sinon, créer à partir de la structure
|
||||||
couplet: couplets.join("\n\n"),
|
if (targetStructure && targetStructure.length) {
|
||||||
refrain: refrains.join("\n\n"),
|
return {
|
||||||
};
|
title,
|
||||||
}, [lyricsData]);
|
sections: targetStructure.map((t) => ({ type: t, lyrics: "" })),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Fallback vide
|
||||||
|
return { title, sections: [] };
|
||||||
|
}, [lyricsData, config]);
|
||||||
|
|
||||||
const [titleValue, setTitleValue] = useState(initial.title || "");
|
const [titleValue, setTitleValue] = useState(initial.title || "");
|
||||||
const [coupletValue, setCoupletValue] = useState(initial.couplet || "");
|
const [sections, setSections] = useState(initial.sections || []);
|
||||||
const [refrainValue, setRefrainValue] = useState(initial.refrain || "");
|
const setSectionAt = (index, value) => {
|
||||||
|
setSections((prev) => {
|
||||||
|
const next = [...prev];
|
||||||
|
if (next[index]) next[index] = { ...next[index], lyrics: value };
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente
|
// Plus d'appel direct à la cloud function ici: on retourne à l'étape précédente
|
||||||
const regenerate = useCallback(() => {
|
const regenerate = useCallback(() => {
|
||||||
@@ -70,10 +80,10 @@ const Lyrics = ({ navigation }) => {
|
|||||||
const user = firebase.auth().currentUser;
|
const user = firebase.auth().currentUser;
|
||||||
const payload = {
|
const payload = {
|
||||||
title: titleValue?.trim() || "",
|
title: titleValue?.trim() || "",
|
||||||
lyrics: {
|
lyrics: (sections || []).map((s) => ({
|
||||||
couplet: coupletValue || "",
|
type: (s?.type || "").toLowerCase(),
|
||||||
refrain: refrainValue || "",
|
lyrics: s?.lyrics || "",
|
||||||
},
|
})),
|
||||||
config: sanitize(config),
|
config: sanitize(config),
|
||||||
selections: sanitize(selections),
|
selections: sanitize(selections),
|
||||||
userId: user ? user.uid : null,
|
userId: user ? user.uid : null,
|
||||||
@@ -88,14 +98,7 @@ const Lyrics = ({ navigation }) => {
|
|||||||
} finally {
|
} finally {
|
||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [
|
}, [titleValue, sections, config, selections, setIsLoading]);
|
||||||
titleValue,
|
|
||||||
coupletValue,
|
|
||||||
refrainValue,
|
|
||||||
config,
|
|
||||||
selections,
|
|
||||||
setIsLoading,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE">
|
<Page headerType="NONE">
|
||||||
@@ -124,40 +127,25 @@ const Lyrics = ({ navigation }) => {
|
|||||||
value={titleValue}
|
value={titleValue}
|
||||||
setValue={setTitleValue}
|
setValue={setTitleValue}
|
||||||
/>
|
/>
|
||||||
<View
|
{sections.map((s, idx) => {
|
||||||
style={{
|
// Calculer l'index humain par type
|
||||||
height: 45,
|
const type = (s?.type || "").toLowerCase();
|
||||||
justifyContent: "center",
|
const countBefore = sections
|
||||||
backgroundColor: "#00000080",
|
.slice(0, idx)
|
||||||
borderRadius: 14,
|
.filter((x) => (x?.type || "").toLowerCase() === type).length;
|
||||||
paddingHorizontal: 12,
|
const labelBase = type === "refrain" ? "Refrain" : "Couplet";
|
||||||
marginTop: 20,
|
const label = `${labelBase} ${countBefore + 1}`;
|
||||||
}}
|
return (
|
||||||
>
|
<CustomInput
|
||||||
<Text
|
key={idx}
|
||||||
style={{
|
label={label}
|
||||||
fontSize: 16,
|
placeholder={labelBase}
|
||||||
color: Palette.white,
|
height={type === "refrain" ? 170 : 225}
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
value={s?.lyrics || ""}
|
||||||
}}
|
setValue={(val) => setSectionAt(idx, val)}
|
||||||
>
|
/>
|
||||||
Introduction instrumentale longue
|
);
|
||||||
</Text>
|
})}
|
||||||
</View>
|
|
||||||
<CustomInput
|
|
||||||
label="Couplet"
|
|
||||||
placeholder="Couplet"
|
|
||||||
height={225}
|
|
||||||
value={coupletValue}
|
|
||||||
setValue={setCoupletValue}
|
|
||||||
/>
|
|
||||||
<CustomInput
|
|
||||||
label="Refrain"
|
|
||||||
placeholder="Refrain"
|
|
||||||
height={170}
|
|
||||||
value={refrainValue}
|
|
||||||
setValue={setRefrainValue}
|
|
||||||
/>
|
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ItemContainer>
|
</ItemContainer>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -5031,6 +5031,11 @@ expo-asset@~11.0.5:
|
|||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
md5-file "^3.2.3"
|
md5-file "^3.2.3"
|
||||||
|
|
||||||
|
expo-audio@~0.3.5:
|
||||||
|
version "0.3.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/expo-audio/-/expo-audio-0.3.5.tgz#1f151ec9919e163019f1aa76a117657e0ea6b613"
|
||||||
|
integrity sha512-gzpDH3vZI1FDL1Q8pXryACtNIW+idZ/zIZ8WqdTRzJuzxucazrG2gLXUS2ngcXQBn09Jyz4RUnU10Tu2N7/Hgg==
|
||||||
|
|
||||||
expo-av@~15.0.2:
|
expo-av@~15.0.2:
|
||||||
version "15.0.2"
|
version "15.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-15.0.2.tgz#65bb08658a7fe3a67aa47da614abbfae9adb684e"
|
resolved "https://registry.yarnpkg.com/expo-av/-/expo-av-15.0.2.tgz#65bb08658a7fe3a67aa47da614abbfae9adb684e"
|
||||||
|
|||||||
Reference in New Issue
Block a user