Web frontend
This commit is contained in:
120
web/README.md
Normal file
120
web/README.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Musik Delphin — the web front-end
|
||||
|
||||
Browse the whole music collection and play any of it from a browser: the six figurines
|
||||
are only five of the twenty-odd albums on the shelf, and everything else was previously
|
||||
unreachable without swapping files around on disk.
|
||||
|
||||
Built from `../claude-design/Dolphin Beats.dc.html`, which is a finished interaction and
|
||||
visual spec rather than a sketch. The keyboard model, the layout, the colours and the
|
||||
German copy are ported from it; what changed is that a real player sits behind it.
|
||||
|
||||
## Running it
|
||||
|
||||
The backend serves the API. Either of these will do - `--simulate` needs no audio device
|
||||
and no libVLC at all, which makes it the easier one to develop against:
|
||||
|
||||
```sh
|
||||
cd ../python-backend
|
||||
python -m musicmouse --config ./config.yml --no-hardware # real audio
|
||||
python -m musicmouse --config ./config.yml --simulate # fake player
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```sh
|
||||
npm install
|
||||
npm run dev # http://localhost:5173, /api and the websocket proxied to :8080
|
||||
```
|
||||
|
||||
For the device, `npm run build` writes `dist/`, and `general.web.static_dir` in
|
||||
`config.yml` points at it so one process serves both.
|
||||
|
||||
## Checks
|
||||
|
||||
```sh
|
||||
npm run test # vitest over src/lib
|
||||
npx tsc --noEmit # strict, noUncheckedIndexedAccess
|
||||
```
|
||||
|
||||
Only the pure modules are tested - the search, the keyboard machine and the time
|
||||
formatting. That is where the behaviour lives; the components are mostly layout.
|
||||
|
||||
## How it is put together
|
||||
|
||||
The split that matters: **what is playing** comes from the backend, **what you are
|
||||
looking at** lives in the browser. The mockup kept both in one state object, but a real
|
||||
player has other front-ends - the buttons on the mouse, a figurine on the reader, Home
|
||||
Assistant - and this UI has to follow them rather than assume it is in charge. So
|
||||
`isPlaying`, `volume`, the position and the current album all arrive over the websocket,
|
||||
and only `search / mode / filter / category / selIndex / view / openAlbumId / showHelp`
|
||||
are local.
|
||||
|
||||
| Path | What it is |
|
||||
|---|---|
|
||||
| `src/lib/keyboard.ts` | The key map, as a pure function from a keypress to a list of actions. Testable without a DOM. |
|
||||
| `src/lib/search.ts` | Filtering and the three-level browse hierarchy. The whole library is in memory, so this is instant. |
|
||||
| `src/lib/covers.ts` | Generated cover art from the album's three colours, and the book-spine treatment. |
|
||||
| `src/hooks/usePlayerState.ts` | The websocket, with reconnect-and-reseed. |
|
||||
| `src/hooks/usePlaybackClock.ts` | Interpolates the 2 Hz position onto animation frames. |
|
||||
| `src/components/` | Layout only. |
|
||||
|
||||
`src/api/types.ts` mirrors `musicmouse/services/web/schemas.py`; the two are hand-kept in
|
||||
step.
|
||||
|
||||
## Keyboard
|
||||
|
||||
The device is meant to be driven from a keyboard, so every screen is reachable without a
|
||||
pointer. `F1` shows the same list in-app.
|
||||
|
||||
| Key | |
|
||||
|---|---|
|
||||
| `SPACE` | play / pause |
|
||||
| `CTRL+H` / `CTRL+L` | previous / next track |
|
||||
| `CTRL+J` / `CTRL+K` | quieter / louder |
|
||||
| `SHIFT+←` / `SHIFT+→` | seek 15 s |
|
||||
| `A-Z`, `0-9` | search albums as you type |
|
||||
| `?` | switch to searching individual tracks |
|
||||
| `TAB` | cycle all / music / audiobooks |
|
||||
| `← ↑ ↓ →` | move the selection while browsing; transport and volume while playing |
|
||||
| `ENTER` | play the selection |
|
||||
| `ESC` | back out one layer at a time |
|
||||
| `/` | back to browsing |
|
||||
| `F1` | this list |
|
||||
|
||||
`SHIFT+arrows` is the one addition to the mockup: plain arrows were already taken, and
|
||||
seeking is the one thing a real player can do that the mockup could not.
|
||||
|
||||
## Parent mode
|
||||
|
||||
`?parentMode=1` opens a settings panel - volume limits, the rotary step, button
|
||||
brightness, and a button to re-read the library. Saving writes the device's
|
||||
`config.yml`.
|
||||
|
||||
It is hidden from the child, not protected from them: there is no authentication on the
|
||||
endpoints behind it. See `../python-backend/README.md`.
|
||||
|
||||
## Installing it on a phone
|
||||
|
||||
It is a PWA: `public/manifest.webmanifest`, icons generated from the mascot, and a small
|
||||
service worker in `public/sw.js`. On iOS, Share → *Zum Home-Bildschirm*; it then opens
|
||||
full-screen with no Safari chrome, because iOS reads the `apple-mobile-web-app-*` meta
|
||||
tags in `index.html` rather than the manifest.
|
||||
|
||||
Two things worth knowing:
|
||||
|
||||
- **The service worker only registers over HTTPS or on `localhost`.** At
|
||||
`http://<mouse>:8080` on the LAN, iOS will still install the icon and run it
|
||||
standalone - that part is the meta tags - but there is no offline caching. Put the
|
||||
mouse behind a TLS-terminating proxy if you want that.
|
||||
- **The worker deliberately caches very little**: the app shell, the font and cover art.
|
||||
Not `/api/state`, not the library, not anything else live. A player that shows stale
|
||||
state is worse than one that says it cannot reach the mouse.
|
||||
|
||||
Nunito is self-hosted in `public/fonts/` (one variable file per subset, ~74 kB) rather
|
||||
than pulled from Google. The mouse may have no internet, and a cached shell rendering in
|
||||
a fallback face looks broken.
|
||||
|
||||
## Not implemented
|
||||
|
||||
The mockup's second page, "Mein Zimmer" (Hue lights, a roller shutter and scenes), is
|
||||
not built. Nothing here depends on it.
|
||||
Reference in New Issue
Block a user