mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 07:15:47 +00:00
refactor(folder-browser): G.87 — extract column component + 3 hooks (cluster, multi-commit) (#654)
* refactor(folder-browser): G.87.1 — extract helpers + types Move ColumnKind / NavPos / Column types + entryToAlbumIfPresent / entryToTrack mappers + isFolderBrowserArrowKey / folderBrowserHasKeyModifiers key-event helpers into src/utils/folderBrowserHelpers.ts. Pure code move. * refactor(folder-browser): G.87.2 — extract FolderBrowserColumn component * refactor(folder-browser): G.87.3 — extract useFolderBrowserNowPlayingPath hook * refactor(folder-browser): G.87.4 — extract useFolderBrowserScrolling hook * refactor(folder-browser): G.87.5 — extract useFolderBrowserKeyboardNav hook
This commit is contained in:
committed by
GitHub
parent
c8e130ecea
commit
d4d3b0e53f
@@ -0,0 +1,161 @@
|
||||
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/folderBrowserHelpers';
|
||||
|
||||
interface Args {
|
||||
columns: Column[];
|
||||
filteredItemsByCol: SubsonicDirectoryEntry[][];
|
||||
columnFilters: Record<number, string>;
|
||||
filterFocusCol: number | null;
|
||||
keyboardPos: NavPos | null;
|
||||
isContextMenuOpen: boolean;
|
||||
filterInputRefs: React.RefObject<Record<number, HTMLInputElement | null>>;
|
||||
wrapperRef: React.RefObject<HTMLDivElement | null>;
|
||||
setKeyboardNavActive: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setKeyboardPos: React.Dispatch<React.SetStateAction<NavPos | null>>;
|
||||
setContextAnchorPos: React.Dispatch<React.SetStateAction<NavPos | null>>;
|
||||
setFilterFocusCol: React.Dispatch<React.SetStateAction<number | null>>;
|
||||
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<HTMLDivElement>) => {
|
||||
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<HTMLElement>(
|
||||
`.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,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user