feat: download playbacks
This commit is contained in:
@@ -1,17 +1,15 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
|
import { Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
|
||||||
import { MaterialCommunityIcons } from '@expo/vector-icons'
|
import { MaterialCommunityIcons } from '@expo/vector-icons'
|
||||||
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
|
||||||
import * as FileSystem from 'expo-file-system'
|
import * as FileSystem from 'expo-file-system'
|
||||||
import { shareAsync } from 'expo-sharing'
|
import { shareAsync } from 'expo-sharing'
|
||||||
import { background } from '../../assets'
|
|
||||||
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
|
||||||
import BorderGradientButton from '../../components/BorderGradientButton'
|
import BorderGradientButton from '../../components/BorderGradientButton'
|
||||||
import MusicLandHeader from '../../components/MusicLandHeader'
|
import MusicLandHeader from '../../components/MusicLandHeader'
|
||||||
import AppCheckbox from '../../components/AppCheckbox'
|
import AppCheckbox from '../../components/AppCheckbox'
|
||||||
import firebase, { projectsRef, serverTimestamp, videosRef } from '../../config/firebase'
|
import firebase, { projectsRef, serverTimestamp } from '../../config/firebase'
|
||||||
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
|
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
|
||||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
|
||||||
import Page from '../../layouts/Page'
|
import Page from '../../layouts/Page'
|
||||||
import { Routes } from '../../navigation'
|
import { Routes } from '../../navigation'
|
||||||
import { goBack, navigate } from '../../navigation/NavigationService'
|
import { goBack, navigate } from '../../navigation/NavigationService'
|
||||||
@@ -137,8 +135,9 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
|
|||||||
|
|
||||||
const PlaybackDownload = ({ route }) => {
|
const PlaybackDownload = ({ route }) => {
|
||||||
const { currentUID } = useUserData()
|
const { currentUID } = useUserData()
|
||||||
const { hasActiveSubscription } = useUser() || {}
|
const { hasActiveSubscription, videos } = useUser() || {}
|
||||||
const { action, uri, project } = route.params || {}
|
const { action: routeAction, uri, project } = route.params || {}
|
||||||
|
const action = routeAction || 'playback'
|
||||||
console.log('[PlaybackDownload] route params', {
|
console.log('[PlaybackDownload] route params', {
|
||||||
action,
|
action,
|
||||||
projectId: project?.id,
|
projectId: project?.id,
|
||||||
@@ -150,18 +149,14 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
const [showConfirmModal, setShowConfirmModal] = useState(false)
|
||||||
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null)
|
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null)
|
||||||
const [isPublishing, setIsPublishing] = useState(false)
|
const [isPublishing, setIsPublishing] = useState(false)
|
||||||
|
const [isDownloading, setIsDownloading] = useState(false)
|
||||||
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
|
||||||
const hasShownAfterPlaybackRef = useRef(false)
|
const hasShownAfterPlaybackRef = useRef(false)
|
||||||
|
|
||||||
const { data: video } = useDataFromRef({
|
|
||||||
ref: videosRef.doc('fr'),
|
|
||||||
simpleRef: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const afterPlaybackUrl = useMemo(() => {
|
const afterPlaybackUrl = useMemo(() => {
|
||||||
if (!video) return null
|
if (!videos) return null
|
||||||
return video?.afterPlayback
|
return videos?.afterPlayback || null
|
||||||
}, [video])
|
}, [videos])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (action !== 'playback') return
|
if (action !== 'playback') return
|
||||||
@@ -179,100 +174,152 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const resolveAudioUrl = useCallback(() => {
|
||||||
|
if (typeof project?.songUrl === 'string' && project.songUrl.trim()) {
|
||||||
|
return project.songUrl.trim()
|
||||||
|
}
|
||||||
|
const urls = Array.isArray(project?.musicUrls) ? project.musicUrls : []
|
||||||
|
if (!urls.length) return null
|
||||||
|
const idx = Number.isFinite(Number(project?.songIndex)) ? Number(project.songIndex) : 0
|
||||||
|
const candidate = urls[idx]
|
||||||
|
if (typeof candidate === 'string' && candidate.trim()) {
|
||||||
|
return candidate.trim()
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}, [project])
|
||||||
|
|
||||||
const handleDownloadUri = async () => {
|
const handleDownloadUri = async () => {
|
||||||
if (isPublishing) return
|
if (isPublishing || isDownloading) return
|
||||||
if (action === 'playback' && project?.id) {
|
if (action !== 'playback') {
|
||||||
if (pendingPlaybackUrl) {
|
setTooltip({
|
||||||
await triggerDownload(pendingPlaybackUrl, project?.title)
|
type: 'error',
|
||||||
return
|
text: 'Téléchargement indisponible pour cette action',
|
||||||
}
|
|
||||||
// Publication du playback
|
|
||||||
console.log('[PlaybackDownload] handleDownloadUri playback', {
|
|
||||||
projectId: project.id,
|
|
||||||
uri,
|
|
||||||
currentUID,
|
|
||||||
})
|
})
|
||||||
const audioUrl = project?.songUrl || null
|
return
|
||||||
if (!audioUrl) {
|
}
|
||||||
setTooltip({
|
|
||||||
type: 'error',
|
const playbackUrlToDownload = pendingPlaybackUrl || project?.playbackUrl || null
|
||||||
text: 'Aucune piste audio disponible pour ce projet',
|
if (playbackUrlToDownload) {
|
||||||
})
|
setIsDownloading(true)
|
||||||
return
|
|
||||||
}
|
|
||||||
let tempSourcePath = null
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
await triggerDownload(playbackUrlToDownload, project?.title)
|
||||||
const { sourcePath, videoUrl } = await uploadSourceRecording({
|
} finally {
|
||||||
uri,
|
setIsDownloading(false)
|
||||||
uid: currentUID,
|
}
|
||||||
projectId: project.id,
|
return
|
||||||
})
|
}
|
||||||
tempSourcePath = sourcePath
|
|
||||||
|
if (!project?.id) {
|
||||||
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
|
setTooltip({
|
||||||
|
type: 'error',
|
||||||
const payload = {
|
text: 'Projet introuvable pour ce playback',
|
||||||
projectId: project?.id,
|
})
|
||||||
videoUrl,
|
return
|
||||||
audioUrl,
|
}
|
||||||
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
|
|
||||||
}
|
if (!uri) {
|
||||||
|
setTooltip({
|
||||||
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
|
type: 'error',
|
||||||
|
text: 'Aucune vidéo trouvée pour ce playback',
|
||||||
const { data: result } = await callable(payload)
|
})
|
||||||
|
return
|
||||||
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
|
}
|
||||||
|
|
||||||
const resultURI = result?.url || null
|
if (!currentUID) {
|
||||||
|
setTooltip({
|
||||||
if (resultURI) {
|
type: 'error',
|
||||||
setPendingPlaybackUrl(resultURI)
|
text: 'Utilisateur non authentifié',
|
||||||
setTooltip({
|
})
|
||||||
type: 'success',
|
return
|
||||||
text: 'Playback prêt à télécharger',
|
}
|
||||||
})
|
|
||||||
await triggerDownload(resultURI, project?.title)
|
const audioUrl = resolveAudioUrl()
|
||||||
if (tempSourcePath) {
|
if (!audioUrl) {
|
||||||
try {
|
setTooltip({
|
||||||
await firebase.storage().ref(tempSourcePath).delete()
|
type: 'error',
|
||||||
} catch (cleanupError) {
|
text: 'Aucune piste audio disponible pour ce projet',
|
||||||
console.log('[PlaybackDownload] unable to delete temp source', {
|
})
|
||||||
message: cleanupError?.message,
|
return
|
||||||
code: cleanupError?.code,
|
}
|
||||||
})
|
|
||||||
}
|
// Publication du playback
|
||||||
}
|
console.log('[PlaybackDownload] handleDownloadUri playback', {
|
||||||
} else {
|
projectId: project.id,
|
||||||
setTooltip({
|
uri,
|
||||||
type: 'error',
|
currentUID,
|
||||||
text: 'Erreur lors de la publication du playback',
|
})
|
||||||
})
|
|
||||||
}
|
let tempSourcePath = null
|
||||||
} catch (error) {
|
try {
|
||||||
console.log('[PlaybackDownload] error upload playback', {
|
setIsDownloading(true)
|
||||||
message: error?.message,
|
setIsLoading(true)
|
||||||
code: error?.code,
|
const { sourcePath, videoUrl } = await uploadSourceRecording({
|
||||||
name: error?.name,
|
uri,
|
||||||
details: error?.details,
|
uid: currentUID,
|
||||||
|
projectId: project.id,
|
||||||
|
})
|
||||||
|
tempSourcePath = sourcePath
|
||||||
|
|
||||||
|
const callable = firebase.functions().httpsCallable('upload-mergeVideoAndAudio')
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
projectId: project?.id,
|
||||||
|
videoUrl,
|
||||||
|
audioUrl,
|
||||||
|
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
|
||||||
|
|
||||||
|
const { data: result } = await callable(payload)
|
||||||
|
|
||||||
|
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
|
||||||
|
|
||||||
|
const resultURI = result?.url || null
|
||||||
|
|
||||||
|
if (resultURI) {
|
||||||
|
setPendingPlaybackUrl(resultURI)
|
||||||
|
setTooltip({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Playback prêt à télécharger',
|
||||||
})
|
})
|
||||||
|
await triggerDownload(resultURI, project?.title)
|
||||||
if (tempSourcePath) {
|
if (tempSourcePath) {
|
||||||
try {
|
try {
|
||||||
await firebase.storage().ref(tempSourcePath).delete()
|
await firebase.storage().ref(tempSourcePath).delete()
|
||||||
} catch { }
|
} catch (cleanupError) {
|
||||||
|
console.log('[PlaybackDownload] unable to delete temp source', {
|
||||||
|
message: cleanupError?.message,
|
||||||
|
code: cleanupError?.code,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
text: String(error?.message || 'Erreur lors de la publication du playback'),
|
text: 'Erreur lors de la publication du playback',
|
||||||
})
|
})
|
||||||
} finally {
|
|
||||||
releaseBlobUrl(uri || null)
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
}
|
||||||
// Handle playback download
|
} catch (error) {
|
||||||
} else {
|
console.log('[PlaybackDownload] error upload playback', {
|
||||||
// Handle song download
|
message: error?.message,
|
||||||
|
code: error?.code,
|
||||||
|
name: error?.name,
|
||||||
|
details: error?.details,
|
||||||
|
})
|
||||||
|
if (tempSourcePath) {
|
||||||
|
try {
|
||||||
|
await firebase.storage().ref(tempSourcePath).delete()
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
setTooltip({
|
||||||
|
type: 'error',
|
||||||
|
text: String(error?.message || 'Erreur lors de la publication du playback'),
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
releaseBlobUrl(uri || null)
|
||||||
|
setIsDownloading(false)
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +346,7 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
}
|
}
|
||||||
let tempSourcePath = null
|
let tempSourcePath = null
|
||||||
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null
|
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null
|
||||||
const audioUrl = project?.songUrl || null
|
const audioUrl = resolveAudioUrl()
|
||||||
if (!audioUrl && !playbackUrlToSave) {
|
if (!audioUrl && !playbackUrlToSave) {
|
||||||
setTooltip({
|
setTooltip({
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -307,6 +354,13 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (!currentUID) {
|
||||||
|
setTooltip({
|
||||||
|
type: 'error',
|
||||||
|
text: 'Utilisateur non authentifié',
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsPublishing(true)
|
setIsPublishing(true)
|
||||||
@@ -417,7 +471,11 @@ const PlaybackDownload = ({ route }) => {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Pressable style={styles.downloadTile} onPress={handleDownloadUri}>
|
<Pressable
|
||||||
|
style={[styles.downloadTile, (isPublishing || isDownloading) && { opacity: 0.6 }]}
|
||||||
|
onPress={handleDownloadUri}
|
||||||
|
disabled={isPublishing || isDownloading}
|
||||||
|
>
|
||||||
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
<MaterialCommunityIcons name="download" size={22} color={Palette.white} />
|
||||||
<Text style={styles.downloadText}>Télécharger le playback</Text>
|
<Text style={styles.downloadText}>Télécharger le playback</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|||||||
Reference in New Issue
Block a user