Files
musicland/src/screens/Studio/SongReady.js
T
2025-10-23 17:04:33 +02:00

412 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* global setInterval, clearInterval */
import { useFocusEffect } from "@react-navigation/native";
import { BlurView } from "expo-blur";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { Image, Platform, Pressable, Text, View } from "react-native";
import { responsiveHeight } from "react-native-responsive-dimensions";
import { background, icons } from "../../assets";
import alert from "../../components/Alert";
import GradientButton from "../../components/GradientButton";
import ValidateModal from "../../components/modal/ValidateModal";
import MusicLandHeader from "../../components/MusicLandHeader";
import Slider from "../../components/Slider";
import { isWeb } from "../../hooks/useLayoutType";
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles";
import { gutters, size } from "../../styles/Style";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
const SongReady = () => {
const {
selectedProjectId: projectId,
selectedProject,
updateProjectData,
updateUserData,
} = useUser();
const [showValidateModal, setShowValidateModal] = useState(false);
const [musicUrls, setMusicUrls] = useState([]);
const [selectedIndex, setSelectedIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState({ 0: false, 1: false });
const [progressInfo, setProgressInfo] = useState({
0: { pos: 0, dur: 0 },
1: { pos: 0, dur: 0 },
});
const player0 = useSharedAudioPlayer(
musicUrls[0] ? { uri: musicUrls[0] } : undefined,
{
id: musicUrls[0] ? `songready-${musicUrls[0]}` : undefined,
title:
(Array.isArray(selectedProject?.musicTitles)
? selectedProject?.musicTitles?.[0]
: null) ||
selectedProject?.title ||
"Option 1",
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index: 0, projectId },
}
);
const player1 = useSharedAudioPlayer(
musicUrls[1] ? { uri: musicUrls[1] } : undefined,
{
id: musicUrls[1] ? `songready-${musicUrls[1]}` : undefined,
title:
(Array.isArray(selectedProject?.musicTitles)
? selectedProject?.musicTitles?.[1]
: null) ||
selectedProject?.title ||
"Option 2",
artwork: selectedProject?.coverUrl || null,
coverUrl: selectedProject?.coverUrl || null,
metadata: { index: 1, projectId },
}
);
const wasPlayingBeforeSeek = useRef({ 0: false, 1: false });
// Sync URLs from provider's selectedProject
useEffect(() => {
const urls = Array.isArray(selectedProject?.musicUrls)
? selectedProject.musicUrls.slice(0, 2)
: [];
setMusicUrls(urls);
}, [selectedProject?.musicUrls]);
// Sync progression depuis les players
useEffect(() => {
// Ensure we consistently work with milliseconds whatever the platform
const toMs = (t) => {
const n = Number(t || 0);
if (!isFinite(n) || n <= 0) return 0;
return n >= 1000 ? n : n * 1000;
};
const id = setInterval(() => {
const d0 = toMs(player0?.duration);
const p0 = toMs(player0?.currentTime);
const d1 = toMs(player1?.duration);
const p1 = toMs(player1?.currentTime);
setProgressInfo({ 0: { pos: p0, dur: d0 }, 1: { pos: p1, dur: d1 } });
setIsPlaying({ 0: !!player0?.playing, 1: !!player1?.playing });
}, 300);
return () => clearInterval(id);
}, [player0, player1]);
const togglePlay = async (idx) => {
setSelectedIndex(idx);
const url = musicUrls[idx];
if (!url) return;
try {
// Pause l'autre piste si elle joue
const other = idx === 0 ? 1 : 0;
if (isPlaying[other]) {
if (other === 0) await player0?.pause?.();
else await player1?.pause?.();
setIsPlaying((p) => ({ ...p, [other]: false }));
}
const player = idx === 0 ? player0 : player1;
if (!player) return;
if (player.playing) {
await player.pause?.();
setIsPlaying((p) => ({ ...p, [idx]: false }));
} else {
await player.play?.();
setIsPlaying((p) => ({ ...p, [idx]: true }));
}
} catch (e) {
console.log("Audio error", e?.message);
}
};
const fmt = (ms) => {
const total = Math.max(0, Math.floor((ms || 0) / 1000));
const m = Math.floor(total / 60)
.toString()
.padStart(1, "0");
const s = (total % 60).toString().padStart(2, "0");
return `${m}:${s}`;
};
const onSeek = async (idx, ratio) => {
try {
const info = progressInfo[idx] || {};
const dur = info.dur || 0;
const pos = Math.floor(dur * ratio);
const player = idx === 0 ? player0 : player1;
if (player && dur > 0) {
// seekTo expects seconds
await player.seekTo?.(Math.floor((pos || 0) / 1000));
}
} catch (e) {
console.log("Seek error", e?.message);
}
};
const validateSelection = async () => {
try {
const url = musicUrls[selectedIndex];
if (!projectId || !url) return;
await updateProjectData({
songIndex: selectedIndex,
songUrl: url,
cover: null,
coverUrl: null,
coverStatus: null,
playbackUrl: null,
playbackStatus: null,
playbackGenerating: null,
});
// Incrémenter le compteur de chansons générées
const currentGeneratedSongs = selectedProject?.user?.generatedSongs || 0;
await updateUserData({
data: {
generatedSongs: currentGeneratedSongs + 1,
},
shouldSetTooltip: false,
});
await player0?.pause?.();
await player1?.pause?.();
navigate(Routes.ChooseCoverType);
} catch (e) {
console.log("Validate error", e?.message);
}
};
// Pause audio when screen loses focus (navigate/reset) and on unmount
useFocusEffect(
useCallback(() => {
return () => {
try {
player0?.pause?.();
player1?.pause?.();
} catch {}
};
}, [player0, player1])
);
useEffect(() => {
return () => {
try {
player0?.pause?.();
player1?.pause?.();
} catch {}
};
}, [player0, player1]);
const handleChooseTrack = () => {
if (isWeb) {
alert(
"Attention !",
"Lorsque tu clique sur valider, tu ne pourra plus changer ni le texte ni la mélodie.",
[
{
text: "Retour",
style: "cancel",
},
{
text: "Valider",
style: "confirm",
onPress: () => {
validateSelection();
},
},
],
{ cancelable: false }
);
return;
}
setShowValidateModal(true);
};
return (
<Page headerType="NONE" backgroundImg={background.studioBG2} maxWidth={800}>
<MusicLandHeader
onPressBack={async () => {
try {
await player0?.pause?.();
await player1?.pause?.();
} catch {}
navigate(Routes.Home);
}}
progress={63}
/>
<View style={{ flex: 1, marginTop: 16 }}>
<CreateLyricsHeader
title="Ta chanson est prête !"
subTitle="Quen penses-tu ?"
containerStyle={{
marginBottom: responsiveHeight(2),
}}
/>
<View style={{ gap: 16 }}>
{[0, 1].map((idx) => {
const isSelected = selectedIndex === idx;
return (
<Pressable
key={idx}
onPress={() => setSelectedIndex(idx)}
style={({ pressed }) => [
{
borderRadius: 20,
borderWidth: isSelected ? 2 : 1,
borderColor: isSelected
? Palette.primary
: Palette.ultraLightWhite,
overflow: "hidden",
},
pressed && { opacity: 0.96 },
]}
>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 40}
tint="dark"
style={{
borderRadius: 18,
overflow: "hidden",
padding: 12,
backgroundColor: "#FFFFFF0A",
}}
// experimentalBlurMethod={
// Platform.OS !== "ios" ? "dimezisBlurView" : "none"
// }
>
<View
style={{
flexDirection: "row",
alignItems: "center",
gap: 12,
}}
>
<Pressable
onPress={() => togglePlay(idx)}
style={{
...Style.containerCenter,
...size({ size: 48 }),
}}
>
<Image
source={isPlaying[idx] ? icons.pause : icons.play}
/>
</Pressable>
<View style={{ flex: 1 }}>
<Text
style={{
color: "white",
marginBottom: responsiveHeight(1),
}}
>{`Morceau ${idx + 1}`}</Text>
<Slider
value={fmt(progressInfo[idx]?.pos)}
maxValue={fmt(progressInfo[idx]?.dur)}
progress={
progressInfo[idx]?.dur
? (progressInfo[idx].pos || 0) /
progressInfo[idx].dur
: 0
}
seekEnabled={!!musicUrls[idx]}
onSeekStart={async () => {
setSelectedIndex(idx);
try {
const player = idx === 0 ? player0 : player1;
wasPlayingBeforeSeek.current[idx] =
!!player?.playing;
if (player?.playing) {
await player.pause?.();
setIsPlaying((p) => ({ ...p, [idx]: false }));
}
} catch (e) {
console.log(
"SongReady pause on seek start",
e?.message
);
}
}}
onSeek={(ratio) => onSeek(idx, ratio)}
onSeekEnd={async () => {
try {
const player = idx === 0 ? player0 : player1;
if (player && wasPlayingBeforeSeek.current[idx]) {
await player.play?.();
setIsPlaying((p) => ({ ...p, [idx]: true }));
}
wasPlayingBeforeSeek.current[idx] = false;
} catch (e) {
console.log(
"SongReady resume after seek",
e?.message
);
}
}}
/>
</View>
<Pressable
onPress={() => setSelectedIndex(idx)}
style={{
...Style.containerCenter,
...size({ size: 24 }),
}}
>
<View
style={{
width: 18,
height: 18,
borderRadius: 9,
borderWidth: 2,
borderColor: isSelected ? Palette.primary : "white",
alignItems: "center",
justifyContent: "center",
}}
>
{isSelected && (
<View
style={{
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: Palette.primary,
}}
/>
)}
</View>
</Pressable>
</View>
</BlurView>
</Pressable>
);
})}
</View>
</View>
<View
style={{
paddingBottom: gutters * 2,
width: "80%",
alignSelf: "center",
gap: 12,
}}
>
<GradientButton
title="Choisir ce morceau"
onPress={handleChooseTrack}
disabled={!musicUrls?.length}
/>
</View>
{!isWeb && (
<ValidateModal
visible={showValidateModal}
onClose={() => setShowValidateModal(false)}
onPressValidate={validateSelection}
/>
)}
</Page>
);
};
export default SongReady;