From b5d54226b11c997994b6d732b5d6b99ff1117263 Mon Sep 17 00:00:00 2001 From: Leon Date: Tue, 24 Feb 2026 10:54:19 +0100 Subject: [PATCH] re choose cover and new prompt --- functions/helpers/prompts.js | 292 ++++++++++++++++++++++----- functions/src/cover.js | 114 ++++++----- src/screens/Home/Home.js | 11 + src/screens/cover/ChooseCoverType.js | 4 +- src/screens/cover/PhotoCover.js | 2 +- src/screens/cover/PouchReady.js | 59 ++++-- src/screens/cover/SongDownload.js | 19 +- src/screens/cover/ValidateCover.js | 3 +- 8 files changed, 376 insertions(+), 128 deletions(-) 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(` + + + + + + + ${safeText} +`) } /** @@ -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 }