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 (
+
+
+
+ );
+};
+
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."
/>