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
+171 -36
View File
@@ -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) && (
<View
style={{
position: "absolute",
left: 0,
right: 0,
bottom: gutters,
alignItems: "center",
justifyContent: "center",
}}
>
{/* Progress circulaire + restart */}
{permissionsGranted &&
(isRecording || showProgress || isPreparing) && (
<View
style={{
width: 100,
height: 100,
borderRadius: 105,
position: "absolute",
left: 0,
right: 0,
bottom: gutters,
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,
width: 120,
height: 120,
borderRadius: 120,
alignItems: "center",
justifyContent: "center",
}}
/>
>
<ProgressRing
size={120}
strokeWidth={8}
progress={
progressInfo.dur
? Math.min(
1,
(progressInfo.pos || 0) / progressInfo.dur
)
: 0
}
/>
<TouchableOpacity
activeOpacity={0.9}
onPress={handleRestartRecording}
style={{
position: "absolute",
width: 70,
height: 70,
borderRadius: 80,
alignItems: "center",
justifyContent: "center",
backgroundColor: Palette.white,
shadowColor: "#000000",
shadowOpacity: 0.12,
shadowRadius: 10,
shadowOffset: { width: 0, height: 4 },
elevation: 4,
}}
>
<RestartSpinnerIcon />
</TouchableOpacity>
</View>
</View>
</View>
)}
)}
</View>
</CameraView>
{/* 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,
}}
>
<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;
// 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 (
<View
style={{