feat(qbz-slint): header redesign, Lucide icons, scrollbar fixes

- Replace every Unicode glyph placeholder with Lucide icons via QbzIcon
  (player transport, carousel nav, album-card overlay, album-page
  actions, gear, search, checkbox, sidebar).
- Header: the search field is centered to the window width and gets a
  search icon; the header and player bar now use the elevated surface,
  so all chrome (header, sidebar, player bar) shares one color over the
  darker content area.
- The sidebar user card moves to the header as a user element (name +
  QBZ logo) that opens a dropdown — Settings, Keyboard Shortcuts,
  Documentation, Report an Issue, About, plus Log Out and Close. Log Out
  clears the session and returns to login; Close quits the app.
- Content ScrollViews disable the horizontal scrollbar — no view scrolls
  horizontally, so the phantom reserved lane is gone.

cc
This commit is contained in:
vicrodh committed 2026-05-18 14:58:58 -06:00
1 parent 636b3e8a06
commit 8c27de5651
13 files changed
+389 -165

No files matched your search

+27 -8
View File
@@ -17,8 +17,10 @@ use tokio::net::TcpListener;
const OAUTH_TIMEOUT: Duration = Duration::from_secs(180);
/// Run the full system-browser OAuth login. Returns the authenticated
/// user id on success.
pub async fn login_via_system_browser<A>(runtime: &Arc<AppRuntime<A>>) -> Result<u64, String>
/// `(user_id, display_name)` on success.
pub async fn login_via_system_browser<A>(
runtime: &Arc<AppRuntime<A>>,
) -> Result<(u64, String), String>
where
A: FrontendAdapter + Send + Sync + 'static,
{
@@ -68,6 +70,7 @@ where
.map_err(|e| e.to_string())?
};
let user_id = session.user_id;
let display_name = session.display_name.clone();
let token = session.user_auth_token.clone();
// Emit LoggedIn through the core (idempotent set_session).
@@ -82,16 +85,18 @@ where
}
log::info!("[qbz-slint] login complete for user {user_id}");
Ok(user_id)
Ok((user_id, display_name))
}
/// Restore a previously saved session from the encrypted token store
/// (keyring + AES-256-GCM file — the same store the Tauri app uses).
///
/// Returns `Ok(Some(user_id))` when a saved token is valid and the session
/// is activated, `Ok(None)` when there is no token. A token that exists
/// but is rejected by Qobuz is cleared and treated as `None`.
pub async fn restore_saved_session<A>(runtime: &Arc<AppRuntime<A>>) -> Result<Option<u64>, String>
/// Returns `Ok(Some((user_id, display_name)))` when a saved token is valid
/// and the session is activated, `Ok(None)` when there is no token. A token
/// that exists but is rejected by Qobuz is cleared and treated as `None`.
pub async fn restore_saved_session<A>(
runtime: &Arc<AppRuntime<A>>,
) -> Result<Option<(u64, String)>, String>
where
A: FrontendAdapter + Send + Sync + 'static,
{
@@ -112,10 +117,11 @@ where
match core.login_with_token(&token).await {
Ok(session) => {
let user_id = session.user_id;
let display_name = session.display_name.clone();
core.set_session(session).await.map_err(|e| e.to_string())?;
runtime.activate(user_id).await?;
log::info!("[qbz-slint] restored saved session for user {user_id}");
Ok(Some(user_id))
Ok(Some((user_id, display_name)))
}
Err(e) => {
log::warn!("[qbz-slint] saved token rejected, clearing: {e}");
@@ -125,6 +131,19 @@ where
}
}
/// Log out: clear the saved token, deactivate the per-user session, and
/// drop the Qobuz client session.
pub async fn logout<A>(runtime: &Arc<AppRuntime<A>>) -> Result<(), String>
where
A: FrontendAdapter + Send + Sync + 'static,
{
let _ = qbz_credentials::clear_oauth_token();
let _ = runtime.core().logout().await;
runtime.deactivate().await?;
log::info!("[qbz-slint] logged out");
Ok(())
}
/// Accept connections until one carries the OAuth code, replying with a
/// minimal success page. Browser noise (favicon requests, etc.) is answered
/// and skipped.
+33 -5
View File
@@ -40,8 +40,10 @@ async fn enter_shell(
runtime: Arc<AppRuntime<SlintAdapter>>,
weak: slint::Weak<AppWindow>,
image_cache: artwork::ImageCache,
user_name: String,
) {
let _ = weak.upgrade_in_event_loop(|w| {
let _ = weak.upgrade_in_event_loop(move |w| {
w.global::<SessionState>().set_user_name(user_name.into());
w.global::<HomeState>().set_loading(true);
w.set_screen(AppScreen::Shell);
});
@@ -112,9 +114,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
log::error!("[qbz-slint] core init failed: {e}");
}
match auth::restore_saved_session(&runtime).await {
Ok(Some(user_id)) => {
Ok(Some((user_id, user_name))) => {
log::info!("[qbz-slint] session restored for user {user_id}");
enter_shell(runtime, weak, image_cache).await;
enter_shell(runtime, weak, image_cache, user_name).await;
}
Ok(None) => {
log::info!("[qbz-slint] no saved session — showing login");
@@ -142,9 +144,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let image_cache = image_cache.clone();
handle.spawn(async move {
match auth::login_via_system_browser(&runtime).await {
Ok(user_id) => {
Ok((user_id, user_name)) => {
log::info!("[qbz-slint] authenticated as user {user_id}");
enter_shell(runtime, weak, image_cache).await;
enter_shell(runtime, weak, image_cache, user_name).await;
}
Err(e) => log::error!("[qbz-slint] sign-in failed: {e}"),
}
@@ -231,6 +233,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
// Log out: clear the session and return to the login screen.
{
let runtime = app_runtime.clone();
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
window.on_logout(move || {
let runtime = runtime.clone();
let weak = weak.clone();
handle.spawn(async move {
if let Err(e) = auth::logout(&runtime).await {
log::error!("[qbz-slint] logout failed: {e}");
}
let _ = weak.upgrade_in_event_loop(|w| {
w.global::<NavState>().set_showing_album(false);
w.global::<SessionState>().set_user_name("".into());
w.set_screen(AppScreen::Login);
});
});
});
}
window.on_close_app(|| {
log::info!("[qbz-slint] closing");
let _ = slint::quit_event_loop();
});
window.on_open_tos(|| {
dispatch(AppCommand::OpenTermsOfService);
if let Err(e) = open::that(QOBUZ_TOS_URL) {
+28 -18
View File
@@ -10,10 +10,11 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { Spacing } from "../foundation/spacing.slint";
import { AlbumState, AlbumTrackItem, NavState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
component CircleAction inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> primary: false;
in property <length> diameter: 40px;
callback clicked();
@@ -25,12 +26,13 @@ component CircleAction inherits Rectangle {
? (ta.has-hover ? Theme.accent-hover : Theme.accent)
: (ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
Text {
text: root.glyph;
color: root.primary ? Theme.accent-text : Theme.text-secondary;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: 17px;
height: 17px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.primary ? Theme.accent-text : Theme.text-secondary;
}
ta := TouchArea {
@@ -133,6 +135,7 @@ export component AlbumPageView inherits Rectangle {
background: Theme.surface-main;
ScrollView {
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
padding-left: 24px;
padding-right: 24px;
@@ -151,11 +154,14 @@ export component AlbumPageView inherits Rectangle {
}
back-row := HorizontalLayout {
spacing: 6px;
Text {
text: "‹";
color: back.has-hover ? Theme.text-secondary : Theme.text-muted;
font-size: 18px;
vertical-alignment: center;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/arrow-left.svg");
width: 16px;
height: 16px;
tint: back.has-hover ? Theme.text-secondary : Theme.text-muted;
}
}
Text {
text: @tr("Back");
@@ -222,18 +228,22 @@ export component AlbumPageView inherits Rectangle {
HorizontalLayout {
alignment: start;
spacing: 12px;
CircleAction { glyph: "▶"; primary: true; diameter: 44px; }
VerticalLayout {
alignment: center;
CircleAction { glyph: "⇄"; }
CircleAction {
icon: @image-url("../assets/icons/play.svg");
primary: true;
diameter: 44px;
}
VerticalLayout {
alignment: center;
CircleAction { glyph: "♡"; }
CircleAction { icon: @image-url("../assets/icons/shuffle.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { glyph: "⋯"; }
CircleAction { icon: @image-url("../assets/icons/heart.svg"); }
}
VerticalLayout {
alignment: center;
CircleAction { icon: @image-url("../assets/icons/ellipsis.svg"); }
}
}
}
+9 -1
View File
@@ -10,7 +10,7 @@ import { LoginScreen } from "login/LoginScreen.slint";
import { AppShell } from "shell/AppShell.slint";
// Re-export the state globals so the Rust layer can populate them.
export { HomeState, AlbumState, NavState } from "state.slint";
export { HomeState, AlbumState, NavState, SessionState } from "state.slint";
// Which top-level screen is shown. The app starts on `splash` while it
// restores a saved session, then resolves to `shell` or `login`.
@@ -32,6 +32,8 @@ export component AppWindow inherits Window {
callback start-offline();
callback open-tos();
callback open-album(string);
callback logout();
callback close-app();
if root.screen == AppScreen.splash: SplashScreen { }
@@ -54,5 +56,11 @@ export component AppWindow inherits Window {
open-album(id) => {
root.open-album(id);
}
logout => {
root.logout();
}
close-app => {
root.close-app();
}
}
}
+14 -12
View File
@@ -11,13 +11,14 @@ import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { Spacing } from "../foundation/spacing.slint";
import { AlbumCardItem } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
// Circular hover-overlay action button.
component OverlayButton inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> primary: false;
in property <length> diameter: 36px;
in property <length> glyph-size: 16px;
in property <length> icon-size: 16px;
width: root.diameter;
height: root.diameter;
@@ -26,12 +27,13 @@ component OverlayButton inherits Rectangle {
border-width: root.primary ? 0px : 1.5px;
border-color: #ffffffe6;
Text {
text: root.glyph;
color: root.primary ? #000000 : #ffffff;
font-size: root.glyph-size;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: root.icon-size;
height: root.icon-size;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.primary ? #000000 : #ffffff;
}
}
@@ -109,17 +111,17 @@ export component AlbumCard inherits Rectangle {
VerticalLayout {
alignment: center;
OverlayButton { glyph: "♡"; }
OverlayButton { icon: @image-url("../assets/icons/heart.svg"); }
}
OverlayButton {
glyph: "▶";
icon: @image-url("../assets/icons/play.svg");
primary: true;
diameter: 44px;
glyph-size: 17px;
icon-size: 18px;
}
VerticalLayout {
alignment: center;
OverlayButton { glyph: "⋯"; glyph-size: 18px; }
OverlayButton { icon: @image-url("../assets/icons/ellipsis.svg"); }
}
}
+11 -9
View File
@@ -9,9 +9,10 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { DiscoverSection } from "../state.slint";
import { AlbumCard } from "AlbumCard.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavButton inherits Rectangle {
in property <string> glyph;
in property <image> icon;
in property <bool> enabled: true;
callback clicked();
@@ -23,12 +24,13 @@ component NavButton inherits Rectangle {
? Theme.surface-hover
: Theme.surface-elevated;
Text {
text: root.glyph;
color: root.enabled ? Theme.text-primary : Theme.text-muted;
font-size: 16px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: root.enabled ? Theme.text-primary : Theme.text-muted;
}
ta := TouchArea {
@@ -77,7 +79,7 @@ export component Carousel inherits VerticalLayout {
VerticalLayout {
alignment: center;
NavButton {
glyph: "‹";
icon: @image-url("../assets/icons/chevron-left.svg");
enabled: root.current-page > 0;
clicked => {
root.page = root.current-page - 1;
@@ -87,7 +89,7 @@ export component Carousel inherits VerticalLayout {
VerticalLayout {
alignment: center;
NavButton {
glyph: "›";
icon: @image-url("../assets/icons/chevron-right.svg");
enabled: root.current-page < root.page-count - 1;
clicked => {
root.page = root.current-page + 1;
+20 -7
View File
@@ -10,6 +10,7 @@ import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { HomeState } from "../state.slint";
import { Carousel } from "Carousel.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
component Tab inherits Rectangle {
@@ -50,8 +51,18 @@ component GenreButton inherits Rectangle {
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
HorizontalLayout {
padding-left: 14px;
padding-left: 12px;
padding-right: 14px;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/list-filter.svg");
width: 14px;
height: 14px;
tint: Theme.text-secondary;
}
}
Text {
text: @tr("Filter by genre");
color: Theme.text-secondary;
@@ -76,12 +87,13 @@ component GearButton inherits Rectangle {
border-radius: 4px;
background: ta.has-hover ? Theme.surface-hover : transparent;
Text {
text: "⚙";
color: ta.has-hover ? Theme.text-primary : Theme.text-muted;
font-size: 17px;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: @image-url("../assets/icons/settings.svg");
width: 18px;
height: 18px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
ta := TouchArea {
@@ -144,6 +156,7 @@ export component HomeView inherits Rectangle {
// --- Scrollable sections -----------------------------------------
ScrollView {
vertical-stretch: 1;
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
padding: 32px;
spacing: 28px;
@@ -1,6 +1,7 @@
// Small square checkbox primitive matching the QBZ dark theme.
import { Theme } from "../foundation/semantic-colors.slint";
import { QbzIcon } from "QbzIcon.slint";
export component QbzCheckbox inherits Rectangle {
in-out property <bool> checked: false;
@@ -13,14 +14,14 @@ export component QbzCheckbox inherits Rectangle {
border-width: root.checked ? 0px : 1.5px;
border-color: Theme.text-muted;
Text {
text: "✓";
QbzIcon {
source: @image-url("../assets/icons/check.svg");
visible: root.checked;
color: Theme.accent-text;
font-size: 13px;
font-weight: 700;
horizontal-alignment: center;
vertical-alignment: center;
width: 12px;
height: 12px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.accent-text;
}
TouchArea {
+10 -1
View File
@@ -13,13 +13,22 @@ import { AlbumPageView } from "../album/AlbumPageView.slint";
export component AppShell inherits Rectangle {
callback open-album(string);
callback logout();
callback close-app();
background: Theme.surface-main;
VerticalLayout {
spacing: 0px;
HeaderBar { }
HeaderBar {
logout => {
root.logout();
}
close-app => {
root.close-app();
}
}
Rectangle {
height: Layout.hairline;
background: Theme.border-subtle;
+185 -22
View File
@@ -1,11 +1,13 @@
// Top header bar — primary navigation tabs and the search field.
// Top header bar — primary navigation tabs, a window-centered search
// field, and the user element with its dropdown menu.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { LineEdit } from "std-widgets.slint";
import { SessionState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component NavTab inherits Rectangle {
in property <string> label;
@@ -37,35 +39,196 @@ component NavTab inherits Rectangle {
}
}
export component HeaderBar inherits Rectangle {
height: Layout.header-height;
background: Theme.surface-main;
component MenuItem inherits Rectangle {
in property <image> icon;
in property <string> label;
callback clicked();
height: 34px;
background: ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: Spacing.md;
padding-right: Spacing.md;
spacing: Spacing.xs;
padding-left: 12px;
padding-right: 18px;
spacing: 10px;
VerticalLayout {
alignment: center;
HorizontalLayout {
spacing: Spacing.xs;
NavTab { label: "Discover"; active: true; }
NavTab { label: "Library"; }
NavTab { label: "Local Library"; }
NavTab { label: "My QBZ"; }
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
tint: Theme.text-secondary;
}
}
VerticalLayout {
alignment: center;
Text {
text: root.label;
color: Theme.text-secondary;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
HorizontalLayout {
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component HeaderBar inherits Rectangle {
callback logout();
callback close-app();
height: Layout.header-height;
background: Theme.surface-card;
// --- Nav tabs (left) -------------------------------------------------
VerticalLayout {
x: Spacing.md;
height: parent.height;
alignment: center;
HorizontalLayout {
spacing: Spacing.xs;
NavTab { label: "Discover"; active: true; }
NavTab { label: "Library"; }
NavTab { label: "Local Library"; }
NavTab { label: "My QBZ"; }
}
}
// --- Search field, centered to the header width ----------------------
Rectangle {
width: 320px;
height: 32px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
border-radius: 6px;
border-width: 1px;
border-color: Theme.border-subtle;
background: Theme.surface-elevated;
HorizontalLayout {
padding-left: 10px;
padding-right: 10px;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 14px;
height: 14px;
tint: Theme.text-muted;
}
}
Text {
text: @tr("Search");
color: Theme.text-muted;
font-size: 13px;
vertical-alignment: center;
horizontal-stretch: 1;
}
}
}
// --- User element + dropdown (right) ---------------------------------
user-area := TouchArea {
width: user-row.preferred-width + 16px;
height: 36px;
x: parent.width - self.width - Spacing.md;
y: (parent.height - self.height) / 2;
mouse-cursor: pointer;
clicked => {
menu.show();
}
Rectangle {
width: 100%;
height: 100%;
border-radius: Radius.sm;
background: user-area.has-hover ? Theme.surface-hover : transparent;
}
user-row := HorizontalLayout {
x: 8px;
height: parent.height;
spacing: 8px;
VerticalLayout {
alignment: center;
Text {
text: SessionState.user-name == ""
? @tr("Guest")
: SessionState.user-name;
color: Theme.text-primary;
font-size: Typography.body;
}
}
VerticalLayout {
alignment: center;
Image {
source: @image-url("../assets/qbz-logo.png");
width: 28px;
height: 28px;
image-fit: contain;
}
}
}
}
menu := PopupWindow {
x: parent.width - 234px - Spacing.md;
y: Layout.header-height - 4px;
width: 234px;
height: 254px;
Rectangle {
background: Theme.surface-elevated;
border-radius: Radius.md;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 24px;
drop-shadow-color: Theme.card-shadow;
drop-shadow-offset-y: 6px;
VerticalLayout {
padding-top: 6px;
padding-bottom: 6px;
MenuItem {
icon: @image-url("../assets/icons/settings.svg");
label: @tr("Settings");
}
MenuItem {
icon: @image-url("../assets/icons/keyboard.svg");
label: @tr("Keyboard Shortcuts");
}
MenuItem {
icon: @image-url("../assets/icons/book-open.svg");
label: @tr("Documentation");
}
MenuItem {
icon: @image-url("../assets/icons/bug.svg");
label: @tr("Report an Issue");
}
MenuItem {
icon: @image-url("../assets/icons/info.svg");
label: @tr("About");
}
Rectangle {
width: 360px;
search := LineEdit {
placeholder-text: "Search";
height: 1px;
background: Theme.border-subtle;
}
MenuItem {
icon: @image-url("../assets/icons/log-out.svg");
label: @tr("Log Out");
clicked => {
root.logout();
}
}
MenuItem {
icon: @image-url("../assets/icons/x.svg");
label: @tr("Close");
clicked => {
root.close-app();
}
}
}
+26 -21
View File
@@ -1,18 +1,18 @@
// Bottom now-playing / transport bar.
//
// Shell-frame increment: controls are laid out and hover-reactive but
// not yet wired to playback. Real playback state arrives in a later
// milestone.
// Controls are laid out and hover-reactive but not yet wired to
// playback. Real playback state arrives in a later milestone.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component GlyphButton inherits Rectangle {
in property <string> glyph;
in property <length> glyph-size: 18px;
component IconButton inherits Rectangle {
in property <image> icon;
in property <length> icon-size: 18px;
callback clicked();
width: 34px;
@@ -20,12 +20,13 @@ component GlyphButton inherits Rectangle {
border-radius: Radius.sm;
background: ta.has-hover ? Theme.surface-hover : transparent;
Text {
text: root.glyph;
font-size: root.glyph-size;
color: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
horizontal-alignment: center;
vertical-alignment: center;
QbzIcon {
source: root.icon;
width: root.icon-size;
height: root.icon-size;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: ta.has-hover ? Theme.text-primary : Theme.text-secondary;
}
ta := TouchArea {
@@ -38,7 +39,7 @@ component GlyphButton inherits Rectangle {
export component PlayerBar inherits Rectangle {
height: Layout.player-bar-height;
background: Theme.surface-main;
background: Theme.surface-card;
VerticalLayout {
spacing: 0px;
@@ -65,11 +66,14 @@ export component PlayerBar inherits Rectangle {
alignment: center;
HorizontalLayout {
spacing: 2px;
GlyphButton { glyph: "⇄"; }
GlyphButton { glyph: "⏮"; }
GlyphButton { glyph: "▶"; glyph-size: 22px; }
GlyphButton { glyph: "⏭"; }
GlyphButton { glyph: "↻"; }
IconButton { icon: @image-url("../assets/icons/shuffle.svg"); }
IconButton { icon: @image-url("../assets/icons/skip-back.svg"); }
IconButton {
icon: @image-url("../assets/icons/play.svg");
icon-size: 22px;
}
IconButton { icon: @image-url("../assets/icons/skip-forward.svg"); }
IconButton { icon: @image-url("../assets/icons/repeat.svg"); }
}
}
@@ -94,7 +98,7 @@ export component PlayerBar inherits Rectangle {
font-size: Typography.body;
}
Text {
text: "QBZ Slint POC";
text: "QBZ MVP";
color: Theme.text-muted;
font-size: 12px;
}
@@ -108,8 +112,9 @@ export component PlayerBar inherits Rectangle {
HorizontalLayout {
alignment: end;
spacing: Spacing.sm;
GlyphButton { glyph: "♡"; }
GlyphButton { glyph: "⤢"; }
IconButton { icon: @image-url("../assets/icons/heart.svg"); }
IconButton { icon: @image-url("../assets/icons/maximize-2.svg"); }
IconButton { icon: @image-url("../assets/icons/volume-2.svg"); }
VerticalLayout {
alignment: center;
Rectangle {
+13 -54
View File
@@ -1,13 +1,13 @@
// Left navigation sidebar — playlists list plus the user card.
// Left navigation sidebar — the playlists list.
//
// The playlist list is empty until real playlist data is wired (the
// sidebar is a later milestone). No placeholder/fixture rows.
// sidebar is a later milestone). The user element now lives in the
// header, not at the bottom of the sidebar.
import { Theme } from "../foundation/semantic-colors.slint";
import { Spacing } from "../foundation/spacing.slint";
import { Typography } from "../foundation/typography.slint";
import { Layout } from "../foundation/layout.slint";
import { Radius } from "../foundation/radius.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ScrollView } from "std-widgets.slint";
export component Sidebar inherits Rectangle {
@@ -29,66 +29,25 @@ export component Sidebar inherits Rectangle {
vertical-alignment: center;
horizontal-stretch: 1;
}
Text {
text: "+";
color: Theme.text-muted;
font-size: 18px;
vertical-alignment: center;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/plus.svg");
width: 16px;
height: 16px;
tint: Theme.text-muted;
}
}
}
ScrollView {
vertical-stretch: 1;
horizontal-scrollbar-policy: ScrollBarPolicy.always-off;
VerticalLayout {
spacing: 2px;
// Real playlist rows are populated when the sidebar
// milestone wires playlist data. No fixtures.
}
}
Rectangle {
height: 56px;
border-radius: Radius.md;
background: Theme.surface-elevated;
HorizontalLayout {
padding: 10px;
spacing: Spacing.sm;
Rectangle {
width: 36px;
height: 36px;
border-radius: Radius.pill;
background: Theme.accent;
Text {
text: "Q";
color: Theme.accent-text;
font-weight: Typography.semibold;
horizontal-alignment: center;
vertical-alignment: center;
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: "QBZ User";
color: Theme.text-primary;
font-size: Typography.body;
}
Text {
text: "QBZ MVP";
color: Theme.text-muted;
font-size: 11px;
}
}
Text {
text: "⚙";
color: Theme.text-muted;
font-size: 16px;
vertical-alignment: center;
}
}
}
}
}
+5
View File
@@ -51,3 +51,8 @@ export global AlbumState {
export global NavState {
in-out property <bool> showing-album: false;
}
// Authenticated user, for the header user element.
export global SessionState {
in property <string> user-name: "";
}