Merge remote-tracking branch 'origin/jacques'

# Conflicts:
#	functions/helpers/gemini.js
#	functions/src/heygen.js
#	functions/src/lyrics.js
#	functions/src/music.js
#	src/screens/CreatePassword.js
#	src/screens/Home/Home.js
#	src/screens/Playback/PlaybackAi.js
#	src/screens/Production/PlaybackDownload.js
This commit is contained in:
leon
2026-09-08 14:37:47 +02:00
63 changed files with 1637 additions and 426 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ const CreatePassword = ({ navigation }) => {
return (
<Page
width={isWeb ? 600 : null}
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
backgroundImg={background.loginBgWeb}
headerType="NAVIGATION"
title="Inscription"
>
+25 -12
View File
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
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'
import { background, img } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import SearchBar from '../../components/SearchBar'
import ShareBtn from '../../components/ShareBtn/ShareBtn'
@@ -18,7 +18,7 @@ import MobileCoinBadge from '../../components/MobileCoinBadge'
import ResearchHeader from '../Library/components/ResearchHeader'
import SearchResultsList from '../Library/components/SearchResultsList'
import SongCard from './components/SongCard'
import ClubCard from '../Home/components/ClubCard'
import SpaceLogoCard from '../../components/SpaceLogoCard'
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
const languageOptions = [
@@ -466,11 +466,25 @@ const HitParade = () => {
})
}, [navigateToMusicDetails, songsList])
const heroTitle = isWeb
? 'Coups de coeur des utilisateurs'
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`
const heroSubtitle = 'Les 3 musiques les plus écoutées du mois reçoivent des tokens.'
const heroContent = {
Chansons: {
title: 'Nouveautés Streaming',
subtitle: 'Découvre les morceaux publiés par la communauté MusicLand.',
},
'Top du mois': {
title: 'Les plus écoutés ce mois-ci',
subtitle: 'Ce classement est basé sur le nombre d’écoutes du mois en cours.',
},
'All Time': {
title: 'Les plus écoutés de tous les temps',
subtitle: 'Ce classement est basé sur le nombre total d’écoutes.',
},
}
const activeHeroContent = heroContent[selectedCategory] || heroContent.Chansons
const heroTitle = isWeb ? 'Streaming : nouveautés et classements' : activeHeroContent.title
const heroSubtitle = isWeb
? 'Découvre les nouveautés, le Top du mois et les titres les plus écoutés de tous les temps.'
: activeHeroContent.subtitle
const renderHero = () => (
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
@@ -681,11 +695,10 @@ const HitParade = () => {
</View>
) : null}
<View style={{ gap: 12, flex: 1 }}>
{/* <Image
source={icons.musicLandLogo}
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
/> */}
<ClubCard />
<SpaceLogoCard
source={img.musiclandStreaming}
accessibilityLabel="Streaming MusicLand"
/>
<StreamingLanguageFilter
selectedLanguage={selectedLanguage}
onSelectLanguage={setSelectedLanguage}
+20 -5
View File
@@ -65,9 +65,12 @@ const STAGE_CARD_CONTENT = [
{
key: 'director',
step: 'ÉTAPE 3',
description:
'Tu vas faire une expérience extraordinaire, tu vas te filmer en train d’interpreter ta chanson en Play Back et je vais te guider pour te faciliter la tâche!',
description: {
fr: 'Je suis Mr Benaï, responsable de MusicLand Production et je vais t’expliquer toute la finalité de Musicland.',
en: "I'm Mr. Benaï, the head of MusicLand Production, and I'm going to explain to you what MusicLand is all about.",
},
image: cardsImg.video,
imageStyle: { transform: [{ translateY: 10 }] },
imagePosition: 'left',
textAlign: 'left',
lockSide: 'right',
@@ -91,6 +94,7 @@ const Home = ({ navigation, route }) => {
const { setTooltip } = useMinuit()
const [videoUrl, setVideoUrl] = useState(null)
const navigateToMusicDetails = useNavigateToMusicDetails()
const preferredLanguage = currentUserData?.preferredLanguage === 'en' ? 'en' : 'fr'
if (!currentUID) {
return <LandingPage />
@@ -314,7 +318,9 @@ const Home = ({ navigation, route }) => {
const songwriterAction = useMemo(() => getStageAction('songwriter', null), [])
const handleStartNew = useCallback(async () => {
const targetRoute = songwriterAction?.route || Routes.CreateLyricsWithAi
const targetRoute = isWeb
? songwriterAction?.route || Routes.CreateLyricsWithAi
: Routes.WritingLyrics
const targetParams = songwriterAction?.params
try {
@@ -390,9 +396,13 @@ const Home = ({ navigation, route }) => {
() =>
STAGE_CARD_CONTENT.map((card) => ({
...card,
description:
typeof card.description === 'string'
? card.description
: card.description[preferredLanguage],
isLocked: stageStatesByKey[card.key]?.isLocked ?? true,
})),
[stageStatesByKey]
[preferredLanguage, stageStatesByKey]
)
const handleStagePress = useCallback(
@@ -413,8 +423,12 @@ const Home = ({ navigation, route }) => {
if (!action?.route) {
return
}
const targetRoute =
!isWeb && stageKey === 'songwriter' && action.route === Routes.CreateLyricsWithAi
? Routes.WritingLyrics
: action.route
console.log("action route is ", action.route)
navigate(action.route, action.params)
navigate(targetRoute, action.params)
},
[currentProject, ensureProjectSelected, hasActiveProject, handleStartNew]
)
@@ -438,6 +452,7 @@ const Home = ({ navigation, route }) => {
step={card.step}
description={card.description}
image={card.image}
imageStyle={card.imageStyle}
imagePosition={card.imagePosition}
textAlign={card.textAlign}
isLocked={card.isLocked}
+2 -2
View File
@@ -16,7 +16,7 @@ const ClubCard = ({ onPress, containerStyle = null, variant = 'compact' }) => {
<Pressable
onPress={onPress}
accessibilityRole="button"
accessibilityLabel="Club MusicLand"
accessibilityLabel="Streaming MusicLand"
style={({ pressed }) => [
styles.card,
cardBaseStyle,
@@ -24,7 +24,7 @@ const ClubCard = ({ onPress, containerStyle = null, variant = 'compact' }) => {
pressed && styles.cardPressed,
]}
>
<Image source={img.musiclandClub} style={imageStyle} />
<Image source={img.musiclandStreaming} style={imageStyle} />
</Pressable>
)
}
+5
View File
@@ -10,6 +10,7 @@ const StageCard = ({
step,
description,
image,
imageStyle = null,
imagePosition = 'left',
textAlign = 'left',
isLocked,
@@ -79,6 +80,7 @@ const StageCard = ({
: isImageOnLeft
? { alignSelf: 'flex-end' }
: { alignSelf: 'flex-start' },
imageStyle,
]}
/>
</View>
@@ -206,6 +208,9 @@ const StageCard = ({
<Pressable
onPress={onPress}
disabled={isLocked}
accessibilityRole="button"
accessibilityLabel={`${step} — ${description}`}
accessibilityState={{ disabled: !!isLocked }}
style={({ pressed }) => [
basePressableStyle,
containerStyle,
+12 -2
View File
@@ -2,7 +2,9 @@ import AsyncStorage from '@react-native-async-storage/async-storage'
import { useCallback, useMemo, useState } from 'react'
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native'
import { background, icons, videos } from '../assets'
import BackgroundVideo from '../components/BackgroundVideo'
import FullscreenIntroVideo from '../components/FullscreenIntroVideo'
import { isWeb } from '../hooks/useLayoutType'
import Page from '../layouts/Page'
import { Routes } from '../navigation'
import { navigate, reset } from '../navigation/NavigationService'
@@ -85,7 +87,15 @@ export default function LandingPage({ route }) {
return (
<>
<Page title="Landing Page" backgroundImg={background.homeBGWeb}>
<Page
title="Landing Page"
backgroundImg={background.homeBGWeb}
backgroundContent={
phase === FLOW_PHASE.LANGUAGE ? (
<BackgroundVideo source={videos.languageBackground} silent />
) : null
}
>
<View
style={{
flex: 1,
@@ -121,7 +131,7 @@ export default function LandingPage({ route }) {
url={activeVideo}
visible
onClose={activeVideoClose}
contentFit="contain"
contentFit={isWeb || isJingle ? 'contain' : 'cover'}
allowSkip={!isJingle}
requireAudio={!isJingle}
offerSoundActivation={isJingle}
+1 -1
View File
@@ -48,7 +48,7 @@ const Library = () => {
hasLikedClips
return (
<Page backgroundImg={background.libraryBG} headerType="NONE">
<Page backgroundImg={background.libraryMainBackground} headerType="NONE">
{Platform.OS !== 'web' ? (
<View
style={{
+1 -1
View File
@@ -111,7 +111,7 @@ const Library = () => {
}, [dropdownVisible, closeDropdown])
return (
<Page backgroundImg={background.libraryBgWeb} headerType="NONE" width={'80%'}>
<Page backgroundImg={background.libraryMainBackground} headerType="NONE" width={'80%'}>
<View style={{ gap: 17, paddingBottom: 20, zIndex: dropdownVisible ? 40 : 1 }}>
<Image
source={icons.musicLandLogo}
+29 -21
View File
@@ -1,16 +1,15 @@
import React, { useCallback, useMemo } from 'react'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import { useIsFocused } from '@react-navigation/native'
import { background } from '../../assets'
import BackgroundVideo from '../../components/BackgroundVideo'
import { background, videos } from '../../assets'
import BorderGradientButton from '../../components/BorderGradientButton'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import Page from '../../layouts/Page'
import { isWeb } from '../../hooks/useLayoutType'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUser, useUserData } from '../../providers/UserDataProvider'
import { useUserData } from '../../providers/UserDataProvider'
import { gutters, Palette } from '../../styles'
import {
getPlaybackAiStatusLabel,
@@ -19,11 +18,25 @@ import {
} from '../../utils/playbackAi'
const Playback = ({ route, navigation }) => {
const isFocused = useIsFocused()
const { project: routeProject, returnToAdventureModal = false } = route.params || {}
const { videos } = useUser()
const { selectedProject } = useUserData() || {}
const theoUrl = isWeb ? videos?.theoWeb : videos?.theo
const { currentUserData, selectedProject } = useUserData() || {}
const [videoUrl, setVideoUrl] = useState(null)
const preferredLanguage = currentUserData
? currentUserData.preferredLanguage === 'en'
? 'en'
: 'fr'
: null
useEffect(() => {
if (!preferredLanguage) {
return
}
setVideoUrl(
preferredLanguage === 'en' ? videos.videoSpaceIntroEn : videos.videoSpaceIntroFr
)
}, [preferredLanguage])
const project = useMemo(() => {
if (routeProject?.id && selectedProject?.id === routeProject.id) {
return selectedProject
@@ -69,14 +82,6 @@ const Playback = ({ route, navigation }) => {
<Page
headerType="NONE"
backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}
backgroundContent={
isFocused ? (
<BackgroundVideo
source={theoUrl}
soundButtonStyle={isWeb ? styles.rightSideControl : undefined}
/>
) : null
}
containerStyle={isWeb ? styles.videoPage : undefined}
>
{/* <Image source={ai.theo} style={styles.img} resizeMode="contain" /> */}
@@ -115,6 +120,13 @@ const Playback = ({ route, navigation }) => {
{/* <BorderGradientButton title="Importer une vidéo" /> */}
</View>
</View>
<FullscreenIntroVideo
url={videoUrl}
visible={!!videoUrl}
onClose={() => setVideoUrl(null)}
contentFit={isWeb ? 'contain' : 'cover'}
skipLabel={preferredLanguage === 'en' ? 'Skip video' : 'Passer la vidéo'}
/>
</Page>
)
}
@@ -126,10 +138,6 @@ const styles = StyleSheet.create({
alignSelf: 'flex-start',
marginLeft: '4%',
},
rightSideControl: {
right: 24,
left: 'auto',
},
blockedText: {
color: Palette.white,
opacity: 0.75,
+1 -1
View File
@@ -193,7 +193,7 @@ const PlaybackAi = ({ route }) => {
return (
<Page
backgroundImg={background.playbackBG2}
backgroundImg={Platform.OS === 'web' ? background.playbackWeb : background.playbackMobile}
backgroundColor="#07111B"
headerType="NONE"
>
+1 -1
View File
@@ -96,7 +96,7 @@ const PlaybackAiStatus = ({ route }) => {
return (
<Page
backgroundImg={background.playbackBG2}
backgroundImg={background.playbackMobile}
backgroundColor="#07111B"
headerType="NONE"
>
+1 -1
View File
@@ -91,7 +91,7 @@ const PlaybackAiStatus = ({ route }) => {
return (
<Page
backgroundImg={background.playbackBG2}
backgroundImg={background.playbackWeb}
backgroundColor="#07111B"
headerType="NONE"
>
+40 -26
View File
@@ -27,14 +27,13 @@ const PlaybackGuide = ({ navigation }) => {
showsVerticalScrollIndicator={false}
>
<Text style={styles.title}>
Le Guide Ultime du Playback de TA Chanson : L'Expérience Créative Totale
Le Guide Ultime du Playback L'Expérience Créative Totale
</Text>
<Text style={styles.sectionTitle}>
Introduction : Tu Es l'Artiste ET le Performeur !
</Text>
<Text style={styles.sectionTitle}>Introduction</Text>
<Text style={styles.sectionTitle}>Tu es l'artiste et l’interprête!</Text>
<Text style={styles.text}>
Imagine ça : tu as créé Ta chanson, TES paroles, TA mélodie, TON univers. Et
Imagine ça : tu as créé TA chanson, TES paroles, TA mélodie, TON univers. Et
maintenant, tu vas lui donner vie en playback. C'est une expérience UNIQUE au monde !
Tu n'imites personne - tu deviens la version scénique de toi-même. C'est comme avoir
un jumeau qui chante pendant que toi, tu performes. Double dose de créativité, double
@@ -53,9 +52,9 @@ const PlaybackGuide = ({ navigation }) => {
transparaître dans chaque mouvement de tes lèvres.
</Text>
<Text style={styles.subTitle}>La Règle des 15 Écoutes - Version Créateur</Text>
<Text style={styles.subTitle}>La Règle des 12 Écoutes - Version Créateur</Text>
<Text style={styles.text}>
Oui, même pour TA chanson, les 15 écoutes sont cruciales ! Mais avec une twist magique
Oui, même pour TA chanson, les 12 écoutes sont cruciales ! Mais avec une twist magique
:
</Text>
<Text style={styles.text}>
@@ -69,12 +68,12 @@ const PlaybackGuide = ({ navigation }) => {
sur-articulés ?{'\n'}• Où sont tes respirations naturelles dans l'enregistrement ?
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Écoutes 11-15 : La Transformation</Text> Maintenant,
détache-toi du "toi qui a enregistré" pour devenir le "toi qui performe". C'est là que
la magie opère !
<Text style={styles.bold}>Écoutes 11-12 : La Transformation</Text> Maintenant,
détache-toi du "toi qui a enregistré" pour devenir le "toi qui interprête". C'est là
que la magie opère !
</Text>
<Text style={styles.subTitle}>Les Secrets du Créateur-Performeur</Text>
<Text style={styles.subTitle}>Les Secrets du PLAYBACKER</Text>
<Text style={styles.text}>
<Text style={styles.bold}>Le Phénomène de Dédoublement</Text> C'est normal de se
sentir bizarre au début - tu entends ta voix mais tu ne chantes pas ! Embrasse cette
@@ -107,8 +106,9 @@ const PlaybackGuide = ({ navigation }) => {
<Text style={styles.subTitle}>Les Angles Qui Racontent Ton Histoire</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Confession" (Très Proche)</Text> Pour les parties
intimes de ta chanson. Le spectateur doit sentir que tu lui confies un secret.
<Text style={styles.bold}>L'Angle "Confession" (Très Proche)</Text> Pour une
interprêtation intimes de ta chanson. Le spectateur doit sentir que tu lui confies un
secret.
</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Angle "Concert" (Plan Large)</Text> Montre l'artiste
@@ -125,9 +125,10 @@ const PlaybackGuide = ({ navigation }) => {
</Text>
<Text style={styles.text}>C'est encore plus puissant quand c'est TA chanson !</Text>
<Text style={styles.text}>
Si tu as composé avec un instrument :{'\n'}• Mime-le même si tu ne l'as pas sous la
main
{'\n'}• Montre les accords que TU as choisis
Si tu as composé avec un instrument :{'\n'}• Trouve l’instrument de ta chanson et mime
comme si tu savait en jouer.
{'\n'}• Si tu n’en as pas, “fais comme si”, fait semblant, montre les accords que tu
as choisis
{'\n'}• Tes mains connaissent les mouvements par cœur
</Text>
<Text style={styles.text}>
@@ -152,7 +153,7 @@ const PlaybackGuide = ({ navigation }) => {
<Text style={styles.subTitle}>Les Scènes Qui Amplifient Ta Création</Text>
<Text style={styles.text}>
<Text style={styles.bold}>L'Endroit Où Tu l'as Écrite - Le Retour aux Sources</Text>{' '}
Si tu as écrit ta chanson dans ta chambre, filme-là ! C'est puissant de montrer
Si tu as écrit ta chanson dans ta chambre, filme là ! C'est puissant de montrer
l'artiste là où la magie a commencé. L'authenticité sera palpable.
</Text>
<Text style={styles.text}>
@@ -174,10 +175,12 @@ const PlaybackGuide = ({ navigation }) => {
<Text style={styles.subTitle}>Suggestions Créatives Uniques :</Text>
<Text style={styles.text}>
• La Voiture : Si c'est là que tu as eu l'idée de la mélodie
{'\n'}• La Douche : Avec le téléphone protégé, si c'est là que tu chantes toujours
{'\n'}• Le Toit/Balcon : Pour une sensation de "crier au monde" ta création
{'\n'}• Devant le Miroir : Le dialogue entre toi créateur et toi performeur
{'\n'}• Dans le Noir avec une Seule Lumière : Pour l'aspect dramatique et intime
{'\n'}• Dans un endroit insolite: une station service, sur un pont, sur une
balançoire, dans un pré, dans une forêt, dans une piscine,... ta seule limite… c’est
ton imagination!
</Text>
<Text style={styles.chapterTitle}>Chapitre 4 : Le Mindset du Créateur-Performeur</Text>
@@ -226,7 +229,7 @@ const PlaybackGuide = ({ navigation }) => {
:{'\n'}• Les gens découvrent ta musique
{'\n'}• Ils voient ta personnalité
{'\n'}• Ils connectent avec l'artiste complet que tu es
{'\n'}• Ils witness la naissance visuelle de ton art
{'\n'}• Ils participe à la naissance visuelle de ton art
</Text>
<Text style={styles.chapterTitle}>Résumé : Ton Parcours Créatif Unique</Text>
@@ -266,8 +269,10 @@ const PlaybackGuide = ({ navigation }) => {
<Text style={styles.chapterTitle}>Ton Défi Ultime : La Première Mondiale !</Text>
<Text style={styles.text}>
Tu es sur le point de faire quelque chose qu'AUCUN autre artiste ne peut faire : créer
la performance visuelle parfaite de TA chanson. C'est ta première mondiale, ton moment
unique !
la performance visuelle de TA chanson. C'est ta première mondiale, ton moment unique !
</Text>
<Text style={[styles.text, styles.bold]}>
Elle ne doit pas être parfaite, elle doit être authentique!
</Text>
<Text style={styles.text}>
Dans les 24 prochaines heures, donne vie visuellement à ta création. Choisis ta
@@ -283,12 +288,12 @@ const PlaybackGuide = ({ navigation }) => {
C'est ton moment. Ta chanson. Ta performance. Ton histoire.
</Text>
<Text style={styles.text}>
Le monde attend de découvrir l'artiste complet que tu es ! 🎤🎵✨
Le monde attend de découvrir l'artiste qui est en toi ! 🎤🎵✨
</Text>
<Text style={styles.text}>
#MonPlaybackMaChanson #CréateurPerformeur #DoubleArtiste
<Text style={[styles.text, styles.bold]}>
#MonPlaybackMaChanson #Playbacker-performer #Artisteletemps d’une chanson
</Text>
<Text style={styles.text}>
<Text style={[styles.text, styles.italic]}>
P.S. : Garde chaque prise, même les "ratées". Dans 10 ans, tu regarderas ces vidéos et
tu verras le début de ton parcours d'artiste. C'est de l'or pur !
</Text>
@@ -302,7 +307,9 @@ const PlaybackGuide = ({ navigation }) => {
const styles = StyleSheet.create({
container: {
flex: 1,
marginHorizontal: Platform.OS === 'web' ? gutters : 0,
width: '100%',
maxWidth: 900,
alignSelf: 'center',
marginBottom: gutters,
borderRadius: 20,
overflow: 'hidden',
@@ -314,6 +321,7 @@ const styles = StyleSheet.create({
scrollContent: {
padding: gutters,
paddingBottom: gutters * 3,
alignItems: 'center',
},
title: {
fontFamily: FONT_FAMILY.InterBold,
@@ -328,6 +336,7 @@ const styles = StyleSheet.create({
color: Palette.primary,
marginBottom: 10,
marginTop: 10,
textAlign: 'center',
},
chapterTitle: {
fontFamily: FONT_FAMILY.InterBold,
@@ -343,6 +352,7 @@ const styles = StyleSheet.create({
color: Palette.white,
marginBottom: 8,
marginTop: 15,
textAlign: 'center',
},
text: {
fontFamily: FONT_FAMILY.InterRegular,
@@ -350,10 +360,14 @@ const styles = StyleSheet.create({
color: Palette.white,
marginBottom: 10,
lineHeight: 24,
textAlign: 'center',
},
bold: {
fontFamily: FONT_FAMILY.InterBold,
},
italic: {
fontStyle: 'italic',
},
})
export default PlaybackGuide
+225 -110
View File
@@ -1,9 +1,16 @@
import { useFocusEffect } from '@react-navigation/native'
import { useFocusEffect, useIsFocused } from '@react-navigation/native'
import { CameraView, useCameraPermissions } from 'expo-camera'
import { activateKeepAwakeAsync, deactivateKeepAwake } from 'expo-keep-awake'
import * as FileSystem from 'expo-file-system'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { BackHandler, Text, TouchableOpacity, View } from 'react-native'
import {
ActivityIndicator,
BackHandler,
StyleSheet,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
import Svg, { Circle } from 'react-native-svg'
import alert from '../../components/Alert'
@@ -25,6 +32,7 @@ import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
const TIME_BEFORE_INCREMENT_MS = 20000 // 20s
const COUNTDOWN_SECONDS = 10
const CAMERA_STARTUP_DELAY_MS = 250
const CAMERA_READY_TIMEOUT_MS = 8000
const LOG_PREFIX = '[RecordPlayback]'
const KEEP_AWAKE_TAG = 'record-playback'
const log =
@@ -39,6 +47,7 @@ const CAMERA_FACING_OPTIONS = [
const RecordPlayback = ({ route }) => {
const { top, bottom } = useSafeAreaInsets()
const isFocused = useIsFocused()
const { project } = route.params || {}
log('Route params received', { hasProject: !!project })
const projectId = project?.id
@@ -76,6 +85,9 @@ const RecordPlayback = ({ route }) => {
const [isRecording, setIsRecording] = useState(false)
const [showProgress, setShowProgress] = useState(false)
const [cameraFacing, setCameraFacing] = useState('front')
const [cameraReady, setCameraReady] = useState(false)
const [cameraMountError, setCameraMountError] = useState(null)
const [cameraMountKey, setCameraMountKey] = useState(0)
// Musique
const songUrl = project?.songUrl || null
@@ -771,142 +783,245 @@ const RecordPlayback = ({ route }) => {
}
const permissionsGranted = !!cameraPermission?.granted
const cameraCanMount = permissionsGranted && isFocused
useEffect(() => {
log('Camera permission state updated', { granted: permissionsGranted })
}, [permissionsGranted])
useEffect(() => {
setCameraReady(false)
setCameraMountError(null)
}, [cameraCanMount, cameraFacing])
useEffect(() => {
if (!cameraCanMount || cameraReady || cameraMountError) return undefined
const timeout = setTimeout(() => {
setCameraMountError("La caméra n'a pas répondu à temps.")
}, CAMERA_READY_TIMEOUT_MS)
return () => clearTimeout(timeout)
}, [cameraCanMount, cameraFacing, cameraMountError, cameraMountKey, cameraReady])
const handleCameraFacingChange = useCallback((nextFacing) => {
setCameraReady(false)
setCameraMountError(null)
setCameraFacing(nextFacing)
}, [])
const handleCameraReady = useCallback(() => {
log('Camera preview ready', { facing: cameraFacing })
setCameraMountError(null)
setCameraReady(true)
}, [cameraFacing])
const handleCameraMountError = useCallback((event) => {
const message = event?.message || "La caméra n'a pas pu démarrer."
console.error(LOG_PREFIX, 'Camera mount error', message)
setCameraReady(false)
setCameraMountError(message)
}, [])
const retryCameraMount = useCallback(() => {
setCameraReady(false)
setCameraMountError(null)
setCameraMountKey((currentKey) => currentKey + 1)
}, [])
return (
<View style={{ flex: 1 }}>
<CameraView ref={cameraRef} style={{ flex: 1 }} facing={cameraFacing} mode="video" mute>
<View
style={{
paddingHorizontal: gutters,
paddingTop: top,
flex: 1,
paddingBottom: gutters * 2,
}}
>
<MusicLandHeader progress={9} onPressBack={handleBackPress} />
<View style={{ flex: 1, backgroundColor: Palette.black }}>
{cameraCanMount && (
<CameraView
key={`${cameraFacing}-${cameraMountKey}`}
ref={cameraRef}
style={StyleSheet.absoluteFillObject}
facing={cameraFacing}
mode="video"
mute
active={isFocused}
onCameraReady={handleCameraReady}
onMountError={handleCameraMountError}
/>
)}
<View style={{ marginTop: 12, alignItems: 'flex-end' }}>
<CameraFacingSelector
value={cameraFacing}
onChange={setCameraFacing}
disabled={!permissionsGranted || isPreparing || isRecording}
/>
</View>
<View
style={{
paddingHorizontal: gutters,
paddingTop: top,
flex: 1,
paddingBottom: gutters * 2,
}}
>
<MusicLandHeader progress={9} onPressBack={handleBackPress} />
<View style={{ flex: 1, marginTop: 11 }}>
{/* Overlay de compte à rebours : on affiche 5→1 pour éviter l'effet visuel à 1 */}
{isPreparing && countdown >= 1 && !showProgress && (
<View
<View style={{ marginTop: 12, alignItems: 'flex-end' }}>
<CameraFacingSelector
value={cameraFacing}
onChange={handleCameraFacingChange}
disabled={!cameraReady || isPreparing || isRecording}
/>
</View>
<View style={{ flex: 1, marginTop: 11 }}>
{/* Overlay de compte à rebours : on affiche 5→1 pour éviter l'effet visuel à 1 */}
{isPreparing && countdown >= 1 && !showProgress && (
<View
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
fontSize: 72,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
<Text
style={{
fontSize: 72,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
{countdown}
</Text>
</View>
)}
{/* Permission prompt */}
{!permissionsGranted && (
<View
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
padding: gutters,
}}
>
<GradientButton
title="Autoriser la caméra"
onPress={async () => {
try {
if (!cameraPermission?.granted) await requestCameraPermission()
} catch (_) {}
}}
/>
</View>
)}
</View>
{/* Start button */}
{permissionsGranted && !isPreparing && !isRecording && (
<GradientButton
title="Lancer ma musique"
containerStyle={{ width: '80%', alignSelf: 'center' }}
disabled={!songUrl}
onPress={startCountdownThenRecord}
/>
{countdown}
</Text>
</View>
)}
{/* Progress circulaire + restart */}
{permissionsGranted && (isRecording || showProgress || isPreparing) && (
{/* Permission prompt */}
{!permissionsGranted && (
<View
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: gutters,
bottom: 0,
padding: gutters,
}}
>
<GradientButton
title="Autoriser la caméra"
onPress={async () => {
try {
if (!cameraPermission?.granted) await requestCameraPermission()
} catch (_) {}
}}
/>
</View>
)}
{cameraCanMount && !cameraReady && !cameraMountError && (
<View
style={{
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
gap: 12,
}}
>
<ActivityIndicator size="large" color={Palette.white} />
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 15,
}}
>
Initialisation de la caméra…
</Text>
</View>
)}
{cameraMountError && (
<View
style={{
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: gutters,
gap: 16,
}}
>
<Text
style={{
color: Palette.white,
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 16,
textAlign: 'center',
}}
>
Impossible d'afficher la caméra. Réessaie dans un instant.
</Text>
<GradientButton
title="Réessayer la caméra"
containerStyle={{ width: '80%' }}
onPress={retryCameraMount}
/>
</View>
)}
</View>
{/* Start button */}
{cameraReady && !isPreparing && !isRecording && (
<GradientButton
title="Lancer ma musique"
containerStyle={{ width: '80%', alignSelf: 'center' }}
disabled={!songUrl}
onPress={startCountdownThenRecord}
/>
)}
{/* Progress circulaire + restart */}
{cameraReady && (isRecording || showProgress || isPreparing) && (
<View
style={{
position: 'absolute',
left: 0,
right: 0,
bottom: gutters,
alignItems: 'center',
justifyContent: 'center',
}}
>
<View
style={{
width: 120,
height: 120,
borderRadius: 120,
alignItems: 'center',
justifyContent: 'center',
}}
>
<View
<ProgressRing
size={120}
strokeWidth={8}
progress={durationMs ? Math.min(1, positionMs / durationMs) : 0}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{
width: 120,
height: 120,
borderRadius: 120,
position: 'absolute',
width: 70,
height: 70,
borderRadius: 80,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.white,
shadowColor: '#000000',
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}}
>
<ProgressRing
size={120}
strokeWidth={8}
progress={durationMs ? Math.min(1, positionMs / durationMs) : 0}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{
position: 'absolute',
width: 70,
height: 70,
borderRadius: 80,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: Palette.white,
shadowColor: '#000000',
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}}
>
<RestartSpinnerIcon />
</TouchableOpacity>
</View>
<RestartSpinnerIcon />
</TouchableOpacity>
</View>
)}
</View>
</CameraView>
</View>
)}
</View>
{/* CreateLyricsHeader overlay outside of CameraView */}
<View
pointerEvents="box-none"
+96 -19
View File
@@ -8,8 +8,10 @@ import useDataFromRef from '../../hooks/useDataFromRef'
import usePlayer from '../../hooks/usePlayer'
import PlaybackItem from './components/PlaybackItem'
import PlaybackCharts from './components/PlaybackCharts'
import PlaybackTypeTabs from './components/PlaybackTypeTabs'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { getPlaybackType, matchesPlaybackType, PLAYBACK_TYPE } from '../../utils/playbackType'
const Playbacks = () => {
const [activeIndex, setActiveIndex] = useState(0)
@@ -21,7 +23,13 @@ const Playbacks = () => {
const initialFocusProjectId = route?.params?.projectId || route?.params?.focusId || null
const [focusProjectId, setFocusProjectId] = useState(initialFocusProjectId)
const [viewMode, setViewMode] = useState(initialFocusProjectId ? 'feed' : 'charts')
const { data: rawPlaybacks = [], loadMore } = useDataFromRef({
const [playbackType, setPlaybackType] = useState(PLAYBACK_TYPE.LIVE)
const {
data: rawPlaybacks = [],
loadMore,
hasMore,
loading,
} = useDataFromRef({
ref: projectsRef.where('playbackUrl', '!=', null),
format: (docs) => docs.filter((item) => item?.playbackPublishedOnMusicLand !== false),
simpleRef: false,
@@ -29,18 +37,23 @@ const Playbacks = () => {
usePagination: true,
batchSize: 6,
})
const filteredPlaybacks = useMemo(
() => rawPlaybacks.filter((item) => matchesPlaybackType(item, playbackType)),
[playbackType, rawPlaybacks]
)
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
return filteredPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [filteredPlaybacks, focusProjectId])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
if (focusIndex < 0) return filteredPlaybacks
const target = filteredPlaybacks[focusIndex]
const before = filteredPlaybacks.slice(0, focusIndex)
const after = filteredPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
}, [filteredPlaybacks, focusIndex])
const onSnap = useCallback(
(index) => {
@@ -64,6 +77,14 @@ const Playbacks = () => {
}
}, [focusProjectId])
useEffect(() => {
if (!focusProjectId) return
const focusedPlayback = rawPlaybacks.find((item) => item?.id === focusProjectId)
if (focusedPlayback) {
setPlaybackType(getPlaybackType(focusedPlayback))
}
}, [focusProjectId, rawPlaybacks])
useEffect(() => {
if (loadMore && triedLoadMoreRef.current < 6) {
triedLoadMoreRef.current += 1
@@ -71,6 +92,12 @@ const Playbacks = () => {
}
}, [loadMore, playbacks])
useEffect(() => {
if (playbacks.length === 0 && hasMore && !loading) {
loadMore?.()
}
}, [hasMore, loadMore, loading, playbacks.length])
useEffect(() => {
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
@@ -108,6 +135,12 @@ const Playbacks = () => {
const showFeed = viewMode === 'feed'
const handlePlaybackTypeChange = useCallback((nextType) => {
setFocusProjectId(null)
setPlaybackType(nextType)
setActiveIndex(0)
}, [])
const openFeedForPlayback = useCallback((project) => {
const nextId =
project && typeof project === 'object'
@@ -116,17 +149,27 @@ const Playbacks = () => {
if (!nextId) {
return
}
if (project && typeof project === 'object') {
setPlaybackType(getPlaybackType(project))
}
setFocusProjectId(nextId)
setViewMode('feed')
}, [])
if (!showFeed) {
return <PlaybackCharts onPlaybackPress={openFeedForPlayback} />
return (
<PlaybackCharts
onPlaybackPress={openFeedForPlayback}
playbackType={playbackType}
onPlaybackTypeChange={handlePlaybackTypeChange}
/>
)
}
return (
<View style={{ flex: 1, backgroundColor: 'black' }}>
<Carousel
key={playbackType}
data={playbacks}
vertical
height={responsiveHeight(100)}
@@ -142,14 +185,28 @@ const Playbacks = () => {
/>
)}
/>
<Pressable
style={styles.backToCharts}
hitSlop={12}
onPress={() => setViewMode('charts')}
accessibilityLabel="Revenir au classement des playbacks"
>
<Text style={styles.backToChartsText}>Classement</Text>
</Pressable>
{playbacks.length === 0 && !loading ? (
<View style={styles.emptyState} pointerEvents="none">
<Text style={styles.emptyStateText}>
Aucun {playbackType === PLAYBACK_TYPE.AI ? 'playback IA' : 'playback live'} pour le moment.
</Text>
</View>
) : null}
<View style={styles.feedHeader}>
<PlaybackTypeTabs
value={playbackType}
onChange={handlePlaybackTypeChange}
compact
/>
<Pressable
style={styles.backToCharts}
hitSlop={12}
onPress={() => setViewMode('charts')}
accessibilityLabel="Revenir au classement des playbacks"
>
<Text style={styles.backToChartsText}>Classement</Text>
</Pressable>
</View>
</View>
)
}
@@ -157,10 +214,18 @@ const Playbacks = () => {
export default Playbacks
const styles = StyleSheet.create({
backToCharts: {
feedHeader: {
position: 'absolute',
top: 32,
right: 20,
left: 16,
right: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8,
zIndex: 20,
},
backToCharts: {
paddingVertical: 8,
paddingHorizontal: 12,
borderRadius: 12,
@@ -172,4 +237,16 @@ const styles = StyleSheet.create({
fontSize: 12,
fontFamily: FONT_FAMILY.InterSemiBold,
},
emptyState: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 24,
},
emptyStateText: {
color: Palette.white,
fontSize: 15,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: 'center',
},
})
+93 -20
View File
@@ -18,6 +18,8 @@ import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import PlaybackItem from './components/PlaybackItem.web'
import PlaybackCharts from './components/PlaybackCharts'
import PlaybackTypeTabs from './components/PlaybackTypeTabs'
import { getPlaybackType, matchesPlaybackType, PLAYBACK_TYPE } from '../../utils/playbackType'
const Playbacks = () => {
const { height: windowHeight } = useWindowDimensions()
@@ -28,6 +30,7 @@ const Playbacks = () => {
const initialFocusProjectId = route?.params?.projectId || route?.params?.focusId || null
const [focusProjectId, setFocusProjectId] = useState(initialFocusProjectId)
const [viewMode, setViewMode] = useState(initialFocusProjectId ? 'feed' : 'charts')
const [playbackType, setPlaybackType] = useState(PLAYBACK_TYPE.LIVE)
const userCache = useRef(new Map())
const { getUserByUid } = useUser() || {}
const isFocused = useIsFocused()
@@ -44,18 +47,23 @@ const Playbacks = () => {
usePagination: true,
batchSize: 6,
})
const filteredPlaybacks = useMemo(
() => rawPlaybacks.filter((item) => matchesPlaybackType(item, playbackType)),
[playbackType, rawPlaybacks]
)
const focusIndex = useMemo(() => {
if (!focusProjectId) return -1
return rawPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [focusProjectId, rawPlaybacks])
return filteredPlaybacks.findIndex((p) => p?.id === focusProjectId)
}, [filteredPlaybacks, focusProjectId])
const playbacks = useMemo(() => {
if (focusIndex < 0) return rawPlaybacks
const target = rawPlaybacks[focusIndex]
const before = rawPlaybacks.slice(0, focusIndex)
const after = rawPlaybacks.slice(focusIndex + 1)
if (focusIndex < 0) return filteredPlaybacks
const target = filteredPlaybacks[focusIndex]
const before = filteredPlaybacks.slice(0, focusIndex)
const after = filteredPlaybacks.slice(focusIndex + 1)
return [target, ...after, ...before]
}, [focusIndex, rawPlaybacks])
}, [filteredPlaybacks, focusIndex])
const activePlayback = playbacks?.[activeIndex] || null
const activeVideoUrl = activePlayback?.playbackUrl || null
@@ -188,6 +196,14 @@ const Playbacks = () => {
focusLoadAttemptsRef.current = 0
}, [focusProjectId])
useEffect(() => {
if (!focusProjectId) return
const focusedPlayback = rawPlaybacks.find((item) => item?.id === focusProjectId)
if (focusedPlayback) {
setPlaybackType(getPlaybackType(focusedPlayback))
}
}, [focusProjectId, rawPlaybacks])
useEffect(() => {
if (!focusProjectId) return
if (focusIndex >= 0 && !hasAppliedFocusRef.current) {
@@ -207,6 +223,12 @@ const Playbacks = () => {
}
}, [focusIndex, focusProjectId, loadMore, playbacks])
useEffect(() => {
if (playbacks.length === 0 && hasMore && !loading) {
loadMore?.()
}
}, [hasMore, loadMore, loading, playbacks.length])
useEffect(() => {
const nextFocusId = route?.params?.projectId || route?.params?.focusId || null
setFocusProjectId(nextFocusId)
@@ -335,6 +357,13 @@ const Playbacks = () => {
const showFeed = viewMode === 'feed'
const handlePlaybackTypeChange = useCallback((nextType) => {
setFocusProjectId(null)
setPlaybackType(nextType)
lastActiveIndexRef.current = 0
setActiveIndex(0)
}, [])
const openFeedForPlayback = useCallback((project) => {
const nextId =
project && typeof project === 'object'
@@ -343,12 +372,21 @@ const Playbacks = () => {
if (!nextId) {
return
}
if (project && typeof project === 'object') {
setPlaybackType(getPlaybackType(project))
}
setFocusProjectId(nextId)
setViewMode('feed')
}, [])
if (!showFeed) {
return <PlaybackCharts onPlaybackPress={openFeedForPlayback} />
return (
<PlaybackCharts
onPlaybackPress={openFeedForPlayback}
playbackType={playbackType}
onPlaybackTypeChange={handlePlaybackTypeChange}
/>
)
}
return (
@@ -378,6 +416,7 @@ const Playbacks = () => {
</View>
) : null}
<FlatList
key={playbackType}
ref={listRef}
data={playbacks}
keyExtractor={(item, index) => String(item?.id || index)}
@@ -402,6 +441,13 @@ const Playbacks = () => {
onScroll={onScroll}
scrollEventThrottle={16}
/>
{playbacks.length === 0 && !loading ? (
<View style={styles.emptyState} pointerEvents="none">
<Text style={styles.emptyStateText}>
Aucun {playbackType === PLAYBACK_TYPE.AI ? 'playback IA' : 'playback live'} pour le moment.
</Text>
</View>
) : null}
{showIndicators && (
<View style={styles.indicatorContainer} pointerEvents="box-none">
<Pressable
@@ -453,15 +499,22 @@ const Playbacks = () => {
</Pressable>
</View>
)}
<Pressable
style={styles.backToCharts}
hitSlop={12}
onPress={() => setViewMode('charts')}
accessibilityLabel="Revenir au classement des playbacks"
>
<Image source={icons.sort} resizeMode="contain" style={styles.backToChartsIcon} />
<Text style={styles.backToChartsText}>Retour au classement</Text>
</Pressable>
<View style={styles.feedHeader}>
<PlaybackTypeTabs
value={playbackType}
onChange={handlePlaybackTypeChange}
compact
/>
<Pressable
style={styles.backToCharts}
hitSlop={12}
onPress={() => setViewMode('charts')}
accessibilityLabel="Revenir au classement des playbacks"
>
<Image source={icons.sort} resizeMode="contain" style={styles.backToChartsIcon} />
<Text style={styles.backToChartsText}>Retour au classement</Text>
</Pressable>
</View>
</View>
// </Page>
)
@@ -488,6 +541,17 @@ const styles = StyleSheet.create({
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(2, 2, 2, 0.35)',
},
feedHeader: {
position: 'absolute',
top: 28,
left: 24,
right: 24,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
gap: 12,
zIndex: 20,
},
topOverlayContainer: {
position: 'absolute',
top: 32,
@@ -572,9 +636,6 @@ const styles = StyleSheet.create({
textAlign: 'center',
},
backToCharts: {
position: 'absolute',
top: 28,
right: 24,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 999,
@@ -596,4 +657,16 @@ const styles = StyleSheet.create({
fontSize: 13,
fontFamily: FONT_FAMILY.InterSemiBold,
},
emptyState: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
paddingHorizontal: 24,
},
emptyStateText: {
color: Palette.white,
fontSize: 15,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: 'center',
},
})
@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { FlatList, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
import { BlurView } from 'expo-blur'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background } from '../../../assets'
import { background, img } from '../../../assets'
import BorderGradientButton from '../../../components/BorderGradientButton'
import MobileCoinBadge from '../../../components/MobileCoinBadge'
import SearchBar from '../../../components/SearchBar'
@@ -17,7 +17,9 @@ import CreateLyricsHeader from '../../Writing/components/CreateLyricsHeader'
import SongCard from '../../HitParade/components/SongCard'
import { Palette, Style } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import ClubCard from '../../Home/components/ClubCard'
import SpaceLogoCard from '../../../components/SpaceLogoCard'
import PlaybackTypeTabs from './PlaybackTypeTabs'
import { matchesPlaybackType, PLAYBACK_TYPE } from '../../../utils/playbackType'
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
@@ -37,7 +39,11 @@ const chunkArray = (items = [], size = 10) => {
return chunks
}
const PlaybackCharts = ({ onPlaybackPress }) => {
const PlaybackCharts = ({
onPlaybackPress,
playbackType = PLAYBACK_TYPE.LIVE,
onPlaybackTypeChange,
}) => {
const isWeb = Platform.OS === 'web'
const [selectedCategory, setSelectedCategory] = useState('Playbacks')
const { data: allPlaybacks, loading: allPlaybacksLoading } = useDataFromRef({
@@ -67,19 +73,25 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
condition: true,
})
const playbacksList = useMemo(
() => (Array.isArray(allPlaybacks) ? allPlaybacks : []),
[allPlaybacks]
)
const playbacksList = useMemo(() => {
const items = Array.isArray(allPlaybacks) ? allPlaybacks : []
return items.filter((item) => matchesPlaybackType(item, playbackType))
}, [allPlaybacks, playbackType])
const topMonthList = useMemo(
() => (Array.isArray(topMonthPlaybacks) ? topMonthPlaybacks : []),
[topMonthPlaybacks]
() => {
const items = Array.isArray(topMonthPlaybacks) ? topMonthPlaybacks : []
return items.filter((item) => matchesPlaybackType(item, playbackType))
},
[playbackType, topMonthPlaybacks]
)
const allTimeList = useMemo(
() => (Array.isArray(allTimePlaybacks) ? allTimePlaybacks : []),
[allTimePlaybacks]
() => {
const items = Array.isArray(allTimePlaybacks) ? allTimePlaybacks : []
return items.filter((item) => matchesPlaybackType(item, playbackType))
},
[allTimePlaybacks, playbackType]
)
const {
@@ -104,7 +116,10 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
)
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics])
const playbackResults = useMemo(() => (Array.isArray(playbacks) ? playbacks : []), [playbacks])
const playbackResults = useMemo(() => {
const items = Array.isArray(playbacks) ? playbacks : []
return items.filter((item) => matchesPlaybackType(item, playbackType))
}, [playbackType, playbacks])
const userResults = useMemo(() => (Array.isArray(users) ? users : []), [users])
const musicsLoading = searchLoading
@@ -252,8 +267,18 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
[onPlaybackPress]
)
const heroTitle = isWeb ? 'Playbacks coups de coeur' : 'Classements des playbacks'
const heroSubtitle = 'Découvre les playbacks les plus écoutés et lance la lecture verticale.'
const playbackTypeLabel = playbackType === PLAYBACK_TYPE.AI ? 'Playbacks IA' : 'Playbacks live'
const heroTitle = isWeb ? `${playbackTypeLabel} coups de coeur` : `Classement des ${playbackTypeLabel.toLowerCase()}`
const heroSubtitle =
playbackType === PLAYBACK_TYPE.AI
? 'Découvre les playbacks créés avec l’intelligence artificielle.'
: 'Découvre les performances enregistrées en live.'
const renderEmptyState = () => (
<View style={styles.emptyState}>
<Text style={styles.emptyStateText}>Aucun {playbackTypeLabel.toLowerCase()} pour le moment.</Text>
</View>
)
const renderHero = () => (
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
@@ -289,6 +314,7 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
data={playbacksList}
keyExtractor={(item) => item.id}
refreshing={allPlaybacksLoading}
ListEmptyComponent={renderEmptyState}
renderItem={({ item }) => (
<SongCard
rank={null}
@@ -359,6 +385,7 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
onPress={() => handlePlaybackSelected(item)}
/>
))}
{playbacksList.length === 0 ? renderEmptyState() : null}
</View>
)
@@ -435,7 +462,7 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
Playbacks
{playbackTypeLabel}
</Text>
</BlurView>
<ScrollView
@@ -544,7 +571,7 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
return (
<Page
backgroundImg={isWeb ? background.playbackBG2 : background.playbackBG}
backgroundImg={background.playbackMainBackground}
headerType="NONE"
width="100%"
maxWidth={1200}
@@ -567,7 +594,13 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
</View>
) : null}
<View style={{ gap: 12, flex: 1 }}>
<ClubCard />
<SpaceLogoCard
source={img.musiclandPlayback}
accessibilityLabel="Playback MusicLand"
imageStyle={styles.playbackLogoImage}
/>
<PlaybackTypeTabs value={playbackType} onChange={onPlaybackTypeChange} />
{isWeb && (
<View
@@ -660,3 +693,20 @@ const PlaybackCharts = ({ onPlaybackPress }) => {
}
export default PlaybackCharts
const styles = StyleSheet.create({
playbackLogoImage: {
transform: [{ scale: 1.02 }],
},
emptyState: {
paddingVertical: 32,
paddingHorizontal: 16,
alignItems: 'center',
},
emptyStateText: {
color: 'rgba(255,255,255,0.72)',
fontSize: 14,
fontFamily: FONT_FAMILY.InterMedium,
textAlign: 'center',
},
})
@@ -0,0 +1,88 @@
import React from 'react'
import { Pressable, StyleSheet, Text, View } from 'react-native'
import { Palette } from '../../../styles'
import { FONT_FAMILY } from '../../../styles/Fonts'
import { PLAYBACK_TYPE } from '../../../utils/playbackType'
const options = [
{ label: 'Playbacks live', value: PLAYBACK_TYPE.LIVE },
{ label: 'Playbacks IA', value: PLAYBACK_TYPE.AI },
]
const PlaybackTypeTabs = ({ value, onChange, compact = false, style }) => (
<View style={[styles.container, compact && styles.compactContainer, style]}>
{options.map((option) => {
const isSelected = value === option.value
return (
<Pressable
key={option.value}
accessibilityRole="tab"
accessibilityState={{ selected: isSelected }}
onPress={() => onChange?.(option.value)}
style={({ pressed }) => [
styles.tab,
compact && styles.compactTab,
isSelected && styles.selectedTab,
pressed && styles.pressedTab,
]}
>
<Text
style={[
styles.label,
compact && styles.compactLabel,
isSelected && styles.selectedLabel,
]}
>
{option.label}
</Text>
</Pressable>
)
})}
</View>
)
export default PlaybackTypeTabs
const styles = StyleSheet.create({
container: {
alignSelf: 'center',
flexDirection: 'row',
padding: 4,
borderRadius: 999,
backgroundColor: 'rgba(9, 6, 16, 0.62)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.18)',
},
compactContainer: {
alignSelf: 'auto',
padding: 3,
},
tab: {
minHeight: 42,
justifyContent: 'center',
paddingHorizontal: 18,
borderRadius: 999,
},
compactTab: {
minHeight: 34,
paddingHorizontal: 11,
},
selectedTab: {
backgroundColor: Palette.white,
},
pressedTab: {
opacity: 0.78,
},
label: {
color: 'rgba(255,255,255,0.72)',
fontSize: 14,
fontFamily: FONT_FAMILY.InterSemiBold,
},
compactLabel: {
fontSize: 12,
},
selectedLabel: {
color: '#0F0C14',
},
})
+21 -2
View File
@@ -12,7 +12,7 @@ import firebase, { getFunctionsClient, projectsRef, serverTimestamp } from '../.
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { navigate } from '../../navigation/NavigationService'
import { useUserData, useUser } from '../../providers/UserDataProvider'
import { useStripe } from '../../providers/StripeProvider'
import { Palette } from '../../styles'
@@ -444,6 +444,15 @@ const PlaybackDownload = ({ route }) => {
isDownloading,
pendingDownloadIntent,
])
const handleGoHome = useCallback(() => {
navigate(Routes.BottomTab, {
screen: Routes.HomeStack,
params: {
screen: Routes.Home,
},
})
}, [])
return (
<>
<FullscreenIntroVideo
@@ -456,7 +465,7 @@ const PlaybackDownload = ({ route }) => {
backgroundColor="#07111B"
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={50} />
<MusicLandHeader onPressBack={handleGoHome} progress={50} />
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<CreateLyricsHeader
title="Ton playback est prêt"
@@ -505,6 +514,12 @@ const PlaybackDownload = ({ route }) => {
<Text style={styles.downloadPendingText}>Paiement en attente de confirmation...</Text>
) : null}
</View>
<BorderGradientButton
title="Retour à l'accueil"
onPress={handleGoHome}
disabled={isPublishing || isDownloading}
containerStyle={styles.homeButton}
/>
</ScrollView>
</Page>
</>
@@ -586,4 +601,8 @@ const styles = StyleSheet.create({
color: Palette.white,
opacity: 0.8,
},
homeButton: {
marginTop: 2,
marginBottom: 18,
},
})
+9 -20
View File
@@ -1,6 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage'
import * as AppleAuthentication from 'expo-apple-authentication'
import DateTimePicker from '@react-native-community/datetimepicker'
import React, { useCallback, useEffect, useState } from 'react'
import { useGlobal } from 'reactn'
import {
@@ -19,6 +18,7 @@ import { background } from '../assets'
import BorderGradientButton from '../components/BorderGradientButton'
import { Input } from '../components/Input'
import ItemContainer from '../components/ItemContainer/ItemContainer'
import NativeDateModal from '../components/modal/NativeDateModal'
import WebDateModal from '../components/modal/WebDateModal'
import firebase from '../config/firebase'
import { isWeb } from '../hooks/useLayoutType'
@@ -152,13 +152,6 @@ const Register = ({ navigation, route }) => {
})
}
const onDateChange = (event, selectedDate) => {
setShowDatePicker(Platform.OS === 'ios')
if (selectedDate) {
setBirthDate(selectedDate)
}
}
const renderFormContent = () => (
<View style={[styles.formContent, isCompactWeb ? styles.compactFormContent : null]}>
<Text style={[styles.title, isCompactWeb ? styles.compactTitle : null]}>
@@ -222,17 +215,13 @@ const Register = ({ navigation, route }) => {
/>
</View>
</Pressable>
{showDatePicker && (
<DateTimePicker
testID="dateTimePicker"
value={birthDate || new Date()}
mode="date"
is24Hour={true}
display="default"
onChange={onDateChange}
maximumDate={new Date()}
/>
)}
<NativeDateModal
visible={showDatePicker}
onClose={() => setShowDatePicker(false)}
onValidate={setBirthDate}
initialDate={birthDate}
language={preferredLanguage}
/>
</>
)}
</View>
@@ -291,7 +280,7 @@ const Register = ({ navigation, route }) => {
<Page
width={isWeb ? '100%' : null}
maxWidth={isWeb ? 640 : 1200}
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
backgroundImg={background.loginBgWeb}
headerType="NAVIGATION"
title="Inscription"
onBackPressed={handleBack}
+1 -1
View File
@@ -55,7 +55,7 @@ const RegistrationChoice = () => {
return (
<Page
width={isWeb ? 720 : null}
backgroundImg={isWeb ? background.loginBgWeb : background.homeBG}
backgroundImg={background.loginBgWeb}
headerType="NONE"
title="Choisis ton expérience"
>
+2 -11
View File
@@ -1,9 +1,8 @@
import React, { useMemo, useRef, useState } from 'react'
import { useIsFocused, useRoute } from '@react-navigation/native'
import { useRoute } from '@react-navigation/native'
import { Dimensions, Modal, Text, View } from 'react-native'
import SwiperFlatList from 'react-native-swiper-flatlist'
import { background, icons } from '../../assets'
import BackgroundVideo from '../../components/BackgroundVideo'
import BorderGradientButton from '../../components/BorderGradientButton'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
@@ -37,7 +36,6 @@ const INSTRUMENT_STEP_INDEX = FIRST_VOICE_STEP_INDEX + VOICE_SECTION_TITLES.leng
const RHYTHM_STEP_INDEX = LAST_STEP_INDEX
const ComposeSong = () => {
const isFocused = useIsFocused()
const scrollRef = useRef(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(18)
@@ -49,7 +47,6 @@ const ComposeSong = () => {
updateProjectData,
currentUserData,
currentUID,
videos,
} = useUser()
// Selections state
@@ -303,13 +300,7 @@ const ComposeSong = () => {
])
return (
<Page
backgroundImg={background.studioBG2}
backgroundContent={
isFocused ? <BackgroundVideo source={videos?.malik} /> : null
}
headerType="NONE"
>
<Page backgroundImg={background.studioBG2} headerType="NONE">
<MusicLandHeader onPressBack={onPressBack} progress={progress} logo={icons.musicLandStudio} />
<View style={{ flex: 1, paddingBottom: gutters, gap: 48 }}>
<View
+46 -2
View File
@@ -30,6 +30,7 @@ const GeneratingSong = () => {
const isFocused = useIsFocused()
const askedRef = useRef(false)
const callingRef = useRef(false)
const statusPollInFlightRef = useRef(false)
const localStartRef = useRef(null)
const errorAlertShownRef = useRef(false)
@@ -132,6 +133,46 @@ const GeneratingSong = () => {
}
}, [selectedProject?.musicStatus])
useEffect(() => {
const taskId = selectedProject?.sunoTaskId
if (!isFocused || selectedProject?.musicStatus !== 'GENERATING' || !taskId) return undefined
let isActive = true
const getSunoStatus = firebase.functions().httpsCallable('music-getSunoStatus')
const pollStatus = async () => {
if (!isActive || statusPollInFlightRef.current) return
statusPollInFlightRef.current = true
try {
const { data } = await getSunoStatus({ taskId })
if (isActive && data?.failed && selectedProjectId) {
await projectsRef.doc(selectedProjectId).set(
{
musicStatus: 'FAILED',
musicError: {
source: 'SUNO_API',
message: data?.errorMessage || 'La génération Suno a échoué.',
},
updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
)
}
} catch (error) {
console.log('Suno status polling failed', error?.message || error)
} finally {
statusPollInFlightRef.current = false
}
}
pollStatus()
const interval = global.setInterval(pollStatus, 15000)
return () => {
isActive = false
global.clearInterval(interval)
statusPollInFlightRef.current = false
}
}, [isFocused, selectedProject?.musicStatus, selectedProject?.sunoTaskId, selectedProjectId])
const effectiveConfig = useMemo(() => {
// Use selectedProject.musicConfig only
const cfg = selectedProject?.musicConfig || {}
@@ -165,8 +206,11 @@ const GeneratingSong = () => {
tempo: cfg?.tempo,
projectId: selectedProjectId || selectedProject?.id || null,
})
const taskId = data?.response?.data?.taskId || data?.response?.data?.task_id
if (selectedProjectId && taskId) {
const taskId = data?.response?.data?.taskId
if (!taskId) {
throw new Error("Suno n'a pas retourné d'identifiant de génération.")
}
if (selectedProjectId) {
const baseUpdate = {
sunoTaskId: taskId,
musicStatus: 'GENERATING',
+1
View File
@@ -126,6 +126,7 @@ const SongReady = () => {
coverUrl: null,
coverStatus: null,
playbackUrl: null,
playbackProvider: null,
playbackStatus: null,
playbackGenerating: null,
songPublishedOnMusicLand: false,
+2 -8
View File
@@ -3,7 +3,6 @@ import { Dimensions, KeyboardAvoidingView, Platform, View } from 'react-native'
import { responsiveHeight } from 'react-native-responsive-dimensions'
import { SwiperFlatList } from 'react-native-swiper-flatlist'
import { background } from '../../assets'
import BackgroundVideo from '../../components/BackgroundVideo'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { OTHER_OBJECTIVE_OPTION, OTHER_STYLE_OPTION } from '../../data/data'
@@ -27,7 +26,7 @@ const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false }
const CTA_BUTTON_MAX_WIDTH = 500
const CreateLyricsWithAi = () => {
const { selectedProject, videos } = useUser()
const { selectedProject } = useUser()
const scrollRef = useRef(null)
const [selectedIndex, setSelectedIndex] = useState(0)
const [progress, setProgress] = useState(16)
@@ -342,15 +341,10 @@ const CreateLyricsWithAi = () => {
}, [selectedIndex, parentLayout?.height])
const isCtaVisible = selectedIndex !== 8
const isQuestionStep = selectedIndex < MAX_STEP_INDEX
return (
<Page
headerType="NONE"
backgroundImg={isQuestionStep ? null : background.writingMobileBackground}
backgroundContent={
isQuestionStep ? <BackgroundVideo source={videos?.celine} /> : null
}
backgroundImg={background.writingMobileBackground}
>
<View
onLayout={(event) => {
+1 -1
View File
@@ -20,7 +20,7 @@ import {
const FAKE_PROGRESS_MAX = 96
const PROGRESS_INTERVAL_MS = 250
const CUSTOM_SANITIZE_OPTIONS = { autoInjectPreChorus: false }
const FUNCTIONS_REGION = 'europe-west1'
const FUNCTIONS_REGION = 'us-central1'
const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
const [progress, setProgress] = useState(0)
+80 -6
View File
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef, useState } from 'react'
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { ScrollView, StyleSheet, Text, View } from 'react-native'
import { background, icons } from '../../assets'
import alert from '../../components/Alert'
@@ -31,7 +31,7 @@ import CustomInput from './components/CustomInput'
const RESPONSIBILITY_CHECKBOX_LABEL =
'Vous êtes responsable du contenu que vous validez et Musicland ne sera en aucun cas tenu responsable du contenu que vous validez.'
const FUNCTIONS_REGION = 'europe-west1'
const FUNCTIONS_REGION = 'us-central1'
const Lyrics = ({ navigation }) => {
const scrollRef = useRef(null)
@@ -142,6 +142,11 @@ const Lyrics = ({ navigation }) => {
const [titleValue, setTitleValue] = useState(initial.title || '')
const [sections, setSections] = useState(initial.sections || [])
const moderationRequestRef = useRef({
signature: '',
data: null,
promise: null,
})
const setSectionAt = (index, value) => {
setSections((prev) => {
const next = [...prev]
@@ -187,6 +192,77 @@ const Lyrics = ({ navigation }) => {
})
}, [])
const getModerationResult = useCallback(({ title, lyrics }) => {
const signature = JSON.stringify({ title, lyrics })
const cachedRequest = moderationRequestRef.current
if (cachedRequest.signature === signature) {
if (cachedRequest.data) return Promise.resolve(cachedRequest.data)
if (cachedRequest.promise) return cachedRequest.promise
}
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
'lyrics-analyseLyricsToxicity'
)
const requestPromise = callable({ title, lyrics })
.then(({ data }) => {
if (
moderationRequestRef.current.signature === signature &&
moderationRequestRef.current.promise === requestPromise
) {
moderationRequestRef.current = {
signature,
data,
promise: null,
}
}
return data
})
.catch((error) => {
if (
moderationRequestRef.current.signature === signature &&
moderationRequestRef.current.promise === requestPromise
) {
moderationRequestRef.current = {
signature,
data: null,
promise: null,
}
}
throw error
})
moderationRequestRef.current = {
signature,
data: null,
promise: requestPromise,
}
return requestPromise
}, [])
useEffect(() => {
const title = (titleValue || '').trim()
const lyrics = (sections || []).map((section) => ({
type: normalizeStructureType(section?.type),
lyrics: segmentRequiresLyrics(normalizeStructureType(section?.type))
? (section?.lyrics || '').trim()
: '',
}))
const hasIncompleteSection = lyrics.some(
(section) => segmentRequiresLyrics(section.type) && !section.lyrics
)
if (!title || hasIncompleteSection) return undefined
const timer = setTimeout(() => {
getModerationResult({ title, lyrics }).catch((error) => {
console.log('Background moderation failed', error?.message || error)
})
}, 400)
return () => clearTimeout(timer)
}, [getModerationResult, sections, titleValue])
const handleSensitiveCancel = useCallback(() => {
closeSensitiveContentModal(false)
}, [closeSensitiveContentModal])
@@ -240,10 +316,7 @@ const Lyrics = ({ navigation }) => {
// 1) Appel de la Cloud Function de modération avant tout enregistrement
try {
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable(
'lyrics-analyseLyricsToxicity'
)
const { data } = await callable({
const data = await getModerationResult({
title: titleTrimmed,
lyrics: normalizedNewLyrics,
})
@@ -375,6 +448,7 @@ const Lyrics = ({ navigation }) => {
projectSelections,
alertMessage,
confirmSensitiveContent,
getModerationResult,
sanitize,
selectedProjectId,
projectHasLyrics,
+23 -6
View File
@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'
import { StyleSheet, Text, View } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { background } from '../../assets'
import { background, videos } from '../../assets'
import GradientButton from '../../components/GradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import { strings } from '../../constants/strings'
@@ -18,15 +18,26 @@ const CTA_BUTTON_HEIGHT = 58
const CTA_BUTTON_MAX_WIDTH = 520
const WritingLyrics = () => {
const { createNewProject, selectedProjectId, updateProjectData, videos } = useUserData()
const { createNewProject, currentUserData, selectedProjectId, updateProjectData } =
useUserData()
const { setIsLoading } = useMinuit()
const [videoUrl, setVideoUrl] = useState(null)
const preferredLanguage = currentUserData
? currentUserData.preferredLanguage === 'en'
? 'en'
: 'fr'
: null
useEffect(() => {
setVideoUrl(isWeb ? videos.celineWeb : videos?.celine)
// setVideoUrl(videos?.test);
}, [])
if (!preferredLanguage) {
return
}
setVideoUrl(
preferredLanguage === 'en' ? videos.writingSpaceIntroEn : videos.writingSpaceIntroFr
)
}, [preferredLanguage])
const startWriting = React.useCallback(async () => {
try {
@@ -66,7 +77,13 @@ const WritingLyrics = () => {
<Text style={styles.secondaryNote}>{strings.writing.onboarding.secondaryNote}</Text>
</View>
</View>
<FullscreenIntroVideo url={videoUrl} visible={!!videoUrl} onClose={() => setVideoUrl(null)} />
<FullscreenIntroVideo
url={videoUrl}
visible={!!videoUrl}
onClose={() => setVideoUrl(null)}
contentFit={isWeb ? 'contain' : 'cover'}
skipLabel={preferredLanguage === 'en' ? 'Skip video' : 'Passer la vidéo'}
/>
</Page>
)
}
+74 -6
View File
@@ -1,4 +1,5 @@
import { BlurView } from 'expo-blur'
import * as ImagePicker from 'expo-image-picker'
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Modal, Platform, Pressable, StyleSheet, Text, View } from 'react-native'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit'
@@ -7,6 +8,7 @@ import GradientButton from '../../components/GradientButton'
import { Input } from '../../components/Input'
import MusicLandHeader from '../../components/MusicLandHeader'
import firebase, { projectsRef, usersRef } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
@@ -16,6 +18,8 @@ import { FONT_FAMILY } from '../../styles/Fonts'
import { buildFullName } from '../../utils/artistName'
import { background } from '../../assets'
const COVER_TEMPLATE = 'MUSICLAND_PLAYBACKER'
export const ChooseCoverType = () => {
const isIOS = Platform.OS === 'ios'
const [choiceVisible, setChoiceVisible] = useState(false)
@@ -27,8 +31,14 @@ export const ChooseCoverType = () => {
const [savingChoice, setSavingChoice] = useState(false)
const [savingPseudo, setSavingPseudo] = useState(false)
const { selectedProject, selectedProjectId, currentUserData, currentUID } = useUserData()
const { setTooltip } = useMinuit()
const {
selectedProject,
selectedProjectId,
updateProjectData,
currentUserData,
currentUID,
} = useUserData()
const { setIsLoading, setTooltip } = useMinuit()
const projectId = selectedProject?.id || selectedProjectId || null
const hasArtistPreference = useMemo(() => {
@@ -104,6 +114,64 @@ export const ChooseCoverType = () => {
ensureArtistPreference(() => navigate(Routes.PouchReady))
}, [ensureArtistPreference])
const pickCenterImage = useCallback(async () => {
try {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [1, 1],
quality: 1,
})
if (result.canceled) return
const uri = result.assets?.[0]?.uri
if (!uri || !projectId) {
throw new Error("Impossible d'identifier l'image ou le projet")
}
await setIsLoading(true)
const { resultURI } = await uploadFileToFirebase({
uri,
path: `musics/${projectId}/cover-center-image.jpg`,
shouldCompress: false,
fileType: 'IMAGE',
})
if (!resultURI) {
throw new Error("Le téléversement de l'image a échoué")
}
await updateProjectData(
{
coverCenterImageURL: resultURI,
coverStatus: 'PENDING',
cover: {
template: COVER_TEMPLATE,
result: null,
generatedBackground: null,
selectedOptionId: COVER_TEMPLATE,
options: [],
},
},
{ merge: true }
)
navigate(Routes.PouchReady)
} catch (error) {
console.log('pickCenterImage error', error?.message)
setTooltip({
type: 'error',
text: error?.message || "Impossible de sélectionner l'image",
})
} finally {
await setIsLoading(false)
}
}, [projectId, setIsLoading, setTooltip, updateProjectData])
const handlePickCenterImage = useCallback(() => {
ensureArtistPreference(pickCenterImage)
}, [ensureArtistPreference, pickCenterImage])
const closeChoiceModal = useCallback(() => {
setChoiceVisible(false)
setChoiceDismissAction(null)
@@ -259,10 +327,10 @@ export const ChooseCoverType = () => {
gap: 12,
}}
>
{/* <BorderGradientButton
title="Choisir une image"
onPress={handlePickUserImage}
/> */}
<BorderGradientButton
title="Choisir l’image centrale"
onPress={handlePickCenterImage}
/>
<GradientButton
title="Voir ma pochette"
onPress={handleShowCover}
+9 -4
View File
@@ -40,6 +40,7 @@ const PouchReady = () => {
? selectedProject.userName.trim()
: currentUserData?.userName || 'MusicLand'
const profilePictureURL = currentUserData?.profilePictureURL || null
const centerImageURL = selectedProject?.coverCenterImageURL || profilePictureURL
const selectedOption = useMemo(() => {
if (!coverUrl) {
@@ -119,7 +120,7 @@ const PouchReady = () => {
}
const coverBlob = await createMusiclandPlaybackCover({
artistName,
profilePictureURL,
profilePictureURL: centerImageURL,
})
const storagePath = `users/${userId}/projects/${selectedProjectId}/cover-musicland-playbacker.png`
const { resultURI } = await uploadFileToFirebase({
@@ -171,7 +172,7 @@ const PouchReady = () => {
}, [
artistName,
coverUrl,
profilePictureURL,
centerImageURL,
selectedOption,
selectedProject,
selectedProjectId,
@@ -184,12 +185,16 @@ const PouchReady = () => {
<View style={styles.pageContent}>
<CreateLyricsHeader
title="Ta pochette MusicLand"
subTitle="Ta photo de profil et ton nom d’artiste apparaissent automatiquement."
subTitle={
selectedProject?.coverCenterImageURL
? "L’image choisie et ton nom d’artiste apparaissent automatiquement."
: "Ta photo de profil et ton nom d’artiste apparaissent automatiquement."
}
/>
<View style={[styles.previewArea, Style.containerCenter]}>
<MusiclandPlaybackCover
artistName={artistName}
profilePictureURL={profilePictureURL}
profilePictureURL={centerImageURL}
coverUrl={coverUrl}
style={isWeb ? styles.coverWeb : styles.coverMobile}
/>