UI cleanup, visual and keyboard navigation

This commit is contained in:
2026-09-11 13:32:07 +02:00
parent df89acd9a8
commit fbf03a9847
13 changed files with 445 additions and 105 deletions

View File

@@ -28,11 +28,11 @@ import { usePlayerState } from "./hooks/usePlayerState";
import { DEFAULT_MANUAL_CONTROL, DEFAULT_TUNABLES } from "./lib/ambienceTunables";
import type { AmbienceTunables, ManualControl } from "./lib/ambienceTunables";
import type { Action, UiState } from "./lib/keyboard";
import { browseBackActions, handleKey, initialUiState, isRootShelf } from "./lib/keyboard";
import { browseBackActions, GROUP_ORDER, handleKey, initialUiState, isRootShelf } from "./lib/keyboard";
import { playPop } from "./lib/pop";
import { targetForAlbum } from "./lib/remote";
import type { Group, Results, SongHit } from "./lib/search";
import { groupOf, results as computeResults } from "./lib/search";
import { categoryMatches, groupOf, results as computeResults } from "./lib/search";
/** How long an armed "A" waits for the digit that completes the shortcut. */
const ASSIGN_PENDING_TIMEOUT_MS = 4000;
@@ -101,6 +101,23 @@ export function App() {
[library.albums, ui.search, ui.mode, ui.group, ui.category],
);
// The bare root's three shelves each compute their own categories independently
// (see BrowseView) - the keyboard handler needs the same lists, by key only, to know
// how far Ctrl+hjkl can move within a row and which category ENTER opens.
const rootShelves = useMemo(
() =>
GROUP_ORDER.map((group) =>
categoryMatches({
albums: library.albums,
search: "",
mode: "albums",
category: null,
group,
}).map((entry) => entry.key),
),
[library.albums],
);
const byId = useMemo(
() => new Map(library.albums.map((album) => [album.id, album])),
[library.albums],
@@ -249,8 +266,14 @@ export function App() {
);
// Held in a ref so the listener is installed once rather than on every state change.
const latest = useRef({ ui, results, run, openAlbumTrackCount: 0 });
latest.current = { ui, results, run, openAlbumTrackCount: openAlbum?.tracks.length ?? 0 };
const latest = useRef({ ui, results, run, openAlbumTrackCount: 0, rootShelves });
latest.current = {
ui,
results,
run,
openAlbumTrackCount: openAlbum?.tracks.length ?? 0,
rootShelves,
};
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
@@ -259,7 +282,13 @@ export function App() {
if (target && /^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName)) return;
const current = latest.current;
const actions = handleKey(event, current.ui, current.results, current.openAlbumTrackCount);
const actions = handleKey(
event,
current.ui,
current.results,
current.openAlbumTrackCount,
current.rootShelves,
);
if (!actions.length) return;
event.preventDefault();
current.run(actions);
@@ -378,6 +407,7 @@ export function App() {
search={ui.search}
category={ui.category}
selIndex={ui.selIndex}
shelfRow={ui.shelfRow}
currentAlbumId={state.album_id}
gridRef={gridRef}
onEnterGroup={onEnterGroup}
@@ -478,7 +508,7 @@ export function App() {
<HelpOverlay onClose={() => setUi((previous) => ({ ...previous, showHelp: false }))} />
)}
{ui.page === "music" && ui.view === "browse" && state && (
{ui.page === "music" && ui.view === "browse" && state && currentAlbum && (
<PlayerBar
state={state}
album={currentAlbum}