feat: restart
This commit is contained in:
@@ -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={{
|
||||
|
||||
Reference in New Issue
Block a user