import { useCallback } from 'react'; import type { SubsonicDirectoryEntry } from '../api/subsonicTypes'; import { usePlayerStore } from '../store/playerStore'; import { entryToTrack, folderBrowserHasKeyModifiers, isFolderBrowserArrowKey, type Column, type NavPos, } from '../utils/componentHelpers/folderBrowserHelpers'; interface Args { columns: Column[]; filteredItemsByCol: SubsonicDirectoryEntry[][]; columnFilters: Record; filterFocusCol: number | null; keyboardPos: NavPos | null; isContextMenuOpen: boolean; filterInputRefs: React.RefObject>; wrapperRef: React.RefObject; setKeyboardNavActive: React.Dispatch>; setKeyboardPos: React.Dispatch>; setContextAnchorPos: React.Dispatch>; setFilterFocusCol: React.Dispatch>; preferredRowIndex: (colIndex: number) => number; fallbackNavPos: (cols: Column[]) => NavPos | null; handleActivate: (colIndex: number, item: SubsonicDirectoryEntry) => void; handleDirClick: (colIndex: number, item: SubsonicDirectoryEntry) => void; setSelectedInColumn: (colIndex: number, itemId: string) => void; clearSelectedInColumn: (colIndex: number) => void; openContextMenuForEntry: (col: Column, item: SubsonicDirectoryEntry, x: number, y: number) => void; clearFiltersRightOf: (colIndex: number) => void; } export function useFolderBrowserKeyboardNav({ columns, filteredItemsByCol, columnFilters, filterFocusCol, keyboardPos, isContextMenuOpen, filterInputRefs, wrapperRef, setKeyboardNavActive, setKeyboardPos, setContextAnchorPos, setFilterFocusCol, preferredRowIndex, fallbackNavPos, handleActivate, handleDirClick, setSelectedInColumn, clearSelectedInColumn, openContextMenuForEntry, clearFiltersRightOf, }: Args) { const enqueue = usePlayerStore(s => s.enqueue); return useCallback((e: React.KeyboardEvent) => { if (isContextMenuOpen) return; const target = e.target as HTMLElement; const inFilterInput = target instanceof HTMLInputElement && target.dataset.folderFilterInput === 'true'; if (inFilterInput) return; const key = e.key; if (e.ctrlKey && e.code === 'KeyF') { e.preventDefault(); const current = keyboardPos ?? fallbackNavPos(columns); if (!current) return; const colIndex = current.colIndex; setFilterFocusCol(colIndex); requestAnimationFrame(() => { const input = filterInputRefs.current?.[colIndex]; if (!input) return; input.focus(); input.select(); }); return; } if (isFolderBrowserArrowKey(e) && folderBrowserHasKeyModifiers(e)) return; if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter'].includes(key)) return; setKeyboardNavActive(true); const current = keyboardPos ?? fallbackNavPos(columns); if (!current) return; const col = columns[current.colIndex]; const visibleItems = filteredItemsByCol[current.colIndex] ?? []; const item = visibleItems[current.rowIndex]; if (!col || !item) return; e.preventDefault(); if (key === 'Enter' && e.ctrlKey) { setContextAnchorPos(current); const rowEl = wrapperRef.current?.querySelector( `.folder-col-row[data-col-index="${current.colIndex}"][data-row-index="${current.rowIndex}"]`, ); const rect = rowEl?.getBoundingClientRect(); const x = rect ? rect.left + 24 : 24; const y = rect ? rect.top + rect.height / 2 : 24; openContextMenuForEntry(col, item, x, y); return; } if (key === 'ArrowUp') { if (current.rowIndex > 0) { const nextRowIndex = current.rowIndex - 1; const nextItem = visibleItems[nextRowIndex]; setKeyboardPos({ colIndex: current.colIndex, rowIndex: nextRowIndex }); if (nextItem.isDir) handleDirClick(current.colIndex, nextItem); else setSelectedInColumn(current.colIndex, nextItem.id); } else if ( current.rowIndex === 0 && (filterFocusCol === current.colIndex || !!columnFilters[current.colIndex]) ) { setFilterFocusCol(current.colIndex); requestAnimationFrame(() => { const input = filterInputRefs.current?.[current.colIndex]; if (!input) return; input.focus(); input.select(); }); } return; } if (key === 'ArrowDown') { if (current.rowIndex < visibleItems.length - 1) { const nextRowIndex = current.rowIndex + 1; const nextItem = visibleItems[nextRowIndex]; setKeyboardPos({ colIndex: current.colIndex, rowIndex: nextRowIndex }); if (nextItem.isDir) handleDirClick(current.colIndex, nextItem); else setSelectedInColumn(current.colIndex, nextItem.id); } return; } if (key === 'ArrowLeft') { if (current.colIndex > 0) { clearSelectedInColumn(current.colIndex); const nextColIndex = current.colIndex - 1; clearFiltersRightOf(nextColIndex); const rowIndex = preferredRowIndex(nextColIndex); if (rowIndex >= 0) setKeyboardPos({ colIndex: nextColIndex, rowIndex }); } return; } if (key === 'ArrowRight') { const nextColIndex = current.colIndex + 1; if (nextColIndex < columns.length) { const nextVisibleItems = filteredItemsByCol[nextColIndex] ?? []; const rowIndex = Math.min(preferredRowIndex(nextColIndex), nextVisibleItems.length - 1); if (rowIndex >= 0) { const nextItem = nextVisibleItems[rowIndex]; setSelectedInColumn(nextColIndex, nextItem.id); setKeyboardPos({ colIndex: nextColIndex, rowIndex }); return; } } if (item.isDir) handleActivate(current.colIndex, item); return; } if (key === 'Enter') { if (e.shiftKey && !item.isDir) { const toAppend = (filteredItemsByCol[current.colIndex] ?? []) .filter(it => !it.isDir) .map(entryToTrack); if (toAppend.length > 0) enqueue(toAppend); return; } handleActivate(current.colIndex, item); } }, [ keyboardPos, fallbackNavPos, columns, preferredRowIndex, handleActivate, handleDirClick, setSelectedInColumn, clearSelectedInColumn, openContextMenuForEntry, isContextMenuOpen, filteredItemsByCol, filterFocusCol, columnFilters, enqueue, clearFiltersRightOf, filterInputRefs, wrapperRef, setKeyboardNavActive, setKeyboardPos, setContextAnchorPos, setFilterFocusCol, ]); }