heygen
This commit is contained in:
+31
-31
@@ -1,18 +1,18 @@
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage"
|
||||
import { getReactNativePersistence, initializeAuth } from "firebase/auth/react-native"
|
||||
import firebase from "firebase/compat/app"
|
||||
import "firebase/compat/auth"
|
||||
import "firebase/compat/firestore"
|
||||
import "firebase/compat/functions"
|
||||
import "firebase/compat/storage"
|
||||
import { Platform } from "react-native"
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
import { getReactNativePersistence, initializeAuth } from 'firebase/auth/react-native'
|
||||
import firebase from 'firebase/compat/app'
|
||||
import 'firebase/compat/auth'
|
||||
import 'firebase/compat/firestore'
|
||||
import 'firebase/compat/functions'
|
||||
import 'firebase/compat/storage'
|
||||
import { Platform } from 'react-native'
|
||||
|
||||
const functionsInstances = {}
|
||||
const emulatorConfigured = {}
|
||||
const emulatorHost = Platform.OS === "web" ? "localhost" : "192.168.1.114"
|
||||
const emulatorHost = Platform.OS === 'web' ? 'localhost' : '192.168.1.60'
|
||||
const USE_FUNCTIONS_EMULATOR = false // Toggle to route functions traffic to the local emulator.
|
||||
|
||||
const configureFunctionsEmulator = (instance, regionKey = "us-central1") => {
|
||||
const configureFunctionsEmulator = (instance, regionKey = 'us-central1') => {
|
||||
const shouldUseEmulator = USE_FUNCTIONS_EMULATOR && instance?.useEmulator
|
||||
|
||||
if (!shouldUseEmulator || emulatorConfigured[regionKey]) {
|
||||
@@ -31,16 +31,16 @@ const configureFunctionsEmulator = (instance, regionKey = "us-central1") => {
|
||||
}
|
||||
|
||||
export const firebaseConfig = {
|
||||
apiKey: "AIzaSyCuHJHdwVN_F-VmUG4Hd7bGiMRqj6rPlLo",
|
||||
authDomain: "musicland-d33f9.firebaseapp.com",
|
||||
projectId: "musicland-d33f9",
|
||||
storageBucket: "musicland-d33f9.firebasestorage.app",
|
||||
messagingSenderId: "305598753437",
|
||||
appId: "1:305598753437:web:9930f10af59e5cf7f28274",
|
||||
measurementId: "G-RV4MS1SVTT",
|
||||
apiKey: 'AIzaSyCuHJHdwVN_F-VmUG4Hd7bGiMRqj6rPlLo',
|
||||
authDomain: 'musicland-d33f9.firebaseapp.com',
|
||||
projectId: 'musicland-d33f9',
|
||||
storageBucket: 'musicland-d33f9.firebasestorage.app',
|
||||
messagingSenderId: '305598753437',
|
||||
appId: '1:305598753437:web:9930f10af59e5cf7f28274',
|
||||
measurementId: 'G-RV4MS1SVTT',
|
||||
}
|
||||
|
||||
if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) {
|
||||
if (!firebase?.apps?.filter(({ name_ }) => name_ === '[DEFAULT]').length) {
|
||||
const defaultApp = firebase.initializeApp(firebaseConfig)
|
||||
|
||||
initializeAuth(defaultApp, {
|
||||
@@ -51,9 +51,9 @@ if (!firebase?.apps?.filter(({ name_ }) => name_ === "[DEFAULT]").length) {
|
||||
// firebase.functions().useEmulator("localhost", 5001);
|
||||
// }
|
||||
const defaultFunctions = firebase.functions()
|
||||
functionsInstances["us-central1"] = defaultFunctions
|
||||
configureFunctionsEmulator(defaultFunctions, "us-central1")
|
||||
console.log("Firebase init")
|
||||
functionsInstances['us-central1'] = defaultFunctions
|
||||
configureFunctionsEmulator(defaultFunctions, 'us-central1')
|
||||
console.log('Firebase init')
|
||||
}
|
||||
|
||||
// Firestore settings for React Native/iOS: avoid streaming transport issues
|
||||
@@ -69,20 +69,20 @@ try {
|
||||
// Ignore if settings were already set elsewhere
|
||||
}
|
||||
|
||||
export const usersRef = firestore.collection("users")
|
||||
export const projectsRef = firestore.collection("projects")
|
||||
export const tasksRef = firestore.collection("tasks")
|
||||
export const playlistsRef = firestore.collection("playlists")
|
||||
export const chatsRef = firestore.collection("chats")
|
||||
export const videosRef = firestore.collection("videos")
|
||||
export const notificationsRef = firestore.collection("notifications")
|
||||
export const reportsRef = firestore.collection("reports")
|
||||
export const usersRef = firestore.collection('users')
|
||||
export const projectsRef = firestore.collection('projects')
|
||||
export const tasksRef = firestore.collection('tasks')
|
||||
export const playlistsRef = firestore.collection('playlists')
|
||||
export const chatsRef = firestore.collection('chats')
|
||||
export const videosRef = firestore.collection('videos')
|
||||
export const notificationsRef = firestore.collection('notifications')
|
||||
export const reportsRef = firestore.collection('reports')
|
||||
|
||||
export const { arrayUnion, arrayRemove, increment, serverTimestamp } = firebase.firestore.FieldValue
|
||||
export const deleteField = firebase.firestore.FieldValue.delete
|
||||
|
||||
export const getFunctionsClient = (region = "us-central1") => {
|
||||
const regionKey = region || "us-central1"
|
||||
export const getFunctionsClient = (region = 'us-central1') => {
|
||||
const regionKey = region || 'us-central1'
|
||||
|
||||
if (!functionsInstances[regionKey]) {
|
||||
functionsInstances[regionKey] = firebase.app().functions(regionKey)
|
||||
|
||||
@@ -18,6 +18,8 @@ import ResetPassword from '../screens/ResetPassword'
|
||||
import ChooseDecor from '../screens/Playback/ChooseDecor'
|
||||
import CreatingDecor from '../screens/Playback/CreatingDecor'
|
||||
import Playback from '../screens/Playback/Playback'
|
||||
import PlaybackAi from '../screens/Playback/PlaybackAi'
|
||||
import PlaybackAiStatus from '../screens/Playback/PlaybackAiStatus'
|
||||
import PlaybackGuide from '../screens/Playback/PlaybackGuide'
|
||||
import PlaybackOnboarding from '../screens/Playback/PlaybackOnboarding'
|
||||
import RecordPlayback from '../screens/Playback/RecordPlayback'
|
||||
@@ -235,6 +237,14 @@ const baseScreens = [
|
||||
name: Routes.PlaybackGuide,
|
||||
component: PlaybackGuide,
|
||||
},
|
||||
{
|
||||
name: Routes.PlaybackAi,
|
||||
component: PlaybackAi,
|
||||
},
|
||||
{
|
||||
name: Routes.PlaybackAiStatus,
|
||||
component: PlaybackAiStatus,
|
||||
},
|
||||
{
|
||||
name: Routes.RecordPlayback,
|
||||
component: RecordPlayback,
|
||||
|
||||
@@ -58,6 +58,8 @@ export const Routes = {
|
||||
PlaybackOnboarding: 'PlaybackOnboarding',
|
||||
Playback: 'Playback',
|
||||
PlaybackGuide: 'PlaybackGuide',
|
||||
PlaybackAi: 'PlaybackAi',
|
||||
PlaybackAiStatus: 'PlaybackAiStatus',
|
||||
RecordPlayback: 'RecordPlayback',
|
||||
RecordedPlayback: 'RecordedPlayback',
|
||||
ChooseDecor: 'ChooseDecor',
|
||||
|
||||
@@ -36,6 +36,8 @@ const HIDDEN_ROUTE_NAMES = new Set([
|
||||
Routes.PlaybackOnboarding,
|
||||
Routes.Playback,
|
||||
Routes.PlaybackGuide,
|
||||
Routes.PlaybackAi,
|
||||
Routes.PlaybackAiStatus,
|
||||
Routes.Playbacks,
|
||||
Routes.RecordPlayback,
|
||||
Routes.RecordedPlayback,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { StyleSheet, View } from 'react-native'
|
||||
import React, { useCallback, useMemo } 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 BorderGradientButton from '../../components/BorderGradientButton'
|
||||
@@ -9,13 +10,31 @@ import Page from '../../layouts/Page'
|
||||
import { isWeb } from '../../hooks/useLayoutType'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUser } from '../../providers/UserDataProvider'
|
||||
import { gutters } from '../../styles'
|
||||
import { useUser, useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import {
|
||||
getPlaybackAiStatusLabel,
|
||||
isPlaybackAiBlockingManual,
|
||||
shouldResumePlaybackAi,
|
||||
} from '../../utils/playbackAi'
|
||||
|
||||
const Playback = ({ route, navigation }) => {
|
||||
const { project, returnToAdventureModal = false } = route.params || {}
|
||||
const isFocused = useIsFocused()
|
||||
const { project: routeProject, returnToAdventureModal = false } = route.params || {}
|
||||
const { videos } = useUser()
|
||||
const { selectedProject } = useUserData() || {}
|
||||
const theoUrl = isWeb ? videos?.theoWeb : videos?.theo
|
||||
const project = useMemo(() => {
|
||||
if (routeProject?.id && selectedProject?.id === routeProject.id) {
|
||||
return selectedProject
|
||||
}
|
||||
return routeProject || null
|
||||
}, [routeProject, selectedProject])
|
||||
const aiStatus = project?.playbackStatus || null
|
||||
const isManualBlocked = isPlaybackAiBlockingManual(project)
|
||||
const aiButtonLabel = shouldResumePlaybackAi(project)
|
||||
? 'Voir mon playback IA'
|
||||
: 'Créer mon playback IA'
|
||||
|
||||
const handleBackPress = useCallback(() => {
|
||||
if (returnToAdventureModal) {
|
||||
@@ -26,18 +45,29 @@ const Playback = ({ route, navigation }) => {
|
||||
}, [navigation, returnToAdventureModal])
|
||||
|
||||
const onPressRecord = useCallback(() => {
|
||||
if (isManualBlocked) return
|
||||
navigate(Routes.RecordPlayback, { project, returnToAdventureModal })
|
||||
}, [project, returnToAdventureModal])
|
||||
}, [isManualBlocked, project, returnToAdventureModal])
|
||||
|
||||
const onPressPlaybackAi = useCallback(() => {
|
||||
if (shouldResumePlaybackAi(project)) {
|
||||
navigate(Routes.PlaybackAiStatus, { project })
|
||||
return
|
||||
}
|
||||
navigate(Routes.PlaybackAi, { project })
|
||||
}, [project])
|
||||
|
||||
return (
|
||||
<Page
|
||||
headerType="NONE"
|
||||
backgroundImg={isWeb ? background.playbackWeb : background.playbackMobile}
|
||||
backgroundContent={
|
||||
<BackgroundVideo
|
||||
source={theoUrl}
|
||||
soundButtonStyle={isWeb ? styles.rightSideControl : undefined}
|
||||
/>
|
||||
isFocused ? (
|
||||
<BackgroundVideo
|
||||
source={theoUrl}
|
||||
soundButtonStyle={isWeb ? styles.rightSideControl : undefined}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
containerStyle={isWeb ? styles.videoPage : undefined}
|
||||
>
|
||||
@@ -51,7 +81,21 @@ const Playback = ({ route, navigation }) => {
|
||||
}}
|
||||
>
|
||||
<View style={{ width: '80%', alignSelf: 'center', gap: 12 }}>
|
||||
<GradientButton title="Enregistrer mon Playback" onPress={onPressRecord} />
|
||||
<GradientButton
|
||||
title="Enregistrer mon Playback"
|
||||
onPress={onPressRecord}
|
||||
disabled={!project || isManualBlocked}
|
||||
/>
|
||||
<BorderGradientButton title={aiButtonLabel} onPress={onPressPlaybackAi} disabled={!project} />
|
||||
{isManualBlocked ? (
|
||||
<Text style={styles.blockedText}>
|
||||
Le playback manuel est bloqué tant que le playback IA est en cours ou en attente de
|
||||
validation.
|
||||
</Text>
|
||||
) : null}
|
||||
{shouldResumePlaybackAi(project) ? (
|
||||
<Text style={styles.statusText}>{getPlaybackAiStatusLabel(aiStatus)}</Text>
|
||||
) : null}
|
||||
<BorderGradientButton
|
||||
title="Guide du Playbacker"
|
||||
onPress={() => navigate(Routes.PlaybackGuide, { returnToAdventureModal })}
|
||||
@@ -74,4 +118,17 @@ const styles = StyleSheet.create({
|
||||
right: 24,
|
||||
left: 'auto',
|
||||
},
|
||||
blockedText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.75,
|
||||
fontSize: 12,
|
||||
lineHeight: 18,
|
||||
textAlign: 'center',
|
||||
},
|
||||
statusText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.55,
|
||||
fontSize: 12,
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
import * as ImagePicker from 'expo-image-picker'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { background } from '../../assets'
|
||||
import ActivityLoader from '../../components/ActivityLoader'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import { getFunctionsClient } from '../../config/firebase'
|
||||
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||
import { useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import { PLAYBACK_AI_STATUS } from '../../utils/playbackAi'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
|
||||
const FUNCTIONS_REGION = 'europe-west1'
|
||||
|
||||
const trimString = (value) => (typeof value === 'string' ? value.trim() : '')
|
||||
|
||||
const isRemoteUrl = (value) => /^https?:\/\//i.test(trimString(value))
|
||||
|
||||
const resolveProject = ({ routeProject, selectedProject }) => {
|
||||
if (routeProject?.id && selectedProject?.id === routeProject.id) {
|
||||
return selectedProject
|
||||
}
|
||||
return routeProject || null
|
||||
}
|
||||
|
||||
const getInitialPhotoUri = ({ routePhotoUrl, project }) =>
|
||||
trimString(routePhotoUrl) || trimString(project?.playbackPhotoUrl) || null
|
||||
|
||||
const callStartPlaybackGeneration = (payload) => {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('heygen-startPlaybackGeneration')
|
||||
return callable(payload)
|
||||
}
|
||||
|
||||
const PlaybackAi = ({ route }) => {
|
||||
const { project: routeProject, initialPhotoUrl = null } = route.params || {}
|
||||
const { selectedProject, currentUID } = useUserData() || {}
|
||||
const { setTooltip } = useMinuit()
|
||||
const [selectedPhotoUri, setSelectedPhotoUri] = useState(() =>
|
||||
getInitialPhotoUri({
|
||||
routePhotoUrl: initialPhotoUrl,
|
||||
project: resolveProject({ routeProject, selectedProject }),
|
||||
})
|
||||
)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const project = useMemo(
|
||||
() => resolveProject({ routeProject, selectedProject }),
|
||||
[routeProject, selectedProject]
|
||||
)
|
||||
|
||||
const handleSelectPhoto = async () => {
|
||||
try {
|
||||
const result = await ImagePicker.launchImageLibraryAsync({
|
||||
mediaTypes: ImagePicker.MediaTypeOptions.Images,
|
||||
allowsEditing: true,
|
||||
aspect: [9, 16],
|
||||
quality: 1,
|
||||
})
|
||||
|
||||
const nextUri = result?.assets?.[0]?.uri || null
|
||||
if (nextUri) {
|
||||
setSelectedPhotoUri(nextUri)
|
||||
}
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: error?.message || 'Impossible de sélectionner une photo',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const handleGenerate = async () => {
|
||||
try {
|
||||
if (!project?.id) {
|
||||
throw new Error('Projet introuvable')
|
||||
}
|
||||
|
||||
if (!currentUID) {
|
||||
throw new Error('Utilisateur non authentifié')
|
||||
}
|
||||
|
||||
if (!selectedPhotoUri) {
|
||||
throw new Error('Choisis une photo avant de lancer la génération')
|
||||
}
|
||||
|
||||
setIsSubmitting(true)
|
||||
|
||||
let photoUrl = trimString(selectedPhotoUri)
|
||||
if (!isRemoteUrl(photoUrl)) {
|
||||
const extension = trimString(selectedPhotoUri.split('.').pop()) || 'jpg'
|
||||
const uploadResult = await uploadFileToFirebase({
|
||||
uri: selectedPhotoUri,
|
||||
path: `users/${currentUID}/projects/${project.id}/heygen/photo-${Date.now()}.${extension}`,
|
||||
shouldCompress: true,
|
||||
fileType: 'IMAGE',
|
||||
})
|
||||
photoUrl = trimString(uploadResult?.resultURI)
|
||||
}
|
||||
|
||||
if (!photoUrl) {
|
||||
throw new Error("Impossible d'envoyer la photo vers Firebase")
|
||||
}
|
||||
|
||||
await callStartPlaybackGeneration({
|
||||
projectId: project.id,
|
||||
photoUrl,
|
||||
})
|
||||
|
||||
navigate(Routes.PlaybackAiStatus, {
|
||||
project: {
|
||||
...project,
|
||||
playbackStatus: PLAYBACK_AI_STATUS.GENERATING,
|
||||
playbackGenerating: true,
|
||||
playbackPhotoUrl: photoUrl,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: error?.message || 'Erreur lors du lancement du playback IA',
|
||||
})
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader progress={31} onPressBack={goBack} />
|
||||
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
|
||||
<CreateLyricsHeader title="Créer ton playback IA" />
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.summaryCard}>
|
||||
<Text style={styles.summaryTitle}>{project?.title || 'Sans titre'}</Text>
|
||||
<Text style={styles.summaryText}>
|
||||
Ajoute une photo portrait de toi. HeyGen générera ensuite une vidéo verticale 9:16
|
||||
où tu chantes la musique de ton projet.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.previewCard}>
|
||||
{selectedPhotoUri ? (
|
||||
<Image source={{ uri: selectedPhotoUri }} style={styles.photoPreview} resizeMode="cover" />
|
||||
) : (
|
||||
<View style={[styles.photoPreview, styles.photoPlaceholder]}>
|
||||
<Text style={styles.photoPlaceholderText}>Aucune photo sélectionnée</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<BorderGradientButton
|
||||
title={selectedPhotoUri ? 'Changer de photo' : 'Choisir une photo'}
|
||||
onPress={handleSelectPhoto}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
<GradientButton
|
||||
title="Générer ma vidéo"
|
||||
onPress={handleGenerate}
|
||||
disabled={isSubmitting || !selectedPhotoUri}
|
||||
/>
|
||||
{isSubmitting ? (
|
||||
<ActivityLoader
|
||||
defaultMessage="Envoi de la photo et lancement de HeyGen..."
|
||||
containerStyle={styles.loader}
|
||||
/>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
flex: 1,
|
||||
width: '90%',
|
||||
alignSelf: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
summaryCard: {
|
||||
padding: 18,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.76)',
|
||||
gap: 8,
|
||||
},
|
||||
summaryTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
summaryText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.78,
|
||||
lineHeight: 20,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
},
|
||||
previewCard: {
|
||||
flex: 1,
|
||||
minHeight: 320,
|
||||
borderRadius: 22,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.8)',
|
||||
padding: 12,
|
||||
},
|
||||
photoPreview: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#141414',
|
||||
},
|
||||
photoPlaceholder: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
photoPlaceholderText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.5,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
},
|
||||
actions: {
|
||||
gap: 12,
|
||||
},
|
||||
loader: {
|
||||
marginTop: 4,
|
||||
},
|
||||
}
|
||||
|
||||
export default PlaybackAi
|
||||
@@ -0,0 +1,279 @@
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import { VideoView, useVideoPlayer } from 'expo-video'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { background } from '../../assets'
|
||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||
import ActivityLoader from '../../components/ActivityLoader'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import { getFunctionsClient, projectsRef } from '../../config/firebase'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate, push } from '../../navigation/NavigationService'
|
||||
import { useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import { PLAYBACK_AI_STATUS } from '../../utils/playbackAi'
|
||||
|
||||
const FUNCTIONS_REGION = 'europe-west1'
|
||||
|
||||
const resolveProject = ({ routeProject, selectedProject, liveProject }) => {
|
||||
if (liveProject?.id) return liveProject
|
||||
if (routeProject?.id && selectedProject?.id === routeProject.id) {
|
||||
return selectedProject
|
||||
}
|
||||
return routeProject || null
|
||||
}
|
||||
|
||||
const callValidatePlaybackDraft = (payload) => {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('heygen-validatePlaybackDraft')
|
||||
return callable(payload)
|
||||
}
|
||||
|
||||
const PlaybackAiStatus = ({ route }) => {
|
||||
const { project: routeProject } = route.params || {}
|
||||
const { selectedProject } = useUserData() || {}
|
||||
const { setTooltip } = useMinuit()
|
||||
const [isValidating, setIsValidating] = useState(false)
|
||||
const projectId = routeProject?.id || selectedProject?.id || null
|
||||
const { data: liveProject = null } = useDataFromRef({
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
refreshArray: [projectId],
|
||||
})
|
||||
const project = useMemo(
|
||||
() => resolveProject({ routeProject, selectedProject, liveProject }),
|
||||
[liveProject, routeProject, selectedProject]
|
||||
)
|
||||
const status =
|
||||
project?.playbackStatus ||
|
||||
(routeProject?.playbackGenerating ? PLAYBACK_AI_STATUS.GENERATING : null)
|
||||
const draftUrl = project?.playbackDraftUrl || routeProject?.playbackDraftUrl || null
|
||||
const player = useVideoPlayer(draftUrl ? { uri: draftUrl } : null, (instance) => {
|
||||
instance.loop = false
|
||||
instance.muted = false
|
||||
})
|
||||
|
||||
const handleValidate = async () => {
|
||||
try {
|
||||
if (!project?.id) {
|
||||
throw new Error('Projet introuvable')
|
||||
}
|
||||
|
||||
setIsValidating(true)
|
||||
await callValidatePlaybackDraft({ projectId: project.id })
|
||||
|
||||
const latestProjectSnap = await projectsRef.doc(project.id).get()
|
||||
const latestProject = latestProjectSnap.exists
|
||||
? { ...latestProjectSnap.data(), id: latestProjectSnap.id }
|
||||
: project
|
||||
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
project: latestProject,
|
||||
})
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: error?.message || 'Impossible de valider le playback IA',
|
||||
})
|
||||
} finally {
|
||||
setIsValidating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
push(Routes.PlaybackAi, {
|
||||
project,
|
||||
initialPhotoUrl: project?.playbackPhotoUrl || null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader progress={38} onPressBack={goBack} />
|
||||
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
|
||||
<CreateLyricsHeader title="Playback IA" />
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.headerCard}>
|
||||
{project?.coverUrl ? (
|
||||
<Image source={{ uri: project.coverUrl }} style={styles.cover} resizeMode="cover" />
|
||||
) : null}
|
||||
<View style={{ flex: 1, gap: 4 }}>
|
||||
<Text style={styles.title}>{project?.title || 'Sans titre'}</Text>
|
||||
<Text style={styles.subtitle}>Format vertical 9:16 généré avec HeyGen</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.GENERATING ? (
|
||||
<View style={styles.stateCard}>
|
||||
<ActivityLoader defaultMessage="HeyGen génère ton playback IA..." />
|
||||
<Text style={styles.helperText}>
|
||||
Tu peux fermer cet écran et revenir plus tard. Une notification te sera envoyée dès
|
||||
que ta vidéo sera prête.
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.FAILED ? (
|
||||
<View style={styles.stateCard}>
|
||||
<Text style={styles.errorTitle}>La génération a échoué</Text>
|
||||
<Text style={styles.helperText}>
|
||||
{project?.playbackError || 'HeyGen n’a pas réussi à générer la vidéo.'}
|
||||
</Text>
|
||||
<BorderGradientButton title="Réessayer" onPress={handleRetry} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.DRAFT_READY ? (
|
||||
<>
|
||||
<View style={styles.videoCard}>
|
||||
{draftUrl ? (
|
||||
<VideoView
|
||||
player={player}
|
||||
nativeControls
|
||||
contentFit="contain"
|
||||
style={styles.video}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.video, styles.emptyVideo]}>
|
||||
<Text style={styles.helperText}>La vidéo HeyGen est introuvable.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<GradientButton
|
||||
title="Valider ce playback"
|
||||
onPress={handleValidate}
|
||||
disabled={isValidating || !draftUrl}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Recommencer"
|
||||
onPress={handleRetry}
|
||||
disabled={isValidating}
|
||||
/>
|
||||
{isValidating ? (
|
||||
<ActivityLoader defaultMessage="Validation du playback IA..." />
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.READY && project?.playbackUrl ? (
|
||||
<View style={styles.stateCard}>
|
||||
<Text style={styles.successTitle}>Le playback IA est déjà validé</Text>
|
||||
<GradientButton
|
||||
title="Continuer"
|
||||
onPress={() =>
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
project,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
flex: 1,
|
||||
width: '92%',
|
||||
alignSelf: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
headerCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: 16,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.76)',
|
||||
},
|
||||
cover: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 14,
|
||||
},
|
||||
title: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
subtitle: {
|
||||
color: Palette.white,
|
||||
opacity: 0.7,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
},
|
||||
stateCard: {
|
||||
flex: 1,
|
||||
minHeight: 240,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.8)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 14,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
videoCard: {
|
||||
width: '60%',
|
||||
maxWidth: 360,
|
||||
aspectRatio: 9 / 16,
|
||||
alignSelf: 'center',
|
||||
borderRadius: 22,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.8)',
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: '#0A0A0A',
|
||||
},
|
||||
emptyVideo: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
helperText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.72,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
lineHeight: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
errorTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
successTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
actions: {
|
||||
gap: 12,
|
||||
},
|
||||
}
|
||||
|
||||
export default PlaybackAiStatus
|
||||
@@ -0,0 +1,283 @@
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { Image, Text, View } from 'react-native'
|
||||
import { background } from '../../assets'
|
||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||
import ActivityLoader from '../../components/ActivityLoader'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import GradientButton from '../../components/GradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
import { getFunctionsClient, projectsRef } from '../../config/firebase'
|
||||
import Page from '../../layouts/Page'
|
||||
import { Routes } from '../../navigation'
|
||||
import { goBack, navigate, push } from '../../navigation/NavigationService'
|
||||
import { useUserData } from '../../providers/UserDataProvider'
|
||||
import { gutters, Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||
import { PLAYBACK_AI_STATUS } from '../../utils/playbackAi'
|
||||
|
||||
const FUNCTIONS_REGION = 'europe-west1'
|
||||
|
||||
const resolveProject = ({ routeProject, selectedProject, liveProject }) => {
|
||||
if (liveProject?.id) return liveProject
|
||||
if (routeProject?.id && selectedProject?.id === routeProject.id) {
|
||||
return selectedProject
|
||||
}
|
||||
return routeProject || null
|
||||
}
|
||||
|
||||
const callValidatePlaybackDraft = (payload) => {
|
||||
const callable = getFunctionsClient(FUNCTIONS_REGION).httpsCallable('heygen-validatePlaybackDraft')
|
||||
return callable(payload)
|
||||
}
|
||||
|
||||
const PlaybackAiStatus = ({ route }) => {
|
||||
const { project: routeProject } = route.params || {}
|
||||
const { selectedProject } = useUserData() || {}
|
||||
const { setTooltip } = useMinuit()
|
||||
const [isValidating, setIsValidating] = useState(false)
|
||||
const projectId = routeProject?.id || selectedProject?.id || null
|
||||
const { data: liveProject = null } = useDataFromRef({
|
||||
ref: projectId ? projectsRef.doc(projectId) : null,
|
||||
simpleRef: true,
|
||||
listener: true,
|
||||
condition: !!projectId,
|
||||
refreshArray: [projectId],
|
||||
})
|
||||
const project = useMemo(
|
||||
() => resolveProject({ routeProject, selectedProject, liveProject }),
|
||||
[liveProject, routeProject, selectedProject]
|
||||
)
|
||||
const status =
|
||||
project?.playbackStatus ||
|
||||
(routeProject?.playbackGenerating ? PLAYBACK_AI_STATUS.GENERATING : null)
|
||||
const draftUrl = project?.playbackDraftUrl || routeProject?.playbackDraftUrl || null
|
||||
|
||||
const handleValidate = async () => {
|
||||
try {
|
||||
if (!project?.id) {
|
||||
throw new Error('Projet introuvable')
|
||||
}
|
||||
|
||||
setIsValidating(true)
|
||||
await callValidatePlaybackDraft({ projectId: project.id })
|
||||
|
||||
const latestProjectSnap = await projectsRef.doc(project.id).get()
|
||||
const latestProject = latestProjectSnap.exists
|
||||
? { ...latestProjectSnap.data(), id: latestProjectSnap.id }
|
||||
: project
|
||||
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
project: latestProject,
|
||||
})
|
||||
} catch (error) {
|
||||
setTooltip({
|
||||
type: 'error',
|
||||
text: error?.message || 'Impossible de valider le playback IA',
|
||||
})
|
||||
} finally {
|
||||
setIsValidating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRetry = () => {
|
||||
push(Routes.PlaybackAi, {
|
||||
project,
|
||||
initialPhotoUrl: project?.playbackPhotoUrl || null,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.playbackBG2}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader progress={38} onPressBack={goBack} />
|
||||
<View style={{ flex: 1, paddingBottom: gutters * 2 }}>
|
||||
<CreateLyricsHeader title="Playback IA" />
|
||||
|
||||
<View style={styles.content}>
|
||||
<View style={styles.headerCard}>
|
||||
{project?.coverUrl ? (
|
||||
<Image source={{ uri: project.coverUrl }} style={styles.cover} resizeMode="cover" />
|
||||
) : null}
|
||||
<View style={{ flex: 1, gap: 4 }}>
|
||||
<Text style={styles.title}>{project?.title || 'Sans titre'}</Text>
|
||||
<Text style={styles.subtitle}>Format vertical 9:16 généré avec HeyGen</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.GENERATING ? (
|
||||
<View style={styles.stateCard}>
|
||||
<ActivityLoader defaultMessage="HeyGen génère ton playback IA..." />
|
||||
<Text style={styles.helperText}>
|
||||
Tu peux fermer cet écran et revenir plus tard. Une notification te sera envoyée dès
|
||||
que ta vidéo sera prête.
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.FAILED ? (
|
||||
<View style={styles.stateCard}>
|
||||
<Text style={styles.errorTitle}>La génération a échoué</Text>
|
||||
<Text style={styles.helperText}>
|
||||
{project?.playbackError || 'HeyGen n’a pas réussi à générer la vidéo.'}
|
||||
</Text>
|
||||
<BorderGradientButton title="Réessayer" onPress={handleRetry} />
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.DRAFT_READY ? (
|
||||
<>
|
||||
<View style={styles.videoCard}>
|
||||
{draftUrl ? (
|
||||
<video
|
||||
src={draftUrl}
|
||||
controls
|
||||
playsInline
|
||||
style={styles.video}
|
||||
/>
|
||||
) : (
|
||||
<View style={[styles.videoFallback, styles.emptyVideo]}>
|
||||
<Text style={styles.helperText}>La vidéo HeyGen est introuvable.</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.actions}>
|
||||
<GradientButton
|
||||
title="Valider ce playback"
|
||||
onPress={handleValidate}
|
||||
disabled={isValidating || !draftUrl}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Recommencer"
|
||||
onPress={handleRetry}
|
||||
disabled={isValidating}
|
||||
/>
|
||||
{isValidating ? (
|
||||
<ActivityLoader defaultMessage="Validation du playback IA..." />
|
||||
) : null}
|
||||
</View>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{status === PLAYBACK_AI_STATUS.READY && project?.playbackUrl ? (
|
||||
<View style={styles.stateCard}>
|
||||
<Text style={styles.successTitle}>Le playback IA est déjà validé</Text>
|
||||
<GradientButton
|
||||
title="Continuer"
|
||||
onPress={() =>
|
||||
navigate(Routes.PlaybackDownload, {
|
||||
action: 'playback',
|
||||
project,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = {
|
||||
content: {
|
||||
flex: 1,
|
||||
width: '92%',
|
||||
alignSelf: 'center',
|
||||
gap: 16,
|
||||
},
|
||||
headerCard: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
padding: 16,
|
||||
borderRadius: 18,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.76)',
|
||||
},
|
||||
cover: {
|
||||
width: 68,
|
||||
height: 68,
|
||||
borderRadius: 14,
|
||||
},
|
||||
title: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
subtitle: {
|
||||
color: Palette.white,
|
||||
opacity: 0.7,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
},
|
||||
stateCard: {
|
||||
flex: 1,
|
||||
minHeight: 240,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.8)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: 14,
|
||||
paddingHorizontal: 20,
|
||||
},
|
||||
videoCard: {
|
||||
width: '60%',
|
||||
maxWidth: 360,
|
||||
aspectRatio: 9 / 16,
|
||||
alignSelf: 'center',
|
||||
borderRadius: 22,
|
||||
overflow: 'hidden',
|
||||
borderWidth: 1,
|
||||
borderColor: 'rgba(255,255,255,0.14)',
|
||||
backgroundColor: 'rgba(5,12,24,0.8)',
|
||||
padding: 12,
|
||||
},
|
||||
video: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#0A0A0A',
|
||||
objectFit: 'contain',
|
||||
},
|
||||
videoFallback: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderRadius: 18,
|
||||
backgroundColor: '#0A0A0A',
|
||||
},
|
||||
emptyVideo: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
helperText: {
|
||||
color: Palette.white,
|
||||
opacity: 0.72,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
lineHeight: 20,
|
||||
textAlign: 'center',
|
||||
},
|
||||
errorTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
successTitle: {
|
||||
color: Palette.white,
|
||||
fontSize: 18,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
actions: {
|
||||
gap: 12,
|
||||
},
|
||||
}
|
||||
|
||||
export default PlaybackAiStatus
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { View, StyleSheet } from 'react-native'
|
||||
import { Pressable, View, StyleSheet } from 'react-native'
|
||||
import { responsiveHeight } from 'react-native-responsive-dimensions'
|
||||
import { SheetManager } from 'react-native-actions-sheet'
|
||||
import { useUser } from '../../../providers/UserDataProvider'
|
||||
@@ -53,6 +53,18 @@ const PlaybackItem = ({ item, isActive, userCache }) => {
|
||||
isActive,
|
||||
})
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
if (!isActive || !videoUrl || !videoPlayer) return
|
||||
|
||||
try {
|
||||
if (videoPlayer.playing) {
|
||||
videoPlayer.pause()
|
||||
} else {
|
||||
videoPlayer.play()
|
||||
}
|
||||
} catch (e) {}
|
||||
}, [isActive, videoPlayer, videoUrl])
|
||||
|
||||
const alignedWords = useMemo(() => {
|
||||
const idx = Number(item?.songIndex) || 0
|
||||
const ts = item?.musicTimestamps?.[idx]
|
||||
@@ -124,7 +136,11 @@ const PlaybackItem = ({ item, isActive, userCache }) => {
|
||||
}, [item?.userId])
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Pressable
|
||||
accessible={false}
|
||||
onPress={togglePlayback}
|
||||
style={styles.container}
|
||||
>
|
||||
<PlaybackVideo videoUrl={videoUrl} videoPlayer={videoPlayer} />
|
||||
<View style={styles.overlay}>
|
||||
<PlaybackActions
|
||||
@@ -148,7 +164,7 @@ const PlaybackItem = ({ item, isActive, userCache }) => {
|
||||
fallbackTitle={item?.title}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { MaterialCommunityIcons } from '@expo/vector-icons'
|
||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||
import * as FileSystem from 'expo-file-system'
|
||||
import { shareAsync } from 'expo-sharing'
|
||||
import { background } from '../../assets'
|
||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||
@@ -424,12 +425,10 @@ const PlaybackDownload = ({ route }) => {
|
||||
onClose={() => setIsAfterPlaybackVideoVisible(false)}
|
||||
/>
|
||||
<Page
|
||||
// backgroundImg={
|
||||
// action === "playback"
|
||||
// ? background.playbackBG2
|
||||
// : background.productionBG2
|
||||
// }
|
||||
backgroundColor={Palette.grayMid}
|
||||
backgroundImg={
|
||||
action === 'playback' ? background.playbackBG2 : background.productionBG2
|
||||
}
|
||||
backgroundColor="#07111B"
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader onPressBack={goBack} progress={50} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useMemo, useRef, useState } from 'react'
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import { useIsFocused, 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'
|
||||
@@ -37,6 +37,7 @@ 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)
|
||||
@@ -295,7 +296,9 @@ const ComposeSong = () => {
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={background.studioBG2}
|
||||
backgroundContent={<BackgroundVideo source={videos?.malik} />}
|
||||
backgroundContent={
|
||||
isFocused ? <BackgroundVideo source={videos?.malik} /> : null
|
||||
}
|
||||
headerType="NONE"
|
||||
>
|
||||
<MusicLandHeader onPressBack={onPressBack} progress={progress} logo={icons.musicLandStudio} />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useRoute } from '@react-navigation/native'
|
||||
import { useIsFocused, useRoute } from '@react-navigation/native'
|
||||
import { FlatList, Modal, StyleSheet, Text, View, useWindowDimensions } from 'react-native'
|
||||
import { background } from '../../assets'
|
||||
import BackgroundVideo from '../../components/BackgroundVideo'
|
||||
@@ -34,6 +34,7 @@ const PAGE_MAX_WIDTH = 1200
|
||||
const PAGE_HORIZONTAL_PADDING = gutters * 2
|
||||
|
||||
const ComposeSong = () => {
|
||||
const isFocused = useIsFocused()
|
||||
const scrollRef = useRef(null)
|
||||
const { width: windowWidth } = useWindowDimensions()
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
@@ -298,10 +299,12 @@ const ComposeSong = () => {
|
||||
<Page
|
||||
backgroundImg={background.studioBG2}
|
||||
backgroundContent={
|
||||
<BackgroundVideo
|
||||
source={videos?.malikWeb}
|
||||
soundButtonStyle={styles.rightSideControl}
|
||||
/>
|
||||
isFocused ? (
|
||||
<BackgroundVideo
|
||||
source={videos?.malikWeb}
|
||||
soundButtonStyle={styles.rightSideControl}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
coinBadgeContainerStyle={styles.rightSideCredits}
|
||||
containerStyle={styles.videoQuestionPage}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
export const PLAYBACK_AI_STATUS = {
|
||||
GENERATING: 'GENERATING',
|
||||
DRAFT_READY: 'DRAFT_READY',
|
||||
FAILED: 'FAILED',
|
||||
READY: 'READY',
|
||||
}
|
||||
|
||||
export const isPlaybackAiBlockingManual = (project) =>
|
||||
project?.playbackStatus === PLAYBACK_AI_STATUS.GENERATING ||
|
||||
project?.playbackStatus === PLAYBACK_AI_STATUS.DRAFT_READY
|
||||
|
||||
export const shouldResumePlaybackAi = (project) =>
|
||||
project?.playbackStatus === PLAYBACK_AI_STATUS.GENERATING ||
|
||||
project?.playbackStatus === PLAYBACK_AI_STATUS.DRAFT_READY ||
|
||||
project?.playbackStatus === PLAYBACK_AI_STATUS.FAILED
|
||||
|
||||
export const getPlaybackAiStatusLabel = (status) => {
|
||||
switch (status) {
|
||||
case PLAYBACK_AI_STATUS.GENERATING:
|
||||
return 'Playback IA en cours de génération'
|
||||
case PLAYBACK_AI_STATUS.DRAFT_READY:
|
||||
return 'Playback IA prêt à être validé'
|
||||
case PLAYBACK_AI_STATUS.FAILED:
|
||||
return 'Le dernier playback IA a échoué'
|
||||
case PLAYBACK_AI_STATUS.READY:
|
||||
return 'Playback IA validé'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ const getStageMetadata = (project) => {
|
||||
project?.playbackGenerating === true ||
|
||||
project?.isPlaybackGenerating === true ||
|
||||
playbackStatus === 'GENERATING'
|
||||
const isPlaybackDraftReady = playbackStatus === 'DRAFT_READY'
|
||||
|
||||
return {
|
||||
lyricsCount,
|
||||
@@ -63,6 +64,7 @@ const getStageMetadata = (project) => {
|
||||
musicStatus,
|
||||
coverStatus,
|
||||
playbackStatus,
|
||||
isPlaybackDraftReady,
|
||||
youtubeStatus,
|
||||
youtubeError,
|
||||
isMusicGenerating: musicStatus === 'GENERATING',
|
||||
@@ -103,6 +105,7 @@ const getStageDescription = (key, metadata) => {
|
||||
hasMusicDraft,
|
||||
isMusicGenerating,
|
||||
isPlaybackGenerating,
|
||||
isPlaybackDraftReady,
|
||||
} = metadata
|
||||
|
||||
switch (key) {
|
||||
@@ -141,6 +144,9 @@ const getStageDescription = (key, metadata) => {
|
||||
if (isPlaybackGenerating) {
|
||||
return 'Votre playback est en cours de préparation'
|
||||
}
|
||||
if (isPlaybackDraftReady) {
|
||||
return 'Votre playback IA est prêt à être validé'
|
||||
}
|
||||
if (hasPlaybackAsset) {
|
||||
return 'Modifier le playback généré'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user