merge
This commit is contained in:
+1
-1
@@ -65,7 +65,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-native": "0.76.9",
|
"react-native": "0.76.9",
|
||||||
"react-native-actions-sheet": "^0.9.7",
|
"react-native-actions-sheet": "^0.9.7",
|
||||||
"react-native-compressor": "^1.8.24",
|
"react-native-compressor": "^1.12.0",
|
||||||
"react-native-country-picker-modal": "^2.0.0",
|
"react-native-country-picker-modal": "^2.0.0",
|
||||||
"react-native-dialog": "^9.3.0",
|
"react-native-dialog": "^9.3.0",
|
||||||
"react-native-figma-squircle": "^0.3.4",
|
"react-native-figma-squircle": "^0.3.4",
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
@@ -1,13 +1,36 @@
|
|||||||
import { Platform } from "react-native";
|
import { Platform } from "react-native";
|
||||||
|
import Compressor from "react-native-compressor";
|
||||||
import firebase from "../config/firebase";
|
import firebase from "../config/firebase";
|
||||||
|
|
||||||
export function uploadFileToFirebase({ uri, path }) {
|
export function uploadFileToFirebase({
|
||||||
|
uri,
|
||||||
|
path,
|
||||||
|
shouldCompress = false,
|
||||||
|
fileType = "",
|
||||||
|
}) {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
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 blob = await response.blob();
|
||||||
|
|
||||||
const uploadTask = firebase.storage().ref(path).put(blob);
|
const uploadTask = firebase.storage().ref(path).put(blob);
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import { useRoute } from "@react-navigation/core";
|
import { useRoute } from "@react-navigation/core";
|
||||||
import { useAudioPlayer } from "expo-audio";
|
import { useAudioPlayer } from "expo-audio";
|
||||||
|
import { Image as ExpoImage } from "expo-image";
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Pressable,
|
Pressable,
|
||||||
|
Image as RNImage,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
View,
|
View,
|
||||||
Image as RNImage,
|
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { Image as ExpoImage } from "expo-image";
|
|
||||||
import { SheetManager } from "react-native-actions-sheet";
|
import { SheetManager } from "react-native-actions-sheet";
|
||||||
|
import {
|
||||||
|
responsiveHeight,
|
||||||
|
responsiveWidth,
|
||||||
|
} from "react-native-responsive-dimensions";
|
||||||
import { useGlobal } from "reactn";
|
import { useGlobal } from "reactn";
|
||||||
import { background, icons, img } from "../../assets";
|
import { background, icons } from "../../assets";
|
||||||
import Slider from "../../components/Slider";
|
import Slider from "../../components/Slider";
|
||||||
import firebase, {
|
import {
|
||||||
arrayRemove,
|
arrayRemove,
|
||||||
arrayUnion,
|
arrayUnion,
|
||||||
increment,
|
increment,
|
||||||
@@ -26,10 +30,6 @@ import Page from "../../layouts/Page";
|
|||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import Style, { gutters, size } from "../../styles/Style";
|
import Style, { gutters, size } from "../../styles/Style";
|
||||||
import {
|
|
||||||
responsiveHeight,
|
|
||||||
responsiveWidth,
|
|
||||||
} from "react-native-responsive-dimensions";
|
|
||||||
|
|
||||||
// 20 secondes
|
// 20 secondes
|
||||||
const timeBeforeIncrement = 20000;
|
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 React, { useMemo } from "react";
|
||||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||||
import Page from "../layouts/Page";
|
|
||||||
import { ai, background } from "../assets";
|
import { ai, background } from "../assets";
|
||||||
import { BlurView } from "expo-blur";
|
import Page from "../layouts/Page";
|
||||||
import { Palette } from "../styles";
|
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
|
||||||
import { Routes } from "../navigation";
|
import { Routes } from "../navigation";
|
||||||
import { navigate } from "../navigation/NavigationService";
|
import { navigate } from "../navigation/NavigationService";
|
||||||
import { useUserData } from "../providers/UserDataProvider";
|
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 palette from "../styles/Palette";
|
||||||
import GeneratingSong from "./Studio/GeneratingSong";
|
|
||||||
|
|
||||||
const CREATE_DATA = [
|
const CREATE_DATA = [
|
||||||
{
|
{
|
||||||
@@ -59,14 +58,27 @@ const NewMusicOptions = ({ route }) => {
|
|||||||
? currentProjet.lyrics.length > 0
|
? currentProjet.lyrics.length > 0
|
||||||
: !!currentProjet?.lyrics;
|
: !!currentProjet?.lyrics;
|
||||||
const hasCover = !!currentProjet?.coverUrl;
|
const hasCover = !!currentProjet?.coverUrl;
|
||||||
|
console.log("current projet cover", currentProjet?.coverUrl);
|
||||||
|
|
||||||
const isLocked = (index) => {
|
const isLocked = (index) => {
|
||||||
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
|
// 0: Songwriter, 1: Beatmaker, 2: Producer, 3: Director
|
||||||
if (index === 3) return true; // Director toujours verrouillé
|
// Rules:
|
||||||
if (!hasProject) return index !== 0; // seulement Songwriter
|
// - If no project: only Songwriter (0) is available.
|
||||||
if (hasCover) return index !== 2; // seulement Producer
|
// - If project exists: Songwriter (0) is always available.
|
||||||
if (hasLyrics) return !(index === 0 || index === 1); // Songwriter + Beatmaker
|
// - If lyrics exist: Beatmaker (1) becomes available.
|
||||||
return index !== 0; // par défaut seulement Songwriter
|
// - 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) => {
|
const onPressOption = (index, item) => {
|
||||||
@@ -90,7 +102,11 @@ const NewMusicOptions = ({ route }) => {
|
|||||||
navigate(Routes.PouchReady, { projectId: currentProjet.id });
|
navigate(Routes.PouchReady, { projectId: currentProjet.id });
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
navigate(Routes.Playback, { projectId: currentProjet.id });
|
console.log("test");
|
||||||
|
navigate(Routes.Playback, {
|
||||||
|
project: currentProjet,
|
||||||
|
});
|
||||||
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { View, Text, StyleSheet, Image } from "react-native";
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { Image, StyleSheet, View } from "react-native";
|
||||||
import { ai, background } from "../../assets";
|
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 BorderGradientButton from "../../components/BorderGradientButton";
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
|
import Page from "../../layouts/Page";
|
||||||
import { Routes } from "../../navigation";
|
import { Routes } from "../../navigation";
|
||||||
|
import { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
|
import { gutters } from "../../styles";
|
||||||
|
|
||||||
const Playback = () => {
|
const Playback = ({ route }) => {
|
||||||
|
const { project } = route.params;
|
||||||
return (
|
return (
|
||||||
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
|
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
|
||||||
<Image source={ai.john} style={styles.img} resizeMode="contain" />
|
<Image source={ai.john} style={styles.img} resizeMode="contain" />
|
||||||
@@ -23,10 +24,10 @@ const Playback = () => {
|
|||||||
>
|
>
|
||||||
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
|
<View style={{ width: "80%", alignSelf: "center", gap: 12 }}>
|
||||||
<BorderGradientButton title="Guide du Playbacker" />
|
<BorderGradientButton title="Guide du Playbacker" />
|
||||||
<BorderGradientButton title="Importer une vidéo" />
|
{/* <BorderGradientButton title="Importer une vidéo" /> */}
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Enregistrer mon Playback"
|
title="Enregistrer mon Playback"
|
||||||
onPress={() => navigate(Routes.RecordPlayback)}
|
onPress={() => navigate(Routes.RecordPlayback, { project })}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</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 { 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 { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
import Svg, { Circle } from "react-native-svg";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
|
||||||
import GradientButton from "../../components/GradientButton";
|
import GradientButton from "../../components/GradientButton";
|
||||||
|
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||||
|
import { increment, projectsRef } from "../../config/firebase";
|
||||||
import { Routes } from "../../navigation";
|
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 { 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 (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
<View style={{ flex: 1 }}>
|
||||||
<CameraView style={{ flex: 1 }} facing="front">
|
<CameraView
|
||||||
|
ref={cameraRef}
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
facing="front"
|
||||||
|
mode="video"
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
paddingHorizontal: gutters,
|
paddingHorizontal: gutters,
|
||||||
@@ -25,6 +296,7 @@ const RecordPlayback = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MusicLandHeader progress={9} onPressBack={goBack} />
|
<MusicLandHeader progress={9} onPressBack={goBack} />
|
||||||
|
|
||||||
<View style={{ flex: 1, marginTop: 11 }}>
|
<View style={{ flex: 1, marginTop: 11 }}>
|
||||||
<CreateLyricsHeader>
|
<CreateLyricsHeader>
|
||||||
<Text
|
<Text
|
||||||
@@ -38,19 +310,143 @@ const RecordPlayback = () => {
|
|||||||
musique, et s'arrêtera à la fin du morceau.
|
musique, et s'arrêtera à la fin du morceau.
|
||||||
</Text>
|
</Text>
|
||||||
</CreateLyricsHeader>
|
</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>
|
</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>
|
||||||
|
|
||||||
|
{/* Start button */}
|
||||||
|
{permissionsGranted && !isPreparing && !isRecording && (
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Lancer ma musique"
|
title="Lancer ma musique"
|
||||||
containerStyle={{
|
containerStyle={{ width: "80%", alignSelf: "center" }}
|
||||||
width: "80%",
|
disabled={!songUrl}
|
||||||
alignSelf: "center",
|
onPress={startCountdownThenRecord}
|
||||||
}}
|
|
||||||
onPress={() => navigate(Routes.RecordedPlayback)}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 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>
|
</View>
|
||||||
</CameraView>
|
</CameraView>
|
||||||
</View>
|
</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;
|
export default RecordPlayback;
|
||||||
|
|||||||
@@ -1,16 +1,111 @@
|
|||||||
import { View, Text, Image } from "react-native";
|
import { useAudioPlayer } from "expo-audio";
|
||||||
import React from "react";
|
import * as FileSystem from "expo-file-system";
|
||||||
import Page from "../../layouts/Page";
|
import { VideoView, useVideoPlayer } from "expo-video";
|
||||||
import { background, img } from "../../assets";
|
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 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 { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { gutters } from "../../styles";
|
import { gutters } from "../../styles";
|
||||||
import Slider from "../../components/Slider";
|
const RecordedPlayback = ({ route }) => {
|
||||||
import GradientButton from "../../components/GradientButton";
|
const { videoUri, project } = route.params || {};
|
||||||
import { Routes } from "../../navigation";
|
const songUrl = project?.songUrl || null;
|
||||||
import BorderGradientButton from "../../components/BorderGradientButton";
|
|
||||||
|
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 (
|
return (
|
||||||
<Page backgroundImg={background.playbackBG2} headerType="NONE">
|
<Page backgroundImg={background.playbackBG2} headerType="NONE">
|
||||||
<MusicLandHeader progress={19} onPressBack={goBack} />
|
<MusicLandHeader progress={19} onPressBack={goBack} />
|
||||||
@@ -18,28 +113,63 @@ const RecordedPlayback = () => {
|
|||||||
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
|
style={{ flex: 1, paddingTop: 12, gap: 14, paddingBottom: gutters * 2 }}
|
||||||
>
|
>
|
||||||
<View style={{ flex: 1, gap: 22 }}>
|
<View style={{ flex: 1, gap: 22 }}>
|
||||||
<Image
|
{!!videoUri && (
|
||||||
source={img.placeholder3}
|
<VideoView
|
||||||
|
player={videoPlayer}
|
||||||
|
nativeControls={false}
|
||||||
|
contentFit="contain"
|
||||||
style={{
|
style={{
|
||||||
width: "80%",
|
width: "80%",
|
||||||
flex: 1,
|
flex: 1,
|
||||||
alignSelf: "center",
|
alignSelf: "center",
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
|
backgroundColor: "#00000066",
|
||||||
|
overflow: "hidden",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Slider value="0:00" maxValue="2:00" />
|
)}
|
||||||
|
<Slider
|
||||||
|
value={fmt(progressInfo.pos)}
|
||||||
|
maxValue={fmt(progressInfo.dur)}
|
||||||
|
progress={sliderProgress}
|
||||||
|
seekEnabled={!!songUrl}
|
||||||
|
onSeek={onSeek}
|
||||||
|
onSeekStart={onSeekStart}
|
||||||
|
onSeekEnd={onSeekEnd}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View
|
<View
|
||||||
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
|
style={{ width: "80%", alignSelf: "center", marginTop: 4, gap: 12 }}
|
||||||
>
|
>
|
||||||
<GradientButton
|
<GradientButton
|
||||||
title="Je valide"
|
title="Je valide"
|
||||||
|
onPress={() => {
|
||||||
|
navigate(Routes.DownloadSongs, {
|
||||||
|
action: "playback",
|
||||||
|
uri: videoUri,
|
||||||
|
project,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
title="Je change de décor"
|
title="Recommencer"
|
||||||
onPress={() => navigate(Routes.ChooseDecor)}
|
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>
|
||||||
</View>
|
</View>
|
||||||
</Page>
|
</Page>
|
||||||
|
|||||||
+293
-20
@@ -1,31 +1,186 @@
|
|||||||
import { View, Text, Image, Pressable, Platform } from "react-native";
|
import { useAudioPlayer } from "expo-audio";
|
||||||
import React from "react";
|
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 { responsiveHeight } from "react-native-responsive-dimensions";
|
||||||
import { icons, img } from "../assets";
|
import { icons, img } from "../assets";
|
||||||
import { size } from "../styles/Style";
|
import { arrayRemove, arrayUnion, projectsRef, usersRef } from "../config/firebase";
|
||||||
import { BlurView } from "expo-blur";
|
import useDataFromRef from "../hooks/useDataFromRef";
|
||||||
|
import { Routes } from "../navigation";
|
||||||
|
import { navigate } from "../navigation/NavigationService";
|
||||||
|
import { useUser } from "../providers/UserDataProvider";
|
||||||
import { Palette } from "../styles";
|
import { Palette } from "../styles";
|
||||||
import { FONT_FAMILY } from "../styles/Fonts";
|
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 (
|
return (
|
||||||
<View style={{ flex: 1 }}>
|
|
||||||
<Carousel
|
|
||||||
data={Array.from({ length: 5 })}
|
|
||||||
vertical
|
|
||||||
height={responsiveHeight(100)}
|
|
||||||
renderItem={() => (
|
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
height: responsiveHeight(100),
|
height: responsiveHeight(100),
|
||||||
position: "relative",
|
position: "relative",
|
||||||
|
backgroundColor: "black",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!!videoUrl ? (
|
||||||
|
<VideoView
|
||||||
|
player={videoPlayer}
|
||||||
|
nativeControls={false}
|
||||||
|
contentFit="cover"
|
||||||
|
style={{ width: "100%", height: "100%" }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Image
|
<Image
|
||||||
source={img.placeholder3}
|
source={img.placeholder3}
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Right side actions */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
@@ -43,7 +198,20 @@ const Playbacks = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<View style={{ gap: 6, alignItems: "center" }}>
|
<View style={{ gap: 6, alignItems: "center" }}>
|
||||||
<Pressable>
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
navigate(Routes.SingerProfile, { userId: item?.userId });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{owner?.profilePictureURL ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: owner.profilePictureURL }}
|
||||||
|
style={{
|
||||||
|
...size({ size: 45 }),
|
||||||
|
borderRadius: 100,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<Image
|
<Image
|
||||||
source={img.profile}
|
source={img.profile}
|
||||||
style={{
|
style={{
|
||||||
@@ -51,8 +219,32 @@ const Playbacks = () => {
|
|||||||
borderRadius: 100,
|
borderRadius: 100,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<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);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<BlurView
|
<BlurView
|
||||||
tint="dark"
|
tint="dark"
|
||||||
intensity={20}
|
intensity={20}
|
||||||
@@ -63,6 +255,7 @@ const Playbacks = () => {
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: Palette.white,
|
borderColor: Palette.white,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
|
backgroundColor: isFollowing ? "#FFFFFF1A" : undefined,
|
||||||
}}
|
}}
|
||||||
experimentalBlurMethod={
|
experimentalBlurMethod={
|
||||||
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
Platform.OS !== "ios" ? "dimezisBlurView" : "none"
|
||||||
@@ -75,17 +268,56 @@ const Playbacks = () => {
|
|||||||
fontFamily: FONT_FAMILY.InterMedium,
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Suivre
|
{isFollowing ? "Ne plus suivre" : "Suivre"}
|
||||||
</Text>
|
</Text>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
<Pressable>
|
<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
|
<Image
|
||||||
source={icons.heartOutline}
|
source={isLiked ? icons.heart : icons.heartOutline}
|
||||||
style={size({ size: 26 })}
|
style={size({ size: 26 })}
|
||||||
resizeMode="contain"
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
|
{!!likesCount && (
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
color: Palette.white,
|
||||||
|
fontSize: 11,
|
||||||
|
marginTop: 4,
|
||||||
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{likesCount}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable>
|
<Pressable>
|
||||||
<Image source={icons.share} style={size({ size: 26 })} />
|
<Image source={icons.share} style={size({ size: 26 })} />
|
||||||
@@ -113,14 +345,55 @@ const Playbacks = () => {
|
|||||||
fontFamily: FONT_FAMILY.InterRegular,
|
fontFamily: FONT_FAMILY.InterRegular,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Description chanson. Viverra enim risus enim enim placerat.
|
{item?.title || "Description chanson"}
|
||||||
Integer pulvinar tristique suscipit risus. Id hendrerit in
|
|
||||||
odio phasellus interdum
|
|
||||||
</Text>
|
</Text>
|
||||||
</BlurView>
|
</BlurView>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</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>
|
</View>
|
||||||
|
|||||||
@@ -1,29 +1,80 @@
|
|||||||
import {
|
import { BlurView } from "expo-blur";
|
||||||
View,
|
|
||||||
Text,
|
|
||||||
StyleSheet,
|
|
||||||
Image,
|
|
||||||
Pressable,
|
|
||||||
Platform,
|
|
||||||
} from "react-native";
|
|
||||||
import React from "react";
|
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 { background, img } from "../../assets";
|
||||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
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 { goBack, navigate } from "../../navigation/NavigationService";
|
||||||
import { Palette } from "../../styles";
|
import { Palette } from "../../styles";
|
||||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
|
||||||
import { size } from "../../styles/Style";
|
import { size } from "../../styles/Style";
|
||||||
import { BlurView } from "expo-blur";
|
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
const DownloadSongs = ({ route }) => {
|
||||||
import { Routes } from "../../navigation";
|
// const params = useRoute().params;
|
||||||
import { useRoute } from "@react-navigation/core";
|
const { action, uri, project } = route.params || {};
|
||||||
|
console.log("project id", project?.id);
|
||||||
|
const { setIsLoading, setTooltip } = useMinuit();
|
||||||
|
|
||||||
const DownloadSongs = () => {
|
const handleDownloadUri = async () => {
|
||||||
const params = useRoute().params;
|
if (action === "playback" && project?.id) {
|
||||||
const action = params?.action;
|
// 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 (
|
return (
|
||||||
<Page
|
<Page
|
||||||
backgroundImg={
|
backgroundImg={
|
||||||
@@ -57,10 +108,16 @@ const DownloadSongs = () => {
|
|||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.itemContainer}
|
style={styles.itemContainer}
|
||||||
onPress={() =>
|
onPress={
|
||||||
navigate(Routes.DownloadPrices, {
|
() => {
|
||||||
action,
|
console.log("test");
|
||||||
})
|
handleDownloadUri();
|
||||||
|
}
|
||||||
|
|
||||||
|
// navigate(Routes.DownloadPrices, {
|
||||||
|
// action,
|
||||||
|
// uri,
|
||||||
|
// })
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<BlurView
|
<BlurView
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
import React, { useState } from "reactn";
|
|
||||||
import { Text, Pressable } from "react-native";
|
|
||||||
import * as ImagePicker from "expo-image-picker";
|
import * as ImagePicker from "expo-image-picker";
|
||||||
|
import { Pressable, Text } from "react-native";
|
||||||
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
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 { responsiveWidth } from "../actions/responsiveSizes.js";
|
||||||
|
import Page from "../layouts/Page";
|
||||||
|
|
||||||
|
import alert from "../components/Alert.js";
|
||||||
import Avatar from "../components/Avatar";
|
import Avatar from "../components/Avatar";
|
||||||
import InputRow from "../components/InputRow.js";
|
import InputRow from "../components/InputRow.js";
|
||||||
import ItemRowList from "../components/ItemRowList.js";
|
import ItemRowList from "../components/ItemRowList.js";
|
||||||
import alert from "../components/Alert.js";
|
|
||||||
|
|
||||||
import { Fonts, gutters, Palette, Style } from "../styles";
|
import { Fonts, gutters, Palette, Style } from "../styles";
|
||||||
|
|
||||||
import { projectsRef } from "../config/firebase";
|
import { projectsRef } from "../config/firebase";
|
||||||
|
|
||||||
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
|
||||||
import { getValueFromKeyState } from "../helpers/index.js";
|
import { getValueFromKeyState } from "../helpers/index.js";
|
||||||
|
import { uploadFileToFirebase } from "../helpers/uploadToFirebase";
|
||||||
|
|
||||||
import { useUserData } from "../providers/UserDataProvider.js";
|
|
||||||
import { Routes } from "../navigation/Routes.js";
|
import { Routes } from "../navigation/Routes.js";
|
||||||
|
import { useUserData } from "../providers/UserDataProvider.js";
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/display-name
|
||||||
export default ({ navigation }) => {
|
export default ({ navigation }) => {
|
||||||
const { setIsLoading, setTooltip } = useMinuit();
|
const { setIsLoading, setTooltip } = useMinuit();
|
||||||
|
|
||||||
|
|||||||
@@ -8665,7 +8665,7 @@ react-native-calendars@^1.1300.0:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
moment "^2.29.4"
|
moment "^2.29.4"
|
||||||
|
|
||||||
react-native-compressor@^1.8.24:
|
react-native-compressor@^1.12.0:
|
||||||
version "1.12.0"
|
version "1.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-native-compressor/-/react-native-compressor-1.12.0.tgz#4c387f100ec6d98adbee10c22496d0e42397d8bf"
|
resolved "https://registry.yarnpkg.com/react-native-compressor/-/react-native-compressor-1.12.0.tgz#4c387f100ec6d98adbee10c22496d0e42397d8bf"
|
||||||
integrity sha512-NMAYpXnTLwx/KecwlLF+9Dnrn/tKV5UVfta8Lk9eROsb05JYnUoKLprRzSSpHcyLjIdQAVaTtx2lPYsappMezw==
|
integrity sha512-NMAYpXnTLwx/KecwlLF+9Dnrn/tKV5UVfta8Lk9eROsb05JYnUoKLprRzSSpHcyLjIdQAVaTtx2lPYsappMezw==
|
||||||
|
|||||||
Reference in New Issue
Block a user