diff --git a/functions/helpers/prompts.js b/functions/helpers/prompts.js
index 2feb1e6..342d002 100644
--- a/functions/helpers/prompts.js
+++ b/functions/helpers/prompts.js
@@ -1,3 +1,192 @@
+const STYLE_PRESET_MAP = {
+ 'Réaliste (style cinématographique)': {
+ style: 'Cinematic realism, film still, anamorphic lens, shallow depth of field',
+ palettes: [
+ 'cool teal and amber highlights, muted neutrals',
+ 'stormy blues with soft gold accents',
+ 'charcoal, steel blue, and warm tungsten glows',
+ ],
+ lighting: [
+ 'dramatic key light with soft fill and deep shadows',
+ 'low-key cinematic lighting with subtle haze',
+ ],
+ textures: ['subtle film grain and crisp detail', 'matte finish with fine grain'],
+ typography: ['clean cinematic title card, subtle embossing'],
+ compositions: ['cinematic framing with a strong subject focus'],
+ },
+ 'Artistique (style peinture moderne)': {
+ style: 'Modern painting, expressive brushstrokes, gallery-ready composition',
+ palettes: [
+ 'rich pigments with bold contrast',
+ 'muted pastels with a sharp accent color',
+ 'deep jewel tones and warm highlights',
+ ],
+ lighting: ['soft gallery lighting with gentle gradients'],
+ textures: ['visible canvas texture and layered paint', 'acrylic strokes with matte finish'],
+ typography: ['refined serif lettering integrated into the paint'],
+ compositions: ['balanced composition with painterly focal point'],
+ },
+ 'Style Dessins': {
+ style: 'Hand-drawn illustration, clean linework, ink and colored pencil',
+ palettes: [
+ 'warm paper tones with soft color fills',
+ 'limited palette with bold accent color',
+ ],
+ lighting: ['soft diffused light with minimal shadows'],
+ textures: ['paper texture with visible grain'],
+ typography: ['hand-lettered title with slight texture'],
+ compositions: ['centered illustration with clear silhouette'],
+ },
+ 'Style rétros': {
+ style: 'Retro poster art, vintage print, halftone patterns, aged paper',
+ palettes: [
+ 'sun-faded oranges, teal, and cream',
+ 'muted reds, mustard, and dark green',
+ ],
+ lighting: ['flat poster lighting with strong contrast'],
+ textures: ['distressed print texture and subtle scratches'],
+ typography: ['vintage serif lettering with screen print texture'],
+ compositions: ['poster-style layout with bold hierarchy'],
+ },
+ 'Style cyberpunk': {
+ style: 'Cyberpunk aesthetic, futuristic city glow, reflective surfaces',
+ palettes: [
+ 'neon magenta and electric blue on dark backgrounds',
+ 'cyan, violet, and deep black with sharp highlights',
+ ],
+ lighting: ['neon rim light and high-contrast highlights'],
+ textures: ['glossy surfaces with subtle rain speckles'],
+ typography: ['neon tube lettering embedded in the scene'],
+ compositions: ['dynamic perspective with strong depth'],
+ },
+ 'Style Afro': {
+ style: 'Afrofuturist motifs, bold patterns, textile-inspired geometry',
+ palettes: [
+ 'warm earth tones with vibrant accents',
+ 'gold, terracotta, deep blue, and emerald',
+ ],
+ lighting: ['warm directional light with rich contrast'],
+ textures: ['woven fabric texture and layered patterns'],
+ typography: ['bold lettering with subtle pattern fills'],
+ compositions: ['symmetrical layout with iconic central motif'],
+ },
+ 'Style grunge': {
+ style: 'Grunge collage, distressed textures, gritty mixed media',
+ palettes: [
+ 'smoky blacks with muted reds and greys',
+ 'dirty neutrals with high-contrast highlights',
+ ],
+ lighting: ['moody low-key lighting with harsh shadows'],
+ textures: ['torn paper, scratches, and distressed overlays'],
+ typography: ['distressed lettering with rough edges'],
+ compositions: ['layered collage with imperfect alignment'],
+ },
+ 'Style pop art': {
+ style: 'Pop art illustration, bold outlines, Ben-Day dots, screen print',
+ palettes: [
+ 'bold primary colors with black and white',
+ 'bright yellow, red, and cyan with clean contrast',
+ ],
+ lighting: ['flat graphic lighting with crisp separation'],
+ textures: ['screen print texture with halftone dots'],
+ typography: ['bold outlined letters with halftone texture'],
+ compositions: ['graphic layout with strong shapes and punchy hierarchy'],
+ },
+}
+
+const DEFAULT_STYLE_VARIANTS = [
+ 'Conceptual album art, abstract surrealism, high fidelity',
+ 'Graphic collage, mixed media, layered textures',
+ 'Minimalist design, bold shapes, strong negative space',
+ 'Dreamlike photography, cinematic framing, subtle grain',
+ 'Geometric abstraction, clean gradients, modern design',
+ 'Stylized illustration, crisp edges, bold composition',
+]
+
+const COMPOSITION_VARIANTS = [
+ 'centered focal subject with balanced symmetry',
+ 'asymmetrical layout with dynamic diagonal flow',
+ 'minimal composition with strong negative space',
+ 'collage-like layout with layered elements',
+ 'close-up framing for intimacy and impact',
+ 'wide cinematic framing with a distant focal point',
+]
+
+const LIGHTING_VARIANTS = [
+ 'dramatic chiaroscuro with deep shadows',
+ 'soft diffused light with gentle gradients',
+ 'high-contrast lighting with sharp highlights',
+ 'backlit silhouette with glowing edges',
+ 'moody low-key lighting with selective highlights',
+]
+
+const PALETTE_VARIANTS = [
+ 'deep navy with warm amber accents',
+ 'muted earth tones with copper highlights',
+ 'cool blues with silver accents',
+ 'bold primary colors with black and white',
+ 'monochrome with a single accent color',
+ 'pastel gradients with a sharp contrasting highlight',
+ 'electric blue and magenta on dark tones',
+]
+
+const TEXTURE_VARIANTS = [
+ 'subtle film grain with a matte finish',
+ 'canvas texture with visible brushwork',
+ 'paper cutout layers with rough edges',
+ 'glossy highlights with clean edges',
+ 'distressed print texture with scratches',
+]
+
+const TYPOGRAPHY_VARIANTS = [
+ 'embossed metallic lettering integrated into the art',
+ 'minimalist sans-serif with generous tracking',
+ 'hand-drawn lettering with slight texture',
+ 'neon-style lettering embedded in the scene',
+ 'classic serif with elegant spacing',
+]
+
+const SUBJECT_VARIANTS = [
+ 'a single strong symbol or scene that represents the song',
+ 'an abstract motif with a clear focal point',
+ 'a cinematic moment captured mid-action',
+ 'a minimalist icon or silhouette with strong presence',
+ 'a surreal object or landscape that feels iconic',
+]
+
+const ATMOSPHERE_DARK = [
+ 'cinematic and moody with deep contrast',
+ 'noir ambience with subtle haze and high contrast',
+ 'tense and dramatic with low-key depth',
+]
+
+const ATMOSPHERE_ENERGETIC = [
+ 'dynamic and high energy with punchy contrast',
+ 'electric and fast-paced with vivid saturation',
+ 'bold and kinetic with sharp highlights',
+]
+
+const ATMOSPHERE_SOFT = [
+ 'calm and spacious with balanced mood',
+ 'introspective and gentle with airy depth',
+ 'dreamlike and serene with soft transitions',
+]
+
+const hashSeed = (value) => {
+ const str = String(value || '')
+ let hash = 0
+ for (let i = 0; i < str.length; i += 1) {
+ hash = (hash * 31 + str.charCodeAt(i)) >>> 0
+ }
+ return hash
+}
+
+const pickVariant = (list, seed, salt = 0) => {
+ if (!Array.isArray(list) || list.length === 0) return ''
+ const idx = (seed + salt) % list.length
+ return list[idx]
+}
+
exports.generatePicturePrompt = (project = {}) => {
const {
title = '',
@@ -5,6 +194,7 @@ exports.generatePicturePrompt = (project = {}) => {
musicConfig = {},
coverStyle: coverStyleRaw = '',
artistName: artistNameRaw = '',
+ variantSeed = '',
} = project || {}
// --- 1. Nettoyage et Normalisation ---
@@ -20,10 +210,38 @@ exports.generatePicturePrompt = (project = {}) => {
const artistName = sanitizeInline(artistNameRaw) || sanitizeInline(project?.userName || '')
const hasArtistName = artistName.length > 0
- const { genres = [], tempo = '', mood = '', instruments = [] } = musicConfig || {}
+ const { genres = [], tempo = '', mood = '' } = musicConfig || {}
const userStyle = sanitizeInline(coverStyleRaw)
- // --- 2. Intelligence Visuelle (Mapping) ---
+ const seedInput = [titleForPrompt, artistName, userStyle, String(variantSeed || '')].join('|')
+ const baseSeed = hashSeed(seedInput)
+ const seed = baseSeed || Math.floor(Math.random() * 1_000_000_000)
+
+ // --- 2. Intelligence Visuelle (Mapping & Variations) ---
+ const preset = STYLE_PRESET_MAP[userStyle] || null
+ const styleDescriptor = preset?.style || userStyle || pickVariant(DEFAULT_STYLE_VARIANTS, seed, 7)
+ const renderingStyle = `Art Style: ${styleDescriptor}.`
+
+ const compositionChoice =
+ preset?.compositions?.length > 0
+ ? pickVariant(preset.compositions, seed, 1)
+ : pickVariant(COMPOSITION_VARIANTS, seed, 1)
+ const lightingChoice =
+ preset?.lighting?.length > 0
+ ? pickVariant(preset.lighting, seed, 2)
+ : pickVariant(LIGHTING_VARIANTS, seed, 2)
+ const paletteChoice =
+ preset?.palettes?.length > 0
+ ? pickVariant(preset.palettes, seed, 3)
+ : pickVariant(PALETTE_VARIANTS, seed, 3)
+ const textureChoice =
+ preset?.textures?.length > 0
+ ? pickVariant(preset.textures, seed, 4)
+ : pickVariant(TEXTURE_VARIANTS, seed, 4)
+ const typographyChoice =
+ preset?.typography?.length > 0
+ ? pickVariant(preset.typography, seed, 5)
+ : pickVariant(TYPOGRAPHY_VARIANTS, seed, 5)
// Détermination de l'énergie visuelle
const isEnergetic =
@@ -31,47 +249,21 @@ exports.generatePicturePrompt = (project = {}) => {
const isDark =
mood && /\b(sombre|triste|dark|sad|mélancoli|nuit|night|eerie)\b/i.test(String(mood))
- // Construction de la Palette & Lumière
- let visualAtmosphere = ''
- if (isDark) {
- visualAtmosphere =
- 'Atmosphere: Cinematic atmosphere, moody shadows, deep contrast. Palette: Midnight blue, obsidian, deep purple, metallic accents.'
- } else if (isEnergetic) {
- visualAtmosphere =
- 'Atmosphere: Dynamic atmosphere, high energy, vibrant saturation. Palette: Neon colors, electric blue, magenta, bright yellow, high contrast.'
- } else {
- visualAtmosphere =
- 'Atmosphere: Soft atmosphere, harmonious and ethereal. Palette: Pastel tones, warm gold, soft coral, balanced and elegant colors.'
- }
-
- // Définition du Style de Rendu (Si l'utilisateur est vague, on renforce)
- let renderingStyle = userStyle ? `Art Style: ${userStyle}` : 'Art Style: Digital Art, Mixed Media'
-
- if (userStyle.toLowerCase().includes('realist') || userStyle.toLowerCase().includes('photo')) {
- renderingStyle +=
- ', 8k resolution, highly detailed texture, photorealistic, cinematic depth of field, raytracing.'
- } else if (
- userStyle.toLowerCase().includes('illu') ||
- userStyle.toLowerCase().includes('dessin')
- ) {
- renderingStyle +=
- ', vector art, clean lines, professional illustration, flat design or detailed painting.'
- } else {
- // Style par défaut "Album Cover" qui marche bien
- renderingStyle += ', abstract surrealism, conceptual album art, high fidelity, masterpiece.'
- }
+ const atmosphereChoice = isDark
+ ? pickVariant(ATMOSPHERE_DARK, seed, 6)
+ : isEnergetic
+ ? pickVariant(ATMOSPHERE_ENERGETIC, seed, 6)
+ : pickVariant(ATMOSPHERE_SOFT, seed, 6)
// --- 3. Extraction de l'Inspiration (Lyrics) ---
-
- // On cherche le REFRAIN en priorité pour l'image, car c'est le cœur visuel
const sections = Array.isArray(lyricsRaw) ? lyricsRaw : []
const chorus = sections.find((s) => s.type === 'refrain' || s.type === 'chorus')
const verse = sections.find((s) => s.type === 'couplet' || s.type === 'verse')
- // On prend 2 lignes max du refrain, ou du premier couplet
const visualHook = (chorus?.lyrics || verse?.lyrics || '')
.split('\n')
- .filter((l) => l.length > 10) // On évite les lignes trop courtes
+ .map((line) => line.trim())
+ .filter((line) => line.length > 10)
.slice(0, 2)
.join('. ')
@@ -79,36 +271,38 @@ exports.generatePicturePrompt = (project = {}) => {
? `Visual Inspiration: An interpretation of these lyrics: "${visualHook}".`
: "Visual Inspiration: Abstract visual representation of the song's mood."
- // --- 4. Construction du Prompt Final (Structure Optimisée Imagen 3) ---
+ const subjectChoice = pickVariant(SUBJECT_VARIANTS, seed, 8)
+ // --- 4. Construction du Prompt Final (Structure Optimisée Imagen 3) ---
const promptParts = [
- // Rôle
'Design a professional, high-quality music album cover.',
- // 1. Le Texte (Crucial pour Imagen 3 - Doit être au début ou très clair)
- `**Typography & Text:**`,
+ '**Typography & Text:**',
`The song title "${titleForPrompt}" must be the CENTERPIECE. Write it in a distinct font that matches the mood.`,
hasArtistName
? `The artist name "${artistName}" must appear smaller, elegant, and legible near the bottom or top.`
: '',
- 'Ensure perfect spelling. The text should be integrated into the artwork (e.g., metallic texture, neon glow, or bold cut-out), not just pasted on top.',
+ `Typography treatment: ${typographyChoice}.`,
+ 'Ensure perfect spelling. The text should be integrated into the artwork (e.g., embossed texture, neon tube, or bold cut-out), not just pasted on top.',
- // 2. Le Visuel
- `**Visuals:**`,
+ '**Visual Direction:**',
renderingStyle,
+ `Composition: ${compositionChoice}.`,
+ `Surface & Texture: ${textureChoice}.`,
+ `Subject: ${subjectChoice}.`,
imageryPrompt,
- `Subject: A central visual element that represents the song. ${isEnergetic ? 'Dynamic composition.' : 'Balanced, centered composition.'}`,
- // 3. L'Atmosphère (Context from music config)
- `**Mood & Color:**`,
- visualAtmosphere,
+ '**Mood & Color:**',
+ `Atmosphere: ${atmosphereChoice}.`,
+ `Palette: ${paletteChoice}.`,
+ `Lighting: ${lightingChoice}.`,
genres.length > 0 ? `Musical Vibe Reference: ${genres.join(', ')}.` : '',
- // 4. Contraintes Négatives (Phrasées positivement pour l'IA)
'**Constraints:**',
'Use a square 1:1 aspect ratio.',
- 'Do NOT depict literal musical instruments (like guitars or microphones) unless they are part of a surreal abstract composition.',
- 'No blurry text. No messy borders. No watermarks.',
+ 'No watermarks or logos.',
+ 'No blurry text or messy borders.',
+ 'Avoid generic stock imagery and clichéd instruments unless essential to the concept.',
]
return promptParts.filter(Boolean).join('\n\n')
diff --git a/functions/src/cover.js b/functions/src/cover.js
index 5974634..a297f58 100644
--- a/functions/src/cover.js
+++ b/functions/src/cover.js
@@ -2,8 +2,6 @@ const { onDocumentCreated } = require('firebase-functions/v2/firestore')
const admin = require('firebase-admin')
const { FieldValue } = require('firebase-admin/firestore')
const logger = require('firebase-functions/logger')
-const path = require('path')
-const fs = require('fs')
const axios = require('axios')
const sharp = require('sharp')
const crypto = require('crypto')
@@ -16,23 +14,47 @@ const { sendNotification } = require('./notifications')
// Configuration
const bucket = admin.storage().bucket()
-const LOGO_PATH = path.resolve(__dirname, '../assets/musicLandLogo.png')
+const BRAND_TEXT = 'By Musicland.ai'
+const BRAND_FONT_FAMILY = 'Poppins, Montserrat, Arial, sans-serif'
-// Cache mémoire pour le buffer du logo (évite les I/O disque à chaque appel sur instance chaude)
-let _cachedLogoBuffer = null
+const escapeSvgText = (value = '') =>
+ String(value).replace(/&/g, '&').replace(//g, '>')
-/**
- * Récupère le buffer du logo depuis le cache ou le disque
- */
-const getLogoBuffer = async () => {
- if (_cachedLogoBuffer) return _cachedLogoBuffer
- try {
- _cachedLogoBuffer = await fs.promises.readFile(LOGO_PATH)
- return _cachedLogoBuffer
- } catch (error) {
- logger.error('❌ [Cover] Impossible de lire le fichier logo', error)
- throw new Error('Asset Logo manquant sur le serveur')
- }
+const buildBrandSvg = ({ width = 1024, height = 1024, text = BRAND_TEXT } = {}) => {
+ const safeWidth = Math.max(320, Math.round(width))
+ const safeHeight = Math.max(320, Math.round(height))
+ const margin = Math.round(safeWidth * 0.04)
+ const fontSize = Math.max(22, Math.round(safeWidth * 0.045))
+ const letterSpacing = Math.max(1, Math.round(fontSize * 0.06))
+ const strokeWidth = Math.max(2, Math.round(fontSize * 0.08))
+ const shadowDy = Math.max(2, Math.round(fontSize * 0.08))
+ const shadowBlur = Math.max(4, Math.round(fontSize * 0.18))
+ const safeText = escapeSvgText(text)
+
+ return Buffer.from(`
+`)
}
/**
@@ -96,39 +118,25 @@ async function resolveArtistName(project = {}) {
}
/**
- * Ajoute le logo en filigrane sur l'image générée
+ * Ajoute le texte de marque en filigrane sur l'image générée
*/
-async function buildCoverWithLogo(backgroundUrl, targetPath) {
- logger.info('🖼️ [Cover] Compositing logo...')
+async function buildCoverWithBrandText(backgroundUrl, targetPath) {
+ logger.info('🖼️ [Cover] Compositing brand text...')
try {
- // Téléchargement background + Lecture Logo (parallèle)
- const [bgResponse, logoBuffer] = await Promise.all([
- axios.get(backgroundUrl, { responseType: 'arraybuffer' }),
- getLogoBuffer(),
- ])
+ // Téléchargement background
+ const bgResponse = await axios.get(backgroundUrl, { responseType: 'arraybuffer' })
const baseImage = sharp(bgResponse.data)
const metadata = await baseImage.metadata()
const width = metadata.width || 1024
const height = metadata.height || 1024
-
- // Calcul dynamique de la taille du logo (32% de la largeur)
- const desiredWidth = Math.round(width * 0.32)
- const margin = Math.round(width * 0.04)
-
- // Redimensionnement du logo
- const resizedLogo = await sharp(logoBuffer).resize({ width: desiredWidth }).png().toBuffer()
-
- // Positionnement (Bas Droite)
- const logoMetadata = await sharp(resizedLogo).metadata()
- const left = Math.max(width - logoMetadata.width - margin, 0)
- const top = Math.max(height - logoMetadata.height - margin, 0)
+ const brandOverlay = buildBrandSvg({ width, height, text: BRAND_TEXT })
// Composition
const stampedBuffer = await baseImage
.ensureAlpha()
- .composite([{ input: resizedLogo, left, top, blend: 'over' }])
+ .composite([{ input: brandOverlay, left: 0, top: 0, blend: 'over' }])
.png()
.toBuffer()
@@ -147,8 +155,8 @@ async function buildCoverWithLogo(backgroundUrl, targetPath) {
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(targetPath)}?alt=media&token=${token}`
} catch (error) {
- logger.error('❌ [Cover] buildCoverWithLogo failed', error)
- // En cas d'échec du logo, on renvoie l'URL originale pour ne pas tout perdre
+ logger.error('❌ [Cover] buildCoverWithBrandText failed', error)
+ // En cas d'échec du texte, on renvoie l'URL originale pour ne pas tout perdre
return backgroundUrl
}
}
@@ -160,18 +168,6 @@ async function performCoverGeneration(project) {
const t0 = Date.now()
const artistName = await resolveArtistName(project)
- // Génération du Prompt optimisé
- const prompt = generatePicturePrompt({
- ...project,
- artistName,
- })
-
- logger.info('🎨 [Cover] Prompt generated', {
- projectId: project.id,
- artistName,
- promptPreview: prompt.slice(0, 100) + '...',
- })
-
const baseTimestamp = Date.now()
const GENERATION_COUNT = 2 // Nombre de variantes simultanées
@@ -181,6 +177,18 @@ async function performCoverGeneration(project) {
const storageBasePath = `users/${project.userId}/projects/${project.id}`
const generatedPath = `${storageBasePath}/generated-${uniqueSuffix}.png`
const stampedPath = `${storageBasePath}/cover-${uniqueSuffix}-stamped.png`
+ const prompt = generatePicturePrompt({
+ ...project,
+ artistName,
+ variantSeed: `${project.id || 'project'}-${uniqueSuffix}`,
+ })
+
+ logger.info('🎨 [Cover] Prompt generated', {
+ projectId: project.id,
+ artistName,
+ variant: uniqueSuffix,
+ promptPreview: prompt.slice(0, 100) + '...',
+ })
try {
// 1. Appel IA (Imagen 3) - S'exécute en parallèle des autres
@@ -189,7 +197,7 @@ async function performCoverGeneration(project) {
if (!generatedUrl) throw new Error("URL vide retournée par l'IA")
// 2. Ajout du Logo
- const finalCoverUrl = await buildCoverWithLogo(generatedUrl, stampedPath)
+ const finalCoverUrl = await buildCoverWithBrandText(generatedUrl, stampedPath)
logger.info(`✅ [Cover] Option ${index + 1}/${GENERATION_COUNT} ready`)
diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js
index 20ff4f0..0d15098 100644
--- a/src/screens/Home/Home.js
+++ b/src/screens/Home/Home.js
@@ -217,6 +217,17 @@ const Home = ({ navigation, route }) => {
const hasSongUrl = typeof menuProject?.songUrl === 'string' && menuProject.songUrl.length > 0
if (hasSongUrl) {
+ items.push({
+ label: 'Télécharger',
+ onPress: () => {
+ projectDropdownRef.current?.close?.()
+ navigate(Routes.SongDownload, {
+ project: menuProject,
+ skipAdventureGate: true,
+ promptPurchaseConfirm: true,
+ })
+ },
+ })
items.push({
label: 'Jouer la musique',
onPress: () => {
diff --git a/src/screens/cover/ChooseCoverType.js b/src/screens/cover/ChooseCoverType.js
index 8a7e979..0d79261 100644
--- a/src/screens/cover/ChooseCoverType.js
+++ b/src/screens/cover/ChooseCoverType.js
@@ -34,6 +34,7 @@ export const ChooseCoverType = () => {
const hasGeneratedOptions = Array.isArray(selectedProject?.cover?.options)
? selectedProject.cover.options.length > 0
: false
+ const generateCoverLabel = hasFinalCover ? 'Modifier la pochette' : 'Générer la pochette'
const hasArtistPreference = useMemo(() => {
if (currentUserData?.userName) return true
@@ -223,9 +224,8 @@ export const ChooseCoverType = () => {
onPress={handlePickUserImage}
/> */}
diff --git a/src/screens/cover/PhotoCover.js b/src/screens/cover/PhotoCover.js
index f5f5bc7..c25aaba 100644
--- a/src/screens/cover/PhotoCover.js
+++ b/src/screens/cover/PhotoCover.js
@@ -76,7 +76,7 @@ const PhotoCover = () => {
diff --git a/src/screens/cover/PouchReady.js b/src/screens/cover/PouchReady.js
index f4f7740..1ad5070 100644
--- a/src/screens/cover/PouchReady.js
+++ b/src/screens/cover/PouchReady.js
@@ -33,18 +33,14 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import ProgressBar from '../../components/ProgressBar'
const COVER_STYLE_PRESETS = [
- 'Cyberpunk',
- 'Dessins animé',
- 'Dessins animé rétro',
- 'Portrait théâtralisé',
- 'Livre de coloriage',
- 'Shooting',
- 'Réaliste',
- 'Artistique',
- 'Afro',
- 'Grunge',
- 'Pop Art'
-
+ 'Réaliste (style cinématographique)',
+ 'Artistique (style peinture moderne)',
+ 'Style Dessins',
+ 'Style rétros',
+ 'Style cyberpunk',
+ 'Style Afro',
+ 'Style grunge',
+ 'Style pop art',
]
const COVER_PROGRESS_MAX = 98
const COVER_PROGRESS_INTERVAL_MS = 500
@@ -120,10 +116,10 @@ const PouchReady = () => {
}, [selectedProject?.coverStyle])
const generateCover = useCallback(
- async (styleValue) => {
+ async (styleValue, { allowRegeneration = false } = {}) => {
if (!selectedProjectId) return
- // We allow regeneration if isEditing is true
- if (hasGeneratedOptions && !isEditing) {
+ // We allow regeneration if isEditing or explicitly requested
+ if (hasGeneratedOptions && !isEditing && !allowRegeneration) {
return
}
const trimmedStyle = String(styleValue || '').trim()
@@ -171,6 +167,18 @@ const PouchReady = () => {
generateCover(trimmedStyle)
}, [customStyle, generateCover, selectedPresetStyle, styleMode])
+ const handleRegenerateSameStyle = useCallback(() => {
+ const storedStyle =
+ typeof selectedProject?.coverStyle === 'string' ? selectedProject.coverStyle.trim() : ''
+ const fallbackStyle = styleMode === 'preset' ? selectedPresetStyle : customStyle
+ const trimmedStyle = (storedStyle || fallbackStyle || '').trim()
+ if (!trimmedStyle) {
+ alert('Attention', 'Merci de renseigner un style pour la pochette.', [{ text: 'OK' }])
+ return
+ }
+ generateCover(trimmedStyle, { allowRegeneration: true })
+ }, [customStyle, generateCover, selectedPresetStyle, selectedProject?.coverStyle, styleMode])
+
const coverPreviewUrl =
selectedProject?.cover?.result || selectedProject?.cover?.generatedBackground || null
@@ -542,15 +550,21 @@ const PouchReady = () => {
setIsPresetDropdownOpen(false)
}}
/>
-
-
- Donne les instructions que tu veux pour créer ta pochette
+
+ (donne les instructions que tu veux pour créer ta pochette)
{styleMode === 'custom' && (
{
<>
-
+
setIsEditing(true)}
diff --git a/src/screens/cover/SongDownload.js b/src/screens/cover/SongDownload.js
index 4d39f19..27b14d0 100644
--- a/src/screens/cover/SongDownload.js
+++ b/src/screens/cover/SongDownload.js
@@ -40,6 +40,8 @@ const SongDownload = ({ route }) => {
project: routeProject,
selectedOption: routeSelectedOption,
coverOptions: routeCoverOptions,
+ skipAdventureGate = false,
+ promptPurchaseConfirm = false,
} = route?.params || {}
const { selectedProject, updateProjectData, hasActiveSubscription } = useUser()
const { createSongDownloadCheckout } = useStripe()
@@ -53,7 +55,9 @@ const SongDownload = ({ route }) => {
const [showIntro, setShowIntro] = useState(null)
const [showAdventureModal, setShowAdventureModal] = useState(false)
const [showShareModal, setShowShareModal] = useState(false)
- const [adventureChoice, setAdventureChoice] = useState('pending')
+ const [adventureChoice, setAdventureChoice] = useState(() =>
+ skipAdventureGate ? 'stop' : 'pending'
+ )
const adventureGateTriggeredRef = useRef(false)
const projectForStage = useMemo(() => {
@@ -467,6 +471,18 @@ const SongDownload = ({ route }) => {
)
return
}
+ if (promptPurchaseConfirm) {
+ Alert.alert(
+ 'Acheter ce morceau',
+ 'Voulez-vous acheter ce morceau pour 1,99€ ?',
+ [
+ { text: 'Annuler', style: 'cancel' },
+ { text: 'Acheter', onPress: () => startSongDownloadCheckout() },
+ ],
+ { cancelable: true }
+ )
+ return
+ }
startSongDownloadCheckout()
}, [
canDownloadDirectly,
@@ -474,6 +490,7 @@ const SongDownload = ({ route }) => {
handleDownload,
isCheckoutLaunching,
isDownloading,
+ promptPurchaseConfirm,
startSongDownloadCheckout,
])
diff --git a/src/screens/cover/ValidateCover.js b/src/screens/cover/ValidateCover.js
index 4f434f0..82eedb7 100644
--- a/src/screens/cover/ValidateCover.js
+++ b/src/screens/cover/ValidateCover.js
@@ -55,8 +55,7 @@ const ValidateCover = () => {
if (
!option ||
option?.id === selectedOptionId ||
- isSelecting ||
- selectedOptionId // Prevent re-selection
+ isSelecting
) {
return
}