generate two covers
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
exports.generatePicturePrompt = (project = {}) => {
|
exports.generatePicturePrompt = (project = {}) => {
|
||||||
const { title = "", lyrics: lyricsRaw, musicConfig = {} } = project || {};
|
const {
|
||||||
|
title = "",
|
||||||
|
lyrics: lyricsRaw,
|
||||||
|
musicConfig = {},
|
||||||
|
coverStyle: coverStyleRaw = "",
|
||||||
|
} = project || {};
|
||||||
|
|
||||||
const {
|
const {
|
||||||
genres = [],
|
genres = [],
|
||||||
@@ -44,28 +49,33 @@ exports.generatePicturePrompt = (project = {}) => {
|
|||||||
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
|
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
|
||||||
: "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)";
|
: "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)";
|
||||||
|
|
||||||
|
const coverStyle = String(coverStyleRaw || "").trim();
|
||||||
const tagsLine = tags.join(" ; ") || "Non spécifié";
|
const tagsLine = tags.join(" ; ") || "Non spécifié";
|
||||||
const lyricsLine = lyricsSample || "Pas d'extraits fournis";
|
const lyricsLine = lyricsSample || "Pas d'extraits fournis";
|
||||||
|
const styleLine = coverStyle
|
||||||
|
? `${coverStyle}. ${paletteHint}`
|
||||||
|
: `${paletteHint}. Style libre, artistique et lumineux.`;
|
||||||
|
|
||||||
const prompt = `
|
const prompt = `<BRIEF>
|
||||||
<BRIEF>
|
<OBJECTIF>Créer une pochette d'album en adéquation avec les paroles de la musique</OBJECTIF>
|
||||||
<OBJECTIF>Créer une pochette lumineuse.</OBJECTIF>
|
|
||||||
<TITRE>${title || "Sans titre"}</TITRE>
|
<TITRE>${title || "Sans titre"}</TITRE>
|
||||||
<STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL>
|
<STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL>
|
||||||
<EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES>
|
<EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES>
|
||||||
</BRIEF>
|
</BRIEF>
|
||||||
|
|
||||||
<CONTEXTES_VISUELS>
|
<CONTEXTES_VISUELS>
|
||||||
<FORMAT>Image carrée 1024x1024 pixels.</FORMAT>
|
<FORMAT>Image carrée 1024x1024 pixels, résolution haute.</FORMAT>
|
||||||
<STYLE>Abstrait, graphique, formes géométriques et dégradés.</STYLE>
|
<STYLE>${styleLine}</STYLE>
|
||||||
<COMPOSITION>Remplis toute la surface sans laisser de bordures ou zones vides.</COMPOSITION>
|
<COMPOSITION>La composition doit être dynamique et remplir toute la surface, sans laisser de bordures ni de zones vides.</COMPOSITION>
|
||||||
|
<TYPOGRAPHIE>Intègre de manière artistique et lisible le titre de la chanson ${title || "Sans titre"}. La typographie doit compléter le style abstrait et lumineux de la pochette. Priorise la lisibilité.</TYPOGRAPHIE>
|
||||||
|
<LOGO_RESERVE>Réserve un espace net et dégagé en bas à droite, occupant environ 25% de la largeur totale. Cette zone doit être légèrement plus sombre ou avoir un contraste suffisant.</LOGO_RESERVE>
|
||||||
</CONTEXTES_VISUELS>
|
</CONTEXTES_VISUELS>
|
||||||
|
|
||||||
<CONTRAINTES>
|
<CONTRAINTES>
|
||||||
<INTERDIT>Texte ou typographie.</INTERDIT>
|
<INTERDIT>Personnes, visages ou silhouettes reconnaissables.</INTERDIT>
|
||||||
<INTERDIT>Personnes ou visages.</INTERDIT>
|
<INTERDIT>Fonds blancs ou bordures délimitant l'image.</INTERDIT>
|
||||||
<INTERDIT>Fond blanc ou bordures.</INTERDIT>
|
<INTERDIT>Tout autre logo, filigrane ou texte promotionnel que le titre, l'artiste et la zone réservée pour "MusicLand Production".</INTERDIT>
|
||||||
<INTERDIT>Autre logo ou filigrane que celui prévu en bas à droite.</INTERDIT>
|
<INTERDIT>Texte illisible ou trop petit.</INTERDIT>
|
||||||
</CONTRAINTES>
|
</CONTRAINTES>
|
||||||
`.trim();
|
`.trim();
|
||||||
|
|
||||||
|
|||||||
+59
-21
@@ -75,49 +75,65 @@ async function performCoverGeneration(project) {
|
|||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
promptPreview: String(prompt).slice(0, 160),
|
promptPreview: String(prompt).slice(0, 160),
|
||||||
});
|
});
|
||||||
let coverUrl = "";
|
const baseTimestamp = Date.now();
|
||||||
const timestamp = Date.now();
|
const options = [];
|
||||||
|
|
||||||
|
for (let index = 0; index < 2; index += 1) {
|
||||||
|
const uniqueSuffix = `${baseTimestamp}-${index}`;
|
||||||
|
const storageBasePath = `users/${project.userId}/projects/${project.id}`;
|
||||||
|
const generatedPath = `${storageBasePath}/generated-${uniqueSuffix}.png`;
|
||||||
|
let generatedUrl = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Utiliser une taille fixe de 1024 pixels
|
generatedUrl = await generateImageV2(prompt, 1024, generatedPath);
|
||||||
coverUrl = await generateImageV2(
|
logger.info("🎨 [Cover] Candidate generated", {
|
||||||
prompt,
|
projectId: project.id,
|
||||||
1024,
|
candidateIndex: index,
|
||||||
`users/${project.userId}/projects/${project.id}/generated-${timestamp}.png`,
|
});
|
||||||
);
|
|
||||||
console.log("coverUrl", coverUrl);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("❌ [Cover] generateImageV2 failed", {
|
logger.error("❌ [Cover] generateImageV2 failed", {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
candidateIndex: index,
|
||||||
error: e?.message || String(e),
|
error: e?.message || String(e),
|
||||||
});
|
});
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
if (!coverUrl) {
|
|
||||||
|
if (!generatedUrl) {
|
||||||
throw new Error("Génération d'image échouée (URL vide)");
|
throw new Error("Génération d'image échouée (URL vide)");
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalCoverUrl = "";
|
let finalCoverUrl = generatedUrl;
|
||||||
try {
|
try {
|
||||||
const stampedPath = `users/${project.userId}/projects/${project.id}/cover-${timestamp}-stamped.png`;
|
const stampedPath = `${storageBasePath}/cover-${uniqueSuffix}-stamped.png`;
|
||||||
finalCoverUrl = await buildCoverWithLogo(coverUrl, stampedPath);
|
finalCoverUrl = await buildCoverWithLogo(generatedUrl, stampedPath);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("❌ [Cover] Logo overlay failed", {
|
logger.error("❌ [Cover] Logo overlay failed", {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
|
candidateIndex: index,
|
||||||
error: e?.message || String(e),
|
error: e?.message || String(e),
|
||||||
});
|
});
|
||||||
// En cas d'échec, on retient au moins la cover générée
|
|
||||||
finalCoverUrl = coverUrl;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mettre à jour le projet avec l'URL et le statut
|
options.push({
|
||||||
|
id: uniqueSuffix,
|
||||||
|
generatedUrl,
|
||||||
|
finalUrl: finalCoverUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [firstOption] = options;
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.collection("projects")
|
.collection("projects")
|
||||||
.doc(project.id)
|
.doc(project.id)
|
||||||
.set(
|
.set(
|
||||||
{
|
{
|
||||||
cover: {
|
cover: {
|
||||||
generatedBackground: coverUrl,
|
generatedBackground: firstOption?.generatedUrl || null,
|
||||||
result: finalCoverUrl,
|
result: firstOption?.finalUrl || null,
|
||||||
|
selectedOptionId: firstOption?.id || null,
|
||||||
|
options,
|
||||||
},
|
},
|
||||||
coverStatus: "GENERATED",
|
coverStatus: "GENERATED",
|
||||||
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
@@ -127,11 +143,11 @@ async function performCoverGeneration(project) {
|
|||||||
|
|
||||||
logger.info("✅ [Cover] Saved", {
|
logger.info("✅ [Cover] Saved", {
|
||||||
projectId: project.id,
|
projectId: project.id,
|
||||||
coverUrl,
|
optionsCount: options.length,
|
||||||
finalCoverUrl,
|
|
||||||
ms: Date.now() - t0,
|
ms: Date.now() - t0,
|
||||||
});
|
});
|
||||||
return finalCoverUrl;
|
|
||||||
|
return firstOption?.finalUrl || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Firestore trigger: création d'une tâche de génération de cover
|
// Firestore trigger: création d'une tâche de génération de cover
|
||||||
@@ -185,12 +201,34 @@ exports.onTaskCreateGenerateCover = onDocumentCreated(
|
|||||||
}
|
}
|
||||||
project.id = projectId;
|
project.id = projectId;
|
||||||
|
|
||||||
|
const existingOptionsCount = Array.isArray(project?.cover?.options)
|
||||||
|
? project.cover.options.length
|
||||||
|
: 0;
|
||||||
|
|
||||||
logger.info("🔎 [Task] Project loaded", {
|
logger.info("🔎 [Task] Project loaded", {
|
||||||
projectId,
|
projectId,
|
||||||
hasGeneratedBackground: !!project?.cover?.generatedBackground,
|
hasGeneratedBackground: !!project?.cover?.generatedBackground,
|
||||||
hasUserForeground: !!project?.cover?.userForeground,
|
hasUserForeground: !!project?.cover?.userForeground,
|
||||||
|
existingOptionsCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (existingOptionsCount > 0) {
|
||||||
|
logger.warn("⛔ [Task] Cover already generated, skipping", {
|
||||||
|
projectId,
|
||||||
|
existingOptionsCount,
|
||||||
|
});
|
||||||
|
await db.collection("projects").doc(projectId).update({
|
||||||
|
coverStatus: "GENERATED",
|
||||||
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
await event.data.ref.update({
|
||||||
|
status: "CANCELLED",
|
||||||
|
error: "Cover already generated",
|
||||||
|
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (type === "cover") {
|
if (type === "cover") {
|
||||||
logger.info("🎨 [Task] Start cover generation", { projectId });
|
logger.info("🎨 [Task] Start cover generation", { projectId });
|
||||||
coverUrl = await performCoverGeneration(project);
|
coverUrl = await performCoverGeneration(project);
|
||||||
|
|||||||
@@ -125,6 +125,7 @@ const Studio = () => {
|
|||||||
navigate(Routes.SongReady);
|
navigate(Routes.SongReady);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{!selected?.coverUrl && (
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Générer une pochette"
|
title="Générer une pochette"
|
||||||
containerStyle={{
|
containerStyle={{
|
||||||
@@ -135,6 +136,7 @@ const Studio = () => {
|
|||||||
navigate(Routes.PouchReady);
|
navigate(Routes.PouchReady);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,9 +1,23 @@
|
|||||||
import React from "react";
|
import React, {
|
||||||
import { Image, StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import {
|
||||||
|
Image,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
View,
|
||||||
|
} from "react-native";
|
||||||
import { icons } from "../../assets";
|
import { icons } from "../../assets";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import {
|
import {
|
||||||
|
arraysAreSame,
|
||||||
formatStructureLabel,
|
formatStructureLabel,
|
||||||
getSegmentMeta,
|
getSegmentMeta,
|
||||||
OPTIONAL_STRUCTURE_SEGMENTS,
|
OPTIONAL_STRUCTURE_SEGMENTS,
|
||||||
@@ -17,17 +31,16 @@ const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
|
|||||||
|
|
||||||
const randomSuffix = () => Math.random().toString(36).slice(2, 8);
|
const randomSuffix = () => Math.random().toString(36).slice(2, 8);
|
||||||
|
|
||||||
const buildSortableItems = (values = []) => {
|
const createSortableItem = (type, index = 0) => ({
|
||||||
const counters = {};
|
id: `${type}-${index}-${randomSuffix()}`,
|
||||||
return sanitizeStructureList(values).map((segment, index) => {
|
type,
|
||||||
counters[segment] = (counters[segment] || 0) + 1;
|
value: type,
|
||||||
return {
|
|
||||||
id: `${segment}-${index}-${randomSuffix()}`,
|
|
||||||
type: segment,
|
|
||||||
value: segment,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
const buildSortableItems = (values = []) =>
|
||||||
|
sanitizeStructureList(values).map((type, index) =>
|
||||||
|
createSortableItem(type, index)
|
||||||
|
);
|
||||||
|
|
||||||
const reorder = (list, from, to) => {
|
const reorder = (list, from, to) => {
|
||||||
if (from === to) return list;
|
if (from === to) return list;
|
||||||
@@ -40,87 +53,114 @@ const reorder = (list, from, to) => {
|
|||||||
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
||||||
|
|
||||||
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
||||||
const sanitized = React.useMemo(
|
const sanitizedBase = useMemo(
|
||||||
() => sanitizeStructureList(baseStructure),
|
() => sanitizeStructureList(baseStructure),
|
||||||
[baseStructure]
|
[baseStructure]
|
||||||
);
|
);
|
||||||
const [items, setItems] = React.useState(() => buildSortableItems(sanitized));
|
const [sortableItems, setSortableItems] = useState(() =>
|
||||||
const [activeItemId, setActiveItemId] = React.useState(null);
|
buildSortableItems(sanitizedBase)
|
||||||
const containerRef = React.useRef(null);
|
);
|
||||||
const dragStateRef = React.useRef({
|
const [activeItemId, setActiveItemId] = useState(null);
|
||||||
|
const containerRef = useRef(null);
|
||||||
|
const dragStateRef = useRef({
|
||||||
itemId: null,
|
itemId: null,
|
||||||
containerTop: 0,
|
containerTop: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
React.useEffect(() => {
|
const extractValues = useCallback(
|
||||||
console.debug("[CustomizeSongStructure.web] sanitized", sanitized);
|
(items) =>
|
||||||
setItems(buildSortableItems(sanitized));
|
(items || []).map((item) => item?.type || item?.value).filter(Boolean),
|
||||||
dragStateRef.current = { itemId: null, containerTop: 0 };
|
[]
|
||||||
setActiveItemId(null);
|
|
||||||
}, [sanitized]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
console.debug("[CustomizeSongStructure.web] items", items);
|
|
||||||
onChange?.(
|
|
||||||
items.map((item) => (item?.type || item?.value)).filter(Boolean)
|
|
||||||
);
|
);
|
||||||
}, [items, onChange]);
|
|
||||||
|
|
||||||
const labeledItems = React.useMemo(() => {
|
useEffect(() => {
|
||||||
|
setSortableItems((prev) => {
|
||||||
|
const currentValues = extractValues(prev);
|
||||||
|
if (arraysAreSame(currentValues, sanitizedBase)) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return buildSortableItems(sanitizedBase);
|
||||||
|
});
|
||||||
|
}, [sanitizedBase, extractValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onChange?.(extractValues(sortableItems));
|
||||||
|
}, [sortableItems, extractValues, onChange]);
|
||||||
|
|
||||||
|
const labeledItems = useMemo(() => {
|
||||||
const counts = {};
|
const counts = {};
|
||||||
return items.map((item) => {
|
return sortableItems.map((item) => {
|
||||||
const type = item?.type || item?.value;
|
const baseType = item.type || item.value;
|
||||||
counts[type] = (counts[type] || 0) + 1;
|
const nextCount = (counts[baseType] || 0) + 1;
|
||||||
|
counts[baseType] = nextCount;
|
||||||
return {
|
return {
|
||||||
...item,
|
...item,
|
||||||
type,
|
type: baseType,
|
||||||
value: type,
|
value: baseType,
|
||||||
label: formatStructureLabel(type, counts[type]),
|
label: formatStructureLabel(baseType, nextCount),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}, [items]);
|
}, [sortableItems]);
|
||||||
|
|
||||||
const selectedTypes = React.useMemo(() => {
|
const optionalSegments = useMemo(
|
||||||
const set = new Set();
|
|
||||||
items.forEach((item) => {
|
|
||||||
if (item?.type) set.add(item.type);
|
|
||||||
else if (item?.value) set.add(item.value);
|
|
||||||
});
|
|
||||||
return set;
|
|
||||||
}, [items]);
|
|
||||||
|
|
||||||
const optionalSegments = React.useMemo(
|
|
||||||
() =>
|
() =>
|
||||||
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
OPTIONAL_STRUCTURE_SEGMENTS.map((type) => {
|
||||||
const meta = getSegmentMeta(type);
|
const meta = getSegmentMeta(type);
|
||||||
return { type, label: meta?.label };
|
return { type, label: meta?.label || formatStructureLabel(type, 1) };
|
||||||
}),
|
}),
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
const toggleOptionalSegment = React.useCallback((type) => {
|
const segmentCounts = useMemo(() => {
|
||||||
setItems((prev) => {
|
const counts = {};
|
||||||
const currentValues = prev.map((item) => item.type || item.value);
|
extractValues(sortableItems).forEach((type) => {
|
||||||
if (currentValues.includes(type)) {
|
counts[type] = (counts[type] || 0) + 1;
|
||||||
const filtered = currentValues.filter((value) => value !== type);
|
});
|
||||||
return buildSortableItems(filtered);
|
return counts;
|
||||||
}
|
}, [sortableItems, extractValues]);
|
||||||
|
|
||||||
|
const handleRemoveItem = useCallback(
|
||||||
|
(itemId) => {
|
||||||
|
setSortableItems((prev) => {
|
||||||
|
const remaining = prev.filter((item) => item.id !== itemId);
|
||||||
|
const sanitized = sanitizeStructureList(extractValues(remaining));
|
||||||
|
return buildSortableItems(sanitized);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[extractValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleOptionalSegmentPress = useCallback(
|
||||||
|
(type) => {
|
||||||
const meta = getSegmentMeta(type);
|
const meta = getSegmentMeta(type);
|
||||||
|
setSortableItems((prev) => {
|
||||||
|
const currentValues = extractValues(prev);
|
||||||
let nextValues = [...currentValues];
|
let nextValues = [...currentValues];
|
||||||
|
|
||||||
if (meta?.exclusiveGroup) {
|
if (meta?.exclusiveGroup) {
|
||||||
nextValues = nextValues.filter((value) => {
|
nextValues = nextValues.filter((value) => {
|
||||||
const itemMeta = getSegmentMeta(value);
|
const itemMeta = getSegmentMeta(value);
|
||||||
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
|
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
nextValues.push(type);
|
|
||||||
return buildSortableItems(nextValues);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleGrant = (index, event) => {
|
const allowsMultiple = meta?.allowMultiple !== false;
|
||||||
const item = labeledItems[index];
|
if (!allowsMultiple && nextValues.includes(type)) {
|
||||||
if (!item) return;
|
const filtered = nextValues.filter((value) => value !== type);
|
||||||
|
const sanitized = sanitizeStructureList(filtered);
|
||||||
|
return buildSortableItems(sanitized);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextValues.push(type);
|
||||||
|
const sanitized = sanitizeStructureList(nextValues);
|
||||||
|
return buildSortableItems(sanitized);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[extractValues]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleGrant = useCallback(
|
||||||
|
(itemId, event) => {
|
||||||
const rect = containerRef.current?.getBoundingClientRect();
|
const rect = containerRef.current?.getBoundingClientRect();
|
||||||
const scrollY = getScrollY();
|
const scrollY = getScrollY();
|
||||||
const nativeEvent = event.nativeEvent || {};
|
const nativeEvent = event.nativeEvent || {};
|
||||||
@@ -132,16 +172,18 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
: scrollY;
|
: scrollY;
|
||||||
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
const containerTop = rect ? rect.top + scrollY : fallbackY;
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
itemId: item.id,
|
itemId,
|
||||||
containerTop,
|
containerTop,
|
||||||
};
|
};
|
||||||
setActiveItemId(item.id);
|
setActiveItemId(itemId);
|
||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.body.style.userSelect = "none";
|
document.body.style.userSelect = "none";
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const handleMove = (event) => {
|
const handleMove = useCallback((event) => {
|
||||||
const { itemId, containerTop } = dragStateRef.current;
|
const { itemId, containerTop } = dragStateRef.current;
|
||||||
if (!itemId) return;
|
if (!itemId) return;
|
||||||
const nativeEvent = event.nativeEvent || {};
|
const nativeEvent = event.nativeEvent || {};
|
||||||
@@ -154,7 +196,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
: scrollY;
|
: scrollY;
|
||||||
const pointerY = absoluteY - containerTop;
|
const pointerY = absoluteY - containerTop;
|
||||||
|
|
||||||
setItems((prev) => {
|
setSortableItems((prev) => {
|
||||||
const currentIndex = prev.findIndex((it) => it.id === itemId);
|
const currentIndex = prev.findIndex((it) => it.id === itemId);
|
||||||
if (currentIndex === -1) return prev;
|
if (currentIndex === -1) return prev;
|
||||||
const targetIndex = clamp(
|
const targetIndex = clamp(
|
||||||
@@ -165,11 +207,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
if (targetIndex === currentIndex) return prev;
|
if (targetIndex === currentIndex) return prev;
|
||||||
return reorder(prev, currentIndex, targetIndex);
|
return reorder(prev, currentIndex, targetIndex);
|
||||||
});
|
});
|
||||||
};
|
}, []);
|
||||||
|
|
||||||
const handleRelease = () => {
|
const handleRelease = useCallback(() => {
|
||||||
setItems((prev) =>
|
setSortableItems((prev) =>
|
||||||
buildSortableItems(prev.map((item) => item.type || item.value))
|
buildSortableItems(extractValues(prev))
|
||||||
);
|
);
|
||||||
dragStateRef.current = {
|
dragStateRef.current = {
|
||||||
itemId: null,
|
itemId: null,
|
||||||
@@ -179,87 +221,95 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
if (typeof document !== "undefined") {
|
if (typeof document !== "undefined") {
|
||||||
document.body.style.userSelect = "";
|
document.body.style.userSelect = "";
|
||||||
}
|
}
|
||||||
};
|
}, [extractValues]);
|
||||||
|
|
||||||
if (!labeledItems.length) {
|
const StructureRow = ({ item }) => {
|
||||||
|
const baseType = item?.type || item?.value;
|
||||||
|
const meta = getSegmentMeta(baseType);
|
||||||
|
const canRemove = meta?.optional === true;
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
|
|
||||||
<CreateLyricsHeader
|
|
||||||
title="Personnalise la structure de ta chanson"
|
|
||||||
subTitle="Sélectionne une structure pour commencer"
|
|
||||||
/>
|
|
||||||
<View style={styles.optionsContainer}>
|
|
||||||
{optionalSegments.map((segment) => {
|
|
||||||
const selected = selectedTypes.has(segment.type);
|
|
||||||
return (
|
|
||||||
<TouchableOpacity
|
|
||||||
key={segment.type}
|
|
||||||
activeOpacity={0.85}
|
|
||||||
onPress={() => toggleOptionalSegment(segment.type)}
|
|
||||||
style={[styles.optionItem, selected && styles.selectedItem]}
|
|
||||||
>
|
|
||||||
<Text style={styles.optionText}>{segment.label}</Text>
|
|
||||||
<Image
|
|
||||||
source={selected ? icons.check : icons.add}
|
|
||||||
style={[styles.optionIcon]}
|
|
||||||
/>
|
|
||||||
</TouchableOpacity>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
|
|
||||||
<CreateLyricsHeader
|
|
||||||
title="Personnalise la structure de ta chanson"
|
|
||||||
subTitle="Glisse-dépose pour réorganiser"
|
|
||||||
/>
|
|
||||||
<View style={styles.listContainer} ref={containerRef}>
|
|
||||||
{labeledItems.map((item, index) => (
|
|
||||||
<View
|
<View
|
||||||
key={item.id}
|
|
||||||
style={[
|
style={[
|
||||||
styles.itemContainer,
|
styles.itemContainer,
|
||||||
activeItemId === item.id && styles.activeItem,
|
activeItemId === item.id && styles.activeItem,
|
||||||
]}
|
]}
|
||||||
|
>
|
||||||
|
<View style={styles.row}>
|
||||||
|
<Text style={styles.rowText}>{item.label}</Text>
|
||||||
|
<View style={styles.rowActions}>
|
||||||
|
{canRemove && (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={() => handleRemoveItem(item.id)}
|
||||||
|
style={styles.removeButton}
|
||||||
|
hitSlop={{ top: 8, right: 8, bottom: 8, left: 8 }}
|
||||||
|
>
|
||||||
|
<Image source={icons.close} style={styles.removeIcon} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
)}
|
||||||
|
<View
|
||||||
|
style={styles.handleWrapper}
|
||||||
onStartShouldSetResponder={() => true}
|
onStartShouldSetResponder={() => true}
|
||||||
onResponderGrant={(event) => handleGrant(index, event)}
|
onResponderGrant={(event) => handleGrant(item.id, event)}
|
||||||
onResponderMove={handleMove}
|
onResponderMove={handleMove}
|
||||||
onResponderRelease={handleRelease}
|
onResponderRelease={handleRelease}
|
||||||
onResponderTerminate={handleRelease}
|
onResponderTerminate={handleRelease}
|
||||||
>
|
>
|
||||||
<View style={styles.row}>
|
|
||||||
<Text style={styles.rowText}>{item.label}</Text>
|
|
||||||
<Image source={icons.dragDots} style={styles.handleIcon} />
|
<Image source={icons.dragDots} style={styles.handleIcon} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={{ flex: 1, gap: 10, marginTop: 16 }}>
|
||||||
|
<CreateLyricsHeader
|
||||||
|
title="Personnalise la structure de ta chanson"
|
||||||
|
subTitle="Réorganise par glisser-déposer"
|
||||||
|
/>
|
||||||
|
<View style={{ flex: 1 }}>
|
||||||
|
<ScrollView
|
||||||
|
contentContainerStyle={styles.scrollContent}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
<View style={styles.sectionTag}>
|
||||||
|
<Text style={styles.sectionTagText}>Structure actuelle</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.listContainer} ref={containerRef}>
|
||||||
|
{labeledItems.map((item) => (
|
||||||
|
<StructureRow key={item.id} item={item} />
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[styles.sectionTag, styles.addSectionTag]}>
|
||||||
|
<Text style={styles.sectionTagText}>Éléments à ajouter</Text>
|
||||||
|
</View>
|
||||||
<View style={styles.optionsContainer}>
|
<View style={styles.optionsContainer}>
|
||||||
{optionalSegments.map((segment) => {
|
{optionalSegments.map((segment) => {
|
||||||
const selected = selectedTypes.has(segment.type);
|
const count = segmentCounts[segment.type] || 0;
|
||||||
|
const selected = count > 0;
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
key={segment.type}
|
key={segment.type}
|
||||||
activeOpacity={0.85}
|
activeOpacity={0.85}
|
||||||
onPress={() => toggleOptionalSegment(segment.type)}
|
onPress={() => handleOptionalSegmentPress(segment.type)}
|
||||||
style={[styles.optionItem, selected && styles.selectedItem]}
|
style={[styles.optionItem, selected && styles.selectedItem]}
|
||||||
>
|
>
|
||||||
<Text style={styles.optionText}>{segment.label}</Text>
|
<Text style={styles.rowText}>{segment.label}</Text>
|
||||||
<Image
|
<View
|
||||||
source={selected ? icons.check : icons.add}
|
|
||||||
style={[
|
style={[
|
||||||
styles.optionIcon,
|
styles.countBadge,
|
||||||
selected && styles.optionIconSelected,
|
selected && styles.countBadgeActive,
|
||||||
]}
|
]}
|
||||||
/>
|
>
|
||||||
|
<Text style={styles.countBadgeText}>{count}</Text>
|
||||||
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -267,8 +317,29 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
|
|||||||
export default CustomizeSongStructure;
|
export default CustomizeSongStructure;
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
|
scrollContent: {
|
||||||
|
paddingBottom: 24,
|
||||||
|
gap: 12,
|
||||||
|
},
|
||||||
|
sectionTag: {
|
||||||
|
alignSelf: "flex-start",
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.12)",
|
||||||
|
marginTop: 12,
|
||||||
|
},
|
||||||
|
addSectionTag: {
|
||||||
|
marginTop: 24,
|
||||||
|
},
|
||||||
|
sectionTagText: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||||
|
letterSpacing: 1,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
},
|
||||||
listContainer: {
|
listContainer: {
|
||||||
flex: 1,
|
|
||||||
gap: 12,
|
gap: 12,
|
||||||
},
|
},
|
||||||
itemContainer: {
|
itemContainer: {
|
||||||
@@ -298,6 +369,25 @@ const styles = StyleSheet.create({
|
|||||||
color: Palette.white,
|
color: Palette.white,
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
},
|
},
|
||||||
|
rowActions: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
removeButton: {
|
||||||
|
padding: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
removeIcon: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
tintColor: Palette.white,
|
||||||
|
},
|
||||||
|
handleWrapper: {
|
||||||
|
padding: 6,
|
||||||
|
borderRadius: 999,
|
||||||
|
},
|
||||||
handleIcon: {
|
handleIcon: {
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 22,
|
height: 22,
|
||||||
@@ -305,8 +395,8 @@ const styles = StyleSheet.create({
|
|||||||
cursor: "grab",
|
cursor: "grab",
|
||||||
},
|
},
|
||||||
optionsContainer: {
|
optionsContainer: {
|
||||||
marginTop: 16,
|
|
||||||
gap: 12,
|
gap: 12,
|
||||||
|
marginTop: 12,
|
||||||
},
|
},
|
||||||
optionItem: {
|
optionItem: {
|
||||||
height: ROW_HEIGHT,
|
height: ROW_HEIGHT,
|
||||||
@@ -323,19 +413,25 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
},
|
},
|
||||||
optionText: {
|
|
||||||
fontSize: 16,
|
|
||||||
color: Palette.white,
|
|
||||||
fontFamily: FONT_FAMILY.InterRegular,
|
|
||||||
},
|
|
||||||
optionIcon: {
|
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
tintColor: Palette.white,
|
|
||||||
},
|
|
||||||
|
|
||||||
selectedItem: {
|
selectedItem: {
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Palette.white,
|
borderColor: Palette.white,
|
||||||
},
|
},
|
||||||
|
countBadge: {
|
||||||
|
minWidth: 28,
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: "rgba(255,255,255,0.08)",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
|
countBadgeActive: {
|
||||||
|
backgroundColor: Palette.transparentWhite,
|
||||||
|
},
|
||||||
|
countBadgeText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: Palette.white,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ export const ChooseCoverType = () => {
|
|||||||
const { setIsLoading, setTooltip } = useMinuit();
|
const { setIsLoading, setTooltip } = useMinuit();
|
||||||
|
|
||||||
const projectId = selectedProject?.id || selectedProjectId || null;
|
const projectId = selectedProject?.id || selectedProjectId || null;
|
||||||
|
const hasFinalCover = !!selectedProject?.coverUrl;
|
||||||
|
const hasGeneratedOptions = Array.isArray(selectedProject?.cover?.options)
|
||||||
|
? selectedProject.cover.options.length > 0
|
||||||
|
: false;
|
||||||
|
|
||||||
const hasArtistPreference = useMemo(() => {
|
const hasArtistPreference = useMemo(() => {
|
||||||
if (currentUserData?.userName) return true;
|
if (currentUserData?.userName) return true;
|
||||||
@@ -160,8 +164,12 @@ export const ChooseCoverType = () => {
|
|||||||
// }, [ensureArtistPreference, pickUserImage]);
|
// }, [ensureArtistPreference, pickUserImage]);
|
||||||
|
|
||||||
const handleGenerateCover = useCallback(() => {
|
const handleGenerateCover = useCallback(() => {
|
||||||
|
if (hasFinalCover || hasGeneratedOptions) {
|
||||||
|
navigate(Routes.ValidateCover);
|
||||||
|
return;
|
||||||
|
}
|
||||||
ensureArtistPreference(() => navigate(Routes.PouchReady));
|
ensureArtistPreference(() => navigate(Routes.PouchReady));
|
||||||
}, [ensureArtistPreference]);
|
}, [ensureArtistPreference, hasFinalCover, hasGeneratedOptions]);
|
||||||
|
|
||||||
const closeChoiceModal = useCallback(() => {
|
const closeChoiceModal = useCallback(() => {
|
||||||
setChoiceVisible(false);
|
setChoiceVisible(false);
|
||||||
@@ -296,8 +304,11 @@ export const ChooseCoverType = () => {
|
|||||||
onPress={handlePickUserImage}
|
onPress={handlePickUserImage}
|
||||||
/> */}
|
/> */}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Générer une pochette"
|
title={
|
||||||
|
hasFinalCover ? "Pochette déjà validée" : "Générer une pochette"
|
||||||
|
}
|
||||||
onPress={handleGenerateCover}
|
onPress={handleGenerateCover}
|
||||||
|
disabled={hasFinalCover}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { Image as ExpoImage } from "expo-image";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { ActivityIndicator, Text, View } from "react-native";
|
import { ActivityIndicator, Text, View } from "react-native";
|
||||||
import { background } from "../../assets";
|
import { background } from "../../assets";
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
import loaderMessages from "../../config/loaderMessages";
|
import loaderMessages from "../../config/loaderMessages";
|
||||||
@@ -21,9 +20,14 @@ const PhotoCover = () => {
|
|||||||
? loaderMessages.photoCoverGenerationWeb
|
? loaderMessages.photoCoverGenerationWeb
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
|
const coverOptions = Array.isArray(selectedProject?.cover?.options)
|
||||||
|
? selectedProject.cover.options
|
||||||
|
: [];
|
||||||
const coverUrl =
|
const coverUrl =
|
||||||
selectedProject?.cover?.result ||
|
selectedProject?.cover?.result ||
|
||||||
selectedProject?.cover?.generatedBackground ||
|
selectedProject?.cover?.generatedBackground ||
|
||||||
|
coverOptions?.[0]?.finalUrl ||
|
||||||
|
coverOptions?.[0]?.generatedUrl ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,11 +119,6 @@ const PhotoCover = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BorderGradientButton
|
|
||||||
onPress={() => navigate(Routes.PouchReady)}
|
|
||||||
title="Regénérer la pochette"
|
|
||||||
disabled={isGenerating}
|
|
||||||
/>
|
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Valider la pochette"
|
title="Valider la pochette"
|
||||||
onPress={() => navigate(Routes.ValidateCover)}
|
onPress={() => navigate(Routes.ValidateCover)}
|
||||||
|
|||||||
+118
-78
@@ -1,6 +1,5 @@
|
|||||||
import { useIsFocused } from "@react-navigation/native";
|
|
||||||
import { Image as ExpoImage } from "expo-image";
|
import { Image as ExpoImage } from "expo-image";
|
||||||
import React, { useCallback, useEffect } from "react";
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { ActivityIndicator, Text, View } from "react-native";
|
import { ActivityIndicator, 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, icons } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
@@ -17,20 +16,48 @@ import { goBack, navigate } from "../../navigation/NavigationService";
|
|||||||
import { useUser } from "../../providers/UserDataProvider";
|
import { useUser } from "../../providers/UserDataProvider";
|
||||||
import { gutters, Palette, Style } from "../../styles";
|
import { gutters, Palette, Style } from "../../styles";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
import CustomInput from "../Writing/components/CustomInput";
|
||||||
|
|
||||||
const PouchReady = () => {
|
const PouchReady = () => {
|
||||||
const { selectedProjectId, selectedProject } = useUser();
|
const { selectedProjectId, selectedProject, updateProjectData } = useUser();
|
||||||
const { setIsLoading } = useMinuit();
|
const { setIsLoading } = useMinuit();
|
||||||
const isGenerating = selectedProject?.coverStatus === "GENERATING";
|
const isGenerating = selectedProject?.coverStatus === "GENERATING";
|
||||||
const isFocused = useIsFocused();
|
const coverOptions = Array.isArray(selectedProject?.cover?.options)
|
||||||
|
? selectedProject.cover.options
|
||||||
|
: [];
|
||||||
|
const hasGeneratedOptions = coverOptions.length > 0;
|
||||||
const coverBackgroundMessage = isWeb
|
const coverBackgroundMessage = isWeb
|
||||||
? loaderMessages.pouchReadyGenerationWeb
|
? loaderMessages.pouchReadyGenerationWeb
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const generateCover = useCallback(async () => {
|
const [coverStyle, setCoverStyle] = useState(
|
||||||
|
selectedProject?.coverStyle || ""
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCoverStyle(selectedProject?.coverStyle || "");
|
||||||
|
}, [selectedProject?.coverStyle]);
|
||||||
|
|
||||||
|
const generateCover = useCallback(async (styleValue) => {
|
||||||
if (!selectedProjectId) return;
|
if (!selectedProjectId) return;
|
||||||
|
if (hasGeneratedOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const trimmedStyle = String(styleValue || "").trim();
|
||||||
|
if (!trimmedStyle) {
|
||||||
|
alert("Attention", "Merci de renseigner un style pour la pochette.", [
|
||||||
|
{ text: "OK" },
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
|
await updateProjectData(
|
||||||
|
{
|
||||||
|
coverStyle: trimmedStyle,
|
||||||
|
},
|
||||||
|
{ merge: true }
|
||||||
|
);
|
||||||
await tasksRef.add({
|
await tasksRef.add({
|
||||||
type: "cover",
|
type: "cover",
|
||||||
projectId: selectedProjectId,
|
projectId: selectedProjectId,
|
||||||
@@ -42,40 +69,49 @@ const PouchReady = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedProjectId, setIsLoading]);
|
}, [hasGeneratedOptions, selectedProjectId, setIsLoading, updateProjectData]);
|
||||||
|
|
||||||
|
const requestCoverGeneration = useCallback(() => {
|
||||||
|
const trimmedStyle = (coverStyle || "").trim();
|
||||||
|
if (!trimmedStyle) {
|
||||||
|
alert("Attention", "Merci de renseigner un style pour la pochette.", [
|
||||||
|
{ text: "OK" },
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert("Attention", "Générer une pochette ?", [
|
||||||
|
{
|
||||||
|
text: "Annuler",
|
||||||
|
style: "cancel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: "Oui",
|
||||||
|
onPress: () => generateCover(trimmedStyle),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}, [coverStyle, generateCover]);
|
||||||
|
|
||||||
const validateCover = () => {
|
const validateCover = () => {
|
||||||
navigate(Routes.ValidateCover);
|
navigate(Routes.ValidateCover);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
isFocused &&
|
|
||||||
!selectedProject?.cover?.generatedBackground &&
|
|
||||||
selectedProject?.coverStatus !== "GENERATING"
|
|
||||||
) {
|
|
||||||
alert(
|
|
||||||
"Attention",
|
|
||||||
"Générer une pochette ?",
|
|
||||||
[
|
|
||||||
{
|
|
||||||
text: "Oui",
|
|
||||||
onPress: generateCover,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
text: "Non",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
{ cancelable: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [generateCover, isFocused, selectedProject]);
|
|
||||||
|
|
||||||
const coverPreviewUrl =
|
const coverPreviewUrl =
|
||||||
selectedProject?.cover?.result ||
|
selectedProject?.cover?.result ||
|
||||||
selectedProject?.cover?.generatedBackground ||
|
selectedProject?.cover?.generatedBackground ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
const displayOptions = hasGeneratedOptions
|
||||||
|
? coverOptions
|
||||||
|
: coverPreviewUrl
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: "preview",
|
||||||
|
finalUrl: selectedProject?.cover?.result || null,
|
||||||
|
generatedUrl: coverPreviewUrl,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||||
<MusicLandHeader onPressBack={goBack} progress={72} />
|
<MusicLandHeader onPressBack={goBack} progress={72} />
|
||||||
@@ -89,21 +125,54 @@ const PouchReady = () => {
|
|||||||
// subTitle="Qu’en penses-tu ?"
|
// subTitle="Qu’en penses-tu ?"
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||||
<View style={{ width: "80%", position: "relative" }}>
|
<View style={{ width: "80%", gap: 24 }}>
|
||||||
{coverPreviewUrl && !isGenerating ? (
|
<View
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
flexDirection: isWeb ? "row" : "column",
|
||||||
|
gap: 16,
|
||||||
|
justifyContent: "center",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{displayOptions.length > 0 ? (
|
||||||
|
displayOptions.map((option, index) => {
|
||||||
|
const optionUri = option?.finalUrl || option?.generatedUrl || "";
|
||||||
|
if (!optionUri) return null;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
key={option?.id || index}
|
||||||
|
style={{
|
||||||
|
width: isWeb ? 280 : "100%",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ExpoImage
|
<ExpoImage
|
||||||
source={{ uri: coverPreviewUrl }}
|
source={{ uri: optionUri }}
|
||||||
cachePolicy="memory-disk"
|
cachePolicy="memory-disk"
|
||||||
priority="high"
|
priority="high"
|
||||||
contentFit="cover"
|
contentFit="cover"
|
||||||
transition={120}
|
transition={120}
|
||||||
style={{
|
style={{
|
||||||
width: isWeb ? 300 : "100%",
|
width: "100%",
|
||||||
height: 300,
|
height: 260,
|
||||||
borderRadius: 20,
|
borderRadius: 20,
|
||||||
alignSelf: "center",
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{hasGeneratedOptions && (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: Palette.white,
|
||||||
|
opacity: 0.7,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Pochette {index + 1}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@@ -147,47 +216,16 @@ const PouchReady = () => {
|
|||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{/*<View*/}
|
</View>
|
||||||
{/* style={{*/}
|
<CustomInput
|
||||||
{/* position: "absolute",*/}
|
label="Style de la pochette"
|
||||||
{/* alignSelf: "center",*/}
|
placeholder="Exemple : Collage rétro futuriste lumineux"
|
||||||
{/* alignItems: "center",*/}
|
value={coverStyle}
|
||||||
{/* top: 10,*/}
|
setValue={setCoverStyle}
|
||||||
{/* }}*/}
|
multiline={false}
|
||||||
{/*>*/}
|
height={55}
|
||||||
{/* <Text*/}
|
maxLength={120}
|
||||||
{/* style={{*/}
|
/>
|
||||||
{/* fontSize: 22,*/}
|
|
||||||
{/* color: Palette.white,*/}
|
|
||||||
{/* fontFamily: FONT_FAMILY.InterSemiBold,*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* {title || "Titre"}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Text*/}
|
|
||||||
{/* style={{*/}
|
|
||||||
{/* fontSize: 14,*/}
|
|
||||||
{/* color: Palette.gray,*/}
|
|
||||||
{/* fontFamily: FONT_FAMILY.InterRegular,*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* MusicLand*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
{/*<View*/}
|
|
||||||
{/* style={{*/}
|
|
||||||
{/* position: "absolute",*/}
|
|
||||||
{/* alignItems: "center",*/}
|
|
||||||
{/* bottom: 10,*/}
|
|
||||||
{/* width: "100%",*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/*>*/}
|
|
||||||
{/* <Image*/}
|
|
||||||
{/* source={icons.musicLandLogo}*/}
|
|
||||||
{/* style={{ width: "100%", height: 30 }}*/}
|
|
||||||
{/* resizeMode="contain"*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -199,12 +237,14 @@ const PouchReady = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!hasGeneratedOptions && (
|
||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
title={isGenerating ? "Génération en cours..." : "Regénérer"}
|
title={isGenerating ? "Génération en cours..." : "Générer la pochette"}
|
||||||
icon={icons.stars}
|
icon={icons.stars}
|
||||||
onPress={generateCover}
|
onPress={requestCoverGeneration}
|
||||||
disabled={isGenerating}
|
disabled={isGenerating}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title={isGenerating ? "Veuillez patienter..." : "Valider"}
|
title={isGenerating ? "Veuillez patienter..." : "Valider"}
|
||||||
disabled={isGenerating || !coverPreviewUrl}
|
disabled={isGenerating || !coverPreviewUrl}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import { Image as ExpoImage } from "expo-image";
|
||||||
import { Image, View } from "react-native";
|
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 useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||||
import { background, img } from "../../assets";
|
import { background } from "../../assets";
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
|
||||||
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";
|
||||||
@@ -10,14 +10,34 @@ import Page from "../../layouts/Page";
|
|||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { useUserData } from "../../providers/UserDataProvider";
|
import { useUserData } from "../../providers/UserDataProvider";
|
||||||
import { gutters, Style } from "../../styles";
|
import { gutters, Palette, Style } from "../../styles";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
|
|
||||||
const ValidateCover = () => {
|
const ValidateCover = () => {
|
||||||
const { selectedProject, updateProjectData } = useUserData();
|
const { selectedProject, updateProjectData } = useUserData();
|
||||||
|
|
||||||
const { setIsLoading } = useMinuit();
|
const { setIsLoading } = useMinuit();
|
||||||
|
|
||||||
|
const coverOptions = useMemo(() => {
|
||||||
|
if (!Array.isArray(selectedProject?.cover?.options)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return selectedProject.cover.options.filter((option) => option);
|
||||||
|
}, [selectedProject]);
|
||||||
|
|
||||||
|
const selectedOptionId = selectedProject?.cover?.selectedOptionId || null;
|
||||||
|
const selectedOption = useMemo(() => {
|
||||||
|
if (!coverOptions.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const found = coverOptions.find(
|
||||||
|
(option) => option?.id === selectedOptionId
|
||||||
|
);
|
||||||
|
return found || coverOptions[0] || null;
|
||||||
|
}, [coverOptions, selectedOptionId]);
|
||||||
|
|
||||||
|
const isMultipleOptions = coverOptions.length > 1;
|
||||||
|
const [isSelecting, setIsSelecting] = useState(false);
|
||||||
|
|
||||||
async function onStartAgain() {
|
async function onStartAgain() {
|
||||||
try {
|
try {
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
@@ -32,11 +52,59 @@ const ValidateCover = () => {
|
|||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSelectOption = useCallback(
|
||||||
|
async (option) => {
|
||||||
|
if (!option || option?.id === selectedOptionId || isSelecting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const existingCover = selectedProject?.cover || {};
|
||||||
|
const finalUrl = option.finalUrl || option.generatedUrl || null;
|
||||||
|
setIsSelecting(true);
|
||||||
|
try {
|
||||||
|
await updateProjectData({
|
||||||
|
cover: {
|
||||||
|
...existingCover,
|
||||||
|
options: coverOptions,
|
||||||
|
selectedOptionId: option.id,
|
||||||
|
result: finalUrl,
|
||||||
|
generatedBackground: option.generatedUrl || option.finalUrl || null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.log("ValidateCover: unable to select cover", e?.message);
|
||||||
|
} finally {
|
||||||
|
setIsSelecting(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
coverOptions,
|
||||||
|
isSelecting,
|
||||||
|
selectedOptionId,
|
||||||
|
selectedProject,
|
||||||
|
updateProjectData,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
async function onValidatePicture() {
|
async function onValidatePicture() {
|
||||||
|
if (!selectedOption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
await setIsLoading(true);
|
await setIsLoading(true);
|
||||||
|
const existingCover = selectedProject?.cover || {};
|
||||||
|
const finalUrl =
|
||||||
|
selectedOption.finalUrl || selectedOption.generatedUrl || null;
|
||||||
await updateProjectData({
|
await updateProjectData({
|
||||||
coverUrl: selectedProject?.cover?.result,
|
cover: {
|
||||||
|
...existingCover,
|
||||||
|
options: coverOptions,
|
||||||
|
selectedOptionId: selectedOption.id,
|
||||||
|
result: finalUrl,
|
||||||
|
generatedBackground:
|
||||||
|
selectedOption.generatedUrl || selectedOption.finalUrl || null,
|
||||||
|
},
|
||||||
|
coverUrl: finalUrl,
|
||||||
});
|
});
|
||||||
navigate(Routes.Home);
|
navigate(Routes.Home);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -45,80 +113,124 @@ const ValidateCover = () => {
|
|||||||
await setIsLoading(false);
|
await setIsLoading(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
<Page backgroundImg={background.studioBG2} headerType="NONE">
|
||||||
<MusicLandHeader onPressBack={goBack} progress={90} />
|
<MusicLandHeader onPressBack={goBack} progress={90} />
|
||||||
<View style={{ flex: 1, marginTop: 16 }}>
|
<View style={{ flex: 1, marginTop: 16 }}>
|
||||||
<CreateLyricsHeader
|
<CreateLyricsHeader
|
||||||
title="Ta pochette est prête!"
|
title="Ta pochette est prête!"
|
||||||
subTitle="Qu’en penses-tu ?"
|
subTitle="Choisis ta version préférée."
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1, ...Style.containerCenter }}>
|
<View style={{ flex: 1, ...Style.containerCenter }}>
|
||||||
<View style={{ width: "80%", position: "relative" }}>
|
<View
|
||||||
<Image
|
|
||||||
source={img.placeholder}
|
|
||||||
style={{
|
style={{
|
||||||
width: isWeb ? 300 : "100%",
|
width: "90%",
|
||||||
height: 300,
|
gap: 16,
|
||||||
borderRadius: 20,
|
flexDirection: isMultipleOptions ? "row" : "column",
|
||||||
transform: [{ rotateY: "180deg" }],
|
flexWrap: isMultipleOptions ? "wrap" : "nowrap",
|
||||||
alignSelf: "center",
|
justifyContent: "center",
|
||||||
|
alignItems: isMultipleOptions ? "stretch" : "center",
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
{coverOptions.map((option, index) => {
|
||||||
|
const isSelected = option?.id === selectedOption?.id;
|
||||||
|
const imageSource =
|
||||||
|
option?.finalUrl || option?.generatedUrl || null;
|
||||||
|
|
||||||
|
if (!imageSource) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
key={option?.id || index}
|
||||||
|
onPress={() => handleSelectOption(option)}
|
||||||
|
disabled={isSelecting}
|
||||||
|
style={{
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: isSelected ? 2 : 1,
|
||||||
|
borderColor: isSelected
|
||||||
|
? Palette.primary
|
||||||
|
: Palette.ultraLightWhite,
|
||||||
|
overflow: "hidden",
|
||||||
|
width: isMultipleOptions
|
||||||
|
? isWeb
|
||||||
|
? 280
|
||||||
|
: "48%"
|
||||||
|
: isWeb
|
||||||
|
? 300
|
||||||
|
: "90%",
|
||||||
|
maxWidth: isWeb ? 320 : "100%",
|
||||||
|
position: "relative",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<ExpoImage
|
||||||
|
source={{ uri: imageSource }}
|
||||||
|
cachePolicy="memory-disk"
|
||||||
|
contentFit="cover"
|
||||||
|
transition={120}
|
||||||
|
style={{ width: "100%", aspectRatio: 1 }}
|
||||||
/>
|
/>
|
||||||
<View
|
<View
|
||||||
style={{ position: "absolute", width: "100%", height: "100%" }}
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
source={{ uri: selectedProject?.cover?.result }}
|
|
||||||
style={{
|
style={{
|
||||||
width: isWeb ? 300 : "100%",
|
position: "absolute",
|
||||||
height: "100%",
|
top: 12,
|
||||||
borderRadius: 20,
|
right: 12,
|
||||||
alignSelf: "center",
|
backgroundColor: Palette.transparentBlack,
|
||||||
|
paddingHorizontal: 10,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 12,
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: Palette.white,
|
||||||
|
fontWeight: "600",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{`Option ${index + 1}`}
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
{/*<View*/}
|
{isSelected && (
|
||||||
{/* style={{*/}
|
<View
|
||||||
{/* position: "absolute",*/}
|
style={{
|
||||||
{/* alignSelf: "center",*/}
|
position: "absolute",
|
||||||
{/* alignItems: "center",*/}
|
bottom: 12,
|
||||||
{/* top: 10,*/}
|
left: 12,
|
||||||
{/* }}*/}
|
backgroundColor: Palette.primary,
|
||||||
{/*>*/}
|
paddingHorizontal: 12,
|
||||||
{/* <Text*/}
|
paddingVertical: 6,
|
||||||
{/* style={{*/}
|
borderRadius: 12,
|
||||||
{/* fontSize: 22,*/}
|
}}
|
||||||
{/* color: Palette.white,*/}
|
>
|
||||||
{/* fontFamily: FONT_FAMILY.InterSemiBold,*/}
|
<Text
|
||||||
{/* }}*/}
|
style={{
|
||||||
{/* >*/}
|
color: Palette.white,
|
||||||
{/* Lust for Life*/}
|
fontWeight: "600",
|
||||||
{/* </Text>*/}
|
fontSize: 12,
|
||||||
{/* <Text*/}
|
}}
|
||||||
{/* style={{*/}
|
>
|
||||||
{/* fontSize: 14,*/}
|
Sélectionnée
|
||||||
{/* color: Palette.gray,*/}
|
</Text>
|
||||||
{/* fontFamily: FONT_FAMILY.InterRegular,*/}
|
</View>
|
||||||
{/* }}*/}
|
)}
|
||||||
{/* >*/}
|
</Pressable>
|
||||||
{/* Lana del Rey*/}
|
);
|
||||||
{/* </Text>*/}
|
})}
|
||||||
{/*</View>*/}
|
{!coverOptions.length && (
|
||||||
{/*<View*/}
|
<Text
|
||||||
{/* style={{*/}
|
style={{
|
||||||
{/* position: "absolute",*/}
|
color: Palette.white,
|
||||||
{/* alignItems: "center",*/}
|
textAlign: "center",
|
||||||
{/* bottom: 10,*/}
|
opacity: 0.8,
|
||||||
{/* width: "100%",*/}
|
}}
|
||||||
{/* }}*/}
|
>
|
||||||
{/*>*/}
|
Les options de pochette seront disponibles à la fin de la
|
||||||
{/* <Image*/}
|
génération.
|
||||||
{/* source={icons.musicLandLogo}*/}
|
</Text>
|
||||||
{/* style={{ width: "100%", height: 30 }}*/}
|
)}
|
||||||
{/* resizeMode="contain"*/}
|
|
||||||
{/* />*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -130,13 +242,14 @@ const ValidateCover = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BorderGradientButton
|
{/* <BorderGradientButton
|
||||||
title="Recommencer la création"
|
title="Recommencer la création"
|
||||||
onPress={onStartAgain}
|
onPress={onStartAgain}
|
||||||
/>
|
/> */}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Valider la pochette"
|
title="Valider la pochette"
|
||||||
onPress={onValidatePicture}
|
onPress={onValidatePicture}
|
||||||
|
disabled={!selectedOption || isSelecting}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ const getStageLockState = (key, metadata) => {
|
|||||||
case "songwriter":
|
case "songwriter":
|
||||||
return metadata.hasSongUrl;
|
return metadata.hasSongUrl;
|
||||||
case "beatmaker":
|
case "beatmaker":
|
||||||
|
if (metadata.hasCover) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return metadata.lyricsCount <= 0;
|
return metadata.lyricsCount <= 0;
|
||||||
case "director":
|
case "director":
|
||||||
return !metadata.hasCover;
|
return !metadata.hasCover;
|
||||||
@@ -78,6 +81,9 @@ const getStageDescription = (key, metadata) => {
|
|||||||
}
|
}
|
||||||
return "Modifier les paroles créées";
|
return "Modifier les paroles créées";
|
||||||
case "beatmaker":
|
case "beatmaker":
|
||||||
|
if (metadata.hasCover) {
|
||||||
|
return "Impossible de modifier la cover ou la production musicale";
|
||||||
|
}
|
||||||
if (!hasLyrics) {
|
if (!hasLyrics) {
|
||||||
return "Écrivez vos paroles pour débloquer la musique";
|
return "Écrivez vos paroles pour débloquer la musique";
|
||||||
}
|
}
|
||||||
@@ -117,6 +123,9 @@ const getStageLockedDescription = (key, metadata) => {
|
|||||||
? "Impossible de modifier les paroles"
|
? "Impossible de modifier les paroles"
|
||||||
: undefined;
|
: undefined;
|
||||||
case "beatmaker":
|
case "beatmaker":
|
||||||
|
if (metadata.hasCover) {
|
||||||
|
return "Impossible de modifier la cover ou la production musicale";
|
||||||
|
}
|
||||||
return metadata.lyricsCount > 0
|
return metadata.lyricsCount > 0
|
||||||
? undefined
|
? undefined
|
||||||
: "Créez vos paroles pour débloquer le studio";
|
: "Créez vos paroles pour débloquer le studio";
|
||||||
|
|||||||
Reference in New Issue
Block a user