feat(slint): library/sidebar UX overhaul + theme switching + fixes

Shell & navigation:
- Three-state sidebar (open/mini/closed); collapse cycles via header button.
- Section nav as right-flyouts (sidebar) + compact header nav (closed) +
  full header tabs (nav-in-sidebar off), via a shared HeaderMenuOverlay.
- Collapsed-folder right-flyout popup (max 4 + scrollbar), Rust-populated.
- Thin auto-hiding sidebar scrollbar pinned to the edge (Flickable, drag-safe).
- Slack-style bezel: rounded inset content panel over surface-card chrome.
- Header restructure: nav buttons, window-centered search, app menu.

Themes & components:
- Live theme switching: Dark / OLED Black / Tokyo Night / System (std-widgets).
- SegmentedTabBar (Discovery-style) with count badges for Library + Local Library.
- MP3 quality-badge tier (format-first classification) across badges.
- QbzSelect (Group/Sort) trimmed 20px.

Behavior fixes:
- Artist + Local Library Folder cover art now reach the rendered (visible/
  grouped) models, not just the source set.
- Favorites Playlists/Labels random buttons.
- Playlist track click plays from the clicked track in visible order.
- Dropdown idle-close pauses while the cursor rests on the menu.

cc
This commit is contained in:
vicrodh committed 2026-05-30 15:40:35 -06:00
1 parent 201082d4a1
commit 682719cab0
27 files changed
+1495 -619

No files matched your search

@@ -29,95 +29,23 @@ import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { TrackRow } from "../primitives/TrackRow.slint";
import { ToggleButton } from "../primitives/ToggleButton.slint";
import { MultiSelectBar } from "../primitives/MultiSelectBar.slint";
import { SegmentedTabBar, SegTab } from "../primitives/SegmentedTabBar.slint";
import { ListView } from "std-widgets.slint";
// One tab in the Local Library tab menu — underline style (ADR-008: NO
// pills). 1:1 with Favorites' FavTab so the chrome reads identically.
component LibTab inherits Rectangle {
in property <string> label;
in property <string> id;
in property <int> count;
callback selected(string);
property <bool> active: LocalLibraryState.active-tab == root.id;
height: 34px;
width: tab-row.preferred-width;
tab-row := HorizontalLayout {
padding-left: 4px;
padding-right: 4px;
spacing: 6px;
Text {
text: root.label;
color: root.active ? Theme.text-primary : Theme.text-muted;
font-size: Typography.body;
font-weight: root.active ? Typography.semibold : Typography.regular;
vertical-alignment: center;
}
if root.count > 0: Text {
text: root.count;
color: Theme.text-muted;
font-size: Typography.legal;
vertical-alignment: center;
}
}
// Active underline (NOT a pill background).
Rectangle {
y: parent.height - 2px;
height: 2px;
width: parent.width;
background: root.active ? Theme.accent : transparent;
border-radius: 1px;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.selected(root.id);
}
}
}
// The Local Library tabs as one floating group (row 1, right side). Order
// mirrors the existing HeaderBar "Local Library" menu (Albums / Artists /
// Folders / Tracks). Counts come from LocalLibraryState (0 until seeded).
component LibTabMenu inherits HorizontalLayout {
spacing: 18px;
VerticalLayout {
alignment: center;
LibTab {
label: @tr("Albums");
id: "albums";
count: LocalLibraryState.album-count;
selected(id) => { LocalLibraryActions.select-tab(id); }
}
}
VerticalLayout {
alignment: center;
LibTab {
label: @tr("Artists");
id: "artists";
count: LocalLibraryState.artist-count;
selected(id) => { LocalLibraryActions.select-tab(id); }
}
}
VerticalLayout {
alignment: center;
LibTab {
label: @tr("Folders");
id: "folders";
count: LocalLibraryState.folder-count;
selected(id) => { LocalLibraryActions.select-tab(id); }
}
}
VerticalLayout {
alignment: center;
LibTab {
label: @tr("Tracks");
id: "tracks";
count: LocalLibraryState.track-count;
selected(id) => { LocalLibraryActions.select-tab(id); }
}
}
// The Local Library tabs as one floating group (row 1, right side) — now
// the shared Discovery-style SegmentedTabBar, counts rendered as badges.
// Order mirrors the existing HeaderBar "Local Library" menu (Albums /
// Artists / Folders / Tracks). Counts come from LocalLibraryState (seeded
// up front so the badges show without visiting each tab).
component LibTabMenu inherits SegmentedTabBar {
active-id: LocalLibraryState.active-tab;
tabs: [
{ id: "albums", label: @tr("Albums"), count: LocalLibraryState.album-count },
{ id: "artists", label: @tr("Artists"), count: LocalLibraryState.artist-count },
{ id: "folders", label: @tr("Folders"), count: LocalLibraryState.folder-count },
{ id: "tracks", label: @tr("Tracks"), count: LocalLibraryState.track-count },
];
selected(id) => { LocalLibraryActions.select-tab(id); }
}
// Standalone icon-only circle (ADR-008 permits a full circle for an
@@ -445,10 +373,6 @@ export component LocalLibraryView inherits Rectangle {
padding-right: 32px;
padding-top: 11px;
spacing: 16px;
VerticalLayout {
alignment: center;
NavButtons { }
}
VerticalLayout {
alignment: center;
Text {
@@ -458,11 +382,7 @@ export component LocalLibraryView inherits Rectangle {
font-weight: Typography.bold;
}
}
Rectangle { horizontal-stretch: 1; }
VerticalLayout {
alignment: center;
LibTabMenu { }
}
// Settings gear — inline, to the right of the title.
VerticalLayout {
alignment: center;
CircleIconBtn {
@@ -472,6 +392,7 @@ export component LocalLibraryView inherits Rectangle {
}
}
}
Rectangle { horizontal-stretch: 1; }
}
Rectangle { height: 12px; }
@@ -486,6 +407,12 @@ export component LocalLibraryView inherits Rectangle {
padding-right: 32px;
spacing: 8px;
// The section tab menu sits under the title (left of row 2).
VerticalLayout {
alignment: center;
LibTabMenu { }
}
Rectangle { horizontal-stretch: 1; }
// Albums toolbar — search + sort (both server-side). The