feat: next music from playlist

This commit is contained in:
2025-11-03 15:40:20 +01:00
parent c361505175
commit 1adf5ba5e4
3 changed files with 343 additions and 18 deletions
+201 -2
View File
@@ -64,6 +64,8 @@ const noop = () => {};
const DEFAULT_CONTEXT = {
currentTrack: null,
queue: [],
queueInfo: null,
queueIndex: -1,
status: "idle",
isPlaying: false,
isBuffering: false,
@@ -209,7 +211,12 @@ const normalizeTrack = (trackInput = {}, options = {}) => {
const PlayerProvider = ({ children }) => {
const [activeRouteName] = useGlobal("activeRouteName");
const [currentTrack, setCurrentTrack] = useState(null);
const [queue, setQueue] = useState([]);
const queueRef = useRef([]);
const queueInfoRef = useRef({ id: null, type: null, name: null });
const queueIndexRef = useRef(-1);
const [queue, setQueueState] = useState([]);
const [queueInfo, setQueueInfoState] = useState(queueInfoRef.current);
const [queueIndex, setQueueIndexState] = useState(queueIndexRef.current);
const [playback, setPlayback] = useState({
status: "idle",
isPlaying: false,
@@ -222,6 +229,127 @@ const PlayerProvider = ({ children }) => {
const autoPlayRef = useRef(false);
const pendingSeekValueRef = useRef(null);
const didJustFinishRef = useRef(false);
const setQueueIndexValue = useCallback((index = -1) => {
const list = queueRef.current;
const hasItems = Array.isArray(list) && list.length > 0;
let target = Number.isInteger(index) ? index : queueIndexRef.current;
if (!hasItems) {
target = -1;
} else {
target = Math.min(Math.max(target, -1), list.length - 1);
}
if (queueIndexRef.current === target) return;
queueIndexRef.current = target;
setQueueIndexState(target);
}, []);
const setQueueInfoValue = useCallback((info) => {
const next = {
id: info?.id ?? null,
type: info?.type ?? null,
name: info?.name ?? null,
};
const prev = queueInfoRef.current;
if (
prev.id === next.id &&
prev.type === next.type &&
prev.name === next.name
) {
return;
}
queueInfoRef.current = next;
setQueueInfoState(next);
}, []);
const updateQueue = useCallback(
(items = [], options = {}) => {
const normalized = Array.isArray(items)
? items
.map((item) => {
if (!item) return null;
if (typeof item === "object") {
const { metadata, context, ...rest } = item;
const safeMetadata =
metadata && typeof metadata === "object"
? { ...metadata }
: undefined;
if (safeMetadata) {
delete safeMetadata.queue;
}
const safeContext =
context && typeof context === "object"
? { ...context }
: undefined;
if (safeContext) {
delete safeContext.queue;
}
return normalizeTrack(
{
...rest,
...(safeMetadata ? { metadata: safeMetadata } : {}),
...(safeContext ? { context: safeContext } : {}),
},
{}
);
}
return normalizeTrack(item, {});
})
.filter((track) => !!track?.source)
: [];
queueRef.current = normalized;
setQueueState(normalized);
const info = {
id: options.id ?? options.queueId ?? null,
type: options.type ?? options.queueType ?? null,
name: options.name ?? options.queueName ?? null,
};
setQueueInfoValue(info);
const currentTrackId =
options.currentTrackId && typeof options.currentTrackId === "string"
? options.currentTrackId
: null;
const indexCandidate =
typeof options.index === "number"
? options.index
: typeof options.queueIndex === "number"
? options.queueIndex
: currentTrackId
? normalized.findIndex((track) => track.id === currentTrackId)
: queueIndexRef.current;
setQueueIndexValue(indexCandidate);
return normalized;
},
[setQueueIndexValue, setQueueInfoValue]
);
const setQueue = useCallback(
(itemsOrConfig, maybeOptions = {}) => {
if (typeof itemsOrConfig === "function") {
const result = itemsOrConfig([...(queueRef.current || [])]);
if (Array.isArray(result)) {
updateQueue(result, maybeOptions);
}
return;
}
if (Array.isArray(itemsOrConfig)) {
updateQueue(itemsOrConfig, maybeOptions);
return;
}
if (itemsOrConfig && typeof itemsOrConfig === "object") {
const { items = [], ...rest } = itemsOrConfig;
updateQueue(items, { ...rest, ...maybeOptions });
return;
}
updateQueue([], {});
},
[updateQueue]
);
const source = useMemo(() => {
if (!currentTrack?.source) return null;
@@ -328,6 +456,19 @@ const PlayerProvider = ({ children }) => {
setError(null);
if (Array.isArray(options.queue)) {
updateQueue(options.queue, {
id: options.queueId,
type: options.queueType,
name: options.queueName,
queueIndex:
typeof options.queueIndex === "number"
? options.queueIndex
: undefined,
currentTrackId: normalized.id,
});
}
if (sameTrack) {
setCurrentTrack((prev) => ({ ...prev, ...normalized }));
try {
@@ -371,7 +512,7 @@ const PlayerProvider = ({ children }) => {
}));
setCurrentTrack(normalized);
},
[currentTrack?.id, player, toPlayerSeekValue]
[currentTrack?.id, player, toPlayerSeekValue, updateQueue]
);
const resume = useCallback(async () => {
@@ -474,10 +615,65 @@ const PlayerProvider = ({ children }) => {
setIsLooping((prev) => !prev);
}, []);
useEffect(() => {
if (!currentTrack?.id) {
if (queueIndexRef.current !== -1) {
setQueueIndexValue(-1);
}
return;
}
const idx = queueRef.current.findIndex((item) => item.id === currentTrack.id);
if (idx !== queueIndexRef.current) {
setQueueIndexValue(idx);
}
}, [currentTrack?.id, setQueueIndexValue]);
const handleTrackDidFinish = useCallback(() => {
if (isLooping) return;
const items = queueRef.current;
if (!Array.isArray(items) || items.length === 0) return;
let idx = queueIndexRef.current;
const currentId = currentTrack?.id ?? null;
if ((idx == null || idx < 0) && currentId) {
idx = items.findIndex((item) => item.id === currentId);
}
if (idx == null || idx < 0) return;
const nextIdx = idx + 1;
if (nextIdx >= items.length) return;
const nextTrack = items[nextIdx];
if (!nextTrack) return;
play(nextTrack, {
autoPlay: true,
startPositionMs: 0,
queueId: queueInfoRef.current.id,
queueType: queueInfoRef.current.type,
queueName: queueInfoRef.current.name,
}).catch((err) => {
setError(err);
});
}, [currentTrack?.id, isLooping, play]);
useEffect(() => {
const finished = !!status?.didJustFinish;
if (finished) {
if (!didJustFinishRef.current) {
didJustFinishRef.current = true;
handleTrackDidFinish();
}
} else {
didJustFinishRef.current = false;
}
}, [status?.didJustFinish, handleTrackDidFinish]);
const contextValue = useMemo(
() => ({
currentTrack,
queue,
queueInfo,
queueIndex,
status: playback.status,
isPlaying: playback.isPlaying,
isBuffering: playback.isBuffering,
@@ -508,6 +704,8 @@ const PlayerProvider = ({ children }) => {
playback.positionMs,
playback.status,
queue,
queueInfo,
queueIndex,
resume,
seekBy,
seekTo,
@@ -516,6 +714,7 @@ const PlayerProvider = ({ children }) => {
isLooping,
setLooping,
toggleLooping,
setQueue,
]
);
+71 -8
View File
@@ -1,5 +1,5 @@
import { BlurView } from "expo-blur";
import React, { useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { Image, Pressable, Text, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useGlobal } from "reactn";
@@ -9,6 +9,7 @@ import { playlistsRef, projectsRef } from "../../config/firebase";
import useDataFromArrayDocId from "../../hooks/useDataFromArrayId";
import useDataFromRef from "../../hooks/useDataFromRef";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import usePlayer from "../../hooks/usePlayer";
import Page from "../../layouts/Page";
import { useUser } from "../../providers/UserDataProvider";
import { gutters, Palette, Style } from "../../styles";
@@ -42,6 +43,74 @@ const AllMyPlaylist = ({ route }) => {
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const navigateToMusicDetails = useNavigateToMusicDetails();
const { setQueue } = usePlayer() || {};
const playlistQueueItems = useMemo(() => {
if (!Array.isArray(musics)) return [];
return musics
.map((project) => {
const projectId =
typeof project?.id === "string" ? project.id : project?.projectId ?? null;
const songUrl =
typeof project?.songUrl === "string" && project.songUrl.length > 0
? project.songUrl
: null;
if (!songUrl) return null;
const descriptorId = projectId ? `project-${projectId}` : songUrl;
return {
id: descriptorId,
uri: songUrl,
songUrl,
title: project?.title || "Sans titre",
artist: project?.userName || "",
artwork: project?.coverUrl ?? null,
coverUrl: project?.coverUrl ?? null,
metadata: {
projectId,
playlistId: selectedPlaylistId ?? null,
playlistName: playlist?.name ?? null,
},
context: {
projectId,
playlistId: selectedPlaylistId ?? null,
playlistName: playlist?.name ?? null,
},
};
})
.filter(Boolean);
}, [musics, playlist?.name, selectedPlaylistId]);
const handlePlayFromPlaylist = useCallback(
(project) => {
if (!project?.id) return;
if (Array.isArray(playlistQueueItems) && playlistQueueItems.length > 0) {
const queueIndex = playlistQueueItems.findIndex(
(item) => item?.metadata?.projectId === project.id
);
if (queueIndex >= 0 && typeof setQueue === "function") {
setQueue({
items: playlistQueueItems,
id: selectedPlaylistId,
type: "playlist",
name: playlist?.name ?? null,
index: queueIndex,
});
}
}
navigateToMusicDetails({
projectId: project.id,
songUrl: project?.songUrl,
project,
});
},
[
navigateToMusicDetails,
playlist?.name,
playlistQueueItems,
selectedPlaylistId,
setQueue,
]
);
// delete playlist
const confirmDelete = () => {
@@ -209,13 +278,7 @@ const AllMyPlaylist = ({ route }) => {
imageUri={p?.coverUrl || null}
projectId={p?.id}
likedBy={p?.likedBy || []}
onPress={() =>
navigateToMusicDetails({
projectId: p.id,
songUrl: p?.songUrl,
project: p,
})
}
onPress={() => handlePlayFromPlaylist(p)}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuPosition(posTop);
+71 -8
View File
@@ -1,5 +1,5 @@
import { useRoute } from "@react-navigation/native";
import React, { useState } from "react";
import React, { useCallback, useMemo, useState } from "react";
import { Image, Pressable, View } from "react-native";
import { SheetManager } from "react-native-actions-sheet";
import { useDataFromRef } from "react-native-minuit/src/hooks";
@@ -9,6 +9,7 @@ import { background, icons } from "../../assets";
import MoreMenu from "../../components/MoreMenu";
import { playlistsRef, projectsRef } from "../../config/firebase";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import usePlayer from "../../hooks/usePlayer";
import Page from "../../layouts/Page";
import { goBack } from "../../navigation/NavigationService";
import { gutters } from "../../styles";
@@ -40,6 +41,74 @@ const PlaylistDetails = () => {
const [showMenu, setShowMenu] = useState(false);
const [selectedProjectId, setSelectedProjectId] = useState(null);
const navigateToMusicDetails = useNavigateToMusicDetails();
const { setQueue } = usePlayer() || {};
const playlistQueueItems = useMemo(() => {
if (!Array.isArray(musics)) return [];
return musics
.map((project) => {
const projectId =
typeof project?.id === "string" ? project.id : project?.projectId ?? null;
const songUrl =
typeof project?.songUrl === "string" && project.songUrl.length > 0
? project.songUrl
: null;
if (!songUrl) return null;
const descriptorId = projectId ? `project-${projectId}` : songUrl;
return {
id: descriptorId,
uri: songUrl,
songUrl,
title: project?.title || "Sans titre",
artist: project?.userName || "",
artwork: project?.coverUrl ?? null,
coverUrl: project?.coverUrl ?? null,
metadata: {
projectId,
playlistId: playlistId ?? null,
playlistName: playlist?.name ?? null,
},
context: {
projectId,
playlistId: playlistId ?? null,
playlistName: playlist?.name ?? null,
},
};
})
.filter(Boolean);
}, [musics, playlist?.name, playlistId]);
const handlePlayFromPlaylist = useCallback(
(project) => {
if (!project?.id) return;
if (Array.isArray(playlistQueueItems) && playlistQueueItems.length > 0) {
const queueIndex = playlistQueueItems.findIndex(
(item) => item?.metadata?.projectId === project.id
);
if (queueIndex >= 0 && typeof setQueue === "function") {
setQueue({
items: playlistQueueItems,
id: playlistId,
type: "playlist",
name: playlist?.name ?? null,
index: queueIndex,
});
}
}
navigateToMusicDetails({
projectId: project.id,
songUrl: project?.songUrl,
project,
});
},
[
navigateToMusicDetails,
playlist?.name,
playlistId,
playlistQueueItems,
setQueue,
]
);
const confirmDelete = () => {
SheetManager.show("Delete", {
@@ -88,13 +157,7 @@ const PlaylistDetails = () => {
imageUri={p?.coverUrl || null}
projectId={p?.id}
likedBy={p?.likedBy || []}
onPress={() =>
navigateToMusicDetails({
projectId: p.id,
songUrl: p?.songUrl,
project: p,
})
}
onPress={() => handlePlayFromPlaylist(p)}
onPressMore={(posTop) => {
setSelectedProjectId(p.id);
setMenuPosition(posTop);