feat: fixes and formatter

This commit is contained in:
2026-01-12 16:01:32 +01:00
parent 85c6084351
commit 11e632acff
353 changed files with 23315 additions and 27361 deletions
+226 -264
View File
@@ -1,353 +1,326 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import {
Image,
Platform,
Pressable,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background } from "../../assets";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import BorderGradientButton from "../../components/BorderGradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
import AppCheckbox from "../../components/AppCheckbox";
import firebase, {
projectsRef,
serverTimestamp,
videosRef,
} from "../../config/firebase";
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData, useUser } from "../../providers/UserDataProvider";
import { Palette } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import { size } from "../../styles/Style";
import { getBlobForUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
import ClubAdvantagesCard from "../Profile/components/ClubAdvantagesCard";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import { Image as ExpoImage } from "expo-image";
import SubscriptionConfirmModal from "../../components/SubscriptionConfirmModal";
import { isWeb } from "../../hooks/useLayoutType";
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Image, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native'
import { MaterialCommunityIcons } from '@expo/vector-icons'
import useMinuit from 'react-native-minuit/src/hooks/useMinuit.js'
import { background } from '../../assets'
import FullscreenIntroVideo from '../../components/FullscreenIntroVideo'
import BorderGradientButton from '../../components/BorderGradientButton'
import MusicLandHeader from '../../components/MusicLandHeader'
import AppCheckbox from '../../components/AppCheckbox'
import firebase, { projectsRef, serverTimestamp, videosRef } from '../../config/firebase'
import { uploadFileToFirebase } from '../../helpers/uploadToFirebase'
import useDataFromRef from '../../hooks/useDataFromRef'
import Page from '../../layouts/Page'
import { Routes } from '../../navigation'
import { goBack, navigate } from '../../navigation/NavigationService'
import { useUserData, useUser } from '../../providers/UserDataProvider'
import { Palette } from '../../styles'
import { FONT_FAMILY } from '../../styles/Fonts'
import { size } from '../../styles/Style'
import { getBlobForUrl, releaseBlobUrl } from '../../utils/blobUrlCache'
import ClubAdvantagesCard from '../Profile/components/ClubAdvantagesCard'
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
import { Image as ExpoImage } from 'expo-image'
import SubscriptionConfirmModal from '../../components/SubscriptionConfirmModal'
import { isWeb } from '../../hooks/useLayoutType'
const triggerWebDownload = async (url, title) => {
if (Platform.OS !== "web") return;
if (!url) return;
if (typeof document === "undefined") return;
const baseName = (title || "Playback").toString().trim() || "Playback";
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, "-");
const extension = guessExtension(url) || "mp4";
const filename = `${sanitized}.${extension}`;
if (Platform.OS !== 'web') return
if (!url) return
if (typeof document === 'undefined') return
const baseName = (title || 'Playback').toString().trim() || 'Playback'
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, '-')
const extension = guessExtension(url) || 'mp4'
const filename = `${sanitized}.${extension}`
try {
const response = await fetch(url);
if (!response.ok || response.type === "opaque") {
throw new Error(`download_failed_${response.status || "opaque"}`);
const response = await fetch(url)
if (!response.ok || response.type === 'opaque') {
throw new Error(`download_failed_${response.status || 'opaque'}`)
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = blobUrl;
anchor.download = filename;
anchor.rel = "noopener noreferrer";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
URL.revokeObjectURL(blobUrl);
const blob = await response.blob()
const blobUrl = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = blobUrl
anchor.download = filename
anchor.rel = 'noopener noreferrer'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
URL.revokeObjectURL(blobUrl)
} catch (error) {
console.log("[PlaybackDownload] web download fallback", {
console.log('[PlaybackDownload] web download fallback', {
message: error?.message,
});
})
try {
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.rel = "noopener noreferrer";
anchor.target = "_blank";
document.body.appendChild(anchor);
anchor.click();
document.body.removeChild(anchor);
return;
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.rel = 'noopener noreferrer'
anchor.target = '_blank'
document.body.appendChild(anchor)
anchor.click()
document.body.removeChild(anchor)
return
} catch (fallbackError) {
console.log("[PlaybackDownload] anchor fallback failed", {
console.log('[PlaybackDownload] anchor fallback failed', {
message: fallbackError?.message,
});
})
}
try {
window.open(url, "_blank", "noopener,noreferrer");
} catch { }
window.open(url, '_blank', 'noopener,noreferrer')
} catch {}
}
};
}
const guessExtension = (inputUri = "") => {
const cleaned = inputUri.split("?")[0] || "";
const match = cleaned.match(/\.([a-z0-9]+)$/i);
if (match && match[1]) return match[1].toLowerCase();
if (Platform.OS === "web") return "webm";
return "mp4";
};
const guessExtension = (inputUri = '') => {
const cleaned = inputUri.split('?')[0] || ''
const match = cleaned.match(/\.([a-z0-9]+)$/i)
if (match && match[1]) return match[1].toLowerCase()
if (Platform.OS === 'web') return 'webm'
return 'mp4'
}
const uploadSourceRecording = async ({ uri, uid, projectId }) => {
console.log("[PlaybackDownload] uploadSourceRecording params", {
console.log('[PlaybackDownload] uploadSourceRecording params', {
hasUri: Boolean(uri),
uid,
projectId,
});
})
if (!uri) throw new Error("Aucune vidéo trouvée");
if (!uid) throw new Error("Utilisateur non authentifié");
if (!uri) throw new Error('Aucune vidéo trouvée')
if (!uid) throw new Error('Utilisateur non authentifié')
const extension = guessExtension(uri);
const sourcePath = `users/${uid}/projects/${projectId}/recordings/source-${Date.now()}.${extension}`;
console.log("source path : ", sourcePath);
console.log("[PlaybackDownload] uploadSourceRecording source path", {
const extension = guessExtension(uri)
const sourcePath = `users/${uid}/projects/${projectId}/recordings/source-${Date.now()}.${extension}`
console.log('source path : ', sourcePath)
console.log('[PlaybackDownload] uploadSourceRecording source path', {
extension,
sourcePath,
});
})
const cachedBlob = getBlobForUrl(uri);
console.log("[PlaybackDownload] uploadSourceRecording blob cache", {
const cachedBlob = getBlobForUrl(uri)
console.log('[PlaybackDownload] uploadSourceRecording blob cache', {
hasBlob: Boolean(cachedBlob),
});
})
const { resultURI: videoUrl } = await uploadFileToFirebase({
uri,
path: sourcePath,
shouldCompress: true,
fileType: "VIDEO",
fileType: 'VIDEO',
blob: cachedBlob || undefined,
});
})
return { sourcePath, videoUrl };
};
return { sourcePath, videoUrl }
}
const PlaybackDownload = ({ route }) => {
const { currentUID } = useUserData();
const { hasActiveSubscription } = useUser() || {};
const { action, uri, project } = route.params || {};
console.log("[PlaybackDownload] route params", {
const { currentUID } = useUserData()
const { hasActiveSubscription } = useUser() || {}
const { action, uri, project } = route.params || {}
console.log('[PlaybackDownload] route params', {
action,
projectId: project?.id,
hasUri: Boolean(uri),
currentUID,
});
const { setIsLoading, setTooltip } = useMinuit();
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] =
useState(false);
const [showConfirmModal, setShowConfirmModal] = useState(false);
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(
project?.playbackUrl || null,
);
const [isPublishing, setIsPublishing] = useState(false);
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true);
const hasShownAfterPlaybackRef = useRef(false);
})
const { setIsLoading, setTooltip } = useMinuit()
const [isAfterPlaybackVideoVisible, setIsAfterPlaybackVideoVisible] = useState(false)
const [showConfirmModal, setShowConfirmModal] = useState(false)
const [pendingPlaybackUrl, setPendingPlaybackUrl] = useState(project?.playbackUrl || null)
const [isPublishing, setIsPublishing] = useState(false)
const [hasAcceptedPublication, setHasAcceptedPublication] = useState(true)
const hasShownAfterPlaybackRef = useRef(false)
const { data: video } = useDataFromRef({
ref: videosRef.doc("fr"),
ref: videosRef.doc('fr'),
simpleRef: true,
});
})
const afterPlaybackUrl = useMemo(() => {
if (!video) return null;
return video?.afterPlayback;
}, [video]);
if (!video) return null
return video?.afterPlayback
}, [video])
useEffect(() => {
if (action !== "playback") return;
if (!afterPlaybackUrl) return;
if (hasShownAfterPlaybackRef.current) return;
hasShownAfterPlaybackRef.current = true;
setIsAfterPlaybackVideoVisible(true);
}, [action, afterPlaybackUrl]);
if (action !== 'playback') return
if (!afterPlaybackUrl) return
if (hasShownAfterPlaybackRef.current) return
hasShownAfterPlaybackRef.current = true
setIsAfterPlaybackVideoVisible(true)
}, [action, afterPlaybackUrl])
useEffect(() => {
if (project?.playbackUrl) {
setPendingPlaybackUrl(project.playbackUrl);
setPendingPlaybackUrl(project.playbackUrl)
}
}, [project?.playbackUrl]);
}, [project?.playbackUrl])
useEffect(() => {
return () => {
releaseBlobUrl(uri || null);
};
}, [uri]);
releaseBlobUrl(uri || null)
}
}, [uri])
const handleDownloadUri = async () => {
if (isPublishing) return;
if (action === "playback" && project?.id) {
if (isPublishing) return
if (action === 'playback' && project?.id) {
if (pendingPlaybackUrl) {
await triggerWebDownload(pendingPlaybackUrl, project?.title);
return;
await triggerWebDownload(pendingPlaybackUrl, project?.title)
return
}
// Publication du playback
console.log("[PlaybackDownload] handleDownloadUri playback", {
console.log('[PlaybackDownload] handleDownloadUri playback', {
projectId: project.id,
uri,
currentUID,
});
const audioUrl = project?.songUrl || null;
})
const audioUrl = project?.songUrl || null
if (!audioUrl) {
setTooltip({
type: "error",
text: "Aucune piste audio disponible pour ce projet",
});
return;
type: 'error',
text: 'Aucune piste audio disponible pour ce projet',
})
return
}
let tempSourcePath = null;
let tempSourcePath = null
try {
setIsLoading(true);
setIsLoading(true)
const { sourcePath, videoUrl } = await uploadSourceRecording({
uri,
uid: currentUID,
projectId: project.id,
});
tempSourcePath = sourcePath;
})
tempSourcePath = sourcePath
const callable = firebase
.functions()
.httpsCallable("upload-mergeVideoAndAudio");
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,
);
console.log('[PlaybackDownload] calling upload-mergeVideoAndAudio', payload)
const { data: result } = await callable(payload);
const { data: result } = await callable(payload)
console.log(
"[PlaybackDownload] upload-mergeVideoAndAudio result",
result,
);
console.log('[PlaybackDownload] upload-mergeVideoAndAudio result', result)
const resultURI = result?.url || null;
const resultURI = result?.url || null
if (resultURI) {
setPendingPlaybackUrl(resultURI);
setPendingPlaybackUrl(resultURI)
setTooltip({
type: "success",
text: "Playback prêt à télécharger",
});
await triggerWebDownload(resultURI, project?.title);
type: 'success',
text: 'Playback prêt à télécharger',
})
await triggerWebDownload(resultURI, project?.title)
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
await firebase.storage().ref(tempSourcePath).delete()
} catch (cleanupError) {
console.log("[PlaybackDownload] unable to delete temp source", {
console.log('[PlaybackDownload] unable to delete temp source', {
message: cleanupError?.message,
code: cleanupError?.code,
});
})
}
}
} else {
setTooltip({
type: "error",
text: "Erreur lors de la publication du playback",
});
type: 'error',
text: 'Erreur lors de la publication du playback',
})
}
} catch (error) {
console.log("[PlaybackDownload] error upload playback", {
console.log('[PlaybackDownload] error upload playback', {
message: error?.message,
code: error?.code,
name: error?.name,
details: error?.details,
});
})
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
} catch { }
await firebase.storage().ref(tempSourcePath).delete()
} catch {}
}
setTooltip({
type: "error",
text: String(
error?.message || "Erreur lors de la publication du playback",
),
});
type: 'error',
text: String(error?.message || 'Erreur lors de la publication du playback'),
})
} finally {
releaseBlobUrl(uri || null);
setIsLoading(false);
releaseBlobUrl(uri || null)
setIsLoading(false)
}
// Handle playback download
} else {
// Handle song download
}
};
}
const handlePublish = async () => {
if (isPublishing) return;
if (isPublishing) return
if (!hasAcceptedPublication) {
setTooltip({
type: "error",
text: "Confirme la diffusion sur Musicland et YouTube avant de publier",
});
return;
type: 'error',
text: 'Confirme la diffusion sur Musicland et YouTube avant de publier',
})
return
}
if (!project?.id) {
if (action === "playback") {
if (action === 'playback') {
setTooltip({
type: "success",
text: "Playback publié !",
});
navigate(Routes.Home);
type: 'success',
text: 'Playback publié !',
})
navigate(Routes.Home)
} else {
navigate(Routes.SongRelease, { action });
navigate(Routes.SongRelease, { action })
}
return;
return
}
let tempSourcePath = null;
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null;
const audioUrl = project?.songUrl || null;
let tempSourcePath = null
let playbackUrlToSave = pendingPlaybackUrl || project?.playbackUrl || null
const audioUrl = project?.songUrl || null
if (!audioUrl && !playbackUrlToSave) {
setTooltip({
type: "error",
text: "Aucune piste audio disponible pour ce projet",
});
return;
type: 'error',
text: 'Aucune piste audio disponible pour ce projet',
})
return
}
try {
setIsPublishing(true);
setIsLoading(true);
setIsPublishing(true)
setIsLoading(true)
if (!playbackUrlToSave) {
const { sourcePath, videoUrl } = await uploadSourceRecording({
uri,
uid: currentUID,
projectId: project.id,
});
tempSourcePath = sourcePath;
})
tempSourcePath = sourcePath
const callable = firebase
.functions()
.httpsCallable("upload-mergeVideoAndAudio");
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] publish: calling merge", payload);
const { data: result } = await callable(payload);
playbackUrlToSave = result?.url || null;
if (!playbackUrlToSave) {
throw new Error("merge_failed");
}
setPendingPlaybackUrl(playbackUrlToSave);
console.log('[PlaybackDownload] publish: calling merge', payload)
const { data: result } = await callable(payload)
playbackUrlToSave = result?.url || null
if (!playbackUrlToSave) {
throw new Error('merge_failed')
}
setPendingPlaybackUrl(playbackUrlToSave)
}
await projectsRef.doc(project.id).set(
@@ -355,47 +328,45 @@ const PlaybackDownload = ({ route }) => {
playbackUrl: playbackUrlToSave,
updatedAt: serverTimestamp(),
},
{ merge: true },
);
{ merge: true }
)
if (action === "playback") {
if (action === 'playback') {
setTooltip({
type: "success",
text: "Playback publié !",
});
navigate(Routes.Home);
type: 'success',
text: 'Playback publié !',
})
navigate(Routes.Home)
} else {
navigate(Routes.SongRelease, { action });
navigate(Routes.SongRelease, { action })
}
} catch (error) {
console.log("[PlaybackDownload] publish error", {
console.log('[PlaybackDownload] publish error', {
message: error?.message,
code: error?.code,
name: error?.name,
details: error?.details,
});
})
setTooltip({
type: "error",
text: String(error?.message || "Publication impossible"),
});
type: 'error',
text: String(error?.message || 'Publication impossible'),
})
} finally {
if (tempSourcePath) {
try {
await firebase.storage().ref(tempSourcePath).delete();
await firebase.storage().ref(tempSourcePath).delete()
} catch (cleanupError) {
console.log("[PlaybackDownload] unable to delete temp source", {
console.log('[PlaybackDownload] unable to delete temp source', {
message: cleanupError?.message,
code: cleanupError?.code,
});
})
}
}
setIsPublishing(false);
setIsLoading(false);
setIsPublishing(false)
setIsLoading(false)
}
};
const continueLabel = hasActiveSubscription
? "Publier"
: "Publier sans générer de revenus";
}
const continueLabel = hasActiveSubscription ? 'Publier' : 'Publier sans générer de revenus'
return (
<>
<Page
@@ -408,11 +379,8 @@ const PlaybackDownload = ({ route }) => {
headerType="NONE"
>
<MusicLandHeader onPressBack={goBack} progress={50} />
<ScrollView
contentContainerStyle={styles.container}
showsVerticalScrollIndicator={false}
>
<CreateLyricsHeader title={"Publier ton playback"} />
<ScrollView contentContainerStyle={styles.container} showsVerticalScrollIndicator={false}>
<CreateLyricsHeader title={'Publier ton playback'} />
<View style={styles.coverRow}>
{project?.coverUrl ? (
@@ -428,11 +396,7 @@ const PlaybackDownload = ({ route }) => {
)}
<Pressable style={styles.downloadTile} onPress={handleDownloadUri}>
<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>
</Pressable>
</View>
@@ -442,9 +406,7 @@ const PlaybackDownload = ({ route }) => {
<View style={styles.publishConsentContainer}>
<AppCheckbox
selected={hasAcceptedPublication}
onPress={() =>
setHasAcceptedPublication((prevState) => !prevState)
}
onPress={() => setHasAcceptedPublication((prevState) => !prevState)}
label="J'accepte la diffusion de mon playback sur Musicland et YouTube."
/>
<Text style={styles.publishConsentDescription}>
@@ -456,9 +418,9 @@ const PlaybackDownload = ({ route }) => {
title={continueLabel}
onPress={() => {
if (hasActiveSubscription) {
handlePublish();
handlePublish()
} else {
setShowConfirmModal(true);
setShowConfirmModal(true)
}
}}
disabled={isPublishing || !hasAcceptedPublication}
@@ -467,17 +429,17 @@ const PlaybackDownload = ({ route }) => {
</ScrollView>
</Page>
<SubscriptionConfirmModal
continueLabel={"Continuer vers la publication de playback"}
continueLabel={'Continuer vers la publication de playback'}
isVisible={showConfirmModal}
setIsVisible={setShowConfirmModal}
onJoinClub={() => navigate(Routes.Payments)}
onContinue={handlePublish}
/>
</>
);
};
)
}
export default PlaybackDownload;
export default PlaybackDownload
const styles = StyleSheet.create({
container: {
@@ -487,25 +449,25 @@ const styles = StyleSheet.create({
gap: 14,
},
coverRow: {
flexDirection: isWeb ? "row" : "column",
alignItems: "center",
justifyContent: isWeb ? "center" : "center",
flexDirection: isWeb ? 'row' : 'column',
alignItems: 'center',
justifyContent: isWeb ? 'center' : 'center',
gap: 12,
},
coverImage: {
...size({ size: 140 }),
borderRadius: 14,
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.18)",
borderColor: 'rgba(255, 255, 255, 0.18)',
},
downloadTile: {
flexDirection: "row",
alignItems: "center",
flexDirection: 'row',
alignItems: 'center',
gap: 10,
paddingVertical: 10,
paddingHorizontal: 14,
borderRadius: 14,
backgroundColor: "#8C4BFF",
backgroundColor: '#8C4BFF',
},
downloadText: {
fontFamily: FONT_FAMILY.InterSemiBold,
@@ -528,4 +490,4 @@ const styles = StyleSheet.create({
continueButton: {
marginTop: 10,
},
});
})