From 6981890be0e798f6fa379a61433f097042da0baa Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Fri, 2 Oct 2026 21:53:06 +0200 Subject: [PATCH] Web frontend: hide app title and row titles, regroup help sheet, drop Ctrl+d/u - New SHOW_APP_TITLE switch (off): the mascot + title header goes, the status pill stays on a slim row of its own. - SHOW_ROW_TITLES off; with only icons, the shelf icon moves into a column left of the tiles so an icon-only shelf is no taller than one without. - Help sheet regrouped into sections in a compact two-column grid, and reachable from a small keyboard button (not on phones or the typing page). - Ctrl+d/Ctrl+u half-page jumps removed. Co-Authored-By: Claude Opus 5.5 --- web/src/App.tsx | 25 +++++ web/src/components/AppHeader.tsx | 21 +++++ web/src/components/BrowseView.tsx | 74 ++++++++------- web/src/components/HelpOverlay.tsx | 122 ++++++++++++++++--------- web/src/lib/__tests__/keyboard.test.ts | 17 ---- web/src/lib/keyboard.ts | 14 +-- web/src/lib/theme.ts | 13 ++- 7 files changed, 180 insertions(+), 106 deletions(-) diff --git a/web/src/App.tsx b/web/src/App.tsx index e2686f7..77a0aaa 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -663,6 +663,31 @@ export function App() { )} {PERF_ENABLED && perfOpen && setPerfOpen(false)} />} + {/* The F1 sheet, reachable without knowing about F1. Not on a phone (no keyboard) + or the typing page (TippenApp has its own help and owns every key there). */} + {!phone && ui.page !== "typing" && !ui.showHelp && ( + + )} + {(!state || library.loading) && } {debugDynamicUI && debugSnapshot && ( diff --git a/web/src/components/AppHeader.tsx b/web/src/components/AppHeader.tsx index 8fa84d4..969ab00 100644 --- a/web/src/components/AppHeader.tsx +++ b/web/src/components/AppHeader.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from "react"; +import { SHOW_APP_TITLE } from "../lib/theme"; + interface Props { title?: string; mascot?: string; @@ -15,6 +17,25 @@ export function AppHeader({ status, compact = false, }: Props) { + if (!SHOW_APP_TITLE) { + // Still clear the translucent standalone-mode status bar, and keep the status + // pill - it is the only place an offline connection shows on these screens. + return ( +
+ {status} +
+ ); + } + const mascotSize = compact ? 44 : 64; return (
- {(SHOW_ROW_TITLES || SHOW_ROW_ICONS) && ( + {SHOW_ROW_TITLES && (
{SHOW_ROW_ICONS && {GROUP_ICON[shelfGroup]}} - {SHOW_ROW_TITLES && GROUP_LABEL[shelfGroup]} - {SHOW_ROW_TITLES && ›} + {GROUP_LABEL[shelfGroup]} + ›
)} - {shelfCategories.length === 0 ? ( -
- Noch nichts hier -
- ) : ( - - {shelfCategories.map((entry, tileIndex) => ( - { - // Otherwise this bubbles to the panel's own onClick, which would - // open the group root instead of the category just clicked. - event.stopPropagation(); - onEnterGroup(shelfGroup, entry.key); - }} - /> - ))} - + {SHOW_ROW_ICONS && !SHOW_ROW_TITLES && ( + {GROUP_ICON[shelfGroup]} )} +
+ {shelfCategories.length === 0 ? ( +
+ Noch nichts hier +
+ ) : ( + + {shelfCategories.map((entry, tileIndex) => ( + { + // Otherwise this bubbles to the panel's own onClick, which would + // open the group root instead of the category just clicked. + event.stopPropagation(); + onEnterGroup(shelfGroup, entry.key); + }} + /> + ))} + + )} +
))} diff --git a/web/src/components/HelpOverlay.tsx b/web/src/components/HelpOverlay.tsx index 86e528c..de0195f 100644 --- a/web/src/components/HelpOverlay.tsx +++ b/web/src/components/HelpOverlay.tsx @@ -1,66 +1,104 @@ /** The Zaubertasten sheet (F1). Mirrors lib/keyboard.ts - if a binding changes there, * it changes here. */ -const KEYS: Array<[caps: string[], label: string]> = [ - [["LEER"], "Play / Pause"], - [["SHIFT+H", "SHIFT+L"], "Song zurück / vor"], - [["SHIFT+J", "SHIFT+K"], "Leiser / lauter"], - [["SHIFT+M"], "Stumm"], - [["⇧←", "⇧→"], "15 Sekunden zurück / vor"], - [["← ↑ ↓ →"], "Auswahl bewegen"], - [["CTRL+H/J/K/L"], "Auswahl bewegen (Raster oder Titelliste)"], - [["CTRL+D", "CTRL+U"], "Eine halbe Seite weiter / zurück"], - [["A-Z"], "Album oder Hörbuch suchen"], - [["?"], "Einzelne Titel suchen"], - [["TAB"], "Musik / Hörbücher / Podcasts"], - [["ENTER"], "Auswahl abspielen"], - [["⇧ENTER"], "Titelliste öffnen"], - [["A", "0-9"], "Aktuellen Titel einer Fernbedienungs-Taste zuweisen"], - [["F1"], "Diese Hilfe"], - [["ESC"], "Schließen / Suche löschen"], +import { Fragment } from "react"; + +const SECTIONS: Array<[title: string, rows: Array<[caps: string[], label: string]>]> = [ + [ + "Abspielen", + [ + [["LEER"], "Play / Pause"], + [["⇧H", "⇧L"], "Song zurück / vor"], + [["⇧J", "⇧K"], "Leiser / lauter"], + [["⇧M"], "Stumm"], + [["⇧←", "⇧→"], "15 Sekunden zurück / vor"], + ], + ], + [ + "Auswählen", + [ + [["← ↑ ↓ →"], "Auswahl bewegen"], + [["STRG+HJKL"], "Auswahl bewegen"], + [["ENTER"], "Auswahl abspielen"], + [["⇧ENTER"], "Titelliste öffnen"], + [["TAB"], "Musik / Hörbücher / Podcasts"], + ], + ], + [ + "Suchen", + [ + [["A-Z"], "Album oder Hörbuch suchen"], + [["?"], "Einzelne Titel suchen"], + [["/"], "Suche öffnen / Alben ↔ Titel"], + ], + ], + [ + "Sonstiges", + [ + [["A", "0-9"], "Titel auf Fernbedienungs-Taste legen"], + [["F1"], "Diese Hilfe"], + [["ESC"], "Schließen / Suche löschen"], + ], + ], ]; +const CAP_STYLE = { minWidth: 0, padding: "3px 7px", fontSize: 11 } as const; + export function HelpOverlay({ onClose }: { onClose: () => void }) { return (
-
+
⌨️ Zaubertasten
-
- {KEYS.map(([caps, label]) => ( -
-
- {caps.map((cap) => ( -
1 ? 0 : 56 }}> - {cap} -
- ))} -
-
- {label} -
-
- ))} -
- Tippe irgendwohin, um zu schließen + {SECTIONS.map(([title, rows]) => ( + +
+ {title} +
+ {rows.map(([caps, label]) => ( + +
+ {caps.map((cap) => ( + + {cap} + + ))} +
+
+ {label} +
+
+ ))} +
+ ))}
diff --git a/web/src/lib/__tests__/keyboard.test.ts b/web/src/lib/__tests__/keyboard.test.ts index 7aeaa0c..d06cca3 100644 --- a/web/src/lib/__tests__/keyboard.test.ts +++ b/web/src/lib/__tests__/keyboard.test.ts @@ -122,23 +122,6 @@ describe("keyboard", () => { ); }); - it("jumps a half-page with Ctrl+d/Ctrl+u", () => { - const ui = { ...initialUiState, group: "music" as const, cols: 2 }; - const grid: Results = { - songs: [], - categories: Array.from({ length: 10 }, (_, i) => ({ key: String(i), albums: [] })), - albums: [], - total: 10, - }; - // 3 rows of 2 columns each = 6. - expect(handleKey(key("d", { ctrlKey: true }), { ...ui, selIndex: 0 }, grid)).toEqual([ - { type: "ui", patch: { selIndex: 6 } }, - ]); - expect(handleKey(key("u", { ctrlKey: true }), { ...ui, selIndex: 8 }, grid)).toEqual([ - { type: "ui", patch: { selIndex: 2 } }, - ]); - }); - it("does nothing with Ctrl+hjkl outside the browse grid, so it never fights the play view's own bindings", () => { expect(press("l", { ...initialUiState, view: "play" }, { ctrlKey: true })).toEqual([]); }); diff --git a/web/src/lib/keyboard.ts b/web/src/lib/keyboard.ts index 508922d..56bc0cf 100644 --- a/web/src/lib/keyboard.ts +++ b/web/src/lib/keyboard.ts @@ -85,9 +85,6 @@ export const GROUP_ORDER: Group[] = ["music", "audiobooks", "podcasts"]; export const VOLUME_STEP = 10; export const SEEK_STEP = 15; -/** How far Ctrl+d/Ctrl+u jump through the grid - vim's own half-page scroll, applied to - * rows of cards instead of lines of text. */ -export const PAGE_ROWS = 3; /** Shift+h/j/k/l/m: transport, reachable from anywhere, matching the muscle memory of * every other vim-ish media app. Keyed on the exact shifted letter, so this only ever @@ -292,8 +289,7 @@ export function handleKey( // Ctrl+hjkl moves the highlight in whichever list is on screen - the open album's // track list if one is open, otherwise the browse grid - the same job the arrow keys - // already do, just reachable without leaving the home row. Ctrl+d/u are vim's own - // half-page jump, applied to rows of cards. + // already do, just reachable without leaving the home row. if (event.ctrlKey || event.metaKey) { if (state.openAlbumId !== null) { switch (key.toLowerCase()) { @@ -317,7 +313,7 @@ export function handleKey( } if (state.view !== "browse" || state.showHelp) return []; const lower = key.toLowerCase(); - if (!"hjkldu".includes(lower)) return []; + if (!"hjkl".includes(lower)) return []; if (isRootShelf(state)) { switch (lower) { case "h": @@ -325,10 +321,8 @@ export function handleKey( case "l": return moveRootShelf(state, 1, 0, rootShelves); case "j": - case "d": return moveRootShelf(state, 0, 1, rootShelves); case "k": - case "u": return moveRootShelf(state, 0, -1, rootShelves); default: return []; @@ -343,10 +337,6 @@ export function handleKey( return moveSelection(state, results, 0, 1); case "k": return moveSelection(state, results, 0, -1); - case "d": - return moveSelection(state, results, 0, PAGE_ROWS); - case "u": - return moveSelection(state, results, 0, -PAGE_ROWS); default: return []; } diff --git a/web/src/lib/theme.ts b/web/src/lib/theme.ts index 8443864..3fa226b 100644 --- a/web/src/lib/theme.ts +++ b/web/src/lib/theme.ts @@ -21,12 +21,19 @@ import type { UiState } from "./keyboard"; export const SHOW_ROW_TINT = false; /** Show each shelf's title text ("Musik", "Hörbücher", "Podcasts") above its tiles. */ -export const SHOW_ROW_TITLES = true; +export const SHOW_ROW_TITLES = false; -/** Show the small icon badge (🎵/📖/🎙️) beside the title. Stands alone fine with - * `SHOW_ROW_TITLES` off (an icon-only header), but the original look needs both. */ +/** Show the small icon badge (🎵/📖/🎙️) beside the title. With `SHOW_ROW_TITLES` + * off it moves out of the header into a column left of the tiles, so an icon-only + * shelf is no taller than one with neither; the original look needs both. */ export const SHOW_ROW_ICONS = true; +/** Show the mascot + title header ("Musik Delfin", "Mein Zimmer") at the top of the + * browse and room screens. Off reclaims its vertical space; the connection/figure + * status pill it carries still shows, on a slim row of its own, whenever it has + * something to say. */ +export const SHOW_APP_TITLE = false; + /** Vertical gap between shelves on the root browse screen, in px. */ export const ROW_SPACING = 25;