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; } 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(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 }; }