feat(kiosk): KioskShell touch shell + NavRail (P1, axis B)
Add AppScreen.kiosk mounting a KioskShell sibling of AppShell in the same AppWindow: a thin back bar + persistent PlayerBarSmall + a bottom NavRail of 6 tiles replace the HeaderBar/Sidebar, sharing every *State global verbatim. The NavRail routes through the SAME NavState routes the desktop uses (header-menu-navigate / request-settings / ContentView.search). Content reuses the real Home/Album/Artist/Favorites/Search/Settings views (verbatim subset of AppShell mounts) with the arithmetic content-height pattern to avoid the layout-collapse footgun. Rust lands the Kiosk screen post-login when the kiosk profile is active. Validated through the Slint 1.16.1 compiler (slintcheck stub); remaining routes + touch-interaction remap are P2/P3. cc
This commit is contained in:
4 files changed
+426
-3
No files matched your search
@@ -16,6 +16,7 @@ import { ThemeState } from "foundation/theme-state.slint";
|
||||
import { SplashScreen } from "splash/SplashScreen.slint";
|
||||
import { LoginScreen } from "login/LoginScreen.slint";
|
||||
import { AppShell } from "shell/AppShell.slint";
|
||||
import { KioskShell } from "shell/KioskShell.slint";
|
||||
import { Toast } from "primitives/Toast.slint";
|
||||
import { ShellState, MiniPlayerState, NowPlayingState, AppearanceState, UiScale, WindowControlActions } from "state.slint";
|
||||
import { MiniShell } from "miniplayer/MiniShell.slint";
|
||||
@@ -33,7 +34,7 @@ export { HomeState, HomeActions, RecentAlbumsState, DiscoverState, DiscoverActio
|
||||
|
||||
// Which top-level screen is shown. The app starts on `splash` while it
|
||||
// restores a saved session, then resolves to `shell` or `login`.
|
||||
export enum AppScreen { splash, login, shell }
|
||||
export enum AppScreen { splash, login, shell, kiosk }
|
||||
|
||||
export component AppWindow inherits Window {
|
||||
// "Show track in window title" (Appearance settings): when enabled and a
|
||||
@@ -179,6 +180,60 @@ export component AppWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// Kiosk touch shell — a sibling of AppShell (small-panel profile). Same
|
||||
// conditional-mount geometry pin as AppShell (a conditional child sizes to
|
||||
// preferred size, so width/height are bound explicitly). Forwards the same
|
||||
// callbacks to the AppWindow root.
|
||||
if root.screen == AppScreen.kiosk: KioskShell {
|
||||
width: root.width;
|
||||
height: root.height;
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
open-purchase-album(id) => {
|
||||
root.open-purchase-album(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
header-menu-navigate(route) => {
|
||||
root.header-menu-navigate(route);
|
||||
}
|
||||
discover-view-all(e, t) => {
|
||||
root.discover-view-all(e, t);
|
||||
}
|
||||
settings-bool(k, v) => {
|
||||
root.settings-bool(k, v);
|
||||
}
|
||||
settings-select(k, i) => {
|
||||
root.settings-select(k, i);
|
||||
}
|
||||
settings-slider(k, v) => {
|
||||
root.settings-slider(k, v);
|
||||
}
|
||||
settings-string(k, s) => {
|
||||
root.settings-string(k, s);
|
||||
}
|
||||
settings-reset => {
|
||||
root.settings-reset();
|
||||
}
|
||||
settings-release-device => {
|
||||
root.settings-release-device();
|
||||
}
|
||||
logout => {
|
||||
root.logout();
|
||||
}
|
||||
close-app => {
|
||||
root.close-app();
|
||||
}
|
||||
recovery-login => {
|
||||
root.recovery-login();
|
||||
}
|
||||
}
|
||||
|
||||
// App-global in-app toast overlay. Declared last so it floats over
|
||||
// every screen (splash / login / shell). Driven by ToastState via
|
||||
// crate::toast.
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
// KioskShell — small-panel touch shell (2.0.2 frente #3, Axis B).
|
||||
//
|
||||
// A SIBLING of AppShell inside the same AppWindow, mounted when
|
||||
// `AppScreen.kiosk` is active. It shares every `*State` global verbatim with the
|
||||
// desktop shell (no mirroring), so it re-hosts the SAME view components; the
|
||||
// only differences from AppShell are the chrome: a thin back bar + bottom
|
||||
// NavRail replace the HeaderBar + Sidebar, the transport is forced to
|
||||
// PlayerBarSmall, and there is no queue/lyrics side panel.
|
||||
//
|
||||
// P1 scope: the frame + NavRail + persistent transport + global back, routing
|
||||
// the reused Home/Album/Artist/Favorites/Search/Settings views. Remaining
|
||||
// ContentView routes (mix, playlist, purchases, local-library, detail views,
|
||||
// offline placeholder) are P2 — the view-mount block below is a verbatim subset
|
||||
// of AppShell's and grows there. NOTE: keeping this block in sync with AppShell
|
||||
// is temporary; a shared `ShellContent` extraction is the DRY follow-up once the
|
||||
// kiosk shell is build-verified (kept isolated here so the desktop shell is
|
||||
// untouched and cannot regress).
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Layout } from "../foundation/layout.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { NavState, ContentView, OfflineState } from "../state.slint";
|
||||
import { PlayerBarSmall } from "PlayerBarSmall.slint";
|
||||
import { NavRail } from "NavRail.slint";
|
||||
import { HomeView } from "../discover/HomeView.slint";
|
||||
import { AlbumPageView } from "../album/AlbumPageView.slint";
|
||||
import { ArtistPageView } from "../artist/ArtistPageView.slint";
|
||||
import { FavoritesView } from "../favorites/FavoritesView.slint";
|
||||
import { SearchResultsView } from "../search/SearchResultsView.slint";
|
||||
import { SettingsView } from "../settings/SettingsView.slint";
|
||||
|
||||
export component KioskShell inherits Rectangle {
|
||||
// Public interface mirrors AppShell 1:1 (app.slint forwards these to the
|
||||
// AppWindow root callbacks handled in Rust).
|
||||
callback open-album(string);
|
||||
callback open-artist(string);
|
||||
callback open-purchase-album(string);
|
||||
callback media-action(string, string, string);
|
||||
callback header-menu-navigate(string);
|
||||
callback discover-view-all(string, string);
|
||||
callback settings-bool(string, bool);
|
||||
callback settings-select(string, int);
|
||||
callback settings-slider(string, int);
|
||||
callback settings-string(string, string);
|
||||
callback settings-reset();
|
||||
callback settings-release-device();
|
||||
callback logout();
|
||||
callback close-app();
|
||||
callback recovery-login();
|
||||
|
||||
// Kiosk chrome heights. Topbar + transport are one header-row tall each; the
|
||||
// NavRail is a fat touch target.
|
||||
property <length> navrail-height: 80px;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
|
||||
// --- Back bar (workstream B: the persistent global back affordance,
|
||||
// wired to NavState history, honoring ADR-004). ------------------------
|
||||
Rectangle {
|
||||
height: Layout.header-height;
|
||||
background: Theme.surface-card;
|
||||
HorizontalLayout {
|
||||
padding-left: 6px;
|
||||
padding-right: 6px;
|
||||
alignment: start;
|
||||
back-btn := Rectangle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
y: (parent.height - self.height) / 2;
|
||||
border-radius: Radius.sm;
|
||||
background: back-ta.has-hover && NavState.can-back ? Theme.alpha-8 : transparent;
|
||||
opacity: NavState.can-back ? 1.0 : 0.32;
|
||||
Image {
|
||||
source: @image-url("../assets/icons/chevron-left.svg");
|
||||
colorize: Theme.text-secondary;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
back-ta := TouchArea {
|
||||
enabled: NavState.can-back;
|
||||
clicked => {
|
||||
NavState.request-back();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Content frame (mirrors AppShell:237-279 arithmetic-height pattern).
|
||||
// A conditionally-mounted view will not stretch to fill a layout cell, so
|
||||
// the inner Rectangle takes an EXPLICIT height and the views fill it via
|
||||
// clip — copy this rationale, it is the documented layout-collapse
|
||||
// footgun. ------------------------------------------------------------
|
||||
Rectangle {
|
||||
vertical-stretch: 1;
|
||||
background: Theme.surface-card;
|
||||
VerticalLayout {
|
||||
vertical-stretch: 1;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
padding-top: 0px;
|
||||
padding-bottom: 8px;
|
||||
|
||||
Rectangle {
|
||||
// window - back bar - transport(PlayerBarSmall = one header
|
||||
// row) - NavRail - this VL's bottom padding.
|
||||
height: root.height - Layout.header-height - Layout.header-height
|
||||
- root.navrail-height - 8px;
|
||||
vertical-stretch: 1;
|
||||
background: Theme.surface-main;
|
||||
border-radius: Radius.md;
|
||||
clip: true;
|
||||
|
||||
// Verbatim subset of AppShell's view mounts — same conditions,
|
||||
// same callback wiring; the views read global state.
|
||||
if NavState.view == ContentView.home && !OfflineState.offline: HomeView {
|
||||
album-clicked(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
discover-view-all(e, t) => {
|
||||
root.discover-view-all(e, t);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.album && !OfflineState.offline: AlbumPageView {
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.artist && !OfflineState.offline: ArtistPageView {
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.favorites && !OfflineState.offline: FavoritesView {
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.search && !OfflineState.offline: SearchResultsView {
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
}
|
||||
if NavState.view == ContentView.settings: SettingsView {
|
||||
settings-bool(k, v) => {
|
||||
root.settings-bool(k, v);
|
||||
}
|
||||
settings-select(k, i) => {
|
||||
root.settings-select(k, i);
|
||||
}
|
||||
settings-slider(k, v) => {
|
||||
root.settings-slider(k, v);
|
||||
}
|
||||
settings-string(k, s) => {
|
||||
root.settings-string(k, s);
|
||||
}
|
||||
settings-reset => {
|
||||
root.settings-reset();
|
||||
}
|
||||
settings-release-device => {
|
||||
root.settings-release-device();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Persistent transport: force PlayerBarSmall (npb=2 seam). Reused
|
||||
// verbatim; it self-heights to one header row and reads its own globals.
|
||||
PlayerBarSmall {
|
||||
height: Layout.header-height;
|
||||
open-album(id) => {
|
||||
root.open-album(id);
|
||||
}
|
||||
open-artist(id) => {
|
||||
root.open-artist(id);
|
||||
}
|
||||
media-action(kind, id, action) => {
|
||||
root.media-action(kind, id, action);
|
||||
}
|
||||
settings-bool(k, v) => {
|
||||
root.settings-bool(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Bottom NavRail (net-new). Maps each tile to the real route the
|
||||
// Sidebar/Rust dispatcher already uses. -------------------------------
|
||||
NavRail {
|
||||
height: root.navrail-height;
|
||||
discover => {
|
||||
root.header-menu-navigate("discover-home");
|
||||
}
|
||||
library => {
|
||||
root.header-menu-navigate("favorites-tracks");
|
||||
}
|
||||
search => {
|
||||
NavState.view = ContentView.search;
|
||||
}
|
||||
playing => {
|
||||
root.media-action("npb-view", "", "immersive");
|
||||
}
|
||||
visualizer => {
|
||||
root.media-action("npb-view", "", "immersive");
|
||||
}
|
||||
menu => {
|
||||
NavState.request-settings();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Bottom NavRail — small-panel touch navigation (2.0.2 frente #3, Axis B).
|
||||
//
|
||||
// Six ~80px icon+label tiles (Radius.sm rectangles, NO pills — ADR-008)
|
||||
// replacing the desktop Sidebar on the kiosk panel, driving the SAME NavState
|
||||
// routes. Net-new component; patterned after the Sidebar's nav rows and the
|
||||
// control-alignment standard: tokens/accent/radii only, no invented values,
|
||||
// icons from the existing `assets/icons` set. Labels reuse existing `@tr`
|
||||
// msgids (Discover/Library/Search/Now Playing/Visualizer/Settings) so no new
|
||||
// `.po` strings are introduced.
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { NavState, ContentView, ImmersiveState } from "../state.slint";
|
||||
|
||||
component NavTile inherits Rectangle {
|
||||
in property <image> icon;
|
||||
in property <string> label;
|
||||
in property <bool> active;
|
||||
callback clicked;
|
||||
|
||||
horizontal-stretch: 1;
|
||||
border-radius: Radius.sm;
|
||||
background: root.active
|
||||
? Theme.accent.with-alpha(0.12)
|
||||
: ta.has-hover ? Theme.alpha-6 : transparent;
|
||||
border-width: root.active ? 1px : 0px;
|
||||
border-color: Theme.accent.with-alpha(0.30);
|
||||
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
spacing: 5px;
|
||||
Image {
|
||||
source: root.icon;
|
||||
colorize: root.active ? Theme.accent : Theme.text-muted;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
horizontal-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: root.label;
|
||||
color: root.active ? Theme.accent : Theme.text-muted;
|
||||
font-size: 11.5px;
|
||||
font-weight: 600;
|
||||
horizontal-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
ta := TouchArea {
|
||||
clicked => {
|
||||
root.clicked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export component NavRail inherits Rectangle {
|
||||
// One callback per destination; KioskShell maps each to the real route.
|
||||
callback discover();
|
||||
callback library();
|
||||
callback search();
|
||||
callback playing();
|
||||
callback visualizer();
|
||||
callback menu();
|
||||
|
||||
background: Theme.surface-main;
|
||||
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
// Top hairline separating the rail from the transport bar.
|
||||
Rectangle {
|
||||
height: 1px;
|
||||
background: Theme.border-subtle;
|
||||
}
|
||||
HorizontalLayout {
|
||||
padding: 8px;
|
||||
spacing: 6px;
|
||||
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/compass.svg");
|
||||
label: @tr("Discover");
|
||||
active: NavState.view == ContentView.home;
|
||||
clicked => {
|
||||
root.discover();
|
||||
}
|
||||
}
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/library-big.svg");
|
||||
label: @tr("Library");
|
||||
active: NavState.view == ContentView.favorites;
|
||||
clicked => {
|
||||
root.library();
|
||||
}
|
||||
}
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/search.svg");
|
||||
label: @tr("Search");
|
||||
active: NavState.view == ContentView.search;
|
||||
clicked => {
|
||||
root.search();
|
||||
}
|
||||
}
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/play-fill.svg");
|
||||
label: @tr("Now Playing");
|
||||
active: ImmersiveState.open;
|
||||
clicked => {
|
||||
root.playing();
|
||||
}
|
||||
}
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/sliders-horizontal.svg");
|
||||
label: @tr("Visualizer");
|
||||
active: false;
|
||||
clicked => {
|
||||
root.visualizer();
|
||||
}
|
||||
}
|
||||
NavTile {
|
||||
icon: @image-url("../assets/icons/settings.svg");
|
||||
label: @tr("Settings");
|
||||
active: NavState.view == ContentView.settings;
|
||||
clicked => {
|
||||
root.menu();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user