fix queue bug

This commit is contained in:
Boof2015
2026-07-07 15:25:34 -04:00
parent 5f02e3e222
commit 675b072f41
5 changed files with 142 additions and 26 deletions
+18 -4
View File
@@ -439,6 +439,10 @@ function moveOriginalOrderIfUnshuffled(fromIndex: number, toIndex: number): void
originalOrder.splice(boundedTo, 0, moved);
}
interface QueueRemoveOptions {
updateMirror?: boolean;
}
/** Replace everything after the current track with `upcoming` (in order). */
export async function setUpcoming(upcoming: RntpTrack[]): Promise<void> {
await queueLoadSettled();
@@ -511,18 +515,28 @@ export async function requeueManyToTop(absoluteIndices: number[]): Promise<void>
}
/** Remove a single track at an absolute queue index. */
export async function removeFromQueue(absoluteIndex: number): Promise<void> {
export async function removeFromQueue(
absoluteIndex: number,
options: QueueRemoveOptions = {}
): Promise<void> {
await queueLoadSettled();
await TrackPlayer.remove(absoluteIndex);
useQueueStore.getState().removeIndices([absoluteIndex]);
if (options.updateMirror !== false) {
useQueueStore.getState().removeIndices([absoluteIndex]);
}
syncOriginalOrderFromMirrorIfUnshuffled();
}
/** Remove a group of tracks at absolute queue indices. */
export async function removeManyFromQueue(absoluteIndices: number[]): Promise<void> {
export async function removeManyFromQueue(
absoluteIndices: number[],
options: QueueRemoveOptions = {}
): Promise<void> {
if (absoluteIndices.length === 0) return;
await queueLoadSettled();
await TrackPlayer.remove(absoluteIndices);
useQueueStore.getState().removeIndices(absoluteIndices);
if (options.updateMirror !== false) {
useQueueStore.getState().removeIndices(absoluteIndices);
}
syncOriginalOrderFromMirrorIfUnshuffled();
}
+27 -22
View File
@@ -56,6 +56,10 @@ import {
setUpcoming
} from '@/audio/playbackController';
import { useQueue } from './useQueue';
import {
removeQueueEntryAt,
resolveSelectedQueueAction,
} from './queueActions';
const QUEUE_ROW_HEIGHT = 64;
const ART = 42;
@@ -399,9 +403,11 @@ export function QueueTray({ onClose }: QueueTrayProps) {
const remove = useCallback(
(localIndex: number) => {
const nextEntries = entriesRef.current.filter((_, index) => index !== localIndex);
setOptimisticEntries(nextEntries);
runAndRefresh(removeFromQueue(baseOffset + localIndex));
const action = removeQueueEntryAt(entriesRef.current, localIndex, baseOffset);
if (!action) return;
setOptimisticEntries(action.nextEntries);
runAndRefresh(removeFromQueue(action.absoluteIndex, { updateMirror: false }));
},
[baseOffset, runAndRefresh, setOptimisticEntries]
);
@@ -420,30 +426,29 @@ export function QueueTray({ onClose }: QueueTrayProps) {
setSelectedKeys(new Set());
}, []);
const selectedAbsoluteIndices = useCallback((): number[] => {
const indices: number[] = [];
entriesRef.current.forEach((entry, index) => {
if (visibleSelectedKeys.has(entry.key)) indices.push(baseOffset + index);
});
return indices;
}, [baseOffset, visibleSelectedKeys]);
const groupPlayNext = useCallback(() => {
const selected = new Set(visibleSelectedKeys);
const moved = entriesRef.current.filter((entry) => selected.has(entry.key));
const rest = entriesRef.current.filter((entry) => !selected.has(entry.key));
setOptimisticEntries([...moved, ...rest]);
runAndRefresh(requeueManyToTop(selectedAbsoluteIndices()));
const action = resolveSelectedQueueAction(entriesRef.current, visibleSelectedKeys, baseOffset);
if (action.absoluteIndices.length === 0) {
exitEdit();
return;
}
setOptimisticEntries(action.entriesWithSelectedFirst);
runAndRefresh(requeueManyToTop(action.absoluteIndices));
exitEdit();
}, [exitEdit, runAndRefresh, selectedAbsoluteIndices, setOptimisticEntries, visibleSelectedKeys]);
}, [baseOffset, exitEdit, runAndRefresh, setOptimisticEntries, visibleSelectedKeys]);
const groupRemove = useCallback(() => {
const selected = new Set(visibleSelectedKeys);
const nextEntries = entriesRef.current.filter((entry) => !selected.has(entry.key));
setOptimisticEntries(nextEntries);
runAndRefresh(removeManyFromQueue(selectedAbsoluteIndices()));
const action = resolveSelectedQueueAction(entriesRef.current, visibleSelectedKeys, baseOffset);
if (action.absoluteIndices.length === 0) {
exitEdit();
return;
}
setOptimisticEntries(action.entriesWithoutSelected);
runAndRefresh(removeManyFromQueue(action.absoluteIndices, { updateMirror: false }));
exitEdit();
}, [exitEdit, runAndRefresh, selectedAbsoluteIndices, setOptimisticEntries, visibleSelectedKeys]);
}, [baseOffset, exitEdit, runAndRefresh, setOptimisticEntries, visibleSelectedKeys]);
const renderBackdrop = useCallback(
(props: BottomSheetBackdropProps) => (
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
removeQueueEntryAt,
resolveSelectedQueueAction,
type KeyedQueueEntry,
} from './queueActions.ts';
function keyed(keys: string[]): KeyedQueueEntry[] {
return keys.map((key) => ({ key }));
}
function entryKeys(entries: readonly KeyedQueueEntry[]): string[] {
return entries.map((entry) => entry.key);
}
test('resolves swipe remove before the optimistic mirror mutation', () => {
const upcoming = keyed(['A', 'B', 'C']);
const action = removeQueueEntryAt(upcoming, 1, 1);
if (!action) throw new Error('expected a remove action');
assert.equal(action.absoluteIndex, 2);
assert.deepEqual(entryKeys(action.nextEntries), ['A', 'C']);
const mirrorAfterNativeCompletionWithSkippedUpdate = action.nextEntries;
assert.deepEqual(entryKeys(mirrorAfterNativeCompletionWithSkippedUpdate), ['A', 'C']);
});
test('captures selected absolute indices before optimistic batch mutation', () => {
const upcoming = keyed(['A', 'B', 'C', 'D']);
const selectedKeys = new Set(['B', 'D']);
const action = resolveSelectedQueueAction(upcoming, selectedKeys, 1);
assert.deepEqual(action.absoluteIndices, [2, 4]);
assert.deepEqual(entryKeys(action.entriesWithoutSelected), ['A', 'C']);
assert.deepEqual(entryKeys(action.entriesWithSelectedFirst), ['B', 'D', 'A', 'C']);
const afterOptimisticRemoval = resolveSelectedQueueAction(
action.entriesWithoutSelected,
selectedKeys,
1
);
assert.deepEqual(afterOptimisticRemoval.absoluteIndices, []);
});
+52
View File
@@ -0,0 +1,52 @@
export interface KeyedQueueEntry {
key: string;
}
export interface QueueItemRemoveAction<T extends KeyedQueueEntry> {
absoluteIndex: number;
nextEntries: T[];
}
export interface SelectedQueueAction<T extends KeyedQueueEntry> {
absoluteIndices: number[];
entriesWithSelectedFirst: T[];
entriesWithoutSelected: T[];
}
export function removeQueueEntryAt<T extends KeyedQueueEntry>(
entries: readonly T[],
localIndex: number,
baseOffset: number
): QueueItemRemoveAction<T> | null {
if (localIndex < 0 || localIndex >= entries.length) return null;
return {
absoluteIndex: baseOffset + localIndex,
nextEntries: entries.filter((_, index) => index !== localIndex),
};
}
export function resolveSelectedQueueAction<T extends KeyedQueueEntry>(
entries: readonly T[],
selectedKeys: ReadonlySet<string>,
baseOffset: number
): SelectedQueueAction<T> {
const selectedEntries: T[] = [];
const remainingEntries: T[] = [];
const absoluteIndices: number[] = [];
entries.forEach((entry, index) => {
if (selectedKeys.has(entry.key)) {
selectedEntries.push(entry);
absoluteIndices.push(baseOffset + index);
} else {
remainingEntries.push(entry);
}
});
return {
absoluteIndices,
entriesWithSelectedFirst: [...selectedEntries, ...remainingEntries],
entriesWithoutSelected: remainingEntries,
};
}