412 lines
13 KiB
JavaScript
412 lines
13 KiB
JavaScript
/* 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="Qu’en 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;
|