mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
cb1a110afb
Relocate the playback/queue/transport/audio-output engine out of the type-first
store/ + utils/playback/ + utils/audio/ dirs into a cohesive src/features/playback/,
structure-preserving:
store/<x> -> features/playback/store/<x>
store/audioListenerSetup/<x> -> features/playback/store/audioListenerSetup/<x>
utils/playback/<x> -> features/playback/utils/playback/<x>
utils/audio/<x> -> features/playback/utils/audio/<x>
184 files moved (107 source + 77 tests), 365 consumers rewritten. Pure move — no
behavior change, no state-split (the playerStore state-split stays a separate M5
question). Enabled by this session's decouple seams (artist/offline/orbit/auth →
core registries), so the engine carries no inbound core->feature inversion: store/
now holds only the 50 cross-cutting global stores (auth family, the seams, library
index, UI/settings stores).
KEPT OUT of the move (would re-create global->engine edges): the 3 pure config
helpers utils/audio/{loudnessPreAnalysisSlider,hiResCrossfadeResample} +
utils/playback/autodjOverlapCap (authStore + settings UI read them — they stay in
utils/). Ambiguous view-state stores (eqStore, queueToolbarStore,
playerBarLayoutStore) stay global (no engine imports).
Consumers use DEEP paths (@/features/playback/...), no barrel — matches the lib/
approach and avoids barrel-mock-collapse across the 140 usePlayerStore consumers.
Two tolerated type-only core->feature edges remain (localPlaybackStore->QueueItemRef,
localPlaybackMigration->HotCacheEntry, both erased).
tsc 0, lint 0, full suite 319/2353 green, iron-rule clean (no runtime store->feature
import). Behavior-touching only via the prerequisite bridge seam (already QA-flagged);
the move itself is pure.
109 lines
4.3 KiB
TypeScript
109 lines
4.3 KiB
TypeScript
import React, { useCallback, useEffect, useRef } from 'react';
|
|
import type { SubsonicSong } from '@/lib/api/subsonicTypes';
|
|
import { useSelectionStore } from '@/store/selectionStore';
|
|
import { useDragDrop } from '@/lib/dnd/DragDropContext';
|
|
import { songToTrack } from '@/features/playback/utils/playback/songToTrack';
|
|
|
|
interface UseAlbumTrackListSelectionArgs {
|
|
songs: SubsonicSong[];
|
|
tracklistRef: React.RefObject<HTMLDivElement | null>;
|
|
}
|
|
|
|
interface UseAlbumTrackListSelectionResult {
|
|
inSelectMode: boolean;
|
|
allSelected: boolean;
|
|
onToggleSelect: (id: string, globalIdx: number, shift: boolean) => void;
|
|
onDragStart: (song: SubsonicSong, me: MouseEvent) => void;
|
|
toggleAll: () => void;
|
|
}
|
|
|
|
/**
|
|
* Bulk selection + drag wiring for `AlbumTrackList`:
|
|
* - Clears selection whenever the song list changes (album switch or
|
|
* filter applied) and on mousedown outside the tracklist — but not when
|
|
* the click lands on the bulk-action toolbar, which belongs to the selection.
|
|
* - `onToggleSelect` supports shift-click ranges anchored against the
|
|
* last toggled row.
|
|
* - `onDragStart` promotes a single-row drag into a multi-row drag when
|
|
* the dragged song is part of the active selection.
|
|
*
|
|
* Subscribes only to `selectedIds.size` so the host component re-renders
|
|
* once when select-mode flips on/off; per-row state stays inside
|
|
* `TrackRow`'s own primitive selector for O(1) toggles.
|
|
*/
|
|
export function useAlbumTrackListSelection({
|
|
songs,
|
|
tracklistRef,
|
|
}: UseAlbumTrackListSelectionArgs): UseAlbumTrackListSelectionResult {
|
|
const psyDrag = useDragDrop();
|
|
const selectedCount = useSelectionStore(s => s.selectedIds.size);
|
|
const inSelectMode = selectedCount > 0;
|
|
const allSelected = selectedCount === songs.length && songs.length > 0;
|
|
const lastSelectedIdxRef = useRef<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
useSelectionStore.getState().clearAll();
|
|
lastSelectedIdxRef.current = null;
|
|
}, [songs]);
|
|
|
|
useEffect(() => {
|
|
if (!inSelectMode) return;
|
|
const handler = (e: MouseEvent) => {
|
|
const target = e.target as HTMLElement;
|
|
if (!tracklistRef.current || tracklistRef.current.contains(target)) return;
|
|
// The bulk-action toolbar (filter, add-to-playlist picker, clear button)
|
|
// renders outside the tracklist DOM but belongs to the selection — a
|
|
// mousedown there must not wipe the selection before the button's own
|
|
// click handler runs (otherwise "Add to playlist" clears and never opens).
|
|
if (target.closest('.album-track-toolbar')) return;
|
|
useSelectionStore.getState().clearAll();
|
|
};
|
|
document.addEventListener('mousedown', handler);
|
|
return () => document.removeEventListener('mousedown', handler);
|
|
}, [inSelectMode, tracklistRef]);
|
|
|
|
const onToggleSelect = useCallback((id: string, globalIdx: number, shift: boolean) => {
|
|
useSelectionStore.getState().setSelectedIds(prev => {
|
|
const next = new Set(prev);
|
|
if (shift && lastSelectedIdxRef.current !== null) {
|
|
const from = Math.min(lastSelectedIdxRef.current, globalIdx);
|
|
const to = Math.max(lastSelectedIdxRef.current, globalIdx);
|
|
songs.slice(from, to + 1).forEach(s => next.add(s.id));
|
|
} else {
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
}
|
|
lastSelectedIdxRef.current = globalIdx;
|
|
return next;
|
|
});
|
|
}, [songs]);
|
|
|
|
const onDragStart = useCallback((song: SubsonicSong, me: MouseEvent) => {
|
|
const { selectedIds } = useSelectionStore.getState();
|
|
if (selectedIds.has(song.id) && selectedIds.size > 1) {
|
|
const tracks = songs
|
|
.filter(s => selectedIds.has(s.id))
|
|
.map(s => songToTrack(s));
|
|
psyDrag.startDrag(
|
|
{ data: JSON.stringify({ type: 'songs', tracks }), label: `${tracks.length} Songs` },
|
|
me.clientX, me.clientY,
|
|
);
|
|
} else {
|
|
psyDrag.startDrag(
|
|
{ data: JSON.stringify({ type: 'song', track: songToTrack(song) }), label: song.title },
|
|
me.clientX, me.clientY,
|
|
);
|
|
}
|
|
}, [songs, psyDrag]);
|
|
|
|
const toggleAll = useCallback(() => {
|
|
if (allSelected) {
|
|
useSelectionStore.getState().clearAll();
|
|
} else {
|
|
useSelectionStore.getState().setSelectedIds(() => new Set(songs.map(s => s.id)));
|
|
}
|
|
}, [allSelected, songs]);
|
|
|
|
return { inSelectMode, allSelected, onToggleSelect, onDragStart, toggleAll };
|
|
}
|