import { Theme } from "theme.slint"; import { RoundButton } from "widgets.slint"; import { ShelfView, ResultsView } from "browse.slint"; import { PlayerBar, PlayView, AlbumSheet } from "player.slint"; import { RoomView } from "room.slint"; import { AlbumTile, GridRow, CategoryTile, CategoryRow, Shelf, TrackRow, NowPlaying, RoomCard, RoomRow, SceneTile, Swatch, Tab } from "models.slint"; export { AlbumTile, GridRow, CategoryTile, CategoryRow, Shelf, TrackRow, NowPlaying, RoomCard, RoomRow, SceneTile, Swatch, Tab, Theme } export component AppWindow inherits Window { title: "Musik Delfin"; default-font-family: Theme.font; default-font-size: Theme.text-body; // The stage. In the web app a per-track animated canvas sits on top of this; here // it is the whole background, on purpose. A full-viewport repaint loop is a floor // you cannot get under while it runs at all, and it is the first thing `?pi=1` // already removes on the device this is meant to replace. background: @linear-gradient(180deg, Theme.stage-top 0%, Theme.stage-mid 45%, Theme.sea-deep 100%); // ------------------------------------------------------------------- inputs -- // "music" | "room" | "typing" - named rather than an enum so Rust owns the // ordering and the labels, as it does for every other string on screen. in property page: "music"; in property <[Tab]> tabs; in property <[RoomRow]> room-rows; in property <[SceneTile]> room-scenes; in property <[Swatch]> room-swatches; in property <[string]> shutter-presets; in property room-loading: true; in property loading: true; in property splash-text; in property status-label; in property online: false; in property play-view: false; in property show-sheet: false; in property can-go-back: false; in property <[Shelf]> shelves; in property root-screen: true; in property shelf-row: 0; in property group-label; in property category-heading; in property album-heading; in property show-search: false; in property search-text; in property mode-label; in property count-label; in property <[TrackRow]> songs; in property <[CategoryRow]> category-grid; in property <[GridRow]> grid; in property no-results: false; in property sel-index: 0; in property sel-row: 0; in property now; in property playing: false; in property progress: 0.0; in property smooth: true; in property volume: 60; in property has-bar: false; in property sheet-title; in property sheet-subtitle; in property sheet-meta; in property sheet-cover; in property sheet-is-book: false; in property <[TrackRow]> sheet-tracks; in property sheet-index: 0; // How wide the grid may be. Rust reads this back to decide the column count, so the // chunking into `GridRow`s and the layout agree about how many fit. out property grid-width: self.width - 32px - Theme.rail-clearance; // ---------------------------------------------------------------- callbacks -- // The special keys are named here rather than in Rust because this is where the // `Key.*` constants exist; Rust receives "escape"/"left"/"enter"/… or the literal // character that was typed. callback key(string, bool, bool); callback toggle(); callback next(); callback previous(); callback seek(float); callback set-volume(int); callback mute(); callback go-back(); callback enter-group(int); callback enter-category(int, string); callback open-category(string); callback open-album(string); callback play-album(string); callback play-song(string, int); callback open-play-view(); callback close-sheet(); callback play-sheet-track(int); callback select-page(string); callback toggle-light(string); callback set-light-level(string, int); callback set-light-swatch(string, int); callback set-shutter-preset(string, int); callback shutter(string, string); callback activate-scene(string); forward-focus: keys; keys := FocusScope { key-pressed(event) => { root.key( event.text == Key.Escape ? "escape" : event.text == Key.Return ? "enter" : event.text == Key.Backspace ? "backspace" : event.text == Key.Tab ? "tab" : event.text == Key.LeftArrow ? "left" : event.text == Key.RightArrow ? "right" : event.text == Key.UpArrow ? "up" : event.text == Key.DownArrow ? "down" : event.text == Key.F1 ? "f1" : event.text == Key.Space ? "space" : event.text, event.modifiers.control, event.modifiers.shift); accept } VerticalLayout { // ------------------------------------------------------------ header -- if root.page == "music" && !root.play-view: HorizontalLayout { alignment: center; spacing: 16px; padding-top: 18px; padding-bottom: 6px; Text { text: "Musik Delfin"; font-size: 34px; font-weight: 900; color: Theme.paper; } if root.status-label != "": VerticalLayout { alignment: center; Rectangle { background: Theme.paper.with-alpha(0.16); border-radius: Theme.pill; width: status.preferred-width; height: status.preferred-height; status := HorizontalLayout { padding-left: 14px; padding-right: 14px; padding-top: 6px; padding-bottom: 6px; Text { text: root.status-label; font-size: Theme.text-small + 1px; font-weight: 800; color: root.online ? Theme.paper : #ffb4a2; } } } } } // -------------------------------------------------------------- body -- if root.page == "music" && !root.play-view && root.root-screen: ShelfView { shelves: root.shelves; shelf-row: root.shelf-row; sel-index: root.sel-index; enter-group(s) => { root.enter-group(s); } enter-category(s, key) => { root.enter-category(s, key); } } if root.page == "music" && !root.play-view && !root.root-screen: ResultsView { group-label: root.group-label; category-heading: root.category-heading; album-heading: root.album-heading; show-search: root.show-search; search-text: root.search-text; mode-label: root.mode-label; count-label: root.count-label; songs: root.songs; category-grid: root.category-grid; grid: root.grid; sel-index: root.sel-index; sel-row: root.sel-row; empty: root.no-results; play-song(id, i) => { root.play-song(id, i); } open-category(key) => { root.open-category(key); } open-album(id) => { root.open-album(id); } play-album(id) => { root.play-album(id); } } if root.page == "room": RoomView { rows: root.room-rows; scenes: root.room-scenes; swatches: root.room-swatches; presets: root.shutter-presets; loading: root.room-loading; toggle-light(id) => { root.toggle-light(id); } set-level(id, n) => { root.set-light-level(id, n); } set-swatch(id, i) => { root.set-light-swatch(id, i); } set-preset(id, i) => { root.set-shutter-preset(id, i); } shutter(id, what) => { root.shutter(id, what); } activate-scene(id) => { root.activate-scene(id); } } if root.page == "typing": VerticalLayout { alignment: center; Text { text: "Tippen"; font-size: 34px; font-weight: 900; color: Theme.paper; horizontal-alignment: center; } Text { text: "Noch nicht portiert."; font-size: Theme.text-heading; font-weight: 700; color: Theme.text-dim; horizontal-alignment: center; } } if root.page == "music" && root.play-view: PlayView { now: root.now; playing: root.playing; progress: root.progress; smooth: root.smooth; volume: root.volume; toggle => { root.toggle(); } next => { root.next(); } previous => { root.previous(); } seek(f) => { root.seek(f); } set-volume(v) => { root.set-volume(v); } mute => { root.mute(); } open-album => { root.open-album(root.now.title); } } } } // The player bar floats over the browse screen rather than displacing it, which is // why the scrollers above reserve bottom padding for it. Geometry is set here // rather than inside the component: a plain element takes its *preferred* width // from its content, not its parent's width, so a bar that only declared a height // left gaps for the grid behind it to show through. if root.page == "music" && !root.play-view && root.has-bar: PlayerBar { x: 0; y: parent.height - self.height; width: parent.width; now: root.now; playing: root.playing; progress: root.progress; smooth: root.smooth; volume: root.volume; toggle => { root.toggle(); } next => { root.next(); } previous => { root.previous(); } seek(f) => { root.seek(f); } set-volume(v) => { root.set-volume(v); } mute => { root.mute(); } open-play-view => { root.open-play-view(); } } // One fixed round button in the top-left: back, everywhere it applies, so "top // left" always means the same thing. if root.can-go-back: RoundButton { x: 24px; y: 18px; label: "‹"; clicked => { root.go-back(); } } // The vertical tab rail. Right edge, vertically centred - the one fixed control // that is in the same place on every page. tab-rail := Rectangle { x: parent.width - self.width - 16px; y: (parent.height - self.height) / 2; width: 60px; height: rail.preferred-height; border-radius: 30px; background: Theme.paper.with-alpha(0.22); border-width: 1px; border-color: Theme.paper.with-alpha(0.3); rail := VerticalLayout { padding: 8px; spacing: 6px; for tab[i] in root.tabs: Rectangle { width: 44px; height: 44px; border-radius: 22px; background: root.page == tab.id ? Theme.accent : transparent; animate background { duration: 150ms; } Text { text: tab.icon; font-size: 20px; color: root.page == tab.id ? white : Theme.paper; vertical-alignment: center; horizontal-alignment: center; } TouchArea { clicked => { root.select-page(tab.id); } } } } } if root.show-sheet: AlbumSheet { width: 100%; height: 100%; title: root.sheet-title; subtitle: root.sheet-subtitle; meta: root.sheet-meta; cover: root.sheet-cover; is-book: root.sheet-is-book; tracks: root.sheet-tracks; sel-index: root.sheet-index; close => { root.close-sheet(); } play(i) => { root.play-sheet-track(i); } play-all => { root.play-sheet-track(0); } } if root.loading: Rectangle { background: Theme.sea-deep.with-alpha(0.92); TouchArea { } VerticalLayout { alignment: center; spacing: 18px; Text { text: "🐬"; font-size: 96px; horizontal-alignment: center; } Text { text: root.splash-text; font-size: 22px; font-weight: 800; color: Theme.paper; horizontal-alignment: center; } } } }