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:
@@ -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,
|
||||
|
||||
@@ -193,7 +193,7 @@ const PlaybackAi = ({ route }) => {
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundImg={Platform.OS === 'web' ? background.playbackWeb : background.playbackMobile}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
|
||||
@@ -96,7 +96,7 @@ const PlaybackAiStatus = ({ route }) => {
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundImg={background.playbackMobile}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
|
||||
@@ -91,7 +91,7 @@ const PlaybackAiStatus = ({ route }) => {
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundImg={background.playbackWeb}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user