more fixes web + mobile
This commit is contained in:
@@ -729,8 +729,8 @@ const styles = StyleSheet.create({
|
||||
alignItems: isWeb ? "center" : "stretch",
|
||||
justifyContent: isWeb ? "center" : "flex-start",
|
||||
gap: 12,
|
||||
marginTop: isWeb ? 24 : 0,
|
||||
marginBottom: isWeb ? 8 : 16,
|
||||
marginTop: isWeb ? 12 : 0,
|
||||
marginBottom: isWeb ? 4 : 16,
|
||||
paddingHorizontal: isWeb ? 0 : gutters,
|
||||
zIndex: 10,
|
||||
},
|
||||
@@ -786,8 +786,8 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
subtitleWrapper: {
|
||||
width: "100%",
|
||||
marginBottom: 16,
|
||||
marginTop: 15,
|
||||
marginBottom: isWeb ? 12 : 16,
|
||||
marginTop: isWeb ? 8 : 15,
|
||||
alignItems: "center",
|
||||
},
|
||||
subtitleGradient: {
|
||||
@@ -812,7 +812,7 @@ const styles = StyleSheet.create({
|
||||
backgroundColor: Palette.glass,
|
||||
},
|
||||
subtitleInnerWeb: {
|
||||
paddingVertical: 12,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: Palette.lightPurple,
|
||||
},
|
||||
subtitle: {
|
||||
@@ -828,7 +828,7 @@ const styles = StyleSheet.create({
|
||||
flexDirection: "row",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-between",
|
||||
rowGap: 20,
|
||||
rowGap: isWeb ? 16 : 20,
|
||||
},
|
||||
cardsStack: {
|
||||
width: "100%",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Image as ExpoImage } from "expo-image";
|
||||
import { FONT_FAMILY } from "../../../styles/Fonts";
|
||||
import { Palette } from "../../../styles";
|
||||
import { icons } from "../../../assets";
|
||||
import { isWeb } from "../../../hooks/useLayoutType.js";
|
||||
|
||||
const ClubCard = ({ image, onPress, hasActiveSubscription = false }) => {
|
||||
const subtitle = hasActiveSubscription
|
||||
@@ -32,7 +33,7 @@ export default memo(ClubCard);
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
marginTop: 28,
|
||||
marginTop: isWeb ? 16 : 28,
|
||||
borderRadius: 20,
|
||||
backgroundColor: "#252438",
|
||||
overflow: "hidden",
|
||||
|
||||
@@ -71,8 +71,8 @@ const StageCard = ({
|
||||
style={[
|
||||
{
|
||||
width: "100%",
|
||||
height: isMobileVariant ? 190 : "100%",
|
||||
minHeight: isMobileVariant ? 190 : 210,
|
||||
height: 190,
|
||||
minHeight: 190,
|
||||
borderRadius: 0,
|
||||
},
|
||||
isMobileVariant
|
||||
@@ -92,7 +92,7 @@ const StageCard = ({
|
||||
width: "100%",
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
marginBottom: isMobileVariant ? 8 : 6,
|
||||
},
|
||||
isMobileVariant
|
||||
? { justifyContent: "center" }
|
||||
@@ -125,7 +125,7 @@ const StageCard = ({
|
||||
const textBlockBaseStyle = {
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
paddingVertical: 20,
|
||||
paddingVertical: isMobileVariant ? 20 : 16,
|
||||
paddingHorizontal: 20,
|
||||
justifyContent: "center",
|
||||
alignSelf: "center",
|
||||
|
||||
@@ -63,6 +63,8 @@ const MusicDetails = ({ route }) => {
|
||||
const [fav, setFav] = useState(false);
|
||||
const [currentUID] = useGlobal("currentUID");
|
||||
const wasPlayingBeforeSeek = useRef(false);
|
||||
const hasCapturedSeekStateRef = useRef(false);
|
||||
const lastSeekTargetMsRef = useRef(null);
|
||||
const listenedMsRef = useRef(0);
|
||||
const incrementDoneRef = useRef(false);
|
||||
const timerRef = useRef(null);
|
||||
@@ -330,8 +332,12 @@ const MusicDetails = ({ route }) => {
|
||||
|
||||
const handleSliderSeekStart = useCallback(async () => {
|
||||
if (!trackDescriptor) return;
|
||||
try {
|
||||
lastSeekTargetMsRef.current = null;
|
||||
if (!hasCapturedSeekStateRef.current) {
|
||||
hasCapturedSeekStateRef.current = true;
|
||||
wasPlayingBeforeSeek.current = isTrackPlaying;
|
||||
}
|
||||
try {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: positionMs, autoPlay: false });
|
||||
}
|
||||
@@ -355,6 +361,7 @@ const MusicDetails = ({ route }) => {
|
||||
const dur = sliderDurationMs || 0;
|
||||
if (!trackDescriptor || dur <= 0) return;
|
||||
const targetMs = Math.max(0, Math.floor(dur * ratio));
|
||||
lastSeekTargetMsRef.current = targetMs;
|
||||
try {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: targetMs, autoPlay: false });
|
||||
@@ -422,11 +429,24 @@ const MusicDetails = ({ route }) => {
|
||||
]);
|
||||
|
||||
const handleSliderSeekEnd = useCallback(async () => {
|
||||
const targetMs =
|
||||
typeof lastSeekTargetMsRef.current === "number"
|
||||
? Math.max(0, lastSeekTargetMsRef.current)
|
||||
: null;
|
||||
try {
|
||||
if (wasPlayingBeforeSeek.current) {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: positionMs, autoPlay: true });
|
||||
await ensureLoaded({
|
||||
startPositionMs:
|
||||
targetMs !== null && Number.isFinite(targetMs)
|
||||
? targetMs
|
||||
: positionMs,
|
||||
autoPlay: true,
|
||||
});
|
||||
} else {
|
||||
if (targetMs !== null && Number.isFinite(targetMs)) {
|
||||
await seekTrackTo(targetMs);
|
||||
}
|
||||
await resumeTrack();
|
||||
}
|
||||
}
|
||||
@@ -434,8 +454,10 @@ const MusicDetails = ({ route }) => {
|
||||
console.log("MusicDetails seek end error", e?.message);
|
||||
} finally {
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
hasCapturedSeekStateRef.current = false;
|
||||
lastSeekTargetMsRef.current = null;
|
||||
}
|
||||
}, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack]);
|
||||
}, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack, seekTrackTo]);
|
||||
|
||||
const handleSeekBySeconds = useCallback(
|
||||
async (deltaSeconds) => {
|
||||
|
||||
@@ -49,7 +49,7 @@ const Playback = ({ route, navigation }) => {
|
||||
|
||||
return (
|
||||
<Page headerType="NONE" backgroundImg={background.playbackBG2}>
|
||||
<Image source={ai.john} style={styles.img} resizeMode="contain" />
|
||||
<Image source={ai.theo} style={styles.img} resizeMode="contain" />
|
||||
<MusicLandHeader
|
||||
onPressBack={() => navigation.navigate(Routes.Home)}
|
||||
progress={25}
|
||||
|
||||
@@ -52,6 +52,7 @@ const RecordPlayback = ({ route }) => {
|
||||
const countdownTimerRef = useRef(null);
|
||||
const listenTimerRef = useRef(null);
|
||||
const checkSongEndRef = useRef(null);
|
||||
const isPausedRef = useRef(false);
|
||||
const stopRequestedRef = useRef(false);
|
||||
const restartRequestedRef = useRef(false);
|
||||
const manualRestartInFlightRef = useRef(false);
|
||||
@@ -73,6 +74,7 @@ const RecordPlayback = ({ route }) => {
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [showProgress, setShowProgress] = useState(false);
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
const [cameraFacing, setCameraFacing] = useState("front");
|
||||
@@ -100,6 +102,9 @@ const RecordPlayback = ({ route }) => {
|
||||
useEffect(() => {
|
||||
latestLoopingValueRef.current = isLooping ?? false;
|
||||
}, [isLooping]);
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused;
|
||||
}, [isPaused]);
|
||||
|
||||
const musicIndex = useMemo(() => {
|
||||
const i = Number(songIndex);
|
||||
@@ -302,8 +307,13 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
setIsPreparing(false);
|
||||
setIsRecording(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
setShowProgress(false);
|
||||
setCountdown(0);
|
||||
try {
|
||||
await cameraRef.current?.resumePreview?.();
|
||||
} catch (_) {}
|
||||
|
||||
if (player) {
|
||||
try {
|
||||
@@ -392,6 +402,8 @@ const RecordPlayback = ({ route }) => {
|
||||
preserveSongEndWatcher,
|
||||
preserveStopRequest,
|
||||
});
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
if (!preserveRestartFlag) {
|
||||
restartRequestedRef.current = false;
|
||||
manualRestartInFlightRef.current = false;
|
||||
@@ -443,6 +455,11 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
setIsRecording(true);
|
||||
setShowProgress(true);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
try {
|
||||
await cameraRef.current?.resumePreview?.();
|
||||
} catch (_) {}
|
||||
playbackStartedRef.current = false;
|
||||
progressLogRef.current = { bucket: -1, lastPos: -1, lastDur: -1 };
|
||||
log("startRecordingWithMusic", {
|
||||
@@ -566,6 +583,7 @@ const RecordPlayback = ({ route }) => {
|
||||
checkSongEndRef.current = setInterval(() => {
|
||||
try {
|
||||
if (!player) return;
|
||||
if (isPausedRef.current) return;
|
||||
if (stopRequestedRef.current) {
|
||||
const sinceLastRequest = Date.now() - (stopRequestedAtRef.current || 0);
|
||||
if (sinceLastRequest >= 1200) {
|
||||
@@ -630,6 +648,8 @@ const RecordPlayback = ({ route }) => {
|
||||
}
|
||||
|
||||
setIsRecording(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
setShowProgress(false);
|
||||
log("Recording flow completed", { hasVideo: !!video?.uri });
|
||||
playbackStartedRef.current = false;
|
||||
@@ -683,6 +703,8 @@ const RecordPlayback = ({ route }) => {
|
||||
stopRequestedAtRef.current = 0;
|
||||
setIsRecording(false);
|
||||
setIsPreparing(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
setShowProgress(false);
|
||||
if (listenTimerRef.current) {
|
||||
clearInterval(listenTimerRef.current);
|
||||
@@ -786,6 +808,33 @@ const RecordPlayback = ({ route }) => {
|
||||
}, [handleBackPress])
|
||||
);
|
||||
|
||||
const handleTogglePause = useCallback(async () => {
|
||||
try {
|
||||
if (!isRecording) return;
|
||||
if (!isPausedRef.current) {
|
||||
setIsPaused(true);
|
||||
isPausedRef.current = true;
|
||||
stopRequestedRef.current = false;
|
||||
stopRequestedAtRef.current = 0;
|
||||
try {
|
||||
await player?.pause?.();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await cameraRef.current?.pausePreview?.();
|
||||
} catch (_) {}
|
||||
return;
|
||||
}
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
try {
|
||||
await cameraRef.current?.resumePreview?.();
|
||||
} catch (_) {}
|
||||
try {
|
||||
await player?.play?.();
|
||||
} catch (_) {}
|
||||
} catch (_) {}
|
||||
}, [isRecording, player]);
|
||||
|
||||
const handleRestartRecording = async () => {
|
||||
try {
|
||||
const hasRecordingPending = !!activeRecordingPromiseRef.current;
|
||||
@@ -794,6 +843,11 @@ const RecordPlayback = ({ route }) => {
|
||||
isRecording,
|
||||
hasRecordingPending,
|
||||
});
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
try {
|
||||
await cameraRef.current?.resumePreview?.();
|
||||
} catch (_) {}
|
||||
if (isPreparing) {
|
||||
if (hasRecordingPending) {
|
||||
await startCountdownThenRecord({
|
||||
@@ -980,6 +1034,34 @@ const RecordPlayback = ({ route }) => {
|
||||
<RestartSpinnerIcon />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{isRecording && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleTogglePause}
|
||||
style={{
|
||||
marginTop: 14,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 999,
|
||||
backgroundColor: Palette.white,
|
||||
shadowColor: "#000000",
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 10,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.black,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{isPaused ? "Reprendre" : "Mettre en pause"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { background } from "../../assets";
|
||||
import RestartSpinnerIcon from "../../assets/UI/RestartSpinnerIcon";
|
||||
import { registerBlobUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
|
||||
|
||||
const TIME_BEFORE_INCREMENT_MS = 20000;
|
||||
const WEB_PREVIEW_WIDTH = 360;
|
||||
@@ -82,6 +83,7 @@ const RecordPlayback = ({ route }) => {
|
||||
const stopRecordingPromiseRef = useRef(null);
|
||||
const stopRecordingResolveRef = useRef(null);
|
||||
const recordedUrlRef = useRef(null);
|
||||
const preserveRecordingForNextScreenRef = useRef(false);
|
||||
const originalLoopingValueRef = useRef({ hasValue: false, value: false });
|
||||
const latestLoopingValueRef = useRef(isLooping ?? false);
|
||||
const [mediaReady, setMediaReady] = useState(false);
|
||||
@@ -91,14 +93,18 @@ const RecordPlayback = ({ route }) => {
|
||||
const [countdown, setCountdown] = useState(0);
|
||||
const [isPreparing, setIsPreparing] = useState(false);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
const [pos, setPos] = useState(0);
|
||||
const [dur, setDur] = useState(0);
|
||||
|
||||
// timers / refs
|
||||
const isPausedRef = useRef(false);
|
||||
const progressTimerRef = useRef(null);
|
||||
const listenTimerRef = useRef(null);
|
||||
const countdownTimerRef = useRef(null);
|
||||
const perfStartRef = useRef(null);
|
||||
const pausedAtRef = useRef(null);
|
||||
const pausedMsRef = useRef(0);
|
||||
const listenedMsRef = useRef(0);
|
||||
const viewsIncrementedRef = useRef(false);
|
||||
const correctedInitialJumpRef = useRef(false);
|
||||
@@ -106,6 +112,9 @@ const RecordPlayback = ({ route }) => {
|
||||
useEffect(() => {
|
||||
latestLoopingValueRef.current = isLooping ?? false;
|
||||
}, [isLooping]);
|
||||
useEffect(() => {
|
||||
isPausedRef.current = isPaused;
|
||||
}, [isPaused]);
|
||||
|
||||
// Lyrics
|
||||
const alignedWords = useMemo(() => {
|
||||
@@ -131,6 +140,10 @@ const RecordPlayback = ({ route }) => {
|
||||
const resetUI = () => {
|
||||
setIsPreparing(false);
|
||||
setIsRecording(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
pausedAtRef.current = null;
|
||||
pausedMsRef.current = 0;
|
||||
setCountdown(0);
|
||||
setPos(0);
|
||||
setDur(0);
|
||||
@@ -141,12 +154,13 @@ const RecordPlayback = ({ route }) => {
|
||||
};
|
||||
|
||||
const releaseRecordingUrl = useCallback(() => {
|
||||
if (recordedUrlRef.current) {
|
||||
try {
|
||||
URL.revokeObjectURL(recordedUrlRef.current);
|
||||
} catch {}
|
||||
recordedUrlRef.current = null;
|
||||
if (!recordedUrlRef.current) {
|
||||
preserveRecordingForNextScreenRef.current = false;
|
||||
return;
|
||||
}
|
||||
releaseBlobUrl(recordedUrlRef.current);
|
||||
recordedUrlRef.current = null;
|
||||
preserveRecordingForNextScreenRef.current = false;
|
||||
}, []);
|
||||
|
||||
const startRecorder = useCallback(() => {
|
||||
@@ -203,6 +217,7 @@ const RecordPlayback = ({ route }) => {
|
||||
});
|
||||
url = URL.createObjectURL(blob);
|
||||
recordedUrlRef.current = url;
|
||||
registerBlobUrl(url, blob);
|
||||
} else {
|
||||
releaseRecordingUrl();
|
||||
}
|
||||
@@ -273,8 +288,10 @@ const RecordPlayback = ({ route }) => {
|
||||
} catch (e) {}
|
||||
try {
|
||||
await stopRecorderAndGetUrl();
|
||||
releaseRecordingUrl();
|
||||
} catch (e) {}
|
||||
if (!preserveRecordingForNextScreenRef.current) {
|
||||
releaseRecordingUrl();
|
||||
}
|
||||
}, [player, releaseRecordingUrl, stopRecorderAndGetUrl]);
|
||||
|
||||
const resetSessionRef = useRef(resetSession);
|
||||
@@ -410,11 +427,14 @@ const RecordPlayback = ({ route }) => {
|
||||
mediaStreamRef.current.getTracks().forEach((track) => track.stop());
|
||||
mediaStreamRef.current = null;
|
||||
}
|
||||
releaseRecordingUrl();
|
||||
if (!preserveRecordingForNextScreenRef.current) {
|
||||
releaseRecordingUrl();
|
||||
}
|
||||
};
|
||||
}, [releaseRecordingUrl]);
|
||||
}, [releaseRecordingUrl, preserveRecordingForNextScreenRef]);
|
||||
|
||||
const stopAndNavigate = useCallback(async () => {
|
||||
preserveRecordingForNextScreenRef.current = true;
|
||||
clearAllTimers();
|
||||
try {
|
||||
await player?.pause?.();
|
||||
@@ -429,6 +449,10 @@ const RecordPlayback = ({ route }) => {
|
||||
} catch (e) {}
|
||||
|
||||
setIsRecording(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
pausedAtRef.current = null;
|
||||
pausedMsRef.current = 0;
|
||||
navigate(Routes.RecordedPlayback, { project, videoUri: videoUrl || null });
|
||||
}, [dur, player, pos, project, stopRecorderAndGetUrl]);
|
||||
|
||||
@@ -438,11 +462,19 @@ const RecordPlayback = ({ route }) => {
|
||||
progressTimerRef.current = setInterval(async () => {
|
||||
const rawDur = toSeconds(player?.duration);
|
||||
const rawPos = toSeconds(player?.currentTime);
|
||||
const pausedSince =
|
||||
pausedAtRef.current != null
|
||||
? Math.max(0, performance.now() - pausedAtRef.current)
|
||||
: 0;
|
||||
const pausedTotal = pausedMsRef.current + pausedSince;
|
||||
|
||||
// fallback monotone si le player ne donne rien
|
||||
const fallback =
|
||||
perfStartRef.current != null
|
||||
? Math.max(0, (performance.now() - perfStartRef.current) / 1000)
|
||||
? Math.max(
|
||||
0,
|
||||
(performance.now() - perfStartRef.current - pausedTotal) / 1000
|
||||
)
|
||||
: 0;
|
||||
|
||||
let nextDur = rawDur > 0 ? rawDur : dur || 0;
|
||||
@@ -464,7 +496,7 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
const timeSinceStart =
|
||||
perfStartRef.current != null
|
||||
? performance.now() - perfStartRef.current
|
||||
? Math.max(0, performance.now() - perfStartRef.current - pausedTotal)
|
||||
: null;
|
||||
|
||||
const shouldTrustFallback =
|
||||
@@ -484,6 +516,10 @@ const RecordPlayback = ({ route }) => {
|
||||
setDur(nextDur);
|
||||
setPos(nextPos);
|
||||
|
||||
if (isPausedRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nextDur > 0 && nextPos >= nextDur - 0.3) {
|
||||
void stopAndNavigate();
|
||||
}
|
||||
@@ -516,6 +552,10 @@ const RecordPlayback = ({ route }) => {
|
||||
|
||||
const startPlayback = useCallback(async () => {
|
||||
try {
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
pausedAtRef.current = null;
|
||||
pausedMsRef.current = 0;
|
||||
await player?.pause?.(); // s'assure qu'on repart propre
|
||||
await player?.seekTo?.(0); // tente un seek d'amorçage
|
||||
const recorderStarted = startRecorder();
|
||||
@@ -530,6 +570,9 @@ const RecordPlayback = ({ route }) => {
|
||||
} catch (e) {
|
||||
setIsRecording(false);
|
||||
setIsPreparing(false);
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
pausedAtRef.current = null;
|
||||
const msg = String(e?.message || e || "");
|
||||
if (/not supported on the simulator/i.test(msg)) {
|
||||
alert(
|
||||
@@ -557,6 +600,8 @@ const RecordPlayback = ({ route }) => {
|
||||
const startCountdownThenRecord = useCallback(async () => {
|
||||
if (!songUrl || !canRecord) return;
|
||||
await resetSession();
|
||||
setIsPaused(false);
|
||||
isPausedRef.current = false;
|
||||
setIsPreparing(true);
|
||||
setCountdown(5);
|
||||
if (countdownTimerRef.current) clearInterval(countdownTimerRef.current);
|
||||
@@ -575,6 +620,43 @@ const RecordPlayback = ({ route }) => {
|
||||
}, 1000);
|
||||
}, [songUrl, canRecord, resetSession, startPlayback]);
|
||||
|
||||
const handleTogglePause = useCallback(async () => {
|
||||
try {
|
||||
if (!isRecording) return;
|
||||
if (!isPausedRef.current) {
|
||||
isPausedRef.current = true;
|
||||
setIsPaused(true);
|
||||
pausedAtRef.current = performance.now();
|
||||
try {
|
||||
await player?.pause?.();
|
||||
} catch (e) {}
|
||||
try {
|
||||
const recorder = mediaRecorderRef.current;
|
||||
if (recorder && recorder.state === "recording") {
|
||||
recorder.pause?.();
|
||||
}
|
||||
} catch (e) {}
|
||||
return;
|
||||
}
|
||||
const pausedAt = pausedAtRef.current;
|
||||
if (pausedAt != null) {
|
||||
pausedMsRef.current += performance.now() - pausedAt;
|
||||
pausedAtRef.current = null;
|
||||
}
|
||||
isPausedRef.current = false;
|
||||
setIsPaused(false);
|
||||
try {
|
||||
const recorder = mediaRecorderRef.current;
|
||||
if (recorder && recorder.state === "paused") {
|
||||
recorder.resume?.();
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
await player?.play?.();
|
||||
} catch (e) {}
|
||||
} catch (e) {}
|
||||
}, [isRecording, player]);
|
||||
|
||||
const handleRestartRecording = useCallback(async () => {
|
||||
try {
|
||||
await startCountdownThenRecord();
|
||||
@@ -586,9 +668,7 @@ const RecordPlayback = ({ route }) => {
|
||||
return (
|
||||
<Page
|
||||
style={{ flex: 1 }}
|
||||
width={"100%"}
|
||||
maxWidth={800}
|
||||
containerStyle={{ margin: 0, padding: 0, backgroundColor: Palette.tran }}
|
||||
containerStyle={{ backgroundColor: Palette.tran }}
|
||||
headerType="NONE"
|
||||
backgroundImg={background.playbackBG2}
|
||||
>
|
||||
@@ -766,6 +846,34 @@ const RecordPlayback = ({ route }) => {
|
||||
<RestartSpinnerIcon />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
{isRecording && (
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={handleTogglePause}
|
||||
style={{
|
||||
marginTop: 14,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 18,
|
||||
borderRadius: 999,
|
||||
backgroundColor: Palette.white,
|
||||
shadowColor: "#000000",
|
||||
shadowOpacity: 0.12,
|
||||
shadowRadius: 10,
|
||||
shadowOffset: { width: 0, height: 4 },
|
||||
elevation: 4,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.black,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{isPaused ? "Reprendre" : "Mettre en pause"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import { gutters } from "../../styles";
|
||||
const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project } = route.params || {};
|
||||
const songUrl = project?.songUrl || null;
|
||||
console.log("video uri is : ", videoUri);
|
||||
const audioPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: project?.id ? `recorded-${project.id}` : songUrl ? `recorded-${songUrl}` : undefined,
|
||||
title: typeof project?.title === "string" ? project.title : "Sans titre",
|
||||
|
||||
@@ -17,6 +17,7 @@ import { goBack, navigate } from "../../navigation/NavigationService";
|
||||
import { gutters, Palette } from "../../styles";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import { releaseBlobUrl } from "../../utils/blobUrlCache";
|
||||
|
||||
const fmtSeconds = (s) => {
|
||||
const total = Math.max(0, Math.floor(Number(s || 0)));
|
||||
@@ -36,7 +37,6 @@ const WEB_PREVIEW_WIDTH = 360;
|
||||
const RecordedPlayback = ({ route }) => {
|
||||
const { videoUri, project } = route.params || {};
|
||||
const songUrl = project?.songUrl || null;
|
||||
console.log("video uri is : ", videoUri);
|
||||
// AUDIO PLAYER (expo-audio → seconds)
|
||||
const audioPlayer = useSharedAudioPlayer(
|
||||
songUrl ? { uri: songUrl } : undefined,
|
||||
@@ -58,6 +58,7 @@ const RecordedPlayback = ({ route }) => {
|
||||
// Optionnel: si tu veux quand même un rendu vidéo sur web si tu as une source
|
||||
const videoElRef = useRef(null);
|
||||
const playbackEndedRef = useRef(false);
|
||||
const shouldPreserveBlobRef = useRef(false);
|
||||
|
||||
const [progress, setProgress] = useState({
|
||||
posS: 0, // secondes
|
||||
@@ -76,6 +77,14 @@ const RecordedPlayback = ({ route }) => {
|
||||
} catch {}
|
||||
}, [audioPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!shouldPreserveBlobRef.current) {
|
||||
releaseBlobUrl(videoUri || null);
|
||||
}
|
||||
};
|
||||
}, [videoUri]);
|
||||
|
||||
// Démarrage / arrêt
|
||||
useEffect(() => {
|
||||
playbackEndedRef.current = false;
|
||||
@@ -345,6 +354,7 @@ const RecordedPlayback = ({ route }) => {
|
||||
<GradientButton
|
||||
title="Je valide"
|
||||
onPress={() => {
|
||||
shouldPreserveBlobRef.current = true;
|
||||
navigate(Routes.DownloadSongs, {
|
||||
action: "playback",
|
||||
uri: videoUri || null, // peut être null sur web
|
||||
|
||||
@@ -8,14 +8,21 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Image, Platform, Pressable, Text, View } from "react-native";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Image,
|
||||
Platform,
|
||||
Pressable,
|
||||
Text,
|
||||
View,
|
||||
} from "react-native";
|
||||
import Carousel from "react-native-reanimated-carousel";
|
||||
import { responsiveHeight } from "react-native-responsive-dimensions";
|
||||
import { icons, img } from "../../assets";
|
||||
import { openComments } from "../../components/bottomsheets/CommentsBottomSheet";
|
||||
import KaraokeLyrics from "../../components/KaraokeLyrics";
|
||||
import ProfilePicture from "../../components/ProfilePicture";
|
||||
import { projectsRef, usersRef } from "../../config/firebase";
|
||||
import firebase, { projectsRef, usersRef } from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import { Routes } from "../../navigation";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
@@ -36,14 +43,37 @@ import {
|
||||
import { SheetManager } from "react-native-actions-sheet";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
|
||||
const PLAYBACK_CODEC_TAG = "h264-v1";
|
||||
|
||||
const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
const { currentUID, followUser, unfollowUser } = useUser() || {};
|
||||
const videoUrl = item?.playbackUrl || null;
|
||||
const baseVideoUrl = item?.playbackUrl || null;
|
||||
const [overrideVideoUrl, setOverrideVideoUrl] = useState(null);
|
||||
const videoUrl = overrideVideoUrl || baseVideoUrl;
|
||||
const initialLikedBy = getProjectLikes(item, LIKE_TARGET.PLAYBACK);
|
||||
const [isLiked, setIsLiked] = useState(
|
||||
currentUID ? initialLikedBy.includes(currentUID) : false
|
||||
);
|
||||
const [likesCount, setLikesCount] = useState(initialLikedBy.length);
|
||||
const [repairState, setRepairState] = useState({
|
||||
running: false,
|
||||
error: null,
|
||||
});
|
||||
const repairAttemptedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
setOverrideVideoUrl(null);
|
||||
repairAttemptedRef.current = false;
|
||||
setRepairState({ running: false, error: null });
|
||||
}, [item?.id, baseVideoUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
logPlaybackEvent("state-change", {
|
||||
baseVideoUrl: baseVideoUrl || null,
|
||||
overrideVideoUrl,
|
||||
repairState,
|
||||
});
|
||||
}, [baseVideoUrl, logPlaybackEvent, overrideVideoUrl, repairState]);
|
||||
|
||||
const commentsCount = useMemo(
|
||||
() => Number(item?.commentsCount || 0),
|
||||
@@ -122,31 +152,137 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
return "";
|
||||
}, [item?.userName, owner?.artistName, owner?.displayName, owner?.userName]);
|
||||
|
||||
const needsCodecRepair =
|
||||
Platform.OS === "ios" &&
|
||||
!!baseVideoUrl &&
|
||||
item?.playbackCompatibility?.codec !== PLAYBACK_CODEC_TAG;
|
||||
|
||||
const logPlaybackEvent = useCallback(
|
||||
(event, extra = {}) => {
|
||||
if (Platform.OS !== "ios") return;
|
||||
try {
|
||||
console.log("[Playbacks][iOS]", event, {
|
||||
projectId: item?.id,
|
||||
isActive,
|
||||
hasVideoUrl: !!videoUrl,
|
||||
hasOverride: !!overrideVideoUrl,
|
||||
needsCodecRepair,
|
||||
compatibility: item?.playbackCompatibility || null,
|
||||
repairRunning: repairState.running,
|
||||
repairError: repairState.error?.message || null,
|
||||
...extra,
|
||||
});
|
||||
} catch (loggingError) {
|
||||
// ignore logging errors
|
||||
}
|
||||
},
|
||||
[
|
||||
item?.id,
|
||||
isActive,
|
||||
needsCodecRepair,
|
||||
overrideVideoUrl,
|
||||
repairState.error?.message,
|
||||
repairState.running,
|
||||
videoUrl,
|
||||
]
|
||||
);
|
||||
|
||||
const handleRepair = useCallback(
|
||||
async (force = false) => {
|
||||
if ((!needsCodecRepair && !force) || !item?.id) {
|
||||
logPlaybackEvent("repair-skip", { force });
|
||||
return;
|
||||
}
|
||||
if (repairAttemptedRef.current) {
|
||||
logPlaybackEvent("repair-skip-already-attempted", { force });
|
||||
return;
|
||||
}
|
||||
repairAttemptedRef.current = true;
|
||||
setRepairState({ running: true, error: null });
|
||||
logPlaybackEvent("repair-start", { force });
|
||||
try {
|
||||
const callable =
|
||||
firebase.functions().httpsCallable("upload-reencodePlayback");
|
||||
const { data } = await callable({ projectId: item.id });
|
||||
const nextUrl = data?.url || null;
|
||||
if (nextUrl) {
|
||||
setOverrideVideoUrl(nextUrl);
|
||||
}
|
||||
setRepairState({ running: false, error: null });
|
||||
logPlaybackEvent("repair-success", { nextUrlPresent: !!nextUrl });
|
||||
} catch (error) {
|
||||
console.log("[Playbacks] reencode failed", {
|
||||
projectId: item?.id,
|
||||
message: error?.message || String(error || ""),
|
||||
code: error?.code,
|
||||
});
|
||||
setRepairState({
|
||||
running: false,
|
||||
error:
|
||||
error instanceof Error
|
||||
? error
|
||||
: new Error(String(error || "Playback repair failed")),
|
||||
});
|
||||
logPlaybackEvent("repair-failed", {
|
||||
error: error?.message || String(error || ""),
|
||||
code: error?.code,
|
||||
});
|
||||
}
|
||||
},
|
||||
[item?.id, logPlaybackEvent, needsCodecRepair]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || !needsCodecRepair) return;
|
||||
handleRepair();
|
||||
}, [isActive, needsCodecRepair, handleRepair]);
|
||||
|
||||
const videoPlayer = useVideoPlayer(videoUrl || null, (p) => {
|
||||
p.loop = false;
|
||||
p.muted = false;
|
||||
p.timeUpdateEventInterval = 0.2;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (Platform.OS !== "ios") return;
|
||||
if (!videoPlayer?.addListener) return;
|
||||
const sub = videoPlayer.addListener("error", (event) => {
|
||||
console.log("[Playbacks] video error", {
|
||||
projectId: item?.id,
|
||||
error: event?.error || event,
|
||||
});
|
||||
logPlaybackEvent("video-error", { error: event?.error || event });
|
||||
if (!repairAttemptedRef.current) {
|
||||
handleRepair(true);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
try {
|
||||
sub?.remove?.();
|
||||
} catch (e) {}
|
||||
};
|
||||
}, [videoPlayer, handleRepair, item?.id]);
|
||||
|
||||
const shouldAutoPlay = isActive && !repairState.running;
|
||||
|
||||
useEffect(() => {
|
||||
const toggle = async () => {
|
||||
try {
|
||||
if (isActive) {
|
||||
if (shouldAutoPlay) {
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.currentTime = 0;
|
||||
} catch (e) {}
|
||||
|
||||
// Lancer quasi simultanément (éviter await pour limiter le décalage)
|
||||
try {
|
||||
if (videoPlayer) videoPlayer.play();
|
||||
} catch (e) {}
|
||||
} else {
|
||||
if (videoPlayer?.playing) videoPlayer.pause();
|
||||
} else if (videoPlayer?.playing) {
|
||||
videoPlayer.pause();
|
||||
}
|
||||
} catch (e) {}
|
||||
};
|
||||
toggle();
|
||||
}, [isActive, videoPlayer]);
|
||||
}, [shouldAutoPlay, videoPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -232,6 +368,59 @@ const PlaybackItem = ({ item, isActive, userCache, getUserByUid }) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{Platform.OS === "ios" && repairState.running && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: "#00000066",
|
||||
}}
|
||||
>
|
||||
<ActivityIndicator color={Palette.white} />
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 10,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 15,
|
||||
}}
|
||||
>
|
||||
Optimisation vidéo iOS...
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{Platform.OS === "ios" && repairState.error && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingHorizontal: 20,
|
||||
backgroundColor: "#00000099",
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Impossible de lire cette vidéo. Veuillez réessayer plus tard.
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Right side actions */}
|
||||
<View
|
||||
style={{
|
||||
|
||||
@@ -28,6 +28,32 @@ import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import { size } from "../../styles/Style";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
import { getBlobForUrl, releaseBlobUrl } from "../../utils/blobUrlCache";
|
||||
|
||||
const triggerWebDownload = (url, title) => {
|
||||
if (Platform.OS !== "web") return;
|
||||
if (!url) return;
|
||||
if (typeof document === "undefined") return;
|
||||
const baseName = (title || "Playback").toString().trim() || "Playback";
|
||||
const sanitized = baseName.replace(/[\\/:*?"<>|]/g, "-");
|
||||
const filename = `${sanitized}.mp4`;
|
||||
try {
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
anchor.rel = "noopener noreferrer";
|
||||
document.body.appendChild(anchor);
|
||||
anchor.click();
|
||||
document.body.removeChild(anchor);
|
||||
} catch (error) {
|
||||
console.log("[DownloadSongs] web download fallback", {
|
||||
message: error?.message,
|
||||
});
|
||||
try {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
const guessExtension = (inputUri = "") => {
|
||||
const cleaned = inputUri.split("?")[0] || "";
|
||||
@@ -55,11 +81,17 @@ const uploadSourceRecording = async ({ uri, uid, projectId }) => {
|
||||
sourcePath,
|
||||
});
|
||||
|
||||
const cachedBlob = getBlobForUrl(uri);
|
||||
console.log("[DownloadSongs] uploadSourceRecording blob cache", {
|
||||
hasBlob: Boolean(cachedBlob),
|
||||
});
|
||||
|
||||
const { resultURI: videoUrl } = await uploadFileToFirebase({
|
||||
uri,
|
||||
path: sourcePath,
|
||||
shouldCompress: false,
|
||||
fileType: "VIDEO",
|
||||
blob: cachedBlob || undefined,
|
||||
});
|
||||
|
||||
return { sourcePath, videoUrl };
|
||||
@@ -97,6 +129,12 @@ const DownloadSongs = ({ route }) => {
|
||||
setIsAfterPlaybackVideoVisible(true);
|
||||
}, [action, afterPlaybackUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
releaseBlobUrl(uri || null);
|
||||
};
|
||||
}, [uri]);
|
||||
|
||||
const handleDownloadUri = async () => {
|
||||
if (action === "playback" && project?.id) {
|
||||
// Publication du playback
|
||||
@@ -128,6 +166,7 @@ const DownloadSongs = ({ route }) => {
|
||||
.httpsCallable("upload-mergeVideoAndAudio");
|
||||
|
||||
const payload = {
|
||||
projectId: project?.id,
|
||||
videoUrl,
|
||||
audioUrl,
|
||||
storagePath: `users/${currentUID}/projects/${project.id}/playback.mp4`,
|
||||
@@ -156,6 +195,7 @@ const DownloadSongs = ({ route }) => {
|
||||
type: "success",
|
||||
text: "Vidéo uploadée",
|
||||
});
|
||||
triggerWebDownload(resultURI, project?.title);
|
||||
if (tempSourcePath) {
|
||||
try {
|
||||
await firebase.storage().ref(tempSourcePath).delete();
|
||||
@@ -191,6 +231,7 @@ const DownloadSongs = ({ route }) => {
|
||||
),
|
||||
});
|
||||
} finally {
|
||||
releaseBlobUrl(uri || null);
|
||||
setIsLoading(false);
|
||||
}
|
||||
// Handle playback download
|
||||
|
||||
@@ -28,6 +28,7 @@ const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
|
||||
const OPTIONAL_VOICE_CATEGORIES = new Set(["SENSIBILITÉ", "TECHNIQUE"]);
|
||||
const FIRST_VOICE_STEP_INDEX = 1;
|
||||
const TOTAL_STEPS = 1 + VOICE_SECTION_TITLES.length + 2; // genre + voices + instruments + rhythm
|
||||
const LAST_STEP_INDEX = TOTAL_STEPS - 1;
|
||||
@@ -78,9 +79,18 @@ const ComposeSong = () => {
|
||||
if (selectedIndex === 0) {
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
}
|
||||
if (selectedIndex === FIRST_VOICE_STEP_INDEX) {
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
|
||||
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX;
|
||||
const isVoiceStep =
|
||||
voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length;
|
||||
if (isVoiceStep) {
|
||||
const category = VOICE_SECTION_TITLES[voiceStepIndex];
|
||||
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
|
||||
return true;
|
||||
}
|
||||
return !!(voice && typeof voice === "object" && voice[category]);
|
||||
}
|
||||
|
||||
if (selectedIndex === INSTRUMENT_STEP_INDEX) {
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const CONFIRM_MODAL_MAX_WIDTH = 540;
|
||||
const FUNCTIONS_REGION = "europe-west1";
|
||||
const VOICE_SECTION_TITLES = ["BASE", "SENSIBILITÉ", "TECHNIQUE"];
|
||||
const FIRST_VOICE_STEP_INDEX = 1;
|
||||
const OPTIONAL_VOICE_CATEGORIES = new Set(["SENSIBILITÉ", "TECHNIQUE"]);
|
||||
|
||||
const ComposeSong = () => {
|
||||
const scrollRef = useRef(null);
|
||||
@@ -115,9 +117,18 @@ const ComposeSong = () => {
|
||||
if (selectedIndex === 0) {
|
||||
return Array.isArray(genres) && genres.length > 0;
|
||||
}
|
||||
if (selectedIndex === 1) {
|
||||
return !!(voice && typeof voice === "object" && voice.BASE);
|
||||
|
||||
const voiceStepIndex = selectedIndex - FIRST_VOICE_STEP_INDEX;
|
||||
const isVoiceStep =
|
||||
voiceStepIndex >= 0 && voiceStepIndex < VOICE_SECTION_TITLES.length;
|
||||
if (isVoiceStep) {
|
||||
const category = VOICE_SECTION_TITLES[voiceStepIndex];
|
||||
if (OPTIONAL_VOICE_CATEGORIES.has(category)) {
|
||||
return true;
|
||||
}
|
||||
return !!(voice && typeof voice === "object" && voice[category]);
|
||||
}
|
||||
|
||||
if (selectedIndex === instrumentStepIndex) {
|
||||
return Array.isArray(instruments) && instruments.length > 0;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import ListSelection from "../../components/ListSelection/ListSelection";
|
||||
const SECTION_INSTRUCTIONS = {
|
||||
BASE: "Choisis ta base",
|
||||
SENSIBILITE: "Choisis ta sensibilité",
|
||||
TECHNIQUE: "Choisis ta technique",
|
||||
TECHNIQUE: "Choisis ta technique (Facultatif)",
|
||||
};
|
||||
|
||||
const normalizeCategory = (value) => {
|
||||
|
||||
@@ -40,7 +40,7 @@ const Lyrics = ({ navigation }) => {
|
||||
const hasExistingMusicDraft = useMemo(() => {
|
||||
if (!Array.isArray(selectedProject?.musicUrls)) return false;
|
||||
return selectedProject.musicUrls.some(
|
||||
(url) => typeof url === "string" && url.trim()
|
||||
(url) => typeof url === "string" && url.trim(),
|
||||
);
|
||||
}, [selectedProject?.musicUrls]);
|
||||
const [isFocus, setIsFocus] = useState(null);
|
||||
@@ -53,6 +53,28 @@ const Lyrics = ({ navigation }) => {
|
||||
const [isSensitiveContentAcknowledged, setIsSensitiveContentAcknowledged] =
|
||||
useState(false);
|
||||
const sensitiveContentResolverRef = useRef(null);
|
||||
const updateLayoutAtIndex = useCallback((index, layout) => {
|
||||
if (typeof index !== "number" || !layout) return;
|
||||
setItemsContainerLayout((prev) => {
|
||||
const next = [...prev];
|
||||
next[index] = layout;
|
||||
return next;
|
||||
});
|
||||
}, []);
|
||||
const handleSectionFocus = useCallback(
|
||||
(idx) => {
|
||||
setIsFocus(idx);
|
||||
if (isWeb) return;
|
||||
const targetLayout = itemsContainerLayout[idx + 1];
|
||||
if (typeof targetLayout?.y === "number") {
|
||||
scrollRef?.current?.scrollTo({
|
||||
y: targetLayout.y,
|
||||
animated: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
[itemsContainerLayout, isWeb],
|
||||
);
|
||||
|
||||
const closeSensitiveContentModal = useCallback((result) => {
|
||||
setSensitiveContentModal((prev) => ({ ...prev, visible: false }));
|
||||
@@ -115,7 +137,7 @@ const Lyrics = ({ navigation }) => {
|
||||
});
|
||||
|
||||
const remainingTextual = remaining.filter((item) =>
|
||||
segmentRequiresLyrics(item.type)
|
||||
segmentRequiresLyrics(item.type),
|
||||
);
|
||||
sections.push(...remainingTextual);
|
||||
|
||||
@@ -194,7 +216,7 @@ const Lyrics = ({ navigation }) => {
|
||||
if (invalid) {
|
||||
alertMessage(
|
||||
"Champs incomplets",
|
||||
"Chaque section doit contenir du texte."
|
||||
"Chaque section doit contenir du texte.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -217,7 +239,7 @@ const Lyrics = ({ navigation }) => {
|
||||
normalizedOldLyrics.every(
|
||||
(s, i) =>
|
||||
s.type === normalizedNewLyrics[i]?.type &&
|
||||
s.lyrics === normalizedNewLyrics[i]?.lyrics
|
||||
s.lyrics === normalizedNewLyrics[i]?.lyrics,
|
||||
);
|
||||
|
||||
// 1) Appel de la Cloud Function de modération avant tout enregistrement
|
||||
@@ -241,7 +263,7 @@ const Lyrics = ({ navigation }) => {
|
||||
: null;
|
||||
alertMessage(
|
||||
"Contenu interdit",
|
||||
[data?.message, quotes].filter(Boolean).join("\n\n")
|
||||
[data?.message, quotes].filter(Boolean).join("\n\n"),
|
||||
);
|
||||
return; // stop here
|
||||
}
|
||||
@@ -249,7 +271,7 @@ const Lyrics = ({ navigation }) => {
|
||||
if (data?.errorCode === "ANALYSE_FAILED") {
|
||||
alertMessage(
|
||||
"Analyse indisponible",
|
||||
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard."
|
||||
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -267,18 +289,18 @@ const Lyrics = ({ navigation }) => {
|
||||
|
||||
const proceed = await confirmSensitiveContent(
|
||||
"Contenu potentiellement sensible",
|
||||
msg
|
||||
msg,
|
||||
);
|
||||
if (!proceed) return;
|
||||
}
|
||||
} catch (moderationError) {
|
||||
console.log(
|
||||
"Moderation call failed",
|
||||
moderationError?.message || moderationError
|
||||
moderationError?.message || moderationError,
|
||||
);
|
||||
alertMessage(
|
||||
"Analyse indisponible",
|
||||
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard."
|
||||
"Impossible de vérifier la toxicité pour le moment. Réessayez plus tard.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -339,7 +361,7 @@ const Lyrics = ({ navigation }) => {
|
||||
text: "Continuer vers le Studio",
|
||||
onPress: handleNavigateToStudio,
|
||||
},
|
||||
]
|
||||
],
|
||||
);
|
||||
return;
|
||||
} catch (e) {
|
||||
@@ -404,11 +426,10 @@ const Lyrics = ({ navigation }) => {
|
||||
<Text style={styles.instructions}>
|
||||
{strings.writing.lyrics.instructions}
|
||||
</Text>
|
||||
<View style={styles.personalizationBanner}>
|
||||
<Text style={styles.personalizationText}>
|
||||
{strings.writing.lyrics.personalizationBanner}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.personalizationText}>
|
||||
N’hesites pas a personnaliser les paroles proposées en ajoutant
|
||||
ta touche personnelle mettre mieux en évidence
|
||||
</Text>
|
||||
</View>
|
||||
<CustomInput
|
||||
label="Titre"
|
||||
@@ -419,11 +440,7 @@ const Lyrics = ({ navigation }) => {
|
||||
multiline={false}
|
||||
maxLength={60}
|
||||
onLayout={(e) => {
|
||||
e.persist();
|
||||
setItemsContainerLayout((prev) => [
|
||||
...prev,
|
||||
e?.nativeEvent?.layout,
|
||||
]);
|
||||
updateLayoutAtIndex(0, e?.nativeEvent?.layout);
|
||||
}}
|
||||
/>
|
||||
{sections.map((s, idx) => {
|
||||
@@ -460,18 +477,9 @@ const Lyrics = ({ navigation }) => {
|
||||
height={inputHeight}
|
||||
value={s?.lyrics || ""}
|
||||
setValue={(val) => setSectionAt(idx, val)}
|
||||
onFocus={() => {
|
||||
setIsFocus(idx);
|
||||
scrollRef?.current?.scrollTo({
|
||||
y: itemsContainerLayout[idx + 1]?.y,
|
||||
});
|
||||
}}
|
||||
onFocus={() => handleSectionFocus(idx)}
|
||||
onLayout={(e) => {
|
||||
e.persist();
|
||||
setItemsContainerLayout((prev) => [
|
||||
...prev,
|
||||
e?.nativeEvent?.layout,
|
||||
]);
|
||||
updateLayoutAtIndex(idx + 1, e?.nativeEvent?.layout);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -558,18 +566,12 @@ const styles = StyleSheet.create({
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
},
|
||||
personalizationBanner: {
|
||||
backgroundColor: Palette.ultraLightWhite,
|
||||
borderRadius: 12,
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 12,
|
||||
marginTop: 4,
|
||||
},
|
||||
personalizationText: {
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
fontSize: 14,
|
||||
lineHeight: 20,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
fontSize: 18,
|
||||
lineHeight: 24,
|
||||
marginTop: 8,
|
||||
},
|
||||
instrumentalBlock: {
|
||||
padding: 16,
|
||||
|
||||
@@ -115,49 +115,6 @@ export const ChooseCoverType = () => {
|
||||
[currentUID, projectId],
|
||||
);
|
||||
|
||||
// const pickUserImage = useCallback(async () => {
|
||||
// try {
|
||||
// await setIsLoading(true);
|
||||
// const result = await ImagePicker.launchImageLibraryAsync({
|
||||
// mediaTypes: ["images"],
|
||||
// allowsEditing: true,
|
||||
// aspect: [1, 1],
|
||||
// quality: 1,
|
||||
// });
|
||||
|
||||
// const uri = result?.assets?.[0]?.uri || null;
|
||||
// if (!uri) return;
|
||||
|
||||
// if (!projectId) return;
|
||||
// const path = `musics/${projectId}/userSelectedCover.png`;
|
||||
|
||||
// const { resultURI } = await uploadFileToFirebase({
|
||||
// uri,
|
||||
// path,
|
||||
// shouldCompress: true,
|
||||
// fileType: "IMAGE",
|
||||
// });
|
||||
|
||||
// if (!resultURI) {
|
||||
// throw new Error("Erreur lors de l'upload");
|
||||
// }
|
||||
// await updateProjectData({
|
||||
// cover: {
|
||||
// result: resultURI,
|
||||
// },
|
||||
// });
|
||||
// navigate(Routes.ValidateCover);
|
||||
// } catch (e) {
|
||||
// console.log("onPickUserImage error", e?.message);
|
||||
// } finally {
|
||||
// await setIsLoading(false);
|
||||
// }
|
||||
// }, [projectId, setIsLoading, updateProjectData]);
|
||||
|
||||
// const handlePickUserImage = useCallback(() => {
|
||||
// ensureArtistPreference(pickUserImage);
|
||||
// }, [ensureArtistPreference, pickUserImage]);
|
||||
|
||||
const handleGenerateCover = useCallback(() => {
|
||||
if (hasFinalCover || hasGeneratedOptions) {
|
||||
navigate(Routes.ValidateCover);
|
||||
@@ -300,10 +257,10 @@ export const ChooseCoverType = () => {
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
<FullscreenIntroVideo
|
||||
visible={showIntro}
|
||||
onClose={() => setShowIntro(false)}
|
||||
/>
|
||||
{/*<FullscreenIntroVideo*/}
|
||||
{/* visible={showIntro}*/}
|
||||
{/* onClose={() => setShowIntro(false)}*/}
|
||||
{/*/>*/}
|
||||
<Modal
|
||||
visible={choiceVisible}
|
||||
transparent
|
||||
|
||||
Reference in New Issue
Block a user