more fixes and changes, crash, design …
This commit is contained in:
+307
-359
@@ -11,9 +11,7 @@ import GradientButton from "../../components/GradientButton";
|
||||
import ValidateModal from "../../components/modal/ValidateModal";
|
||||
import MusicLandHeader from "../../components/MusicLandHeader";
|
||||
import Slider from "../../components/Slider";
|
||||
import CreditAmount from "../../components/CreditAmount";
|
||||
import { isWeb } from "../../hooks/useLayoutType";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Routes } from "../../navigation";
|
||||
import { navigate } from "../../navigation/NavigationService";
|
||||
@@ -21,11 +19,20 @@ import { useUser } from "../../providers/UserDataProvider";
|
||||
import { Palette, Style } from "../../styles";
|
||||
import { gutters, size } from "../../styles/Style";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
|
||||
|
||||
const MUSIC_GENERATION_COIN_COST = 8;
|
||||
const SONG_OPTIONS_PER_GENERATION = 2;
|
||||
const REGENERATE_MODAL_MAX_WIDTH = 540;
|
||||
const formatTime = (ms) => {
|
||||
const totalSeconds = Math.max(0, Math.floor((ms || 0) / 1000));
|
||||
const minutes = Math.floor(totalSeconds / 60)
|
||||
.toString()
|
||||
.padStart(1, "0");
|
||||
const seconds = (totalSeconds % 60).toString().padStart(2, "0");
|
||||
return `${minutes}:${seconds}`;
|
||||
};
|
||||
|
||||
const SongReady = () => {
|
||||
const {
|
||||
@@ -38,219 +45,72 @@ const SongReady = () => {
|
||||
const [showRegenerateModal, setShowRegenerateModal] = 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 playerRefs = useRef({});
|
||||
|
||||
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 registerPlayer = useCallback((index, player) => {
|
||||
if (player) {
|
||||
playerRefs.current[index] = player;
|
||||
} else {
|
||||
delete playerRefs.current[index];
|
||||
}
|
||||
}, []);
|
||||
|
||||
const pauseAllExcept = useCallback(async (keepIndex = null) => {
|
||||
const tasks = Object.entries(playerRefs.current).map(
|
||||
async ([key, player]) => {
|
||||
const idx = Number(key);
|
||||
if (!player || idx === keepIndex) return;
|
||||
try {
|
||||
await player.pause?.();
|
||||
} catch (error) {
|
||||
console.log("SongReady pause error", error?.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
await Promise.all(tasks);
|
||||
}, []);
|
||||
|
||||
const pauseAllPlayers = useCallback(async () => {
|
||||
await pauseAllExcept(null);
|
||||
}, [pauseAllExcept]);
|
||||
|
||||
const handleTogglePlayback = useCallback(
|
||||
async (index, isPlayingNow) => {
|
||||
const player = playerRefs.current[index];
|
||||
if (!player) return;
|
||||
if (isPlayingNow) {
|
||||
try {
|
||||
await player.pause?.();
|
||||
} catch (error) {
|
||||
console.log("SongReady pause toggle", error?.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
await pauseAllExcept(index);
|
||||
try {
|
||||
await player.play?.();
|
||||
} catch (error) {
|
||||
console.log("SongReady play error", error?.message);
|
||||
}
|
||||
},
|
||||
[pauseAllExcept],
|
||||
);
|
||||
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 });
|
||||
const prefetchedRef = useRef({
|
||||
0: { url: null, done: false },
|
||||
1: { url: null, done: false },
|
||||
});
|
||||
|
||||
// Sync URLs from provider's selectedProject
|
||||
useEffect(() => {
|
||||
const urls = Array.isArray(selectedProject?.musicUrls)
|
||||
? selectedProject.musicUrls.slice(0, 2)
|
||||
? selectedProject.musicUrls
|
||||
.filter((url) => typeof url === "string" && url.trim())
|
||||
.map((url) => url.trim())
|
||||
: [];
|
||||
setMusicUrls(urls);
|
||||
setSelectedIndex((prev) => {
|
||||
if (!urls.length) return 0;
|
||||
return Math.min(prev, urls.length - 1);
|
||||
});
|
||||
}, [selectedProject?.musicUrls]);
|
||||
|
||||
const firstUrl = musicUrls[0] ?? null;
|
||||
const secondUrl = musicUrls[1] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
const waitForDuration = async (index) => {
|
||||
const getDurationSeconds = () => {
|
||||
const targetPlayer = index === 0 ? player0 : player1;
|
||||
if (!targetPlayer) return 0;
|
||||
const raw = Number(targetPlayer.duration || 0);
|
||||
return Number.isFinite(raw) ? raw : 0;
|
||||
};
|
||||
|
||||
const start = Date.now();
|
||||
while (!isCancelled) {
|
||||
const durationSeconds = getDurationSeconds();
|
||||
if (durationSeconds > 0) {
|
||||
const durationMs = Math.round(durationSeconds * 1000);
|
||||
setProgressInfo((prev) => {
|
||||
const current = prev?.[index] || { pos: 0, dur: 0 };
|
||||
if (Math.abs(current.dur - durationMs) < 5) return prev;
|
||||
return {
|
||||
...prev,
|
||||
[index]: {
|
||||
...current,
|
||||
dur: durationMs,
|
||||
},
|
||||
};
|
||||
});
|
||||
return true;
|
||||
}
|
||||
if (Date.now() - start > 6000) {
|
||||
break;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const prefetch = async () => {
|
||||
const entries = [
|
||||
[0, firstUrl, player0],
|
||||
[1, secondUrl, player1],
|
||||
];
|
||||
|
||||
for (const [idx, url, player] of entries) {
|
||||
const cacheEntry = prefetchedRef.current[idx];
|
||||
|
||||
if (!url || !player?.load) {
|
||||
if (cacheEntry) {
|
||||
cacheEntry.url = url ?? null;
|
||||
cacheEntry.done = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cacheEntry && cacheEntry.url === url && cacheEntry.done) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cacheEntry) {
|
||||
cacheEntry.url = url;
|
||||
cacheEntry.done = false;
|
||||
}
|
||||
|
||||
try {
|
||||
await player.load({ startPositionMs: 0 });
|
||||
const resolved = await waitForDuration(idx);
|
||||
if (cacheEntry && !isCancelled) {
|
||||
cacheEntry.done = resolved;
|
||||
}
|
||||
} catch (err) {
|
||||
if (cacheEntry) {
|
||||
cacheEntry.done = false;
|
||||
}
|
||||
if (!isCancelled) {
|
||||
console.log("SongReady preload error", err?.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (isCancelled) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
prefetch();
|
||||
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [firstUrl, secondUrl, player0, player1]);
|
||||
|
||||
// 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 {
|
||||
@@ -277,8 +137,7 @@ const SongReady = () => {
|
||||
shouldSetTooltip: false,
|
||||
});
|
||||
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
await pauseAllPlayers();
|
||||
navigate(Routes.ChooseCoverType);
|
||||
} catch (e) {
|
||||
console.log("Validate error", e?.message);
|
||||
@@ -289,22 +148,16 @@ const SongReady = () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
return () => {
|
||||
try {
|
||||
player0?.pause?.();
|
||||
player1?.pause?.();
|
||||
} catch {}
|
||||
pauseAllPlayers();
|
||||
};
|
||||
}, [player0, player1]),
|
||||
}, [pauseAllPlayers]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
try {
|
||||
player0?.pause?.();
|
||||
player1?.pause?.();
|
||||
} catch {}
|
||||
pauseAllPlayers();
|
||||
};
|
||||
}, [player0, player1]);
|
||||
}, [pauseAllPlayers]);
|
||||
|
||||
const handleChooseTrack = () => {
|
||||
if (isWeb) {
|
||||
@@ -334,8 +187,7 @@ const SongReady = () => {
|
||||
const handleConfirmRegenerate = async () => {
|
||||
setShowRegenerateModal(false);
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
await pauseAllPlayers();
|
||||
} catch {}
|
||||
navigate(Routes.ComposeSong, { isRegeneration: true });
|
||||
};
|
||||
@@ -368,8 +220,7 @@ const SongReady = () => {
|
||||
<MusicLandHeader
|
||||
onPressBack={async () => {
|
||||
try {
|
||||
await player0?.pause?.();
|
||||
await player1?.pause?.();
|
||||
await pauseAllPlayers();
|
||||
} catch {}
|
||||
navigate(Routes.Home);
|
||||
}}
|
||||
@@ -384,146 +235,19 @@ const SongReady = () => {
|
||||
}}
|
||||
/>
|
||||
<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={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]) {
|
||||
if (player.resume) {
|
||||
await player.resume?.();
|
||||
} else {
|
||||
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>
|
||||
);
|
||||
})}
|
||||
{musicUrls.map((url, idx) => (
|
||||
<SongOptionCard
|
||||
key={`${url || "song"}-${idx}`}
|
||||
index={idx}
|
||||
url={url}
|
||||
isSelected={selectedIndex === idx}
|
||||
onSelect={setSelectedIndex}
|
||||
registerPlayer={registerPlayer}
|
||||
onTogglePlayback={handleTogglePlayback}
|
||||
projectId={projectId}
|
||||
selectedProject={selectedProject}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
<View
|
||||
@@ -562,6 +286,230 @@ const SongReady = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const SongOptionCard = ({
|
||||
index,
|
||||
url,
|
||||
isSelected,
|
||||
onSelect,
|
||||
registerPlayer,
|
||||
onTogglePlayback,
|
||||
projectId,
|
||||
selectedProject,
|
||||
}) => {
|
||||
const trackTitle =
|
||||
(Array.isArray(selectedProject?.musicTitles)
|
||||
? selectedProject.musicTitles?.[index]
|
||||
: null) ||
|
||||
selectedProject?.title ||
|
||||
`Morceau ${index + 1}`;
|
||||
|
||||
const player = useSharedAudioPlayer(url ? { uri: url } : undefined, {
|
||||
id: url ? `songready-${url}` : undefined,
|
||||
title: trackTitle,
|
||||
artwork: selectedProject?.coverUrl || null,
|
||||
coverUrl: selectedProject?.coverUrl || null,
|
||||
metadata: { index, projectId },
|
||||
});
|
||||
|
||||
const [progressInfo, setProgressInfo] = useState({ pos: 0, dur: 0 });
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const wasPlayingBeforeSeek = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
registerPlayer(index, player);
|
||||
return () => registerPlayer(index, null);
|
||||
}, [index, player, registerPlayer]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!player || !url) return;
|
||||
const preload = async () => {
|
||||
try {
|
||||
await player.load?.({ startPositionMs: 0 });
|
||||
} catch (error) {
|
||||
console.log("SongReady preload error", error?.message);
|
||||
}
|
||||
};
|
||||
preload();
|
||||
}, [player, url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!player) {
|
||||
setIsPlaying(false);
|
||||
setProgressInfo({ pos: 0, dur: 0 });
|
||||
return undefined;
|
||||
}
|
||||
const id = setInterval(() => {
|
||||
const durationMs = Math.max(
|
||||
0,
|
||||
Math.round((Number(player.duration) || 0) * 1000),
|
||||
);
|
||||
const positionMs = Math.max(
|
||||
0,
|
||||
Math.round((Number(player.currentTime) || 0) * 1000),
|
||||
);
|
||||
setProgressInfo((prev) => {
|
||||
if (
|
||||
Math.abs((prev?.dur || 0) - durationMs) < 5 &&
|
||||
Math.abs((prev?.pos || 0) - positionMs) < 5
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return { pos: positionMs, dur: durationMs };
|
||||
});
|
||||
setIsPlaying((prev) => {
|
||||
const next = !!player.playing;
|
||||
if (prev !== next) {
|
||||
return next;
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}, 300);
|
||||
return () => clearInterval(id);
|
||||
}, [player]);
|
||||
|
||||
const handleSeek = async (ratio) => {
|
||||
if (!player || !progressInfo?.dur) return;
|
||||
const dur = progressInfo.dur || 0;
|
||||
const pos = Math.max(0, Math.floor(dur * ratio));
|
||||
try {
|
||||
await player.seekTo?.(Math.floor(pos / 1000));
|
||||
setProgressInfo((prev) => ({ ...prev, pos }));
|
||||
} catch (error) {
|
||||
console.log("SongReady seek error", error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeekStart = async () => {
|
||||
onSelect(index);
|
||||
if (!player) return;
|
||||
try {
|
||||
wasPlayingBeforeSeek.current = !!player.playing;
|
||||
if (player.playing) {
|
||||
await player.pause?.();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("SongReady pause on seek start", error?.message);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSeekEnd = async () => {
|
||||
if (!player) return;
|
||||
try {
|
||||
if (wasPlayingBeforeSeek.current) {
|
||||
if (player.resume) {
|
||||
await player.resume?.();
|
||||
} else {
|
||||
await player.play?.();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("SongReady resume after seek", error?.message);
|
||||
} finally {
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = () => {
|
||||
onSelect(index);
|
||||
onTogglePlayback?.(index, isPlaying);
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => onSelect(index)}
|
||||
style={({ pressed }) => [
|
||||
{
|
||||
borderRadius: 20,
|
||||
borderWidth: isSelected ? 2 : 1,
|
||||
borderColor: isSelected ? Palette.primary : Palette.ultraLightWhite,
|
||||
overflow: "hidden",
|
||||
},
|
||||
pressed && { opacity: 0.96 },
|
||||
]}
|
||||
>
|
||||
<BlurView
|
||||
intensity={40}
|
||||
tint="dark"
|
||||
style={{
|
||||
borderRadius: 18,
|
||||
overflow: "hidden",
|
||||
padding: 12,
|
||||
backgroundColor: "#FFFFFF0A",
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<Pressable
|
||||
onPress={handleToggle}
|
||||
style={{
|
||||
...Style.containerCenter,
|
||||
...size({ size: 48 }),
|
||||
}}
|
||||
>
|
||||
<Image source={isPlaying ? icons.pause : icons.play} />
|
||||
</Pressable>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Text
|
||||
style={{
|
||||
color: "white",
|
||||
marginBottom: responsiveHeight(1),
|
||||
}}
|
||||
>{`Morceau ${index + 1}`}</Text>
|
||||
<Slider
|
||||
value={formatTime(progressInfo?.pos)}
|
||||
maxValue={formatTime(progressInfo?.dur)}
|
||||
progress={
|
||||
progressInfo?.dur
|
||||
? (progressInfo.pos || 0) / progressInfo.dur
|
||||
: 0
|
||||
}
|
||||
seekEnabled={!!url}
|
||||
onSeekStart={handleSeekStart}
|
||||
onSeek={handleSeek}
|
||||
onSeekEnd={handleSeekEnd}
|
||||
/>
|
||||
</View>
|
||||
<Pressable
|
||||
onPress={() => onSelect(index)}
|
||||
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>
|
||||
);
|
||||
};
|
||||
|
||||
const RegenerateModal = ({ visible, onClose, onConfirm }) => {
|
||||
return (
|
||||
<Modal
|
||||
|
||||
Reference in New Issue
Block a user