feat: restart

This commit is contained in:
2025-11-17 10:47:36 +01:00
parent 705fda0ce3
commit e0309c24b9
4 changed files with 288 additions and 56 deletions
+151 -16
View File
@@ -1,5 +1,6 @@
import { useFocusEffect } from "@react-navigation/native"; import { useFocusEffect } from "@react-navigation/native";
import { CameraView, useCameraPermissions } from "expo-camera"; import { CameraView, useCameraPermissions } from "expo-camera";
import * as FileSystem from "expo-file-system";
import React, { import React, {
useCallback, useCallback,
useEffect, useEffect,
@@ -9,7 +10,7 @@ import React, {
} from "react"; } from "react";
import { Text, TouchableOpacity, View } from "react-native"; import { Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; 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 alert from "../../components/Alert";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import KaraokeLyrics from "../../components/KaraokeLyrics"; import KaraokeLyrics from "../../components/KaraokeLyrics";
@@ -51,6 +52,7 @@ const RecordPlayback = ({ route }) => {
const listenTimerRef = useRef(null); const listenTimerRef = useRef(null);
const checkSongEndRef = useRef(null); const checkSongEndRef = useRef(null);
const stopRequestedRef = useRef(false); const stopRequestedRef = useRef(false);
const restartRequestedRef = useRef(false);
const startedRef = useRef(false); // empêche les doubles démarrages const startedRef = useRef(false); // empêche les doubles démarrages
const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick const countdownActiveRef = useRef(false); // évite le déclenchement avant 1er tick
const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement const playbackStartedRef = useRef(false); // devient vrai lorsque l'audio progresse réellement
@@ -161,7 +163,7 @@ const RecordPlayback = ({ route }) => {
} }
return out; return out;
}, [alignedWords]); }, [alignedWords]);
// console.log("alignedWords:", alignedWords);
useEffect(() => { useEffect(() => {
listenedMsRef.current = 0; listenedMsRef.current = 0;
incrementDoneRef.current = false; incrementDoneRef.current = false;
@@ -228,6 +230,8 @@ const RecordPlayback = ({ route }) => {
return -1; return -1;
}, [lines, currentTimeS]); }, [lines, currentTimeS]);
const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
// Permissions au mount + cleanup // Permissions au mount + cleanup
useEffect(() => { useEffect(() => {
(async () => { (async () => {
@@ -340,6 +344,7 @@ const RecordPlayback = ({ route }) => {
} }
log("startCountdownThenRecord invoked", { projectId, songUrl }); log("startCountdownThenRecord invoked", { projectId, songUrl });
await resetSession(); await resetSession();
restartRequestedRef.current = false;
setIsPreparing(true); setIsPreparing(true);
setShowProgress(false); setShowProgress(false);
setCountdown(5); setCountdown(5);
@@ -553,6 +558,32 @@ const RecordPlayback = ({ route }) => {
log("Recording flow completed", { hasVideo: !!video?.uri }); log("Recording flow completed", { hasVideo: !!video?.uri });
playbackStartedRef.current = false; playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }; 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) { if (video?.uri) {
log("Navigating to RecordedPlayback with video", { log("Navigating to RecordedPlayback with video", {
@@ -585,6 +616,15 @@ const RecordPlayback = ({ route }) => {
} }
playbackStartedRef.current = false; playbackStartedRef.current = false;
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 }; 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 // Inform the user when using a simulator where recording isn't supported
const msg = String(e?.message || e || ""); const msg = String(e?.message || e || "");
if (/not supported on the simulator/i.test(msg)) { 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; const permissionsGranted = !!cameraPermission?.granted;
useEffect(() => { useEffect(() => {
@@ -700,8 +762,9 @@ const RecordPlayback = ({ route }) => {
/> />
)} )}
{/* Progress circulaire */} {/* Progress circulaire + restart */}
{permissionsGranted && (isRecording || showProgress) && ( {permissionsGranted &&
(isRecording || showProgress || isPreparing) && (
<View <View
style={{ style={{
position: "absolute", position: "absolute",
@@ -714,31 +777,45 @@ const RecordPlayback = ({ route }) => {
> >
<View <View
style={{ style={{
width: 100, width: 120,
height: 100, height: 120,
borderRadius: 105, borderRadius: 120,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
}} }}
> >
<ProgressRing <ProgressRing
size={100} size={120}
strokeWidth={8} strokeWidth={8}
progress={ progress={
progressInfo.dur progressInfo.dur
? Math.min(1, (progressInfo.pos || 0) / progressInfo.dur) ? Math.min(
1,
(progressInfo.pos || 0) / progressInfo.dur
)
: 0 : 0
} }
/> />
<View <TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{ style={{
position: "absolute", position: "absolute",
width: 50, width: 70,
height: 50, height: 70,
borderRadius: 55, borderRadius: 80,
alignItems: "center",
justifyContent: "center",
backgroundColor: Palette.white, backgroundColor: Palette.white,
shadowColor: "#000000",
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}} }}
/> >
<RestartSpinnerIcon />
</TouchableOpacity>
</View> </View>
</View> </View>
)} )}
@@ -792,7 +869,7 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
style={{ style={{
transform: [{ rotate: "-90deg" }], transform: [{ rotate: "-90deg" }],
backgroundColor: "#ffffff3d", backgroundColor: "#ffffff3d",
borderRadius: 55, borderRadius: size / 2,
}} }}
> >
<Circle <Circle
@@ -810,6 +887,60 @@ const ProgressRing = ({ size = 50, strokeWidth = 12, progress = 0 }) => {
); );
}; };
const RestartSpinnerIcon = ({
size = 30,
color = "#F94697", // couleur unie
background = "transparent",
}) => {
return (
<View
style={{
width: size + 8,
height: size + 8,
borderRadius: size,
alignItems: "center",
justifyContent: "center",
backgroundColor: background,
}}
>
<Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
<G clipPath="url(#clip0)">
{/* Cercle re-dessiné sans opacité */}
<Path
d="
M19.7285 10.9286
C20.4412 13.5975 19.7507 16.5633 17.6569 18.6571
C14.5327 21.7813 9.46734 21.7813 6.34315 18.6571
C3.21895 15.5329 3.21895 10.4676 6.34315 7.34338
C9.46734 4.21918 14.5327 4.21918 17.6569 7.34338
L18.364 8.05048
"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
{/* Flèche */}
<Path
d="M14.1214 8.05026H18.364V3.80762"
stroke={color}
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
/>
</G>
<Defs>
<ClipPath id="clip0">
<Rect width="24" height="24" fill="white" />
</ClipPath>
</Defs>
</Svg>
</View>
);
};
export default RecordPlayback; export default RecordPlayback;
// Render previous/current/next line to reduce jumpiness // 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 (
<View <View
style={{ style={{
@@ -150,7 +150,7 @@ const reorder = (list, from, to) => {
return next; 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 CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const [, setTooltip] = useGlobal("_tooltip"); const [, setTooltip] = useGlobal("_tooltip");
+115 -18
View File
@@ -3,10 +3,12 @@ import { ScrollView, StyleSheet, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js"; import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background, icons } from "../../assets"; import { background, icons } from "../../assets";
import alert from "../../components/Alert"; import alert from "../../components/Alert";
import AppCheckbox from "../../components/AppCheckbox";
import BorderGradientButton from "../../components/BorderGradientButton"; import BorderGradientButton from "../../components/BorderGradientButton";
import GradientButton from "../../components/GradientButton"; import GradientButton from "../../components/GradientButton";
import ItemContainer from "../../components/ItemContainer/ItemContainer"; import ItemContainer from "../../components/ItemContainer/ItemContainer";
import MusicLandHeader from "../../components/MusicLandHeader"; import MusicLandHeader from "../../components/MusicLandHeader";
import Overlay from "../../components/Overlay";
import firebase, { projectsRef } from "../../config/firebase"; import firebase, { projectsRef } from "../../config/firebase";
import { strings } from "../../constants/strings"; import { strings } from "../../constants/strings";
import { isWeb } from "../../hooks/useLayoutType"; import { isWeb } from "../../hooks/useLayoutType";
@@ -27,6 +29,9 @@ import {
import { getStageAction } from "../../utils/projectStages"; import { getStageAction } from "../../utils/projectStages";
import CustomInput from "./components/CustomInput"; 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 Lyrics = ({ navigation }) => {
const scrollRef = useRef(null); const scrollRef = useRef(null);
const [containerLayout, setContainerLayout] = useState(null); const [containerLayout, setContainerLayout] = useState(null);
@@ -34,6 +39,23 @@ const Lyrics = ({ navigation }) => {
const { selectedProjectId, selectedProject } = useUser(); const { selectedProjectId, selectedProject } = useUser();
const [isFocus, setIsFocus] = useState(null); const [isFocus, setIsFocus] = useState(null);
const [itemsContainerLayout, setItemsContainerLayout] = useState([]); 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 // Effective sources from provider only
const projectTitle = selectedProject?.title || ""; const projectTitle = selectedProject?.title || "";
@@ -129,24 +151,26 @@ const Lyrics = ({ navigation }) => {
alert(title, message); alert(title, message);
}, []); }, []);
const confirmSensitiveContent = useCallback( const confirmSensitiveContent = useCallback((title, message) => {
(title, message) => return new Promise((resolve) => {
new Promise((resolve) => { sensitiveContentResolverRef.current = resolve;
alert(title, message, [ setSensitiveContentModal({
{ visible: true,
text: "Annuler", title,
style: "cancel", message,
onPress: () => resolve(false), });
}, setIsSensitiveContentAcknowledged(false);
{ });
text: "Continuer", }, []);
style: "destructive",
onPress: () => resolve(true), const handleSensitiveCancel = useCallback(() => {
}, closeSensitiveContentModal(false);
]); }, [closeSensitiveContentModal]);
}),
[] const handleSensitiveConfirm = useCallback(() => {
); if (!isSensitiveContentAcknowledged) return;
closeSensitiveContentModal(true);
}, [closeSensitiveContentModal, isSensitiveContentAcknowledged]);
const onValidate = useCallback(async () => { const onValidate = useCallback(async () => {
try { try {
@@ -461,6 +485,49 @@ const Lyrics = ({ navigation }) => {
)} )}
<GradientButton title="Valider" onPress={onValidate} /> <GradientButton title="Valider" onPress={onValidate} />
</View> </View>
<Overlay
isVisible={sensitiveContentModal.visible}
setIsVisible={(visible) => {
if (visible === false) {
handleSensitiveCancel();
}
}}
contentContainerStyle={{
alignItems: "center",
justifyContent: "center",
}}
>
<View style={styles.sensitiveModal}>
<Text style={styles.sensitiveModalTitle}>
{sensitiveContentModal.title}
</Text>
<Text style={styles.sensitiveModalMessage}>
{sensitiveContentModal.message}
</Text>
<View style={styles.sensitiveCheckboxWrapper}>
<AppCheckbox
selected={isSensitiveContentAcknowledged}
onPress={() =>
setIsSensitiveContentAcknowledged((prev) => !prev)
}
label={RESPONSIBILITY_CHECKBOX_LABEL}
/>
</View>
<View style={styles.sensitiveModalActions}>
<BorderGradientButton
title="Annuler"
onPress={handleSensitiveCancel}
containerStyle={{ flex: 1 }}
/>
<GradientButton
title="Continuer"
onPress={handleSensitiveConfirm}
disabled={!isSensitiveContentAcknowledged}
containerStyle={{ flex: 1 }}
/>
</View>
</View>
</Overlay>
</Page> </Page>
); );
}; };
@@ -513,4 +580,34 @@ const styles = StyleSheet.create({
color: Palette.white, color: Palette.white,
opacity: 0.8, 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,
},
}); });
+1 -1
View File
@@ -11,7 +11,7 @@ const SpecificityContext = ({ context, setContext }) => {
subTitle="Dis-nous en un peu plus pour quon puisse mieux taider." subTitle="Dis-nous en un peu plus pour quon puisse mieux taider."
/> />
<CustomInput <CustomInput
placeholder="Écris à qui s'adresse ta chanson" placeholder="cris le contexte de ta chanson"
height={283} height={283}
value={context} value={context}
setValue={setContext} setValue={setContext}