fix(locallibrary): rebuild view chrome on the mandatory fixed-chrome nav header

The nav-scaffolding view (c50ae171) invented its own chrome: a title+stats
header with no NavButtons (so no back/forward history nav), plus a tab bar
of border-radius:18px pill chips — an ADR-008 violation. Rebuild it 1:1 on
the app's mandatory fixed-chrome header (the same FavoritesView/Discover
use):

- Row 1: NavButtons (back/forward) + "Local Library" title (left), the tab
  menu floated right. Tabs are underline tabs (LibTab/LibTabMenu mirroring
  FavTab/FavTabMenu) — no pills.
- The settings gear becomes a standalone icon-only circle matching
  NavButtons (the one circular shape ADR-008 permits), routing to
  Settings > Local Library.
- Row 2 (per-tab actions + toolbar) is established with vertical-stretch:0
  so it doesn't inflate while empty; filled per slice (Albums next).
- 1px divider, then a single scrolling content area with a placeholder.

Wiring (select-tab / open-settings, nav, mount) is unchanged — only the
.slint chrome was rebuilt.

cc
This commit is contained in:
vicrodh committed 2026-05-28 21:48:06 -06:00
1 parent c50ae17102
commit 1dfc22dd37
1 file changed
+177 -71
@@ -1,106 +1,212 @@
// Local Library — the user's on-disk + Plex music, browsed inside QBZ.
//
// Greenfield Slint port. This shell renders the header stats + the four
// browse tabs (Albums / Artists / Folders / Tracks). All folder management,
// scan, maintenance, and danger-zone actions live under Settings > Local
// Library, NOT in this view — the gear button routes there.
// Greenfield Slint port. This view uses the app's MANDATORY fixed-chrome
// header — the SAME one FavoritesView / Discover use: NavButtons (back/
// forward) + title on row 1 with the tab menu floated right, per-tab action
// buttons + toolbar on row 2, a 1px divider, then a single scrolling content
// area below. No invented header, no pills (ADR-008): the tabs are underline
// tabs (mirroring FavTab), and the settings affordance is a standalone
// icon-only circle (the one circular shape ADR-008 permits).
//
// The per-tab bodies are filled slice by slice (Albums first). For now each
// tab shows an empty placeholder so the wiring is visible end to end.
// Folder management, scan, maintenance, and the danger zone do NOT live in
// this view — they belong under Settings > Local Library. The gear routes
// there.
//
// The per-tab bodies are filled slice by slice (Albums first). Until a slice
// lands the content area shows an empty placeholder so the chrome wiring is
// visible end to end.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { LocalLibraryState, LocalLibraryActions } from "../state.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
component TabButton inherits TouchArea {
// 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 <bool> active;
height: 36px;
horizontal-stretch: 0;
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 {
border-radius: 18px;
background: root.active
? Theme.accent
: (root.has-hover ? Theme.surface-hover : transparent);
HorizontalLayout {
padding-left: 16px;
padding-right: 16px;
Text {
text: root.label;
vertical-alignment: center;
color: root.active ? Theme.accent-text : Theme.text-secondary;
font-size: 14px;
font-weight: root.active ? 700 : 500;
}
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); }
}
}
}
// Standalone icon-only circle (ADR-008 permits a full circle for an
// icon-only control that stands alone) — matches NavButtons' history pair.
// Used for the "Local Library settings" gear (management lives in Settings).
component CircleIconBtn inherits Rectangle {
in property <image> icon;
callback clicked();
width: 28px;
height: 28px;
border-radius: 14px;
background: ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
QbzIcon {
source: root.icon;
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.text-primary;
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked();
}
}
}
export component LocalLibraryView inherits Rectangle {
background: transparent;
background: Theme.surface-main;
VerticalLayout {
padding: 24px;
spacing: 20px;
spacing: 0px;
// ---- Header: title + stats + gear (-> Settings > Local Library) ----
// ===================== FIXED CHROME =====================
// Row 1 — back/forward + title (left), tab menu + settings gear
// (right). Same structure as FavoritesView row 1.
HorizontalLayout {
padding-left: 32px;
padding-right: 32px;
padding-top: 11px;
spacing: 16px;
VerticalLayout {
horizontal-stretch: 1;
spacing: 2px;
alignment: center;
NavButtons { }
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Local Library");
color: Theme.text-primary;
font-size: 28px;
font-weight: 800;
}
Text {
text: @tr("{} albums · {} artists · {} tracks",
LocalLibraryState.album-count,
LocalLibraryState.artist-count,
LocalLibraryState.track-count);
color: Theme.text-muted;
font-size: 13px;
font-size: Typography.section;
font-weight: Typography.bold;
}
}
gear := TouchArea {
width: 38px;
height: 38px;
clicked => { LocalLibraryActions.open-settings(); }
Rectangle {
border-radius: 10px;
background: gear.has-hover ? Theme.surface-hover : transparent;
Image {
source: @image-url("../assets/icons/settings.svg");
width: 20px;
height: 20px;
colorize: Theme.text-secondary;
Rectangle { horizontal-stretch: 1; }
VerticalLayout {
alignment: center;
LibTabMenu { }
}
VerticalLayout {
alignment: center;
CircleIconBtn {
icon: @image-url("../assets/icons/settings.svg");
clicked => {
LocalLibraryActions.open-settings();
}
}
}
}
// ---- Tab bar (Albums / Artists / Folders / Tracks) ----
Rectangle { height: 12px; }
// Row 2 — per-tab action buttons (left) + toolbar (right). Filled as
// each tab slice lands (Albums: play/shuffle + search/sort/view).
// vertical-stretch: 0 is load-bearing (see FavoritesView): without it
// an empty row grabs leftover height and inflates the sub-header.
HorizontalLayout {
vertical-stretch: 0;
padding-left: 32px;
padding-right: 32px;
spacing: 8px;
alignment: start;
for tab in [
{ id: "albums", label: @tr("Albums") },
{ id: "artists", label: @tr("Artists") },
{ id: "folders", label: @tr("Folders") },
{ id: "tracks", label: @tr("Tracks") },
] : TabButton {
label: tab.label;
active: LocalLibraryState.active-tab == tab.id;
clicked => { LocalLibraryActions.select-tab(tab.id); }
}
Rectangle { horizontal-stretch: 1; }
}
// ---- Body (placeholder until each tab slice lands) ----
Rectangle { height: 14px; }
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// ===================== SCROLLING CONTENT =====================
Rectangle {
vertical-stretch: 1;
background: Theme.surface-card;
border-radius: 12px;
clip: true;
// Placeholder until each tab slice lands (Albums is next).
VerticalLayout {
alignment: center;
spacing: 6px;
@@ -108,14 +214,14 @@ export component LocalLibraryView inherits Rectangle {
text: @tr("Local Library port in progress");
horizontal-alignment: center;
color: Theme.text-secondary;
font-size: 16px;
font-weight: 600;
font-size: Typography.body;
font-weight: Typography.semibold;
}
Text {
text: @tr("The “{}” tab is coming next.", LocalLibraryState.active-tab);
text: @tr("Building the browse tabs slice by slice.");
horizontal-alignment: center;
color: Theme.text-muted;
font-size: 13px;
font-size: Typography.legal;
}
}
}