From 0165e8b44d98f6d3c766d1f269a95b48cb06f7c3 Mon Sep 17 00:00:00 2001 From: Leon Morival Date: Tue, 11 Aug 2026 13:19:25 +0200 Subject: [PATCH] feat: filter by lang --- bun.lock | 2 + functions/helpers/gemini.js | 2 +- functions/src/lyrics.js | 13 +++ src/hooks/useSocialAuth.js | 19 +++-- src/providers/UserDataProvider.js | 2 + src/screens/HitParade/HitParade.js | 110 +++++++++++++++++++++----- src/screens/Register.js | 1 + src/screens/Writing/CreatingLyrics.js | 19 ++++- 8 files changed, 139 insertions(+), 29 deletions(-) diff --git a/bun.lock b/bun.lock index ac63c70..61f68ab 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "MusicLand", @@ -110,6 +111,7 @@ }, "overrides": { "firebase": "~9.20.0", + "react-error-overlay": "6.0.9", }, "packages": { "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], diff --git a/functions/helpers/gemini.js b/functions/helpers/gemini.js index d00bb79..7f17714 100644 --- a/functions/helpers/gemini.js +++ b/functions/helpers/gemini.js @@ -7,7 +7,7 @@ const { setTimeout } = require('timers/promises') // --- CONFIGURATION --- // Plus intelligente que le Flash original, ultra rapide, et stable sur l'API. -const TEXT_MODEL_NAME = 'gemini-3.5-flash' +const TEXT_MODEL_NAME = 'gemini-flash-latest' const IMAGE_MODEL_NAME = 'gemini-3-pro-image' // --- SINGLETON PATTERN (WARM START) --- diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js index 45320f5..e27b852 100644 --- a/functions/src/lyrics.js +++ b/functions/src/lyrics.js @@ -140,6 +140,7 @@ const buildModerationBrief = ({ exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = {}, data = {} }) => { try { const { + lang: rawLang, objective = '', context = '', style = '', @@ -148,6 +149,15 @@ exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = { structure: rawStructure = ['couplet', 'refrain', 'couplet', 'refrain'], rhymes = '', } = data + if (!['fr', 'en'].includes(rawLang)) { + throw new HttpsError('invalid-argument', 'La langue doit être "fr" ou "en".') + } + const lang = rawLang + const languageLabel = lang === 'en' ? 'English' : 'français' + const languageInstruction = + lang === 'en' + ? 'Write the title, lyrics and description in English.' + : 'Rédige le titre, les paroles et la description en français.' const rhymesInstruction = resolveRhymesInstruction(rhymes) // 1. Préparation Structure @@ -201,6 +211,7 @@ Tu es le meilleur parolier musical actuel, expert en "Songwriting" pour les IA g Ta spécialité est la **Prosodie** (le rythme naturel des mots) et l'impact émotionnel. TES RÈGLES D'OR : +0. **LANGUE** : ${languageInstruction} 1. **MÉTRIQUE & RYTHME** : Tes vers doivent être "chantables". Compte les syllabes pour qu'elles collent au style musical demandé. Évite les phrases trop longues ou imprononçables rapidement. 2. **RIMES** : Suis strictement du brief. Si "Sans rimes" ou "Mélange des deux" est demandé, respecte l'absence/alternance de rimes. Si rien n'est indiqué, fais des rimes riches et évite les rimes faciles (amour/toujours). 3. **SHOW, DON'T TELL** : N'écris pas "Je suis triste", écris "La pluie brouille mes carreaux". Utilise des détails sensoriels. @@ -224,6 +235,7 @@ Retourne UNIQUEMENT un JSON valide. const prompt = ` + ${languageLabel} ${objective || 'Créer une chanson mémorable'} ${context || 'Libre interprétation'} ${emotion || 'Intense'} @@ -237,6 +249,7 @@ ${structureTags || fallbackStructureTags} + 0. ${languageInstruction} 1. Génère un **Titre** percutant (moins de 6 mots). 2. Pour chaque section
, écris le contenu adapté : - Si c'est "Instrumental" ou "Solo" : laisse le champ 'lyrics' vide ou mets une brève indication d'ambiance entre parenthèses ex: "(Solo de guitare déchirant)". diff --git a/src/hooks/useSocialAuth.js b/src/hooks/useSocialAuth.js index 7868b5f..204b2a1 100644 --- a/src/hooks/useSocialAuth.js +++ b/src/hooks/useSocialAuth.js @@ -101,6 +101,9 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => { typeof overrides?.lastName === 'string' ? overrides.lastName.trim() : null const overrideDisplayName = typeof overrides?.displayName === 'string' ? overrides.displayName.trim() : null + const overridePreferredLanguage = ['fr', 'en'].includes(overrides?.preferredLanguage) + ? overrides.preferredLanguage + : null const { firstName: nameFromDisplay, lastName: lastFromDisplay } = splitDisplayName( user.displayName @@ -124,6 +127,10 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => { payload.lastName = lastFromDisplay } + if (overridePreferredLanguage) { + payload.preferredLanguage = overridePreferredLanguage + } + await docRef.set(payload, { merge: true }) return { isNewUser, @@ -138,7 +145,7 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => { } } -const useSocialAuth = ({ onSuccess } = {}) => { +const useSocialAuth = ({ onSuccess, preferredLanguage = null } = {}) => { const [, setTooltip] = useGlobal('_tooltip') const [isLoading, setIsLoading] = useState(false) const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false) @@ -235,7 +242,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { const userCredential = await firebase.auth().signInWithCredential(credential) const authResult = await ensureUserDocument( userCredential?.user, - {}, + { preferredLanguage }, userCredential?.additionalUserInfo?.isNewUser === true ) if (typeof onSuccess === 'function') { @@ -253,7 +260,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { setIsLoading(false) } }, - [onSuccess, redirectUri, request, setTooltip] + [onSuccess, preferredLanguage, redirectUri, request, setTooltip] ) useEffect(() => { @@ -316,7 +323,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { const userCredential = await firebase.auth().signInWithPopup(provider) const authResult = await ensureUserDocument( userCredential?.user, - {}, + { preferredLanguage }, userCredential?.additionalUserInfo?.isNewUser === true ) if (typeof onSuccess === 'function') { @@ -370,6 +377,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { isLoading, isWeb, onSuccess, + preferredLanguage, promptAsync, setTooltip, ]) @@ -422,6 +430,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { { firstName, lastName, + preferredLanguage, displayName: userCredential?.user?.displayName || [firstName, lastName].filter(Boolean).join(' ') || @@ -447,7 +456,7 @@ const useSocialAuth = ({ onSuccess } = {}) => { setTooltip({ text: message, type: 'error' }) setIsLoading(false) } - }, [isAppleSignInAvailable, isLoading, onSuccess, setTooltip]) + }, [isAppleSignInAvailable, isLoading, onSuccess, preferredLanguage, setTooltip]) return { signInWithGoogle, diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index c39a3bb..133fbc7 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -222,9 +222,11 @@ export default ({ children }) => { try { await setIsLoading(true) const artistDisplayName = getUserPreferredArtistName(currentUserDoc || currentUserData || {}) + const lang = currentUserDoc?.preferredLanguage === 'en' ? 'en' : 'fr' const payload = { userId: currentUID || null, userName: artistDisplayName, + lang, createdAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(), } diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index 6220c26..da0749e 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -1,13 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { - FlatList, - Image, - Platform, - ScrollView, - StyleSheet, - Text, - View, -} from 'react-native' +import { FlatList, Image, Platform, ScrollView, StyleSheet, Text, View } from 'react-native' import { BlurView } from 'expo-blur' import { responsiveHeight } from 'react-native-responsive-dimensions' import { background, icons } from '../../assets' @@ -29,6 +21,30 @@ import SongCard from './components/SongCard' import ClubCard from '../Home/components/ClubCard' const allowedPremiumLevels = new Set(['starter', 'pro', 'premium']) +const languageOptions = [ + { value: 'all', label: 'Toutes' }, + { value: 'fr', label: 'Français' }, + { value: 'en', label: 'English' }, +] + +const StreamingLanguageFilter = ({ selectedLanguage, onSelectLanguage }) => ( + + Langue des musiques + + {languageOptions.map((option) => ( + onSelectLanguage(option.value)} + tint={selectedLanguage === option.value ? 'light' : 'dark'} + containerStyle={styles.languageFilterButton} + titleStyle={styles.languageFilterButtonText} + size="small" + /> + ))} + + +) const normalizePremiumLevel = (value) => { if (typeof value !== 'string') { @@ -48,13 +64,19 @@ const chunkArray = (items = [], size = 10) => { const HitParade = () => { const [selectedCategory, setSelectedCategory] = useState('Chansons') + const [selectedLanguage, setSelectedLanguage] = useState('all') const navigateToMusicDetails = useNavigateToMusicDetails() + const allSongsRef = useMemo(() => { + const songsRef = projectsRef.where('hasSong', '==', true) + return selectedLanguage === 'all' ? songsRef : songsRef.where('lang', '==', selectedLanguage) + }, [selectedLanguage]) const { data: allSongs } = useDataFromRef({ - ref: projectsRef.where('hasSong', '==', true), + ref: allSongsRef, format: (docs) => docs.filter((item) => item?.hasPlayback !== true), simpleRef: false, listener: true, condition: true, + refreshArray: [selectedLanguage], }) const { data: topMonthSongs } = useDataFromRef({ @@ -62,7 +84,8 @@ const HitParade = () => { format: (docs) => docs.filter((item) => item?.hasPlayback !== true), simpleRef: false, listener: false, - condition: true, + condition: selectedLanguage === 'all', + refreshArray: [selectedLanguage], }) const { data: allTimeSongs } = useDataFromRef({ @@ -70,20 +93,31 @@ const HitParade = () => { format: (docs) => docs.filter((item) => item?.hasPlayback !== true), simpleRef: false, listener: false, - condition: true, + condition: selectedLanguage === 'all', + refreshArray: [selectedLanguage], }) const songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs]) - const topMonthList = useMemo( - () => (Array.isArray(topMonthSongs) ? topMonthSongs : []), - [topMonthSongs] - ) + const topMonthList = useMemo(() => { + if (selectedLanguage === 'all') { + return Array.isArray(topMonthSongs) ? topMonthSongs : [] + } + return [...songsList] + .filter((item) => typeof item?.monthViews === 'number' && item.monthViews > 0) + .sort((a, b) => b.monthViews - a.monthViews) + .slice(0, 20) + }, [selectedLanguage, songsList, topMonthSongs]) - const allTimeList = useMemo( - () => (Array.isArray(allTimeSongs) ? allTimeSongs : []), - [allTimeSongs] - ) + const allTimeList = useMemo(() => { + if (selectedLanguage === 'all') { + return Array.isArray(allTimeSongs) ? allTimeSongs : [] + } + return [...songsList] + .filter((item) => typeof item?.views === 'number' && item.views > 0) + .sort((a, b) => b.views - a.views) + .slice(0, 20) + }, [allTimeSongs, selectedLanguage, songsList]) // === RENDUS PAR PLATEFORME === const renderSongsMobile = () => ( @@ -254,7 +288,12 @@ const HitParade = () => { [setSearchSelected] ) - const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics]) + const musicResults = useMemo(() => { + const results = Array.isArray(musics) ? musics : [] + return selectedLanguage === 'all' + ? results + : results.filter((project) => project?.lang === selectedLanguage) + }, [musics, selectedLanguage]) const [creatorsById, setCreatorsById] = useState({}) @@ -632,6 +671,10 @@ const HitParade = () => { style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }} /> */} + {isWeb && ( { } export default HitParade + +const styles = StyleSheet.create({ + languageFilter: { + width: '100%', + maxWidth: 620, + alignSelf: 'center', + gap: 8, + }, + languageFilterLabel: { + color: Palette.white, + fontSize: 13, + fontFamily: FONT_FAMILY.InterSemiBold, + textAlign: 'center', + }, + languageFilterButtons: { + flexDirection: 'row', + gap: 10, + }, + languageFilterButton: { + flex: 1, + }, + languageFilterButtonText: { + fontSize: 13, + }, +}) diff --git a/src/screens/Register.js b/src/screens/Register.js index 48d3c0d..d032ac8 100644 --- a/src/screens/Register.js +++ b/src/screens/Register.js @@ -56,6 +56,7 @@ const Register = () => { isLoading: socialLoading, } = useSocialAuth({ onSuccess: afterSocialAuth, + preferredLanguage, }) const isBusy = socialLoading diff --git a/src/screens/Writing/CreatingLyrics.js b/src/screens/Writing/CreatingLyrics.js index c0dda16..ef5d060 100644 --- a/src/screens/Writing/CreatingLyrics.js +++ b/src/screens/Writing/CreatingLyrics.js @@ -29,7 +29,8 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => { const [error, setError] = useState(null) const hasShownErrorAlert = useRef(false) const { setIsLoading } = useMinuit() - const { updateProjectData } = useUser() + const { selectedProject, updateProjectData } = useUser() + const projectLang = selectedProject?.lang === 'en' ? 'en' : 'fr' const progressValue = Math.max(0, Math.min(100, Math.round(progress))) // Reset when becomes active @@ -74,6 +75,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => { const sanitizedStructure = sanitizeStructureList(config?.structure, CUSTOM_SANITIZE_OPTIONS) const callable = firebase.functions().httpsCallable('lyrics-generateLyrics') const { data } = await callable({ + lang: projectLang, objective: config?.objective, context: config?.context, emotion: config?.emotion, @@ -188,6 +190,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => { } await updateProjectData({ + lang: projectLang, title: result?.title || '', lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], description: result?.lyricsDescription, @@ -206,7 +209,18 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => { if (active && result && progress >= 100 && !saved && !error) { autoSaveAndGo() } - }, [active, result, progress, saved, error, setIsLoading, updateProjectData, config, selections]) + }, [ + active, + result, + progress, + saved, + error, + setIsLoading, + updateProjectData, + config, + selections, + projectLang, + ]) useEffect(() => { if (error && !hasShownErrorAlert.current) { @@ -303,6 +317,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => { await setIsLoading(true) console.log('📄 [CreatingLyrics] Consultation manuelle du texte') await updateProjectData({ + lang: projectLang, title: result?.title || '', lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], config: config || null,