Web frontend: perf panel, cover warmup, incremental browse rendering

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-21 11:42:33 +02:00
parent 9fd106b757
commit 2db4368fc9
24 changed files with 1178 additions and 255 deletions

View File

@@ -15,6 +15,8 @@ import type { Group, Mode, Results } from "./search";
export interface UiState {
search: string;
/** `/` was pressed: show the search box before the first letter arrives. */
searchOpen: boolean;
mode: Mode;
/** `null` is the bare root screen (three shelves); otherwise which one is open. */
group: Group | null;
@@ -41,6 +43,7 @@ export interface UiState {
export const initialUiState: UiState = {
search: "",
searchOpen: false,
mode: "albums",
group: null,
category: null,
@@ -179,7 +182,9 @@ function moveSelection(state: UiState, results: Results, dx: number, dy: number)
* should be one step too, not two, regardless of whether a category was reached that
* way or by opening the group first. Always safe to call. */
export function browseBackActions(state: UiState): Action[] {
if (state.search) return [{ type: "ui", patch: { search: "", selIndex: 0 } }];
if (state.search || state.searchOpen) {
return [{ type: "ui", patch: { search: "", searchOpen: false, selIndex: 0 } }];
}
if (state.mode === "tracks") return [{ type: "ui", patch: { mode: "albums", selIndex: 0 } }];
return [{ type: "ui", patch: { group: null, category: null, selIndex: 0 } }];
}
@@ -201,9 +206,28 @@ function escape(state: UiState): Action[] {
return browseBackActions(state);
}
/** The true root: nothing chosen yet, rendered as three shelves rather than a list. */
/** Whether the search box is on screen: something typed, `/` pressed, or track search. */
export function searchBoxShown(state: UiState): boolean {
return state.search.length > 0 || state.searchOpen || state.mode === "tracks";
}
/** The true root: nothing chosen yet, rendered as three shelves rather than a list. The
* shelves go away the moment the search box appears, before there is a query to filter by. */
export function isRootShelf(state: UiState): boolean {
return state.group === null && !state.search && state.mode === "albums" && state.category === null;
return state.group === null && !searchBoxShown(state) && state.category === null;
}
/** Album search <-> title search, keeping whatever has been typed. `searchOpen` keeps the
* box up when the switch lands on albums with nothing typed, where it would otherwise
* vanish and bring the shelves back. */
export function toggleModeActions(state: UiState): Action[] {
return [
{ type: "pop", freq: 460 },
{
type: "ui",
patch: { mode: state.mode === "tracks" ? "albums" : "tracks", searchOpen: true, selIndex: 0 },
},
];
}
export interface KeyEvent {
@@ -340,7 +364,9 @@ export function handleKey(
];
}
case "/":
return [{ type: "ui", patch: { view: "browse" } }];
// With the box already up, `/` flips what it searches; otherwise it opens it.
if (browsing && searchBoxShown(state)) return toggleModeActions(state);
return [{ type: "ui", patch: { view: "browse", searchOpen: true } }];
case "ArrowRight":
if (event.shiftKey) return [{ type: "seek", delta: SEEK_STEP }];