merge
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
@@ -1,13 +1,36 @@
|
||||
import { Platform } from "react-native";
|
||||
|
||||
import Compressor from "react-native-compressor";
|
||||
import firebase from "../config/firebase";
|
||||
|
||||
export function uploadFileToFirebase({ uri, path }) {
|
||||
export function uploadFileToFirebase({
|
||||
uri,
|
||||
path,
|
||||
shouldCompress = false,
|
||||
fileType = "",
|
||||
}) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
let resultResize = { uri };
|
||||
let workingURI = uri;
|
||||
|
||||
const response = await fetch(resultResize.uri);
|
||||
// Optionally compress before upload
|
||||
try {
|
||||
if (shouldCompress && Platform.OS !== "web") {
|
||||
if (fileType === "VIDEO") {
|
||||
console.log("Compressing video...");
|
||||
workingURI = await Compressor.Video.compress(workingURI);
|
||||
} else if (fileType === "IMAGE") {
|
||||
// Basic image compression
|
||||
workingURI = await Compressor.Image.compress(workingURI, {
|
||||
compressionMethod: "auto",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Compression failed, uploading original file", e?.message);
|
||||
workingURI = uri;
|
||||
}
|
||||
|
||||
const response = await fetch(workingURI);
|
||||
const blob = await response.blob();
|
||||
|
||||
const uploadTask = firebase.storage().ref(path).put(blob);
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { useRoute } from "@react-navigation/core";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
Pressable,
|
||||
Image as RNImage,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
Image as RNImage,
|
||||
} from "react-native";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import {
|
||||
responsiveHeight,
|
||||
responsiveWidth,
|
||||
} from "react-native-responsive-dimensions";
|
||||
import { useGlobal } from "reactn";
|
||||
import { background, icons, img } from "../../assets";
|
||||
import { background, icons } from "../../assets";
|
||||
import Slider from "../../components/Slider";
|
||||
import firebase, {
|
||||
import {
|
||||
arrayRemove,
|
||||
arrayUnion,
|
||||
increment,
|
||||
@@ -26,10 +30,6 @@ import Page from "../../layouts/Page";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import Style, { gutters, size } from "../../styles/Style";
|
||||
import {
|
||||
responsiveHeight,
|
||||
responsiveWidth,
|
||||
} from "react-native-responsive-dimensions";
|
||||
|
||||
// 20 secondes
|
||||
const timeBeforeIncrement = 20000;
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React, { useMemo } from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import Page from "../layouts/Page";
|
||||
import { ai, background } from "../assets";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import Page from "../layouts/Page";
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { useUserData } from "../providers/UserDataProvider";
|
||||
import FontAwesome from "@expo/vector-icons/FontAwesome";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import palette from "../styles/Palette";
|
||||
import GeneratingSong from "./Studio/GeneratingSong";
|
||||
|
||||
const CREATE_DATA = [
|
||||
{
|
||||
@@ -59,14 +58,27 @@ const NewMusicOptions = ({ route }) => {
|
||||
? currentProjet.lyrics.length > 0
|
||||
: !!currentProjet?.lyrics;
|
||||
const hasCover = !!currentProjet?.coverUrl;
|
||||
console.log("current projet cover", currentProjet?.coverUrl);
|
||||
|
||||
const isLocked = (index) => {
|
||||
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
|
||||
if (index === 3) return true; // Director toujours verrouillé
|
||||
if (!hasProject) return index !== 0; // seulement Songwriter
|
||||
if (hasCover) return index !== 2; // seulement Producer
|
||||
if (hasLyrics) return !(index === 0 || index === 1); // Songwriter + Beatmaker
|
||||
return index !== 0; // par défaut seulement Songwriter
|
||||
// Rules:
|
||||
// - If no project: only Songwriter (0) is available.
|
||||
// - If project exists: Songwriter (0) is always available.
|
||||
// - If lyrics exist: Beatmaker (1) becomes available.
|
||||
// - If cover exists: Producer (2) and Director (3) become available.
|
||||
if (!hasProject) return index !== 0;
|
||||
|
||||
const allowed = new Set([0]); // songwriter always allowed when project exists
|
||||
if (hasLyrics) {
|
||||
allowed.add(1);
|
||||
}
|
||||
if (hasCover) {
|
||||
allowed.add(2);
|
||||
allowed.add(3);
|
||||
}
|
||||
|
||||
return !allowed.has(index);
|
||||
};
|
||||
|
||||
const onPressOption = (index, item) => {
|
||||
@@ -90,7 +102,11 @@ const NewMusicOptions = ({ route }) => {
|
||||
navigate(Routes.PouchReady, { projectId: currentProjet.id });
|
||||
break;
|
||||
case 3:
|
||||
navigate(Routes.Playback, { projectId: currentProjet.id });
|
||||
console.log("test");
|
||||
navigate(Routes.Playback, {
|
||||
project: currentProjet,
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { View, Text, StyleSheet, Image } from "react-native";
|
||||
import React from "react";
|
||||
import { Image, StyleSheet, View } from "react-native";
|
||||
import { ai, background } from "../../assets";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters } from "../../styles";
|
||||
import Page from "../../layouts/Page";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters } from "../../styles";
|
||||
|
||||
const Playback = () => {
|
||||
const Playback = ({ route }) => {
|
||||
const { project } = route.params;
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
|
||||
<Image source={ai.john} style={styles.img} resizeMode="contain" />
|
||||
@@ -23,10 +24,10 @@ const Playback = () => {
|
||||
>
|
||||
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
|
||||
<BorderGradientButton title="Guide du Playbacker" />
|
||||
<BorderGradientButton title="Importer une vidéo" />
|
||||
{/* <BorderGradientButton title="Importer une vidéo" /> */}
|
||||
<GradientButton
|
||||
title="Enregistrer mon Playback"
|
||||
onPress={() => navigate(Routes.RecordPlayback)}
|
||||
onPress={() => navigate(Routes.RecordPlayback, { project })}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -1,21 +1,292 @@
|
||||
import { useFocusEffect } from "@react-navigation/native";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { CameraView, useCameraPermissions } from "expo-camera";
|
||||
import React, { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Text, View } from "react-native";
|
||||
import React from "react";
|
||||
import { CameraView } from "expo-camera";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import Svg, { Circle } from "react-native-svg";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { increment, projectsRef } from "../../config/firebase";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
const RecordPlayback = () => {
|
||||
/**
|
||||
* RecordPlayback — refactor robuste du compteur
|
||||
*
|
||||
* Correction de l'auto-start : on garde un flag countdownActiveRef pour
|
||||
* empêcher l'effet de se déclencher tant que le timer n'a pas réellement démarré.
|
||||
* On affiche 5→1 puis on bascule (pas de 0 visible) pour éviter le "bloqué sur 1".
|
||||
*/
|
||||
|
||||
const TIME_BEFORE_INCREMENT_MS = 20000; // 20s
|
||||
|
||||
const RecordPlayback = ({ route }) => {
|
||||
const { top } = useSafeAreaInsets();
|
||||
const { project } = route.params || {};
|
||||
|
||||
// Permissions
|
||||
const [cameraPermission, requestCameraPermission] = useCameraPermissions();
|
||||
|
||||
// Refs
|
||||
const cameraRef = useRef(null);
|
||||
const countdownTimerRef = useRef(null);
|
||||
const listenTimerRef = useRef(null);
|
||||
const checkSongEndRef = useRef(null);
|
||||
const stopRequestedRef = useRef(false);
|
||||
const startedRef = useRef(false); // empêche les doubles démarrages
|
||||
const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick
|
||||
|
||||
// Compteurs vues
|
||||
const listenedMsRef = useRef(0);
|
||||
const incrementDoneRef = useRef(false);
|
||||
|
||||
// UI / state
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [showProgress, setShowProgress] = useState(false);
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
|
||||
// Musique
|
||||
const songUrl = project?.songUrl || null;
|
||||
const player = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
|
||||
useEffect(() => {
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
}, [songUrl]);
|
||||
|
||||
// Poll player -> progress ring
|
||||
useEffect(() => {
|
||||
if (!player) return;
|
||||
const id = setInterval(() => {
|
||||
try {
|
||||
const dur = (player?.duration || 0) * 1000;
|
||||
const pos = (player?.currentTime || 0) * 1000;
|
||||
setProgressInfo({ pos, dur });
|
||||
} catch (_) {}
|
||||
}, 250);
|
||||
return () => clearInterval(id);
|
||||
}, [player]);
|
||||
|
||||
// Permissions au mount + cleanup
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
if (!cameraPermission?.granted) await requestCameraPermission();
|
||||
} catch (_) {}
|
||||
})();
|
||||
return () => {
|
||||
try {
|
||||
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current);
|
||||
if (listenTimerRef.current) clearInterval(listenTimerRef.current);
|
||||
if (checkSongEndRef.current) clearInterval(checkSongEndRef.current);
|
||||
countdownTimerRef.current = null;
|
||||
listenTimerRef.current = null;
|
||||
checkSongEndRef.current = null;
|
||||
} catch (_) {}
|
||||
};
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
// Reset complet
|
||||
const resetSession = useCallback(async () => {
|
||||
try {
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
countdownTimerRef.current = null;
|
||||
}
|
||||
if (listenTimerRef.current) {
|
||||
clearInterval(listenTimerRef.current);
|
||||
listenTimerRef.current = null;
|
||||
}
|
||||
if (checkSongEndRef.current) {
|
||||
clearInterval(checkSongEndRef.current);
|
||||
checkSongEndRef.current = null;
|
||||
}
|
||||
|
||||
startedRef.current = false;
|
||||
stopRequestedRef.current = false;
|
||||
countdownActiveRef.current = false;
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
|
||||
setIsPreparing(false);
|
||||
setIsRecording(false);
|
||||
setShowProgress(false);
|
||||
setCountdown(0);
|
||||
|
||||
if (player) {
|
||||
try {
|
||||
if (player.playing) await player.pause?.();
|
||||
await player.seekTo?.(0);
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}, [player]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
void resetSession();
|
||||
return () => {};
|
||||
}, [resetSession])
|
||||
);
|
||||
|
||||
// Lancer le compte à rebours (le tick décrémente uniquement)
|
||||
const startCountdownThenRecord = async () => {
|
||||
if (!songUrl) return;
|
||||
await resetSession();
|
||||
setIsPreparing(true);
|
||||
setShowProgress(false);
|
||||
setCountdown(5);
|
||||
|
||||
// On démarre l'intervalle puis on active le flag
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
countdownTimerRef.current = null;
|
||||
}
|
||||
countdownTimerRef.current = setInterval(() => {
|
||||
setCountdown((c) => Math.max(0, c - 1));
|
||||
}, 1000);
|
||||
countdownActiveRef.current = true;
|
||||
};
|
||||
|
||||
// Quand le compteur a réellement démarré ET atteint 0, on démarre
|
||||
useEffect(() => {
|
||||
if (!isPreparing) return;
|
||||
if (!countdownActiveRef.current) return; // évite l'auto-start
|
||||
|
||||
if (countdown === 0 && !startedRef.current) {
|
||||
startedRef.current = true;
|
||||
if (countdownTimerRef.current) {
|
||||
clearInterval(countdownTimerRef.current);
|
||||
countdownTimerRef.current = null;
|
||||
}
|
||||
countdownActiveRef.current = false;
|
||||
// Bascule après rendu de la frame courante
|
||||
requestAnimationFrame(() => {
|
||||
setIsPreparing(false);
|
||||
setShowProgress(true);
|
||||
void startRecordingWithMusic();
|
||||
});
|
||||
}
|
||||
}, [countdown, isPreparing]);
|
||||
|
||||
const startRecordingWithMusic = async () => {
|
||||
try {
|
||||
stopRequestedRef.current = false;
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
|
||||
setIsRecording(true);
|
||||
setShowProgress(true);
|
||||
const recordPromise = cameraRef.current?.recordAsync?.({
|
||||
mute: true,
|
||||
maxDuration: 600,
|
||||
});
|
||||
|
||||
if (player && songUrl) {
|
||||
try {
|
||||
await player.seekTo?.(0);
|
||||
} catch (_) {}
|
||||
await player.play?.();
|
||||
}
|
||||
|
||||
// Incrément des vues
|
||||
if (!listenTimerRef.current && project?.id) {
|
||||
listenTimerRef.current = setInterval(async () => {
|
||||
try {
|
||||
if (player?.playing) {
|
||||
listenedMsRef.current += 500;
|
||||
if (
|
||||
!incrementDoneRef.current &&
|
||||
listenedMsRef.current >= TIME_BEFORE_INCREMENT_MS
|
||||
) {
|
||||
incrementDoneRef.current = true;
|
||||
try {
|
||||
await projectsRef
|
||||
.doc(project.id)
|
||||
.set({ views: increment(1) }, { merge: true });
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Fin du morceau -> stop recording
|
||||
if (!checkSongEndRef.current) {
|
||||
checkSongEndRef.current = setInterval(() => {
|
||||
try {
|
||||
if (!player) return;
|
||||
const duration = (player?.duration || 0) * 1000;
|
||||
const currentTime = (player?.currentTime || 0) * 1000;
|
||||
if (
|
||||
(!player.playing && !stopRequestedRef.current) ||
|
||||
(duration > 0 && currentTime >= duration - 600)
|
||||
) {
|
||||
stopRequestedRef.current = true;
|
||||
if (checkSongEndRef.current) {
|
||||
clearInterval(checkSongEndRef.current);
|
||||
checkSongEndRef.current = null;
|
||||
}
|
||||
try {
|
||||
cameraRef.current?.stopRecording?.();
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const video = await recordPromise;
|
||||
|
||||
if (checkSongEndRef.current) {
|
||||
clearInterval(checkSongEndRef.current);
|
||||
checkSongEndRef.current = null;
|
||||
}
|
||||
try {
|
||||
if (player?.playing) await player.pause?.();
|
||||
} catch (_) {}
|
||||
if (listenTimerRef.current) {
|
||||
clearInterval(listenTimerRef.current);
|
||||
listenTimerRef.current = null;
|
||||
}
|
||||
|
||||
setIsRecording(false);
|
||||
setShowProgress(false);
|
||||
|
||||
if (video?.uri)
|
||||
navigate(Routes.RecordedPlayback, { videoUri: video.uri, project });
|
||||
else navigate(Routes.RecordedPlayback, { project });
|
||||
} catch (e) {
|
||||
console.log("RecordPlayback error:", e);
|
||||
setIsRecording(false);
|
||||
setIsPreparing(false);
|
||||
setShowProgress(false);
|
||||
if (listenTimerRef.current) {
|
||||
clearInterval(listenTimerRef.current);
|
||||
listenTimerRef.current = null;
|
||||
}
|
||||
if (checkSongEndRef.current) {
|
||||
clearInterval(checkSongEndRef.current);
|
||||
checkSongEndRef.current = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const permissionsGranted = !!cameraPermission?.granted;
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<CameraView style={{ flex: 1 }} facing="front">
|
||||
<CameraView
|
||||
ref={cameraRef}
|
||||
style={{ flex: 1 }}
|
||||
facing="front"
|
||||
mode="video"
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
paddingHorizontal: gutters,
|
||||
@@ -25,6 +296,7 @@ const RecordPlayback = () => {
|
||||
}}
|
||||
>
|
||||
<MusicLandHeader progress={9} onPressBack={goBack} />
|
||||
|
||||
<View style={{ flex: 1, marginTop: 11 }}>
|
||||
<CreateLyricsHeader>
|
||||
<Text
|
||||
@@ -38,19 +310,143 @@ const RecordPlayback = () => {
|
||||
musique, et s'arrêtera à la fin du morceau.
|
||||
</Text>
|
||||
</CreateLyricsHeader>
|
||||
|
||||
{/* 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={{
|
||||
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>
|
||||
<GradientButton
|
||||
title="Lancer ma musique"
|
||||
containerStyle={{
|
||||
width: "80%",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onPress={() => navigate(Routes.RecordedPlayback)}
|
||||
/>
|
||||
|
||||
{/* Start button */}
|
||||
{permissionsGranted && !isPreparing && !isRecording && (
|
||||
<GradientButton
|
||||
title="Lancer ma musique"
|
||||
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||
disabled={!songUrl}
|
||||
onPress={startCountdownThenRecord}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Progress circulaire */}
|
||||
{permissionsGranted && (isRecording || showProgress) && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: gutters,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 105,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
}}
|
||||
>
|
||||
<ProgressRing
|
||||
size={100}
|
||||
strokeWidth={8}
|
||||
progress={
|
||||
progressInfo.dur
|
||||
? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur)
|
||||
: 0
|
||||
}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: 50,
|
||||
height: 50,
|
||||
borderRadius: 55,
|
||||
backgroundColor: Palette.white,
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</CameraView>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// Progress ring SVG
|
||||
const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
|
||||
const r = size / 2 - strokeWidth / 2;
|
||||
const c = 2 * Math.PI * r;
|
||||
const clamped = Math.max(0, Math.min(1, progress || 0));
|
||||
const offset = c * (1 - clamped);
|
||||
return (
|
||||
<Svg
|
||||
width={size}
|
||||
height={size}
|
||||
style={{
|
||||
transform: [{ rotate: "-90deg" }],
|
||||
backgroundColor: "#ffffff3d",
|
||||
borderRadius: 55,
|
||||
}}
|
||||
>
|
||||
<Circle
|
||||
cx={size / 2}
|
||||
cy={size / 2}
|
||||
r={r}
|
||||
stroke={Palette.white}
|
||||
strokeWidth={strokeWidth}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${c} ${c}`}
|
||||
strokeDashoffset={offset}
|
||||
fill="transparent"
|
||||
/>
|
||||
</Svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecordPlayback;
|
||||
|
||||
@@ -1,16 +1,111 @@
|
||||
import { View, Text, Image } from "react-native";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import { background, img } from "../../assets";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { VideoView, useVideoPlayer } from "expo-video";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import { background } from "../../assets";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import Slider from "../../components/Slider";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters } from "../../styles";
|
||||
import Slider from "../../components/Slider";
|
||||
import GradientButton from "../../components/GradientButton";
|
||||
import { Routes } from "../../navigation";
|
||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
||||
const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project } = route.params || {};
|
||||
const songUrl = project?.songUrl || null;
|
||||
|
||||
const audioPlayer = useAudioPlayer(songUrl ? { uri: songUrl } : undefined);
|
||||
const videoPlayer = useVideoPlayer(videoUri || null, (p) => {
|
||||
p.loop = false;
|
||||
p.muted = true; // recorded video has no audio; keep muted anyway
|
||||
p.timeUpdateEventInterval = 0.2;
|
||||
});
|
||||
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
const wasPlayingBeforeSeek = useRef(false);
|
||||
|
||||
// Format mm:ss
|
||||
const fmt = (ms) => {
|
||||
const total = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||
const m = Math.floor(total / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const s = (total % 60).toString().padStart(2, "0");
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
// Start both players on mount
|
||||
useEffect(() => {
|
||||
const start = async () => {
|
||||
try {
|
||||
if (audioPlayer && songUrl) await audioPlayer.play?.();
|
||||
if (videoPlayer) videoPlayer.play();
|
||||
} catch (e) {}
|
||||
};
|
||||
start();
|
||||
return () => {
|
||||
try {
|
||||
if (audioPlayer?.playing) audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} catch (e) {}
|
||||
};
|
||||
}, [audioPlayer, videoPlayer, songUrl]);
|
||||
|
||||
// Poll from audio player for progress display; keep video in sync if drifting
|
||||
useEffect(() => {
|
||||
const id = global.setInterval(() => {
|
||||
try {
|
||||
const dur = (audioPlayer?.duration || 0) * 1000;
|
||||
const pos = (audioPlayer?.currentTime || 0) * 1000;
|
||||
setProgressInfo({ pos, dur });
|
||||
|
||||
// basic drift correction: if desync > 300ms, align video
|
||||
if (videoPlayer && !Number.isNaN(videoPlayer.currentTime)) {
|
||||
const v = (videoPlayer.currentTime || 0) * 1000;
|
||||
const drift = Math.abs(v - pos);
|
||||
if (drift > 350) {
|
||||
videoPlayer.currentTime = Math.max(0, (pos || 0) / 1000);
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 250);
|
||||
return () => global.clearInterval(id);
|
||||
}, [audioPlayer, videoPlayer]);
|
||||
|
||||
const onSeek = async (ratio) => {
|
||||
try {
|
||||
const dur = progressInfo.dur || 0;
|
||||
const pos = Math.floor(dur * ratio);
|
||||
if (audioPlayer && dur > 0)
|
||||
await audioPlayer.seekTo?.(Math.floor(pos / 1000));
|
||||
if (videoPlayer) videoPlayer.currentTime = Math.max(0, pos / 1000);
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const onSeekStart = async () => {
|
||||
try {
|
||||
wasPlayingBeforeSeek.current = !!audioPlayer?.playing;
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} catch (e) {}
|
||||
};
|
||||
const onSeekEnd = async () => {
|
||||
try {
|
||||
if (wasPlayingBeforeSeek.current) {
|
||||
if (audioPlayer) await audioPlayer.play?.();
|
||||
if (videoPlayer) videoPlayer.play();
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
const sliderProgress = useMemo(() => {
|
||||
return progressInfo.dur
|
||||
? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur)
|
||||
: 0;
|
||||
}, [progressInfo]);
|
||||
|
||||
const RecordedPlayback = () => {
|
||||
return (
|
||||
<Page backgroundImg={background.playbackBG2} headerType="NONE">
|
||||
<MusicLandHeader progress={19} onPressBack={goBack} />
|
||||
@@ -18,28 +113,63 @@ const RecordedPlayback = () => {
|
||||
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
|
||||
>
|
||||
<View style={{ flex: 1, gap: 22 }}>
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
style={{
|
||||
width: "80%",
|
||||
flex: 1,
|
||||
alignSelf: "center",
|
||||
borderRadius: 16,
|
||||
}}
|
||||
{!!videoUri && (
|
||||
<VideoView
|
||||
player={videoPlayer}
|
||||
nativeControls={false}
|
||||
contentFit="contain"
|
||||
style={{
|
||||
width: "80%",
|
||||
flex: 1,
|
||||
alignSelf: "center",
|
||||
borderRadius: 16,
|
||||
backgroundColor: "#00000066",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Slider
|
||||
value={fmt(progressInfo.pos)}
|
||||
maxValue={fmt(progressInfo.dur)}
|
||||
progress={sliderProgress}
|
||||
seekEnabled={!!songUrl}
|
||||
onSeek={onSeek}
|
||||
onSeekStart={onSeekStart}
|
||||
onSeekEnd={onSeekEnd}
|
||||
/>
|
||||
<Slider value="0:00" maxValue="2:00" />
|
||||
</View>
|
||||
<View
|
||||
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
|
||||
>
|
||||
<GradientButton
|
||||
title="Je valide"
|
||||
onPress={() => {
|
||||
navigate(Routes.DownloadSongs, {
|
||||
action: "playback",
|
||||
uri: videoUri,
|
||||
project,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<BorderGradientButton
|
||||
title="Je change de décor"
|
||||
onPress={() => navigate(Routes.ChooseDecor)}
|
||||
title="Recommencer"
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoUri) {
|
||||
const info = await FileSystem.getInfoAsync(videoUri);
|
||||
if (info?.exists)
|
||||
await FileSystem.deleteAsync(videoUri, {
|
||||
idempotent: true,
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
navigate(Routes.RecordPlayback, { project });
|
||||
}}
|
||||
/>
|
||||
<BorderGradientButton title="Recommencer" />
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
|
||||
+368
-95
@@ -1,106 +1,261 @@
|
||||
import { View, Text, Image, Pressable, Platform } from "react-native";
|
||||
import React from "react";
|
||||
import { useAudioPlayer } from "expo-audio";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { VideoView, useVideoPlayer } from "expo-video";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { icons, img } from "../assets";
|
||||
import { size } from "../styles/Style";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { arrayRemove, arrayUnion, projectsRef, usersRef } from "../config/firebase";
|
||||
import useDataFromRef from "../hooks/useDataFromRef";
|
||||
import { Routes } from "../navigation";
|
||||
import { navigate } from "../navigation/NavigationService";
|
||||
import { useUser } from "../providers/UserDataProvider";
|
||||
import { Palette } from "../styles";
|
||||
import { FONT_FAMILY } from "../styles/Fonts";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { size } from "../styles/Style";
|
||||
|
||||
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
const { currentUID, followUser, unfollowUser } = useUser() || {};
|
||||
const videoUrl = item?.playbackUrl || null;
|
||||
const audioSource = useMemo(() => {
|
||||
const fromSong = item?.songUrl ? { uri: item.songUrl } : null;
|
||||
return fromSong;
|
||||
}, [item]);
|
||||
|
||||
const hasExternalAudio = !!audioSource;
|
||||
|
||||
const initialLikedBy = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
||||
const [isLiked, setIsLiked] = useState(
|
||||
currentUID ? initialLikedBy.includes(currentUID) : false
|
||||
);
|
||||
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
|
||||
|
||||
// Fetch owner profile (cached)
|
||||
const [owner, setOwner] = useState(
|
||||
item?.userId && userCache?.current?.get(item.userId)
|
||||
? userCache.current.get(item.userId)
|
||||
: null
|
||||
);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
try {
|
||||
const uid = item?.userId;
|
||||
if (!uid || !userCache) return;
|
||||
const cached = userCache.current.get(uid);
|
||||
if (cached) {
|
||||
if (!cancelled) setOwner(cached);
|
||||
return;
|
||||
}
|
||||
const user = await getUserByUid?.(uid);
|
||||
if (!cancelled && user) {
|
||||
userCache.current.set(uid, user);
|
||||
setOwner(user);
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
run();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [item?.userId, userCache, getUserByUid]);
|
||||
|
||||
// Live sync owner from Firestore to reflect follow changes elsewhere
|
||||
useEffect(() => {
|
||||
const uid = item?.userId;
|
||||
if (!uid) return;
|
||||
const unsub = usersRef.doc(uid).onSnapshot(
|
||||
(doc) => {
|
||||
if (doc?.exists) {
|
||||
const data = { id: doc.id, ...doc.data() };
|
||||
setOwner(data);
|
||||
try {
|
||||
userCache?.current?.set(uid, data);
|
||||
} catch (e) {}
|
||||
}
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
return () => unsub?.();
|
||||
}, [item?.userId, userCache]);
|
||||
|
||||
// Follow state derived from owner.followedBy
|
||||
const [isFollowing, setIsFollowing] = useState(false);
|
||||
useEffect(() => {
|
||||
const list = Array.isArray(owner?.followedBy) ? owner.followedBy : [];
|
||||
setIsFollowing(currentUID ? list.includes(currentUID) : false);
|
||||
}, [owner?.followedBy, currentUID]);
|
||||
|
||||
useEffect(() => {
|
||||
const lb = Array.isArray(item?.likedBy) ? item.likedBy : [];
|
||||
setLikesCount(lb.length);
|
||||
setIsLiked(currentUID ? lb.includes(currentUID) : false);
|
||||
}, [item?.likedBy, currentUID]);
|
||||
console.log("has external audio : ", hasExternalAudio);
|
||||
|
||||
const audioPlayer = useAudioPlayer(audioSource || undefined);
|
||||
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||
p.loop = false;
|
||||
p.muted = true;
|
||||
p.timeUpdateEventInterval = 0.2;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const toggle = async () => {
|
||||
try {
|
||||
if (isActive) {
|
||||
try {
|
||||
if (audioPlayer && hasExternalAudio) await audioPlayer.seekTo?.(0);
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.currentTime = 0;
|
||||
} catch (e) {}
|
||||
|
||||
// Lancer quasi simultanément (éviter await pour limiter le décalage)
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.play();
|
||||
} catch (e) {}
|
||||
try {
|
||||
if (audioPlayer && hasExternalAudio) audioPlayer.play?.();
|
||||
} catch (e) {}
|
||||
} else {
|
||||
if (audioPlayer?.playing) await audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
toggle();
|
||||
}, [isActive, audioPlayer, videoPlayer, hasExternalAudio]);
|
||||
|
||||
// Micro-correction initiale uniquement pour absorber un léger décalage réseau
|
||||
useEffect(() => {
|
||||
if (!isActive || !hasExternalAudio) return;
|
||||
const t = setTimeout(() => {
|
||||
try {
|
||||
const a = audioPlayer?.currentTime || 0;
|
||||
const v = videoPlayer?.currentTime || 0;
|
||||
if (Math.abs(a - v) > 0.2 && videoPlayer) {
|
||||
videoPlayer.currentTime = Math.max(0, a);
|
||||
}
|
||||
} catch (e) {}
|
||||
}, 300);
|
||||
return () => clearTimeout(t);
|
||||
}, [isActive, hasExternalAudio, audioPlayer, videoPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
if (audioPlayer?.playing) audioPlayer.pause?.();
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} catch (e) {}
|
||||
};
|
||||
}, [audioPlayer, videoPlayer]);
|
||||
|
||||
const Playbacks = () => {
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<Carousel
|
||||
data={Array.from({ length: 5 })}
|
||||
vertical
|
||||
height={responsiveHeight(100)}
|
||||
renderItem={() => (
|
||||
<View
|
||||
style={{
|
||||
height: responsiveHeight(100),
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
bottom: 130,
|
||||
gap: 18,
|
||||
<View
|
||||
style={{
|
||||
height: responsiveHeight(100),
|
||||
position: "relative",
|
||||
backgroundColor: "black",
|
||||
}}
|
||||
>
|
||||
{!!videoUrl ? (
|
||||
<VideoView
|
||||
player={videoPlayer}
|
||||
nativeControls={false}
|
||||
contentFit="cover"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
source={img.placeholder3}
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Right side actions */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
bottom: 130,
|
||||
gap: 18,
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
paddingHorizontal: 13,
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 6, alignItems: "center" }}>
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
navigate(Routes.SingerProfile, { userId: item?.userId });
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
alignItems: "center",
|
||||
gap: 20,
|
||||
paddingHorizontal: 13,
|
||||
{owner?.profilePictureURL ? (
|
||||
<Image
|
||||
source={{ uri: owner.profilePictureURL }}
|
||||
style={{
|
||||
...size({ size: 45 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
source={img.profile}
|
||||
style={{
|
||||
...size({ size: 45 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Pressable>
|
||||
{owner?.id && currentUID && owner.id !== currentUID && (
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
const next = !isFollowing;
|
||||
setIsFollowing(next);
|
||||
// Optimistic update of local owner.followedBy
|
||||
setOwner((prev) => {
|
||||
const fb = Array.isArray(prev?.followedBy)
|
||||
? prev.followedBy
|
||||
: [];
|
||||
const newFb = next
|
||||
? Array.from(new Set([...fb, currentUID]))
|
||||
: fb.filter((x) => x !== currentUID);
|
||||
return prev ? { ...prev, followedBy: newFb } : prev;
|
||||
});
|
||||
if (next) await followUser?.(owner.id);
|
||||
else await unfollowUser?.(owner.id);
|
||||
} catch (e) {
|
||||
// rollback on failure
|
||||
setIsFollowing((v) => !v);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={{ gap: 6, alignItems: "center" }}>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={img.profile}
|
||||
style={{
|
||||
...size({ size: 45 }),
|
||||
borderRadius: 100,
|
||||
}}
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.white,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
Suivre
|
||||
</Text>
|
||||
</BlurView>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable>
|
||||
<Image
|
||||
source={icons.heartOutline}
|
||||
style={size({ size: 26 })}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<Image source={icons.share} style={size({ size: 26 })} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: 28 }}>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: Palette.glass,
|
||||
paddingVertical: 6,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
borderWidth: 1,
|
||||
borderColor: Palette.white,
|
||||
overflow: "hidden",
|
||||
backgroundColor: isFollowing ? "#FFFFFF1A" : undefined,
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
@@ -108,19 +263,137 @@ const Playbacks = () => {
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
fontSize: 13,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
}}
|
||||
>
|
||||
Description chanson. Viverra enim risus enim enim placerat.
|
||||
Integer pulvinar tristique suscipit risus. Id hendrerit in
|
||||
odio phasellus interdum
|
||||
{isFollowing ? "Ne plus suivre" : "Suivre"}
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
</Pressable>
|
||||
)}
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (!currentUID || !item?.id) return;
|
||||
const nextLiked = !isLiked;
|
||||
setIsLiked(nextLiked);
|
||||
setLikesCount((c) => Math.max(0, c + (nextLiked ? 1 : -1)));
|
||||
|
||||
const ref = projectsRef.doc(item.id);
|
||||
await ref.set(
|
||||
{
|
||||
likedBy: nextLiked
|
||||
? arrayUnion(currentUID)
|
||||
: arrayRemove(currentUID),
|
||||
// Optionally: updatedAt could be set if needed
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
} catch (e) {
|
||||
// rollback on failure
|
||||
setIsLiked((v) => !v);
|
||||
setLikesCount((c) => (isLiked ? c + 1 : Math.max(0, c - 1)));
|
||||
}
|
||||
}}
|
||||
style={{ alignItems: "center" }}
|
||||
>
|
||||
<Image
|
||||
source={isLiked ? icons.heart : icons.heartOutline}
|
||||
style={size({ size: 26 })}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
{!!likesCount && (
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
fontSize: 11,
|
||||
marginTop: 4,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{likesCount}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
<Pressable>
|
||||
<Image source={icons.share} style={size({ size: 26 })} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<View style={{ paddingHorizontal: 28 }}>
|
||||
<BlurView
|
||||
tint="dark"
|
||||
intensity={20}
|
||||
style={{
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 8,
|
||||
borderRadius: 20,
|
||||
backgroundColor: Palette.glass,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
experimentalBlurMethod={
|
||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||
}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{item?.title || "Description chanson"}
|
||||
</Text>
|
||||
</BlurView>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const Playbacks = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const userCache = useRef(new Map());
|
||||
const { getUserByUid } = useUser() || {};
|
||||
const { data: playbacks = [], loadMore } = useDataFromRef({
|
||||
ref: projectsRef.where("playbackUrl", "!=", null),
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
usePagination: true,
|
||||
batchSize: 2,
|
||||
});
|
||||
|
||||
const onSnap = useCallback(
|
||||
(index) => {
|
||||
setActiveIndex(index);
|
||||
// Pré-charge la suite 2 par 2
|
||||
if (index >= (playbacks?.length || 0) - 2) {
|
||||
loadMore?.();
|
||||
}
|
||||
},
|
||||
[playbacks?.length, loadMore]
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: "black" }}>
|
||||
<Carousel
|
||||
data={playbacks}
|
||||
vertical
|
||||
height={responsiveHeight(100)}
|
||||
pagingEnabled
|
||||
windowSize={5}
|
||||
onSnapToItem={onSnap}
|
||||
renderItem={({ item, index }) => (
|
||||
<PlaybackItem
|
||||
key={item?.id || index}
|
||||
item={item}
|
||||
userCache={userCache}
|
||||
getUserByUid={getUserByUid}
|
||||
isActive={index === activeIndex}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@@ -1,29 +1,80 @@
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
Image,
|
||||
Pressable,
|
||||
Platform,
|
||||
} from "react-native";
|
||||
import { BlurView } from "expo-blur";
|
||||
import React from "react";
|
||||
import Page from "../../layouts/Page";
|
||||
import {
|
||||
Image,
|
||||
Platform,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { background, img } from "../../assets";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import { projectsRef, serverTimestamp } from "../../config/firebase";
|
||||
import { uploadFileToFirebase } from "../../helpers/uploadToFirebase";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
import { size } from "../../styles/Style";
|
||||
import { BlurView } from "expo-blur";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { Routes } from "../../navigation";
|
||||
import { useRoute } from "@react-navigation/core";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
const DownloadSongs = ({ route }) => {
|
||||
// const params = useRoute().params;
|
||||
const { action, uri, project } = route.params || {};
|
||||
console.log("project id", project?.id);
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
|
||||
const DownloadSongs = () => {
|
||||
const params = useRoute().params;
|
||||
const action = params?.action;
|
||||
const handleDownloadUri = async () => {
|
||||
if (action === "playback" && project?.id) {
|
||||
// Publication du playback
|
||||
console.log("project id : ", project.id);
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const { resultURI = null } = await uploadFileToFirebase({
|
||||
uri: uri,
|
||||
path: `musics/${project.id}/playback.mp4`,
|
||||
shouldCompress: true,
|
||||
fileType: "VIDEO",
|
||||
});
|
||||
|
||||
if (!resultURI) throw new Error("Téléversement de l'image impossible");
|
||||
|
||||
if (resultURI) {
|
||||
await projectsRef.doc(project.id).set(
|
||||
{
|
||||
playbackUrl: resultURI,
|
||||
updatedAt: serverTimestamp(),
|
||||
},
|
||||
{ merge: true }
|
||||
);
|
||||
setTooltip({
|
||||
type: "success",
|
||||
text: "Playback publié avec succès",
|
||||
});
|
||||
} else {
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: "Erreur lors de la publication du playback",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("error upload playback", error);
|
||||
setTooltip({
|
||||
type: "error",
|
||||
text: "Erreur lors de la publication du playback",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
// Handle playback download
|
||||
} else {
|
||||
// Handle song download
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={
|
||||
@@ -57,10 +108,16 @@ const DownloadSongs = () => {
|
||||
<View style={{ gap: 10 }}>
|
||||
<Pressable
|
||||
style={styles.itemContainer}
|
||||
onPress={() =>
|
||||
navigate(Routes.DownloadPrices, {
|
||||
action,
|
||||
})
|
||||
onPress={
|
||||
() => {
|
||||
console.log("test");
|
||||
handleDownloadUri();
|
||||
}
|
||||
|
||||
// navigate(Routes.DownloadPrices, {
|
||||
// action,
|
||||
// uri,
|
||||
// })
|
||||
}
|
||||
>
|
||||
<BlurView
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
import React, { useState } from "reactn";
|
||||
import { Text, Pressable } from "react-native";
|
||||
import * as ImagePicker from "expo-image-picker";
|
||||
import { Pressable, Text } from "react-native";
|
||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
||||
import React, { useState } from "reactn";
|
||||
|
||||
import Page from "../layouts/Page";
|
||||
import { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||
import Page from "../layouts/Page";
|
||||
|
||||
import alert from "../components/Alert.js";
|
||||
import Avatar from "../components/Avatar";
|
||||
import InputRow from "../components/InputRow.js";
|
||||
import ItemRowList from "../components/ItemRowList.js";
|
||||
import alert from "../components/Alert.js";
|
||||
|
||||
import { Fonts, gutters, Palette, Style } from "../styles";
|
||||
|
||||
import { projectsRef } from "../config/firebase";
|
||||
|
||||
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
||||
import { getValueFromKeyState } from "../helpers/index.js";
|
||||
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
||||
|
||||
import { useUserData } from "../providers/UserDataProvider.js";
|
||||
import { Routes } from "../navigation/Routes.js";
|
||||
import { useUserData } from "../providers/UserDataProvider.js";
|
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
export default ({ navigation }) => {
|
||||
const { setIsLoading, setTooltip } = useMinuit();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user