test-web / test (push) Successful in 31s
Add a global selection store backing a checkbox per track row and a floating SelectionBar above PlayerBar with Play next / Add to queue / Add to playlist / Like all / Clear. - selection/store.svelte.ts: id Set + TrackRef Map + anchor index; toggleOne, selectRange (shift+click), clearSelection. Singleton — layout effect clears on pathname change. - TrackRow: checkbox slot replaces the track number on hover; row click toggles selection once any row is picked; shift+click extends the range. Esc clears (takes priority over closing the queue drawer). - SelectionBar: floating pill above PlayerBar, mounted inside the QueryClientProvider so the Like-all action can resolve. - player.playNextMany: bulk variant of playNext for "Play next" on a multi-track selection. Covered by selection-store unit tests and three new TrackRow tests (checkbox toggle, sticky select-mode row click, shift+click range).
69 lines
2.2 KiB
TypeScript
69 lines
2.2 KiB
TypeScript
import type { TrackRef } from '$lib/api/types';
|
|
|
|
// Global multi-select state for track lists (album, library/liked,
|
|
// search results, etc.). Reset on route change by the layout effect.
|
|
//
|
|
// The store keeps both a Set of ids (for O(1) isSelected) and a Map of
|
|
// id->TrackRef (so the bulk-action bar can act on the full TrackRef
|
|
// payload without re-querying the source list). Shift-range uses the
|
|
// most recent toggle as the anchor.
|
|
|
|
let _selectedIds = $state(new Set<string>());
|
|
let _selectedTrackMap = $state(new Map<string, TrackRef>());
|
|
let _anchorIndex = $state<number | null>(null);
|
|
|
|
export const selection = {
|
|
get count(): number { return _selectedIds.size; },
|
|
get active(): boolean { return _selectedIds.size > 0; },
|
|
get tracks(): TrackRef[] {
|
|
return Array.from(_selectedTrackMap.values());
|
|
},
|
|
get ids(): string[] { return Array.from(_selectedIds); },
|
|
isSelected(id: string): boolean { return _selectedIds.has(id); }
|
|
};
|
|
|
|
export function toggleOne(track: TrackRef, index: number): void {
|
|
const ids = new Set(_selectedIds);
|
|
const map = new Map(_selectedTrackMap);
|
|
if (ids.has(track.id)) {
|
|
ids.delete(track.id);
|
|
map.delete(track.id);
|
|
} else {
|
|
ids.add(track.id);
|
|
map.set(track.id, track);
|
|
}
|
|
_selectedIds = ids;
|
|
_selectedTrackMap = map;
|
|
_anchorIndex = index;
|
|
}
|
|
|
|
// Shift-click range: select every track from the anchor to `toIndex`
|
|
// inclusive. If no anchor is set (first click ever in this list), the
|
|
// range collapses to a single toggle.
|
|
export function selectRange(toIndex: number, tracks: TrackRef[]): void {
|
|
if (_anchorIndex === null) {
|
|
toggleOne(tracks[toIndex], toIndex);
|
|
return;
|
|
}
|
|
const from = _anchorIndex;
|
|
const [a, b] = from <= toIndex ? [from, toIndex] : [toIndex, from];
|
|
const ids = new Set(_selectedIds);
|
|
const map = new Map(_selectedTrackMap);
|
|
for (let i = a; i <= b; i++) {
|
|
const t = tracks[i];
|
|
if (!t) continue;
|
|
ids.add(t.id);
|
|
map.set(t.id, t);
|
|
}
|
|
_selectedIds = ids;
|
|
_selectedTrackMap = map;
|
|
_anchorIndex = toIndex;
|
|
}
|
|
|
|
export function clearSelection(): void {
|
|
if (_selectedIds.size === 0 && _anchorIndex === null) return;
|
|
_selectedIds = new Set();
|
|
_selectedTrackMap = new Map();
|
|
_anchorIndex = null;
|
|
}
|