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,
]
);