diff --git a/src/screens/Library/MusicDetails.js b/src/screens/Library/MusicDetails.js index ee86413..d257a7a 100644 --- a/src/screens/Library/MusicDetails.js +++ b/src/screens/Library/MusicDetails.js @@ -1,8 +1,15 @@ import { useRoute } from "@react-navigation/core"; import { Audio } from "expo-audio"; -import React, { useEffect, useMemo, useState } from "react"; -import { Pressable, ScrollView, StyleSheet, Text, View, Image as RNImage } from "react-native"; import { Image as ExpoImage } from "expo-image"; +import React, { useEffect, useMemo, useState } from "react"; +import { + Pressable, + Image as RNImage, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; import { SheetManager } from "react-native-actions-sheet"; import { useGlobal } from "reactn"; import { background, icons, img } from "../../assets"; @@ -336,7 +343,7 @@ const MusicDetails = () => { /> {/* Previous (rewind 10s) */} - seekBy(-10)}> + seekBy(-10)}> { ? 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) => { @@ -80,6 +93,12 @@ const NewMusicOptions = ({ route }) => { case 2: navigate(Routes.Production, { action: item.type }); break; + case 3: + console.log("test"); + navigate(Routes.Playback, { + action: item.type, + project: currentProjet, + }); default: break; } diff --git a/src/screens/Playback/Playback.js b/src/screens/Playback/Playback.js index b9db567..9790bfd 100644 --- a/src/screens/Playback/Playback.js +++ b/src/screens/Playback/Playback.js @@ -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 ( @@ -23,10 +24,10 @@ const Playback = () => { > - + {/* */} navigate(Routes.RecordPlayback)} + onPress={() => navigate(Routes.RecordPlayback, { project })} /> diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 56941a1..dadd0b7 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -1,21 +1,181 @@ -import { Text, View } from "react-native"; +import { Audio } from "expo-audio"; +import { CameraView, useCameraPermissions } from "expo-camera"; 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 { Text, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader"; -import { FONT_FAMILY } from "../../styles/Fonts"; import GradientButton from "../../components/GradientButton"; +import MusicLandHeader from "../../components/MusicLandHeader"; 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 = () => { +const RecordPlayback = ({ route }) => { const { top } = useSafeAreaInsets(); + const { project } = route.params || {}; + + // Permissions + const [cameraPermission, requestCameraPermission] = useCameraPermissions(); + + // Refs + const cameraRef = React.useRef(null); + const soundRef = React.useRef(null); + const countdownTimerRef = React.useRef(null); + const stopRequestedRef = React.useRef(false); + + // UI / state + const [isPreparing, setIsPreparing] = React.useState(false); + const [countdown, setCountdown] = React.useState(0); + const [isRecording, setIsRecording] = React.useState(false); + + // Derive song URL robustly + const songUrl = project?.songUrl || null; + + React.useEffect(() => { + // Request permissions on mount if not granted + (async () => { + try { + if (!cameraPermission?.granted) await requestCameraPermission(); + } catch (_) {} + })(); + return () => { + // Cleanup timers and audio on unmount + try { + if (countdownTimerRef.current) { + global.clearInterval(countdownTimerRef.current); + countdownTimerRef.current = null; + } + (async () => { + try { + if (soundRef.current) { + soundRef.current.setOnPlaybackStatusUpdate(null); + await soundRef.current.unloadAsync(); + soundRef.current = null; + } + } catch (_) {} + })(); + } catch (_) {} + }; + }, []); + + const startCountdownThenRecord = async () => { + console.log("start countdown"); + if (!songUrl) return; + console.log("songUrl exists"); + setIsPreparing(true); + setCountdown(5); + // Start music + recording immediately when countdown starts + void startRecordingWithMusic(); + // 5 -> 0 countdown display only + countdownTimerRef.current = global.setInterval(() => { + setCountdown((c) => { + const next = (c || 0) - 1; + if (next <= 0) { + global.clearInterval(countdownTimerRef.current); + countdownTimerRef.current = null; + } + return Math.max(0, next); + }); + }, 1000); + }; + + const startRecordingWithMusic = async () => { + try { + stopRequestedRef.current = false; + console.log("stop requested ref is : ", soundRef.current); + // Prepare audio + if (soundRef.current) { + console.log("if sound ref current"); + try { + soundRef.current.setOnPlaybackStatusUpdate(null); + await soundRef.current.unloadAsync(); + console.log("unload sound ref"); + } catch (_) {} + soundRef.current = null; + } + + // Ensure audio mode allows playback alongside camera recording + try { + await Audio.setAudioModeAsync({ + playsInSilentMode: true, + interruptionMode: "mixWithOthers", + allowsRecording: true, + shouldPlayInBackground: false, + }); + } catch (_) {} + + const { sound } = await Audio.Sound.createAsync( + { uri: songUrl }, + { shouldPlay: false } + ); + + sound.setOnPlaybackStatusUpdate((status) => { + if (!status || !status.isLoaded) return; + if (status.didJustFinish && !stopRequestedRef.current) { + stopRequestedRef.current = true; + try { + cameraRef.current?.stopRecording?.(); + } catch (_) {} + } + }); + soundRef.current = sound; + + // Start recording + setIsRecording(true); + const recordPromise = cameraRef.current?.recordAsync?.({ + mute: true, + maxDuration: 600, // safety cap (10 min) + }); + + // Start playing + await sound.playAsync(); + + // Wait for recording to stop (either by song end or manual stop) + const video = await recordPromise; + + // Ensure audio stops and cleanup + try { + const s = soundRef.current; + if (s) { + s.setOnPlaybackStatusUpdate(null); + await s.stopAsync().catch(() => {}); + await s.unloadAsync().catch(() => {}); + } + } catch (_) {} + soundRef.current = null; + + setIsRecording(false); + setIsPreparing(false); + + // Navigate to next screen with video uri if available + if (video?.uri) { + navigate(Routes.RecordedPlayback, { videoUri: video.uri, project }); + } else { + navigate(Routes.RecordedPlayback, { project }); + } + } catch (e) { + // Fallback on error + setIsRecording(false); + setIsPreparing(false); + try { + soundRef.current?.setOnPlaybackStatusUpdate?.(null); + await soundRef.current?.unloadAsync?.(); + } catch (_) {} + soundRef.current = null; + } + }; + + const permissionsGranted = !!cameraPermission?.granted; return ( - + { musique, et s'arrêtera à la fin du morceau. + + {/* Centered countdown overlay */} + {(isPreparing || isRecording) && countdown > 0 && ( + + + {countdown} + + + )} + + {/* Permission prompt */} + {!permissionsGranted && ( + + { + try { + if (!cameraPermission?.granted) + await requestCameraPermission(); + } catch (_) {} + }} + /> + + )} - navigate(Routes.RecordedPlayback)} - /> + + {/* Start button */} + {permissionsGranted && !isPreparing && !isRecording && ( + + )} diff --git a/src/screens/Playback/RecordedPlayback.js b/src/screens/Playback/RecordedPlayback.js index 999f9c1..c4e4eb3 100644 --- a/src/screens/Playback/RecordedPlayback.js +++ b/src/screens/Playback/RecordedPlayback.js @@ -1,14 +1,14 @@ -import { View, Text, Image } from "react-native"; import React from "react"; -import Page from "../../layouts/Page"; +import { Image, View } from "react-native"; import { background, img } 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 = () => { return ( @@ -32,9 +32,7 @@ const RecordedPlayback = () => { - + navigate(Routes.ChooseDecor)}