re choose cover and new prompt

This commit is contained in:
2026-02-24 10:54:19 +01:00
parent e608c17775
commit b5d54226b1
8 changed files with 376 additions and 128 deletions
+243 -49
View File
@@ -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')
+61 -53
View File
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
/**
* 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(`
<svg width="${safeWidth}" height="${safeHeight}" viewBox="0 0 ${safeWidth} ${safeHeight}" xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="brandShadow" x="-50%" y="-50%" width="200%" height="200%">
<feDropShadow dx="0" dy="${shadowDy}" stdDeviation="${shadowBlur}" flood-color="rgba(0,0,0,0.55)" />
</filter>
</defs>
<text
x="${safeWidth - margin}"
y="${safeHeight - margin}"
text-anchor="end"
dominant-baseline="alphabetic"
font-family="${BRAND_FONT_FAMILY}"
font-size="${fontSize}"
font-style="italic"
font-weight="700"
letter-spacing="${letterSpacing}"
fill="rgba(255,255,255,0.95)"
stroke="rgba(0,0,0,0.45)"
stroke-width="${strokeWidth}"
paint-order="stroke"
filter="url(#brandShadow)"
>${safeText}</text>
</svg>`)
}
/**
@@ -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`)
+11
View File
@@ -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: () => {
+2 -2
View File
@@ -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}
/> */}
<GradientButton
title={hasFinalCover ? 'Pochette déjà validée' : 'Générer la pochette'}
title={generateCoverLabel}
onPress={handleGenerateCover}
disabled={hasFinalCover}
/>
</View>
</View>
+1 -1
View File
@@ -76,7 +76,7 @@ const PhotoCover = () => {
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta pochette MusicLand Production"
subTitle="Le logo est automatiquement ajouté en bas à droite."
subTitle="Le texte Musicland.ai est automatiquement ajouté en bas à droite."
/>
<View style={{ flex: 1, ...Style.containerCenter }}>
<View style={{ width: '80%', position: 'relative' }}>
+39 -20
View File
@@ -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)
}}
/>
<Text style={{ color: Palette.white, fontSize: 12, paddingLeft: 24, paddingTop: 8 }}>
Donne les instructions que tu veux pour créer ta pochette
<Text
style={{
color: Palette.white,
fontSize: 12,
paddingLeft: 24,
paddingTop: 8,
}}
>
(donne les instructions que tu veux pour créer ta pochette)
</Text>
</View>
{styleMode === 'custom' && (
<View style={styles.customInputWrapper}>
<TextInput
placeholder="Exemple : Collage rétro futuriste lumineux"
placeholder="Exemple : Collage rétro futuriste contrasté"
placeholderTextColor={Palette.grayMid}
value={customStyle}
onChangeText={setCustomStyle}
@@ -608,7 +622,12 @@ const PouchReady = () => {
<>
<View style={{ flexDirection: 'column', gap: 12, width: '100%' }}>
<GradientButton title={'Valider la pochette'} onPress={onValidatePicture} />
<BorderGradientButton
title="Regénérer une autre proposition"
onPress={handleRegenerateSameStyle}
disabled={isGenerating}
textStyle={{ fontSize: 13 }}
/>
<BorderGradientButton
title="Pas satisfait ? Modifier ma demande"
onPress={() => setIsEditing(true)}
+18 -1
View File
@@ -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,
])
+1 -2
View File
@@ -55,8 +55,7 @@ const ValidateCover = () => {
if (
!option ||
option?.id === selectedOptionId ||
isSelecting ||
selectedOptionId // Prevent re-selection
isSelecting
) {
return
}