From e0309c24b9a226f5a3b1aad7bcc26ce26af0c8fa Mon Sep 17 00:00:00 2001 From: leon-morival Date: Mon, 17 Nov 2025 10:47:36 +0100 Subject: [PATCH] feat: restart --- src/screens/Playback/RecordPlayback.js | 207 +++++++++++++++--- .../Writing/CustomizeSongStructure.web.js | 2 +- src/screens/Writing/Lyrics.js | 133 +++++++++-- src/screens/Writing/SpecificityContext.js | 2 +- 4 files changed, 288 insertions(+), 56 deletions(-) diff --git a/src/screens/Playback/RecordPlayback.js b/src/screens/Playback/RecordPlayback.js index 67d0be5..1ac0b4c 100644 --- a/src/screens/Playback/RecordPlayback.js +++ b/src/screens/Playback/RecordPlayback.js @@ -1,5 +1,6 @@ import { useFocusEffect } from "@react-navigation/native"; import { CameraView, useCameraPermissions } from "expo-camera"; +import * as FileSystem from "expo-file-system"; import React, { useCallback, useEffect, @@ -9,7 +10,7 @@ import React, { } from "react"; import { Text, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import Svg, { Circle } from "react-native-svg"; +import Svg, { Circle, Path, G, Defs, Rect, ClipPath } from "react-native-svg"; import alert from "../../components/Alert"; import GradientButton from "../../components/GradientButton"; import KaraokeLyrics from "../../components/KaraokeLyrics"; @@ -51,6 +52,7 @@ const RecordPlayback = ({ route }) => { const listenTimerRef = useRef(null); const checkSongEndRef = useRef(null); const stopRequestedRef = useRef(false); + const restartRequestedRef = useRef(false); const startedRef = useRef(false); // empêche les doubles démarrages const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement @@ -161,7 +163,7 @@ const RecordPlayback = ({ route }) => { } return out; }, [alignedWords]); - // console.log("alignedWords:", alignedWords); + useEffect(() => { listenedMsRef.current = 0; incrementDoneRef.current = false; @@ -228,6 +230,8 @@ const RecordPlayback = ({ route }) => { return -1; }, [lines, currentTimeS]); + const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0); + // Permissions au mount + cleanup useEffect(() => { (async () => { @@ -340,6 +344,7 @@ const RecordPlayback = ({ route }) => { } log("startCountdownThenRecord invoked", { projectId, songUrl }); await resetSession(); + restartRequestedRef.current = false; setIsPreparing(true); setShowProgress(false); setCountdown(5); @@ -553,6 +558,32 @@ const RecordPlayback = ({ route }) => { log("Recording flow completed", { hasVideo: !!video?.uri }); playbackStartedRef.current = false; progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }; + const shouldRestart = restartRequestedRef.current; + if (shouldRestart) { + restartRequestedRef.current = false; + } + + if (video?.uri && shouldRestart) { + try { + const info = await FileSystem.getInfoAsync(video.uri); + if (info?.exists) { + await FileSystem.deleteAsync(video.uri, { idempotent: true }); + log("Discarded interim recording file"); + } + } catch (error) { + log("Failed to discard interim recording", { + message: error?.message || String(error || ""), + }); + } + } + + if (shouldRestart) { + log("Restart requested, relaunching countdown"); + requestAnimationFrame(() => { + void startCountdownThenRecord(); + }); + return; + } if (video?.uri) { log("Navigating to RecordedPlayback with video", { @@ -585,6 +616,15 @@ const RecordPlayback = ({ route }) => { } playbackStartedRef.current = false; progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }; + const shouldRestart = restartRequestedRef.current; + if (shouldRestart) { + restartRequestedRef.current = false; + log("Restart requested despite error, restarting flow"); + requestAnimationFrame(() => { + void startCountdownThenRecord(); + }); + return; + } // Inform the user when using a simulator where recording isn't supported const msg = String(e?.message || e || ""); if (/not supported on the simulator/i.test(msg)) { @@ -607,6 +647,28 @@ const RecordPlayback = ({ route }) => { } }; + const handleRestartRecording = async () => { + try { + log("Restart button pressed", { + isPreparing, + isRecording, + }); + if (isPreparing || !isRecording) { + restartRequestedRef.current = false; + await startCountdownThenRecord(); + return; + } + restartRequestedRef.current = true; + stopRequestedRef.current = true; + try { + if (player?.playing) await player.pause?.(); + } catch (_) {} + try { + cameraRef.current?.stopRecording?.(); + } catch (_) {} + } catch (_) {} + }; + const permissionsGranted = !!cameraPermission?.granted; useEffect(() => { @@ -700,48 +762,63 @@ const RecordPlayback = ({ route }) => { /> )} - {/* Progress circulaire */} - {permissionsGranted && (isRecording || showProgress) && ( - + {/* Progress circulaire + restart */} + {permissionsGranted && + (isRecording || showProgress || isPreparing) && ( - + > + + + + + - - )} + )} {/* CreateLyricsHeader overlay outside of CameraView */} @@ -792,7 +869,7 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => { style={{ transform: [{ rotate: "-90deg" }], backgroundColor: "#ffffff3d", - borderRadius: 55, + borderRadius: size / 2, }} > { ); }; +const RestartSpinnerIcon = ({ + size = 30, + color = "#F94697", // couleur unie + background = "transparent", +}) => { + return ( + + + + {/* Cercle re-dessiné sans opacité */} + + + {/* Flèche */} + + + + + + + + + + + ); +}; + export default RecordPlayback; // Render previous/current/next line to reduce jumpiness @@ -851,7 +982,11 @@ const KaraokeLines = ({ lines = [], currentLineIdx = -1 }) => { ); }; -const CameraFacingSelector = ({ value = "front", onChange, disabled = false }) => { +const CameraFacingSelector = ({ + value = "front", + onChange, + disabled = false, +}) => { return ( { return next; }; -const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); +const clamp = (value, min, max) => Math.max(mn, Math.min(max, value)); const CustomizeSongStructure = ({ baseStructure = [], onChange }) => { const [, setTooltip] = useGlobal("_tooltip"); diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js index 708f7c1..437e216 100644 --- a/src/screens/Writing/Lyrics.js +++ b/src/screens/Writing/Lyrics.js @@ -3,10 +3,12 @@ import { ScrollView, StyleSheet, Text, View } from "react-native"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import { background, icons } from "../../assets"; import alert from "../../components/Alert"; +import AppCheckbox from "../../components/AppCheckbox"; import BorderGradientButton from "../../components/BorderGradientButton"; import GradientButton from "../../components/GradientButton"; import ItemContainer from "../../components/ItemContainer/ItemContainer"; import MusicLandHeader from "../../components/MusicLandHeader"; +import Overlay from "../../components/Overlay"; import firebase, { projectsRef } from "../../config/firebase"; import { strings } from "../../constants/strings"; import { isWeb } from "../../hooks/useLayoutType"; @@ -27,6 +29,9 @@ import { import { getStageAction } from "../../utils/projectStages"; import CustomInput from "./components/CustomInput"; +const RESPONSIBILITY_CHECKBOX_LABEL = + "Vous êtes responsable du contenu que vous validez et Musicland ne sera en aucun cas tenu responsable du contenu que vous validez."; + const Lyrics = ({ navigation }) => { const scrollRef = useRef(null); const [containerLayout, setContainerLayout] = useState(null); @@ -34,6 +39,23 @@ const Lyrics = ({ navigation }) => { const { selectedProjectId, selectedProject } = useUser(); const [isFocus, setIsFocus] = useState(null); const [itemsContainerLayout, setItemsContainerLayout] = useState([]); + const [sensitiveContentModal, setSensitiveContentModal] = useState({ + visible: false, + title: "", + message: "", + }); + const [isSensitiveContentAcknowledged, setIsSensitiveContentAcknowledged] = + useState(false); + const sensitiveContentResolverRef = useRef(null); + + const closeSensitiveContentModal = useCallback((result) => { + setSensitiveContentModal((prev) => ({ ...prev, visible: false })); + setIsSensitiveContentAcknowledged(false); + if (sensitiveContentResolverRef.current) { + sensitiveContentResolverRef.current(result); + sensitiveContentResolverRef.current = null; + } + }, []); // Effective sources from provider only const projectTitle = selectedProject?.title || ""; @@ -129,24 +151,26 @@ const Lyrics = ({ navigation }) => { alert(title, message); }, []); - const confirmSensitiveContent = useCallback( - (title, message) => - new Promise((resolve) => { - alert(title, message, [ - { - text: "Annuler", - style: "cancel", - onPress: () => resolve(false), - }, - { - text: "Continuer", - style: "destructive", - onPress: () => resolve(true), - }, - ]); - }), - [] - ); + const confirmSensitiveContent = useCallback((title, message) => { + return new Promise((resolve) => { + sensitiveContentResolverRef.current = resolve; + setSensitiveContentModal({ + visible: true, + title, + message, + }); + setIsSensitiveContentAcknowledged(false); + }); + }, []); + + const handleSensitiveCancel = useCallback(() => { + closeSensitiveContentModal(false); + }, [closeSensitiveContentModal]); + + const handleSensitiveConfirm = useCallback(() => { + if (!isSensitiveContentAcknowledged) return; + closeSensitiveContentModal(true); + }, [closeSensitiveContentModal, isSensitiveContentAcknowledged]); const onValidate = useCallback(async () => { try { @@ -461,6 +485,49 @@ const Lyrics = ({ navigation }) => { )} + { + if (visible === false) { + handleSensitiveCancel(); + } + }} + contentContainerStyle={{ + alignItems: "center", + justifyContent: "center", + }} + > + + + {sensitiveContentModal.title} + + + {sensitiveContentModal.message} + + + + setIsSensitiveContentAcknowledged((prev) => !prev) + } + label={RESPONSIBILITY_CHECKBOX_LABEL} + /> + + + + + + + ); }; @@ -513,4 +580,34 @@ const styles = StyleSheet.create({ color: Palette.white, opacity: 0.8, }, + sensitiveModal: { + width: "90%", + maxWidth: 460, + backgroundColor: Palette.lightPurple, + borderRadius: 18, + paddingVertical: 24, + paddingHorizontal: 20, + gap: 16, + }, + sensitiveModalTitle: { + fontFamily: FONT_FAMILY.InterSemiBold, + fontSize: 20, + color: Palette.white, + textAlign: "center", + }, + sensitiveModalMessage: { + fontFamily: FONT_FAMILY.InterRegular, + fontSize: 15, + color: Palette.white, + lineHeight: 20, + textAlign: "left", + opacity: 0.9, + }, + sensitiveCheckboxWrapper: { + paddingVertical: 4, + }, + sensitiveModalActions: { + flexDirection: "row", + gap: 12, + }, }); diff --git a/src/screens/Writing/SpecificityContext.js b/src/screens/Writing/SpecificityContext.js index 114edd1..ff922ca 100644 --- a/src/screens/Writing/SpecificityContext.js +++ b/src/screens/Writing/SpecificityContext.js @@ -11,7 +11,7 @@ const SpecificityContext = ({ context, setContext }) => { subTitle="Dis-nous en un peu plus pour qu’on puisse mieux t’aider." />