The React UI is sluggish on the kiosk and the reasons are browser-shaped: a compositor deciding what gets its own layer, a requestAnimationFrame loop repainting the viewport, and a JPEG decode per album card per cold start. The last three commits chased that through the service worker and the cover pipeline. This is the other direction - the same backend, the same layout, the same German copy, without a browser. Browse and play only. The typing game, the room-lights page, parent mode and the IR-remote assignment stay web-only, and nothing here touches python-backend. Four things carry the performance claim, in rough order of how much they should matter: Covers are downscaled once, ever. The backend serves one size - 640px on the long edge, no ?size= - and a grid card is 180. src/covers.rs fetches each cover once on a worker thread, resizes it, and writes a JPEG thumbnail to ~/.cache/musicmouse-slint/covers/. Two tiers and only two, which is what makes the cache worth keeping: a per-widget pixel size would give each album a dozen near-identical files and a fresh decode for each. The cache is dropped when the websocket announces a rescan, because that is the one moment the backend rewrites the art behind an unchanged cover URL - the trap sw.js had to learn about the hard way. The grid is virtualized. Rust hands the UI the album list pre-chunked into rows and the view puts those in a ListView, which instantiates only what is on screen. A flat list of 660 cards gives it no rows to skip, hence the chunking. Same purpose as content-visibility: auto on .grid > .card. The progress bar animates between the 2 Hz pushes rather than running a clock, so interpolation costs a property evaluation per frame on the render side and there is no equivalent of usePlaybackClock. It is suppressed for the frame a track changes on, so a new track jumps instead of sliding across two unrelated positions. Nothing on screen animates by itself. The ambient canvas is not ported, for the reason lib/lowPower.ts already gives: a loop repainting the viewport is a floor you cannot get under while it runs at all. The device must not use FemtoVG. On Mesa V3D it draws every runtime-loaded Image as solid black (slint-ui/slint#11785, open), which here means every album cover; Skia and the software renderer are unaffected. So `kiosk` is linuxkms + Skia and FemtoVG stays the default only for desktop development. Both profiles are verified to build; the kiosk binary links libinput/libgbm/libdrm and no X11 or Wayland at all. lib/search.ts, lib/keyboard.ts and lib/format.ts were already pure functions with their own tests, so they port across as pure Rust with theirs: 43 tests, no window required. The .slint files are layout only - nothing in them formats a number or picks a word. Verified against the real 660-album library rather than a fixture. tools/headless-shots.sh renders the UI inside a nested headless compositor and grabs a frame per screen, which is how that was checked on a machine whose session was locked; a Slint window needs a real compositor, and under bare Xvfb nothing maps at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
4.4 KiB
Plaintext
82 lines
4.4 KiB
Plaintext
// The palette, converted once from the web front-end's oklch tokens
|
|
// (`web/src/styles/app.css`) into the sRGB hex Slint speaks. The oklch source value is
|
|
// kept beside each one, because that is the form the design is actually maintained in -
|
|
// if a colour needs adjusting, adjust the oklch and re-convert (`src/oklch.rs`), rather
|
|
// than nudging the hex and letting the two drift.
|
|
|
|
export global Theme {
|
|
// ------------------------------------------------------------------ colours --
|
|
out property <color> ink: #113339; // oklch(30% 0.04 210)
|
|
out property <color> paper: #eef7f9; // oklch(97% 0.01 210)
|
|
out property <color> accent: #de73bd; // oklch(70% 0.16 340)
|
|
out property <color> accent-dim: #f292d3; // oklch(78% 0.14 340)
|
|
out property <color> sea-deep: #001b21; // oklch(20% 0.045 210)
|
|
out property <color> shadow: #001017; // oklch(15% 0.05 210)
|
|
|
|
// The stage gradient, which in the web app sits under an animated canvas. There is
|
|
// no canvas here: a full-viewport repaint loop is the one cost a Pi cannot absorb,
|
|
// and `?pi=1` already removes it on the device this replaces.
|
|
out property <color> stage-top: #397d88; // oklch(55% 0.07 210)
|
|
out property <color> stage-mid: #0e4b54; // oklch(38% 0.06 210)
|
|
|
|
out property <color> text-dim: #c9dbdf; // oklch(88% 0.02 210)
|
|
out property <color> text-sub: #c0d2d5; // oklch(85% 0.02 210)
|
|
out property <color> count-pink: #7d0563; // oklch(40% 0.17 340)
|
|
|
|
out property <color> card-music: #e4f1f4; // oklch(95% 0.015 210)
|
|
out property <color> card-book: #ffd5ab; // oklch(92% 0.09 55)
|
|
out property <color> book-edge-1: #ffc298; // oklch(86% 0.09 55)
|
|
out property <color> book-edge-2: #e5a880; // oklch(78% 0.09 55)
|
|
|
|
out property <color> bar-bg: #00171e; // oklch(18% 0.05 210)
|
|
out property <color> sheet-bg: #e9f4f6; // oklch(96% 0.012 210)
|
|
out property <color> backdrop: #000e12; // oklch(15% 0.03 210)
|
|
// Two track colours, not one with an alpha: the seek bar sits on the pale stage in
|
|
// the play view and on the near-black player bar in the browse view, and a single
|
|
// translucent fill that reads correctly on one is invisible on the other.
|
|
out property <color> progress-track: #1b3236; // oklch(30% 0.03 210)
|
|
out property <color> progress-track-dark: #2e4f57;
|
|
out property <color> vol-empty: #c9dbdf; // oklch(88% 0.02 210)
|
|
|
|
// Glass, without the glass. `backdrop-filter` has no counterpart here, so these are
|
|
// exactly the web app's own `data-blur="off"` fallback: a flat translucent fill.
|
|
// That is a real loss of depth and the honest substitute for it - faking a blur by
|
|
// compositing a pre-blurred copy would reintroduce the per-frame cost the blur was
|
|
// dropped to avoid.
|
|
out property <brush> glass: @linear-gradient(160deg, #eef7f924 0%, #eef7f90d 100%);
|
|
out property <color> glass-border: #eef7f929;
|
|
out property <color> row-idle: #f7fcfd1a;
|
|
out property <color> row-active: #f7fcfd38;
|
|
|
|
// ------------------------------------------------------------------- metrics --
|
|
out property <length> radius-panel: 22px;
|
|
out property <length> radius-card: 16px;
|
|
out property <length> radius-row: 12px;
|
|
out property <length> radius-sheet: 26px;
|
|
out property <length> pill: 999px;
|
|
|
|
// The album grid's track width. The web grid is `minmax(180px, 1fr)`; keeping the
|
|
// same number keeps the same number of columns at 1920, which is what the shelf
|
|
// rows and the measured card height below were both tuned against.
|
|
out property <length> card-width: 180px;
|
|
out property <length> grid-gap: 24px;
|
|
out property <length> shelf-tile: 148px;
|
|
|
|
// How much of the right edge the page must leave clear for nothing to render
|
|
// underneath the corner controls.
|
|
out property <length> rail-clearance: 88px;
|
|
|
|
// ---------------------------------------------------------------- typography --
|
|
// Heavy throughout: 700/800/900. Durations and counters use the monospace family so
|
|
// a ticking clock does not reflow the row it sits in.
|
|
out property <string> font: "Nunito";
|
|
out property <string> mono: "monospace";
|
|
|
|
out property <length> text-hero: 44px;
|
|
out property <length> text-title: 26px;
|
|
out property <length> text-heading: 20px;
|
|
out property <length> text-body: 16px;
|
|
out property <length> text-small: 13px;
|
|
out property <length> text-tiny: 12px;
|
|
}
|