Rework navigation for a consistent music/room toggle and back button

- Replace the "Mein Zimmer"/"Musik" text pills and the help button with
  two fixed circular corner buttons: a music/room toggle (top-right)
  and a single "back one level" button (top-left) used everywhere -
  search, categories, and the play view.
- Split `view` (browse/play) from a new `page` (music/room) in UiState
  so toggling to the room and back leaves the music side - search,
  selection, even the full-screen play view - exactly as it was.
- Back peels one step at a time (search, then track-search mode, then
  group+category together, since a root shelf tile sets both at once
  and undoing that jump should be one step too).
- Album cards: click the cover to open the track list, click the title
  to start playing immediately (matching what Enter already did from
  the keyboard). Audiobook cards drop the artist line and clamp the
  title to a fixed height so covers stay aligned across a row.
- Root shelf group headings ("Musik", "Hörbücher", "Podcasts") are now
  the link into that group's full category grid, replacing the
  separate search-icon button.
- "Taste zuweisen" is now an icon-only round button, moved to the
  bottom-right so it doesn't sit under the new toggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-11 11:12:39 +02:00
parent 69119bb72a
commit ba7f082f48
7 changed files with 237 additions and 205 deletions

View File

@@ -19,7 +19,10 @@ export interface UiState {
group: Group | null;
category: string | null;
selIndex: number;
view: "browse" | "play" | "room";
/** Which top-level page is showing. Orthogonal to `view`/`search`/`group`/… below,
* so toggling to the room and back leaves the music side exactly as it was. */
page: "music" | "room";
view: "browse" | "play";
openAlbumId: string | null;
showHelp: boolean;
cols: number;
@@ -34,6 +37,7 @@ export const initialUiState: UiState = {
group: null,
category: null,
selIndex: 0,
page: "music",
view: "browse",
openAlbumId: null,
showHelp: false,
@@ -100,6 +104,18 @@ function moveSelection(state: UiState, results: Results, dx: number, dy: number)
return [{ type: "ui", patch: { selIndex: index } }];
}
/** One step of "back" within the browse hierarchy - search, then track-search mode,
* then group/category together - the same peeling order ESC and the top-left back
* button both use. `group` and `category` clear as one step because a root shelf tile
* sets both at once (jumping straight to one category's albums); undoing that jump
* 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.mode === "tracks") return [{ type: "ui", patch: { mode: "albums", selIndex: 0 } }];
return [{ type: "ui", patch: { group: null, category: null, selIndex: 0 } }];
}
/** ESC peels one layer off at a time rather than dumping you back at the top. */
function escape(state: UiState): Action[] {
if (state.assignPending) {
@@ -108,17 +124,17 @@ function escape(state: UiState): Action[] {
if (state.showHelp || state.openAlbumId !== null) {
return [{ type: "ui", patch: { showHelp: false, openAlbumId: null } }];
}
if (state.view === "play" || state.view === "room") {
if (state.page === "room") {
return [{ type: "ui", patch: { page: "music" } }];
}
if (state.view === "play") {
return [{ type: "ui", patch: { view: "browse" } }];
}
if (state.search) return [{ type: "ui", patch: { search: "", selIndex: 0 } }];
if (state.mode === "tracks") return [{ type: "ui", patch: { mode: "albums", selIndex: 0 } }];
if (state.category !== null) return [{ type: "ui", patch: { category: null, selIndex: 0 } }];
return [{ type: "ui", patch: { group: null, selIndex: 0 } }];
return browseBackActions(state);
}
/** The true root: nothing chosen yet, rendered as three shelves rather than a list. */
function isRootShelf(state: UiState): boolean {
export function isRootShelf(state: UiState): boolean {
return state.group === null && !state.search && state.mode === "albums" && state.category === null;
}
@@ -157,6 +173,13 @@ export function handleKey(event: KeyEvent, state: UiState, results: Results): Ac
return [{ type: "assign", digit: key }, { type: "ui", patch: { assignPending: false } }];
}
// The room page has no browse hierarchy of its own - only ESC (handled below) and
// transport controls make sense there. Everything else would otherwise mutate the
// hidden music state without anything on screen to show for it.
if (state.page === "room" && key !== "Escape") {
return key === " " ? [{ type: "pop", freq: 340 }, { type: "toggle" }] : [];
}
const browsing = state.view === "browse" && state.openAlbumId === null && !state.showHelp;
switch (key) {