fix global player web
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import { BlurView } from "expo-blur";
|
||||
import { Image as ExpoImage } from "expo-image";
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Pressable,
|
||||
Image as RNImage,
|
||||
@@ -26,7 +32,7 @@ import {
|
||||
usersRef,
|
||||
} from "../../config/firebase";
|
||||
import useDataFromRef from "../../hooks/useDataFromRef";
|
||||
import useSharedAudioPlayer from "../../hooks/useSharedAudioPlayer";
|
||||
import useTrackController from "../../hooks/useTrackController";
|
||||
import Page from "../../layouts/Page";
|
||||
import { Palette } from "../../styles";
|
||||
import { FONT_FAMILY } from "../../styles/Fonts";
|
||||
@@ -76,29 +82,40 @@ const MusicDetails = ({ route }) => {
|
||||
const coverUrl = project?.coverUrl || null;
|
||||
const songUrl = project?.songUrl || null;
|
||||
|
||||
const sharedPlayer = useSharedAudioPlayer(songUrl ? { uri: songUrl } : undefined, {
|
||||
id: projectId ? `project-${projectId}` : songUrl ? `song-${songUrl}` : undefined,
|
||||
const trackId = useMemo(() => {
|
||||
if (projectId) return `project-${projectId}`;
|
||||
if (songUrl) return `song-${songUrl}`;
|
||||
return null;
|
||||
}, [projectId, songUrl]);
|
||||
|
||||
const trackDescriptor = useMemo(() => {
|
||||
if (!trackId || !songUrl) return null;
|
||||
return {
|
||||
id: trackId,
|
||||
uri: songUrl,
|
||||
songUrl,
|
||||
title,
|
||||
artist,
|
||||
artwork: coverUrl,
|
||||
coverUrl,
|
||||
metadata: { projectId, screen: "MusicDetails" },
|
||||
});
|
||||
const playAudio = useCallback(async () => {
|
||||
if (sharedPlayer?.play) await sharedPlayer.play();
|
||||
}, [sharedPlayer]);
|
||||
const pauseAudio = useCallback(async () => {
|
||||
if (sharedPlayer?.pause) await sharedPlayer.pause();
|
||||
}, [sharedPlayer]);
|
||||
const seekAudio = useCallback(
|
||||
(seconds) => {
|
||||
if (sharedPlayer?.seekTo) sharedPlayer.seekTo(seconds);
|
||||
},
|
||||
[sharedPlayer]
|
||||
);
|
||||
const isPlaying = !!sharedPlayer?.playing;
|
||||
const positionMs = Math.max(0, (sharedPlayer?.currentTime || 0) * 1000);
|
||||
const durationMs = Math.max(0, (sharedPlayer?.duration || 0) * 1000);
|
||||
context: { projectId, screen: "MusicDetails" },
|
||||
};
|
||||
}, [trackId, songUrl, title, artist, coverUrl, projectId]);
|
||||
|
||||
const {
|
||||
isCurrent: isCurrentTrack,
|
||||
isPlaying: isTrackPlaying,
|
||||
positionMs,
|
||||
durationMs,
|
||||
ensureLoaded,
|
||||
pause: pauseTrack,
|
||||
resume: resumeTrack,
|
||||
seekTo: seekTrackTo,
|
||||
seekBy: seekTrackBy,
|
||||
} = useTrackController(trackDescriptor);
|
||||
|
||||
const isPlaying = isTrackPlaying;
|
||||
|
||||
// Karaoke aligned words (from timestamps)
|
||||
const alignedWords = useMemo(() => {
|
||||
@@ -120,7 +137,7 @@ const MusicDetails = ({ route }) => {
|
||||
useEffect(() => {
|
||||
listenedMsRef.current = 0;
|
||||
incrementDoneRef.current = false;
|
||||
}, [songUrl]);
|
||||
}, [trackId]);
|
||||
|
||||
// Start/stop a timer to accumulate listened milliseconds while playing
|
||||
useEffect(() => {
|
||||
@@ -165,59 +182,157 @@ const MusicDetails = ({ route }) => {
|
||||
return `${m}:${s}`;
|
||||
};
|
||||
|
||||
const togglePlay = async () => {
|
||||
if (!songUrl) return;
|
||||
const togglePlay = useCallback(async () => {
|
||||
if (!trackDescriptor) return;
|
||||
try {
|
||||
if (isPlaying) {
|
||||
await pauseAudio();
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({
|
||||
startPositionMs: positionMs,
|
||||
autoPlay: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isTrackPlaying) {
|
||||
await pauseTrack();
|
||||
} else {
|
||||
await playAudio();
|
||||
await resumeTrack();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails audio error", e?.message);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
trackDescriptor,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
positionMs,
|
||||
isTrackPlaying,
|
||||
pauseTrack,
|
||||
resumeTrack,
|
||||
]);
|
||||
|
||||
const onSeek = async (ratio) => {
|
||||
const handleSliderSeekStart = useCallback(async () => {
|
||||
if (!trackDescriptor) return;
|
||||
try {
|
||||
wasPlayingBeforeSeek.current = isTrackPlaying;
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({
|
||||
startPositionMs: positionMs,
|
||||
autoPlay: false,
|
||||
});
|
||||
}
|
||||
if (isTrackPlaying) {
|
||||
await pauseTrack();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seek start error", e?.message);
|
||||
}
|
||||
}, [
|
||||
trackDescriptor,
|
||||
isTrackPlaying,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
positionMs,
|
||||
pauseTrack,
|
||||
]);
|
||||
|
||||
const handleSliderSeek = useCallback(
|
||||
async (ratio) => {
|
||||
const dur = durationMs || 0;
|
||||
const pos = Math.floor(dur * ratio);
|
||||
if (dur > 0) {
|
||||
await seekAudio(Math.floor((pos || 0) / 1000));
|
||||
if (!trackDescriptor || dur <= 0) return;
|
||||
const targetMs = Math.max(0, Math.floor(dur * ratio));
|
||||
try {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: targetMs, autoPlay: false });
|
||||
} else {
|
||||
await seekTrackTo(targetMs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seek error", e?.message);
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
trackDescriptor,
|
||||
durationMs,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
seekTrackTo,
|
||||
]
|
||||
);
|
||||
|
||||
const seekBy = async (deltaSeconds) => {
|
||||
const handleSliderSeekEnd = useCallback(async () => {
|
||||
try {
|
||||
const cur = Math.floor((positionMs || 0) / 1000);
|
||||
const next = Math.max(0, cur + deltaSeconds);
|
||||
await seekAudio(next);
|
||||
if (wasPlayingBeforeSeek.current) {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({
|
||||
startPositionMs: positionMs,
|
||||
autoPlay: true,
|
||||
});
|
||||
} else {
|
||||
await resumeTrack();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seek end error", e?.message);
|
||||
} finally {
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
}
|
||||
}, [ensureLoaded, isCurrentTrack, positionMs, resumeTrack]);
|
||||
|
||||
const handleSeekBySeconds = useCallback(
|
||||
async (deltaSeconds) => {
|
||||
if (!trackDescriptor) return;
|
||||
const deltaMs = Number(deltaSeconds || 0) * 1000;
|
||||
const target = Math.max(0, (positionMs || 0) + deltaMs);
|
||||
try {
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: target, autoPlay: true });
|
||||
} else {
|
||||
await seekTrackBy(deltaMs);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails seekBy error", e?.message);
|
||||
}
|
||||
};
|
||||
},
|
||||
[
|
||||
trackDescriptor,
|
||||
positionMs,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
seekTrackBy,
|
||||
]
|
||||
);
|
||||
|
||||
const handleLyricsSeek = React.useCallback(
|
||||
const handleLyricsSeek = useCallback(
|
||||
async (timestampS) => {
|
||||
if (typeof timestampS !== "number" || Number.isNaN(timestampS)) return;
|
||||
const seconds = Math.max(0, Number(timestampS) || 0);
|
||||
const durationS = durationMs > 0 ? durationMs / 1000 : null;
|
||||
const bounded = durationS ? Math.min(seconds, durationS) : seconds;
|
||||
if (
|
||||
!trackDescriptor ||
|
||||
typeof timestampS !== "number" ||
|
||||
Number.isNaN(timestampS)
|
||||
)
|
||||
return;
|
||||
const targetMs = Math.max(0, Number(timestampS) * 1000);
|
||||
|
||||
try {
|
||||
const wasPlaying = isPlaying;
|
||||
seekAudio(bounded);
|
||||
if (wasPlaying) {
|
||||
await playAudio();
|
||||
if (!isCurrentTrack) {
|
||||
await ensureLoaded({ startPositionMs: targetMs, autoPlay: true });
|
||||
return;
|
||||
}
|
||||
await seekTrackTo(targetMs);
|
||||
if (!isTrackPlaying) {
|
||||
await resumeTrack();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("MusicDetails lyrics seek error", e?.message);
|
||||
}
|
||||
},
|
||||
[durationMs, isPlaying, playAudio, seekAudio]
|
||||
[
|
||||
trackDescriptor,
|
||||
isCurrentTrack,
|
||||
ensureLoaded,
|
||||
seekTrackTo,
|
||||
isTrackPlaying,
|
||||
resumeTrack,
|
||||
]
|
||||
);
|
||||
const description = useMemo(() => {
|
||||
// Build a readable text from lyrics with section labels
|
||||
@@ -605,7 +720,7 @@ const MusicDetails = ({ route }) => {
|
||||
}}
|
||||
>
|
||||
{/* Previous (rewind 10s) */}
|
||||
<Pressable onPress={() => seekBy(-10)}>
|
||||
<Pressable onPress={() => handleSeekBySeconds(-10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
style={{
|
||||
@@ -629,7 +744,7 @@ const MusicDetails = ({ route }) => {
|
||||
/>
|
||||
</PressableScale>
|
||||
{/* Next (forward 10s) */}
|
||||
<PressableScale onPress={() => seekBy(10)}>
|
||||
<PressableScale onPress={() => handleSeekBySeconds(10)}>
|
||||
<RNImage
|
||||
source={icons.forward}
|
||||
style={{
|
||||
@@ -644,27 +759,9 @@ const MusicDetails = ({ route }) => {
|
||||
maxValue={fmt(durationMs)}
|
||||
progress={durationMs ? (positionMs || 0) / durationMs : 0}
|
||||
seekEnabled={!!songUrl}
|
||||
onSeekStart={async () => {
|
||||
try {
|
||||
wasPlayingBeforeSeek.current = !!isPlaying;
|
||||
if (isPlaying) {
|
||||
await pauseAudio();
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Pause on seek start error", e?.message);
|
||||
}
|
||||
}}
|
||||
onSeek={onSeek}
|
||||
onSeekEnd={async () => {
|
||||
try {
|
||||
if (wasPlayingBeforeSeek.current) {
|
||||
await playAudio();
|
||||
}
|
||||
wasPlayingBeforeSeek.current = false;
|
||||
} catch (e) {
|
||||
console.log("Resume after seek error", e?.message);
|
||||
}
|
||||
}}
|
||||
onSeekStart={handleSliderSeekStart}
|
||||
onSeek={handleSliderSeek}
|
||||
onSeekEnd={handleSliderSeekEnd}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user