This commit is contained in:
Thomas Demirdjian
2026-08-04 10:59:27 +02:00
parent 761f3cc200
commit c8656d6273
25 changed files with 1743 additions and 75 deletions
@@ -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 na 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