Files
qbz/crates/qbz-slint/ui/locallibrary/LocalLibraryView.slint
T
vicrodh 13f43b802b fix(slint): flyouts/idle, header nav layout, toasts, settings, search
Flyouts & idle (main effort):
- Header dropdowns now glue under the tab: open-menu resets from-sidebar +
  title (a stale sidebar flyout state was flinging the panel far away).
- Idle-close now pauses while the cursor rests on the TRIGGER too (full
  header tabs gained the trigger-hovered logic the others already had).
- Dropped the dropdown caret (saves width on small screens).

Header nav layout:
- Section nav moved INTO the left-controls cluster, after the three sacred
  buttons, so it never overlaps them. Full text tabs when nav-in-sidebar off
  and sidebar not closed; compact icons when closed — mutually exclusive (no
  more duplicate/cluttered header).
- Search field gives up 60px (animated) when the header hosts the nav.

Settings made functional:
- Toast component mounted in AppShell (it was never rendered) + the in-app
  toasts toggle drives ToastState.enabled.
- "Show album artwork in Local Library track list" now drives the Tracks/
  Folders rows. Both rows un-flagged as unsupported.

Collapsible search (ExpandableSearch, app-wide):
- Smooth open AND close (width animates both ways; magnifier fades).
- Reliable auto-focus on open (deferred via a one-shot timer).

cc
2026-05-30 15:59:25 -06:00

1803 lines
86 KiB
Plaintext

// Local Library — the user's on-disk + Plex music, browsed inside QBZ.
//
// 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).
//
// 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, FolderNode, LocalArtistItem, AlphaJump, LibAlbumFilterState, AppearanceState } from "../state.slint";
import { NavButtons } from "../shell/NavButtons.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { AlbumCollectionView } from "../discover/AlbumCollectionView.slint";
import { ExpandableSearch } from "../primitives/ExpandableSearch.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
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";
// 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
// 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();
}
}
}
// Grid/list segmented toggle for the Folders flat mode (mirrors Favorites'
// ViewToggle). ADR-008: small-radius rectangle, NOT a pill.
component ViewToggle inherits Rectangle {
in property <string> view-mode; // "grid" | "list"
callback set-view(string);
width: 60px;
height: 30px;
border-radius: 6px;
background: Theme.surface-elevated;
clip: true;
HorizontalLayout {
ToggleButton {
icon: @image-url("../assets/icons/layout-grid.svg");
active: root.view-mode == "grid";
sm: true;
clicked => { root.set-view("grid"); }
}
ToggleButton {
icon: @image-url("../assets/icons/list.svg");
active: root.view-mode == "list";
sm: true;
clicked => { root.set-view("list"); }
}
}
}
// Flat/tree mode segmented toggle for the Folders tab. flat = the album grid
// grouped by directory; tree = the two-pane filesystem browser.
component ModeToggle inherits Rectangle {
in property <string> mode; // "flat" | "tree"
callback set-mode(string);
width: 60px;
height: 30px;
border-radius: 6px;
background: Theme.surface-elevated;
clip: true;
HorizontalLayout {
ToggleButton {
icon: @image-url("../assets/icons/disc.svg");
active: root.mode == "flat";
sm: true;
clicked => { root.set-mode("flat"); }
}
ToggleButton {
icon: @image-url("../assets/icons/folder.svg");
active: root.mode == "tree";
sm: true;
clicked => { root.set-mode("tree"); }
}
}
}
// One row of the flattened folder tree (tree mode, left rail). Carries its
// `depth` (indent), an expand chevron for folders with content, a folder /
// music icon, the segment name and (for folders) the recursive track count.
component TreeRow inherits Rectangle {
in property <FolderNode> node;
in property <bool> selected;
callback toggle(); // chevron — expand/collapse
callback activate(); // row body — select folder (load detail)
height: 32px;
border-radius: 6px;
background: root.selected
? Theme.surface-elevated
: (row-ta.has-hover ? Theme.surface-hover : transparent);
row-ta := TouchArea {
mouse-cursor: root.node.is-folder ? pointer : default;
clicked => {
if (root.node.is-folder) { root.activate(); }
}
}
// Accent bar marking the selected folder (mirrors the underline-tab
// accent language; ADR-008: a thin rectangle, not a pill).
if root.selected: Rectangle {
x: 0;
width: 3px;
height: parent.height - 8px;
y: 4px;
background: Theme.accent;
border-radius: 1.5px;
}
HorizontalLayout {
padding-left: 8px + root.node.depth * 16px;
padding-right: 10px;
spacing: 6px;
// Expand chevron — its own hit target so toggling doesn't select.
Rectangle {
width: 18px;
VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
if root.node.can-expand: QbzIcon {
source: root.node.expanded
? @image-url("../assets/icons/chevron-down.svg")
: @image-url("../assets/icons/chevron-right.svg");
width: 13px;
height: 13px;
tint: Theme.text-muted;
}
}
}
chev-ta := TouchArea {
mouse-cursor: root.node.can-expand ? pointer : default;
clicked => {
if (root.node.can-expand) { root.toggle(); }
}
}
}
// Folder / track icon.
VerticalLayout {
alignment: center;
QbzIcon {
source: root.node.is-folder
? (root.node.expanded
? @image-url("../assets/icons/folder-open.svg")
: @image-url("../assets/icons/folder.svg"))
: @image-url("../assets/icons/music.svg");
width: 16px;
height: 16px;
tint: root.node.is-folder ? Theme.accent : Theme.text-muted;
}
}
// Name.
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: root.node.segment;
color: root.selected ? Theme.text-primary : Theme.text-secondary;
font-size: Typography.body;
font-weight: root.selected ? Typography.semibold : Typography.regular;
overflow: elide;
}
}
// Recursive track count (folders only).
if root.node.is-folder && root.node.track-count > 0: VerticalLayout {
alignment: center;
Text {
text: root.node.track-count;
color: Theme.text-muted;
font-size: Typography.legal;
}
}
}
}
// Vertical A-Z jump strip on the right edge of the artist rail. Emits
// jump(ordinal, index); the host scrolls its flickable proportionally.
// Ported 1:1 from FavoritesView's AlphaStrip.
component AlphaStrip inherits Rectangle {
in property <[AlphaJump]> jumps;
callback jump(int /* ordinal */, int /* index */);
width: 18px;
VerticalLayout {
alignment: center;
spacing: 0px;
for j[k] in root.jumps: Rectangle {
height: 15px;
width: 18px;
Text {
text: j.letter;
color: la.has-hover ? Theme.accent : Theme.text-muted;
font-size: 9px;
font-weight: Typography.semibold;
horizontal-alignment: center;
vertical-alignment: center;
}
la := TouchArea {
mouse-cursor: pointer;
clicked => {
root.jump(k, j.index);
}
}
}
}
}
// One compact row in the Artists left rail — a 48px round avatar (custom/
// cached photo or a mic-style placeholder) + display name + "N albums ·
// M tracks". 1:1 with Tauri's .artist-card-compact. ADR-008: round avatar is
// the permitted icon-only circle; the row itself is a small-radius rectangle.
component LocalArtistRow inherits Rectangle {
in property <LocalArtistItem> item;
in property <bool> selected;
callback clicked(string /* artist name */);
height: 60px;
border-radius: 8px;
background: root.selected
? Theme.accent
: (ta.has-hover ? Theme.surface-hover : transparent);
HorizontalLayout {
padding-left: 8px;
padding-right: 8px;
spacing: 12px;
VerticalLayout {
alignment: center;
Rectangle {
width: 48px;
height: 48px;
border-radius: 24px;
background: Theme.surface-elevated;
clip: true;
if root.item.image-path == "": QbzIcon {
source: @image-url("../assets/icons/user.svg");
width: 22px;
height: 22px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.text-muted;
}
if root.item.image-path != "": Image {
source: root.item.image;
image-fit: cover;
width: 48px;
height: 48px;
}
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
spacing: 3px;
Text {
text: root.item.display-name;
color: root.selected ? Theme.accent-text : Theme.text-primary;
font-size: Typography.body;
font-weight: root.selected ? Typography.semibold : Typography.medium;
overflow: elide;
}
Text {
text: "\{root.item.album-count} " + @tr("albums") + " · \{root.item.track-count} " + @tr("tracks");
color: root.selected ? Theme.accent-text : Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.clicked(root.item.name);
}
}
}
// One filter chip in the Albums quality/format/source popup. 6px radius (not
// a pill — ADR-008; matches the GenreFilterPopup chip).
component FilterChip inherits Rectangle {
in property <string> label;
in property <bool> active;
callback toggled();
height: 30px;
width: lbl.preferred-width + 22px;
border-radius: 6px;
border-width: 1px;
border-color: root.active ? Theme.accent : Theme.border-subtle;
background: root.active
? Theme.accent
: (chip-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated);
lbl := Text {
text: root.label;
color: root.active ? Theme.accent-text : Theme.text-secondary;
font-size: Typography.legal;
font-weight: root.active ? Typography.semibold : Typography.regular;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
chip-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.toggled();
}
}
}
export component LocalLibraryView inherits Rectangle {
background: Theme.surface-main;
VerticalLayout {
spacing: 0px;
// ===================== 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 {
alignment: center;
Text {
text: @tr("Local Library");
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
}
}
// Settings gear — inline, to the right of the title.
VerticalLayout {
alignment: center;
CircleIconBtn {
icon: @image-url("../assets/icons/settings.svg");
clicked => {
LocalLibraryActions.open-settings();
}
}
}
Rectangle { horizontal-stretch: 1; }
}
Rectangle { height: 12px; }
// Row 2 — per-tab action buttons (left) + toolbar (right). Filled as
// each tab slice lands. 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;
// 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
// grid/list toggle, group-by and filters land with the next slice.
if LocalLibraryState.active-tab == "albums"
&& (LocalLibraryState.albums.length > 0
|| LocalLibraryState.albums-search != ""): HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
ExpandableSearch {
placeholder: @tr("Search");
sm: true;
query <=> LocalLibraryState.albums-search;
edited(q) => {
LocalLibraryActions.albums-search(q);
}
}
}
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 180px;
options: [
@tr("Artist A-Z"),
@tr("Artist Z-A"),
@tr("Title A-Z"),
@tr("Title Z-A"),
@tr("Year (newest)"),
@tr("Year (oldest)"),
];
sm: true;
current-index: LocalLibraryState.albums-sort == "artist-asc" ? 0
: LocalLibraryState.albums-sort == "artist-desc" ? 1
: LocalLibraryState.albums-sort == "title-asc" ? 2
: LocalLibraryState.albums-sort == "title-desc" ? 3
: LocalLibraryState.albums-sort == "year-desc" ? 4
: 5;
selected(i) => {
LocalLibraryActions.albums-set-sort(
i == 0 ? "artist-asc"
: i == 1 ? "artist-desc"
: i == 2 ? "title-asc"
: i == 3 ? "title-desc"
: i == 4 ? "year-desc"
: "year-asc");
}
}
}
// Group-by.
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 160px;
sm: true;
options: [
@tr("No grouping"),
@tr("Group by letter"),
@tr("Group by artist"),
];
current-index: LocalLibraryState.albums-group == "alpha" ? 1
: LocalLibraryState.albums-group == "artist" ? 2
: 0;
selected(i) => {
LocalLibraryActions.albums-set-group(
i == 1 ? "alpha" : i == 2 ? "artist" : "off");
}
}
}
// Quality/format/source filter (popup) + active-count badge.
VerticalLayout {
alignment: center;
Rectangle {
width: 34px;
height: 30px;
border-radius: 6px;
border-width: 1px;
border-color: LibAlbumFilterState.count > 0 ? Theme.accent : Theme.border-subtle;
background: filt-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/list-filter.svg");
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: LibAlbumFilterState.count > 0 ? Theme.accent : Theme.text-secondary;
}
if LibAlbumFilterState.count > 0: Rectangle {
x: parent.width - self.width + 3px;
y: -4px;
width: 15px;
height: 15px;
border-radius: 7.5px;
background: Theme.accent;
Text {
text: LibAlbumFilterState.count;
color: Theme.accent-text;
font-size: 9px;
font-weight: Typography.bold;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
}
filt-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LibAlbumFilterState.open = !LibAlbumFilterState.open;
}
}
}
}
// Grid/list view toggle.
VerticalLayout {
alignment: center;
ViewToggle {
view-mode: LocalLibraryState.albums-view-mode;
set-view(v) => {
LocalLibraryActions.albums-set-view(v);
}
}
}
}
// Tracks toolbar — multi-select toggle + search + group-by.
if LocalLibraryState.active-tab == "tracks"
&& (LocalLibraryState.tracks.length > 0
|| LocalLibraryState.tracks-search != ""): HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
CircleIconBtn {
icon: @image-url("../assets/icons/square-check-big.svg");
clicked => {
LocalLibraryActions.tracks-toggle-multi-select();
}
}
}
VerticalLayout {
alignment: center;
ExpandableSearch {
placeholder: @tr("Search");
sm: true;
query <=> LocalLibraryState.tracks-search;
edited(q) => {
LocalLibraryActions.tracks-search(q);
}
}
}
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 160px;
sm: true;
options: [
@tr("No grouping"),
@tr("By album"),
@tr("By artist"),
@tr("By name"),
];
current-index: LocalLibraryState.tracks-group-mode == "album" ? 1
: LocalLibraryState.tracks-group-mode == "artist" ? 2
: LocalLibraryState.tracks-group-mode == "name" ? 3
: 0;
selected(i) => {
LocalLibraryActions.tracks-set-group(
i == 1 ? "album" : i == 2 ? "artist" : i == 3 ? "name" : "off");
}
}
}
}
// Folders toolbar — search (both modes); flat adds sort + group +
// grid/list; a flat/tree mode toggle sits at the far right.
if LocalLibraryState.active-tab == "folders"
&& (LocalLibraryState.folders.length > 0
|| LocalLibraryState.folders-search != ""): HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
ExpandableSearch {
placeholder: @tr("Search");
sm: true;
query <=> LocalLibraryState.folders-search;
edited(q) => {
LocalLibraryActions.folders-search(q);
}
}
}
// Flat-only controls: sort, group-by, grid/list.
if LocalLibraryState.folders-view-mode == "flat": HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 180px;
options: [
@tr("Artist A-Z"),
@tr("Artist Z-A"),
@tr("Title A-Z"),
@tr("Title Z-A"),
@tr("Year (newest)"),
@tr("Year (oldest)"),
];
sm: true;
current-index: LocalLibraryState.folders-sort == "artist-asc" ? 0
: LocalLibraryState.folders-sort == "artist-desc" ? 1
: LocalLibraryState.folders-sort == "title-asc" ? 2
: LocalLibraryState.folders-sort == "title-desc" ? 3
: LocalLibraryState.folders-sort == "year-desc" ? 4
: 5;
selected(i) => {
LocalLibraryActions.folders-set-sort(
i == 0 ? "artist-asc"
: i == 1 ? "artist-desc"
: i == 2 ? "title-asc"
: i == 3 ? "title-desc"
: i == 4 ? "year-desc"
: "year-asc");
}
}
}
VerticalLayout {
alignment: center;
QbzSelect {
menu-width: 160px;
options: [
@tr("No grouping"),
@tr("Group by letter"),
@tr("Group by artist"),
];
sm: true;
current-index: LocalLibraryState.folders-group == "alpha" ? 1
: LocalLibraryState.folders-group == "artist" ? 2
: 0;
selected(i) => {
LocalLibraryActions.folders-set-group(
i == 1 ? "alpha" : i == 2 ? "artist" : "off");
}
}
}
VerticalLayout {
alignment: center;
ViewToggle {
view-mode: LocalLibraryState.folders-grid-view;
set-view(v) => {
LocalLibraryState.folders-grid-view = v;
}
}
}
}
// Flat/tree mode toggle (always visible).
VerticalLayout {
alignment: center;
ModeToggle {
mode: LocalLibraryState.folders-view-mode;
set-mode(m) => {
LocalLibraryActions.folders-set-mode(m);
}
}
}
}
// Artists toolbar — search (filters the master rail in place).
if LocalLibraryState.active-tab == "artists"
&& LocalLibraryState.artists.length > 0: HorizontalLayout {
spacing: 8px;
VerticalLayout {
alignment: center;
ExpandableSearch {
placeholder: @tr("Search artists");
sm: true;
query <=> LocalLibraryState.artists-search;
edited(q) => {
LocalLibraryActions.artists-search(q);
}
}
}
}
}
Rectangle { height: 14px; }
Rectangle {
height: 1px;
background: Theme.border-subtle;
}
// ===================== SCROLLING CONTENT =====================
Rectangle {
vertical-stretch: 1;
clip: true;
// -------- Albums tab — chunked grid --------------------------
if LocalLibraryState.active-tab == "albums": Rectangle {
// Initial page load.
if LocalLibraryState.albums-loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
}
// Load error + retry.
if !LocalLibraryState.albums-loading
&& LocalLibraryState.albums-load-failed: VerticalLayout {
alignment: center;
spacing: 10px;
Text {
text: @tr("Couldn't load your local albums.");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
horizontal-alignment: center;
}
HorizontalLayout {
alignment: center;
Rectangle {
width: retry-label.preferred-width + 28px;
height: 32px;
border-radius: 6px;
background: retry-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
retry-label := Text {
text: @tr("Retry");
color: Theme.text-primary;
font-size: Typography.legal;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
retry-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LocalLibraryActions.albums-retry();
}
}
}
}
}
// Empty — no albums indexed yet.
if !LocalLibraryState.albums-loading
&& !LocalLibraryState.albums-load-failed
&& LocalLibraryState.albums.length == 0
&& LocalLibraryState.albums-search == "": VerticalLayout {
alignment: center;
Text {
text: @tr("No albums in your local library yet.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// No-match — search or the quality/format/source filter emptied
// the visible set (the full set still has albums).
if !LocalLibraryState.albums-loading
&& !LocalLibraryState.albums-load-failed
&& LocalLibraryState.albums.length > 0
&& LocalLibraryState.albums-visible.length == 0
&& LocalLibraryState.albums-grouped.length == 0: VerticalLayout {
alignment: center;
Text {
text: @tr("No albums match your search.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// Grid / list / grouped — self-scrolling (full-load, derived).
if !LocalLibraryState.albums-loading
&& !LocalLibraryState.albums-load-failed
&& (LocalLibraryState.albums-visible.length > 0
|| LocalLibraryState.albums-grouped.length > 0): Rectangle {
albums-flick := Flickable {
viewport-width: self.width;
viewport-height: albums-page.preferred-height;
albums-page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 16px;
padding-bottom: 100px;
spacing: 0px;
alignment: start;
AlbumCollectionView {
albums: LocalLibraryState.albums-visible;
grouped: LocalLibraryState.albums-grouped;
is-grouped: LocalLibraryState.albums-group != "off";
view-mode: LocalLibraryState.albums-view-mode;
// List view shows the SOURCE column (local/offline/plex).
show-source: true;
// Local albums aren't Qobuz-favoritable — hide the heart.
show-favorite: false;
// Always-visible source badge (local | qobuz-offline | plex).
show-source-badge: true;
open-album(id) => {
LocalLibraryActions.open-album(id);
}
open-artist(id) => {
LocalLibraryActions.open-artist(id);
}
media-action(kind, id, action) => {
LocalLibraryActions.album-action(id, action);
}
}
}
}
// A-Z jump strip (alpha grouping; proportional like the rail).
if LocalLibraryState.albums-group == "alpha"
&& LocalLibraryState.albums-alpha.length > 0: AlphaStrip {
x: parent.width - self.width - 2px;
height: 100%;
jumps: LocalLibraryState.albums-alpha;
jump(ord, idx) => {
albums-flick.viewport-y = -(ord / Math.max(1, LocalLibraryState.albums-alpha.length - 1))
* Math.max(0, (albums-flick.viewport-height - albums-flick.height) / 1px) * 1px;
}
}
ListScrollbar {
x: LocalLibraryState.albums-group == "alpha"
? parent.width - self.width - 22px
: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: albums-flick.viewport-height;
visible-height: albums-flick.height;
viewport-y <=> albums-flick.viewport-y;
}
}
}
// -------- Tracks tab — server-paginated flat list ------------
if LocalLibraryState.active-tab == "tracks": Rectangle {
if LocalLibraryState.tracks-loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
}
if !LocalLibraryState.tracks-loading
&& LocalLibraryState.tracks-load-failed: VerticalLayout {
alignment: center;
spacing: 10px;
Text {
text: @tr("Couldn't load your local tracks.");
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.semibold;
horizontal-alignment: center;
}
HorizontalLayout {
alignment: center;
Rectangle {
width: tretry-label.preferred-width + 28px;
height: 32px;
border-radius: 6px;
background: tretry-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
border-width: 1px;
border-color: Theme.border-subtle;
tretry-label := Text {
text: @tr("Retry");
color: Theme.text-primary;
font-size: Typography.legal;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
tretry-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LocalLibraryActions.tracks-retry();
}
}
}
}
}
if !LocalLibraryState.tracks-loading
&& !LocalLibraryState.tracks-load-failed
&& LocalLibraryState.tracks.length == 0
&& LocalLibraryState.tracks-search == "": VerticalLayout {
alignment: center;
Text {
text: @tr("No tracks in your local library yet.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
if !LocalLibraryState.tracks-loading
&& !LocalLibraryState.tracks-load-failed
&& LocalLibraryState.tracks.length == 0
&& LocalLibraryState.tracks-search != "": VerticalLayout {
alignment: center;
Text {
text: @tr("No tracks match your search.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// No-match (search/filter emptied the visible set).
if !LocalLibraryState.tracks-loading
&& !LocalLibraryState.tracks-load-failed
&& LocalLibraryState.tracks.length > 0
&& LocalLibraryState.tracks-visible.length == 0: VerticalLayout {
alignment: center;
Text {
text: @tr("No tracks match your search.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
if !LocalLibraryState.tracks-loading
&& !LocalLibraryState.tracks-load-failed
&& LocalLibraryState.tracks-visible.length > 0: Rectangle {
VerticalLayout {
padding-left: 32px;
padding-right: 32px;
padding-top: 16px;
// Bulk action bar (Tauri Tracks subset — no make-offline/remove).
if LocalLibraryState.tracks-multi-select: MultiSelectBar {
selected-count: LocalLibraryState.tracks-selected-count;
actions: [
{ id: "select-all", label: @tr("Select all"), danger: false, needs-selection: false },
{ id: "queue", label: @tr("Add to queue"), danger: false, needs-selection: true },
{ id: "play-next", label: @tr("Play next"), danger: false, needs-selection: true },
{ id: "add-to-playlist", label: @tr("Add to playlist"), danger: false, needs-selection: true },
{ id: "clear", label: @tr("Clear"), danger: false, needs-selection: true },
];
action(id) => {
LocalLibraryActions.tracks-bulk-action(id);
}
}
if LocalLibraryState.tracks-multi-select: Rectangle { height: 8px; }
tracks-list := ListView {
vertical-stretch: 1;
vertical-scrollbar-policy: ScrollBarPolicy.always-off;
changed viewport-y => {
if (-self.viewport-y + self.visible-height >= self.viewport-height - 600px
&& LocalLibraryState.tracks-has-more
&& !LocalLibraryState.tracks-loading-more
&& !LocalLibraryState.tracks-loading) {
LocalLibraryActions.tracks-load-more();
}
}
for track[i] in LocalLibraryState.tracks-visible: VerticalLayout {
spacing: 0px;
// Group header rows for album / artist grouping.
if LocalLibraryState.tracks-group-mode == "album" && track.album != ""
&& (i == 0 || LocalLibraryState.tracks-visible[i - 1].album != track.album): Rectangle {
height: 32px;
Text {
x: 2px;
width: parent.width - 4px;
height: 100%;
text: track.album;
color: Theme.text-secondary;
font-size: Typography.legal;
font-weight: Typography.semibold;
vertical-alignment: center;
overflow: elide;
}
}
if LocalLibraryState.tracks-group-mode == "artist" && track.artist != ""
&& (i == 0 || LocalLibraryState.tracks-visible[i - 1].artist != track.artist): Rectangle {
height: 32px;
Text {
x: 2px;
width: parent.width - 4px;
height: 100%;
text: track.artist;
color: Theme.text-secondary;
font-size: Typography.legal;
font-weight: Typography.semibold;
vertical-alignment: center;
overflow: elide;
}
}
TrackRow {
track: track;
index: i;
show-artwork: AppearanceState.local-library-track-artwork;
show-album: LocalLibraryState.tracks-group-mode != "album";
show-favorite: false;
show-download: false;
show-source: true;
multi-select-mode: LocalLibraryState.tracks-multi-select;
media-action(kind, id, action) => {
if (kind == "album" && action == "open") {
LocalLibraryActions.open-album(id);
} else if (kind == "artist" && action == "open") {
LocalLibraryActions.open-artist(id);
} else {
LocalLibraryActions.track-action(id, action);
}
}
}
}
}
}
// A-Z jump strip for name grouping (far right; scrollbar insets).
if LocalLibraryState.tracks-group-mode == "name"
&& LocalLibraryState.tracks-alpha.length > 0: AlphaStrip {
x: parent.width - self.width - 2px;
height: 100%;
jumps: LocalLibraryState.tracks-alpha;
jump(ord, idx) => {
tracks-list.viewport-y = -idx * 50px;
}
}
ListScrollbar {
x: LocalLibraryState.tracks-group-mode == "name"
? parent.width - self.width - 22px
: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: tracks-list.viewport-height;
visible-height: tracks-list.visible-height;
viewport-y <=> tracks-list.viewport-y;
}
}
}
// -------- Folders tab — flat grid/list OR tree two-pane ------
if LocalLibraryState.active-tab == "folders": Rectangle {
// Initial flat load.
if LocalLibraryState.folders-loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
}
// Empty — nothing indexed.
if !LocalLibraryState.folders-loading
&& LocalLibraryState.folders.length == 0
&& LocalLibraryState.folders-search == "": VerticalLayout {
alignment: center;
Text {
text: @tr("No folders in your local library yet.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// ===================== FLAT MODE =====================
if !LocalLibraryState.folders-loading
&& LocalLibraryState.folders.length > 0
&& LocalLibraryState.folders-view-mode == "flat": Rectangle {
// Search matched nothing.
if LocalLibraryState.folders-visible.length == 0
&& LocalLibraryState.folders-grouped.length == 0
&& LocalLibraryState.folders-search != "": VerticalLayout {
alignment: center;
Text {
text: @tr("No folders match your search.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
if LocalLibraryState.folders-visible.length > 0
|| LocalLibraryState.folders-grouped.length > 0: Rectangle {
folders-flick := Flickable {
viewport-width: self.width;
viewport-height: folders-page.preferred-height;
folders-page := VerticalLayout {
width: parent.width;
padding-left: 32px;
padding-right: 32px;
padding-top: 16px;
padding-bottom: 100px;
alignment: start;
AlbumCollectionView {
albums: LocalLibraryState.folders-visible;
grouped: LocalLibraryState.folders-grouped;
is-grouped: LocalLibraryState.folders-group != "off";
view-mode: LocalLibraryState.folders-grid-view;
show-source: false;
show-favorite: false;
show-source-badge: true;
open-album(id) => {
LocalLibraryActions.open-album(id);
}
open-artist(id) => {
LocalLibraryActions.open-artist(id);
}
media-action(kind, id, action) => {
LocalLibraryActions.album-action(id, action);
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: folders-flick.viewport-height;
visible-height: folders-flick.height;
viewport-y <=> folders-flick.viewport-y;
}
}
}
// ===================== TREE MODE =====================
if !LocalLibraryState.folders-loading
&& LocalLibraryState.folders.length > 0
&& LocalLibraryState.folders-view-mode == "tree": HorizontalLayout {
spacing: 0px;
// ---- Left rail: the flattened folder tree ----
Rectangle {
width: 340px;
if LocalLibraryState.folder-tree-loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 28px; }
}
}
if !LocalLibraryState.folder-tree-loading: VerticalLayout {
padding-left: 24px;
padding-right: 8px;
padding-top: 12px;
tree-list := ListView {
for node[i] in LocalLibraryState.folder-tree: TreeRow {
node: node;
selected: node.path == LocalLibraryState.folders-selected-path;
toggle => {
LocalLibraryActions.folders-toggle-node(node.path, !node.expanded);
}
activate => {
LocalLibraryActions.folders-select(node.path, node.segment);
}
}
}
}
}
// Divider.
Rectangle {
width: 1px;
background: Theme.border-subtle;
}
// ---- Right pane: selected folder detail ----
Rectangle {
horizontal-stretch: 1;
// Nothing selected yet.
if LocalLibraryState.folders-selected-path == "": VerticalLayout {
alignment: center;
Text {
text: @tr("Select a folder to see its contents.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
if LocalLibraryState.folders-selected-path != "": Rectangle {
detail-flick := Flickable {
viewport-width: self.width;
viewport-height: detail-page.preferred-height;
detail-page := VerticalLayout {
width: parent.width;
padding-left: 28px;
padding-right: 28px;
padding-top: 16px;
padding-bottom: 100px;
spacing: 14px;
alignment: start;
// Header: folder name + Play all.
HorizontalLayout {
spacing: 12px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: LocalLibraryState.folders-selected-name;
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
overflow: elide;
}
}
VerticalLayout {
alignment: center;
Rectangle {
width: playall-row.preferred-width + 28px;
height: 34px;
border-radius: 6px;
background: playall-ta.has-hover
? Theme.accent-hover : Theme.accent;
playall-row := HorizontalLayout {
x: (parent.width - self.width) / 2;
spacing: 7px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/play-fill.svg");
width: 13px;
height: 13px;
tint: Theme.accent-text;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Play all");
color: Theme.accent-text;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
}
}
playall-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LocalLibraryActions.folders-play-node(
LocalLibraryState.folders-selected-path);
}
}
}
}
}
if LocalLibraryState.folder-detail-loading: HorizontalLayout {
alignment: center;
padding-top: 30px;
LoadingSpinner { size: 28px; }
}
// Subfolders (drill-down), if any.
if !LocalLibraryState.folder-detail-loading
&& LocalLibraryState.folder-detail-subfolders.length > 0: VerticalLayout {
spacing: 6px;
Text {
text: @tr("Subfolders");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
for sub[si] in LocalLibraryState.folder-detail-subfolders: Rectangle {
height: 34px;
border-radius: 6px;
background: sub-ta.has-hover ? Theme.surface-hover : transparent;
HorizontalLayout {
padding-left: 8px;
padding-right: 10px;
spacing: 8px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/folder.svg");
width: 16px;
height: 16px;
tint: Theme.accent;
}
}
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
Text {
text: sub.segment;
color: Theme.text-secondary;
font-size: Typography.body;
overflow: elide;
}
}
if sub.track-count > 0: VerticalLayout {
alignment: center;
Text {
text: sub.track-count;
color: Theme.text-muted;
font-size: Typography.legal;
}
}
}
sub-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LocalLibraryActions.folders-select(sub.path, sub.segment);
}
}
}
}
// Direct tracks.
if !LocalLibraryState.folder-detail-loading
&& LocalLibraryState.folder-detail-tracks.length > 0: VerticalLayout {
spacing: 0px;
for track[ti] in LocalLibraryState.folder-detail-tracks: TrackRow {
track: track;
index: ti;
show-artwork: AppearanceState.local-library-track-artwork;
show-album: true;
show-favorite: false;
show-download: false;
show-source: true;
media-action(kind, id, action) => {
if (kind == "album" && action == "open") {
LocalLibraryActions.open-album(id);
} else if (kind == "artist" && action == "open") {
LocalLibraryActions.open-artist(id);
} else {
LocalLibraryActions.folders-play-track(id);
}
}
}
}
// Folder has no direct tracks (only subfolders).
if !LocalLibraryState.folder-detail-loading
&& LocalLibraryState.folder-detail-tracks.length == 0
&& LocalLibraryState.folder-detail-subfolders.length == 0: VerticalLayout {
padding-top: 24px;
Text {
text: @tr("This folder has no tracks.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: detail-flick.viewport-height;
visible-height: detail-flick.height;
viewport-y <=> detail-flick.viewport-y;
}
}
}
}
}
// -------- Artists tab — two-column master/detail -------------
if LocalLibraryState.active-tab == "artists": Rectangle {
if LocalLibraryState.artists-loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { size: 36px; }
}
}
// Empty library.
if !LocalLibraryState.artists-loading
&& LocalLibraryState.artists.length == 0: VerticalLayout {
alignment: center;
spacing: 10px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/user.svg");
width: 44px;
height: 44px;
tint: Theme.text-muted;
}
}
Text {
text: @tr("No artists in your local library yet.");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// Two-column layout.
if !LocalLibraryState.artists-loading
&& LocalLibraryState.artists.length > 0: HorizontalLayout {
spacing: 0px;
// ---- Left rail: merged, A-Z grouped master list ----
Rectangle {
width: 280px;
clip: true;
VerticalLayout {
padding-left: 24px;
padding-top: 12px;
spacing: 8px;
// Rail header — count after filter.
Text {
text: "\{LocalLibraryState.artists-shown} " + @tr("artists");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
// No-match state.
if LocalLibraryState.artists-shown == 0: Text {
text: @tr("No artists match your search.");
color: Theme.text-muted;
font-size: Typography.legal;
}
if LocalLibraryState.artists-shown > 0: Rectangle {
vertical-stretch: 1;
rail-flick := Flickable {
viewport-width: self.width;
viewport-height: rail-col.preferred-height;
rail-col := VerticalLayout {
width: parent.width;
padding-right: 22px;
padding-bottom: 40px;
spacing: 4px;
alignment: start;
for section[s] in LocalLibraryState.artists-grouped: VerticalLayout {
spacing: 2px;
Text {
text: section.letter;
color: Theme.text-secondary;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
for artist[i] in section.artists: LocalArtistRow {
item: artist;
selected: LocalLibraryState.artists-selected-name == artist.name;
clicked(name) => {
LocalLibraryActions.artists-select(name);
}
}
}
}
}
if LocalLibraryState.artists-alpha.length > 0: AlphaStrip {
x: parent.width - self.width - 2px;
height: 100%;
jumps: LocalLibraryState.artists-alpha;
jump(ord, idx) => {
rail-flick.viewport-y = -(ord / Math.max(1, LocalLibraryState.artists-alpha.length - 1))
* Math.max(0, (rail-flick.viewport-height - rail-flick.height) / 1px) * 1px;
}
}
}
}
}
// Divider.
Rectangle {
width: 1px;
background: Theme.border-subtle;
}
// ---- Right pane: selected artist's albums ----
Rectangle {
horizontal-stretch: 1;
clip: true;
// Nothing selected.
if LocalLibraryState.artists-selected-name == "": VerticalLayout {
alignment: center;
spacing: 10px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/user.svg");
width: 44px;
height: 44px;
tint: Theme.text-muted;
}
}
Text {
text: @tr("Select an artist to view their albums");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
if LocalLibraryState.artists-selected-name != "": Rectangle {
VerticalLayout {
padding-left: 28px;
padding-right: 8px;
padding-top: 14px;
spacing: 12px;
// Header — artist name + album count.
VerticalLayout {
spacing: 2px;
Text {
text: LocalLibraryState.artists-selected-display;
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
overflow: elide;
}
Text {
text: "\{LocalLibraryState.artists-selected-albums.length} " + @tr("albums");
color: Theme.text-muted;
font-size: Typography.legal;
}
}
if LocalLibraryState.artists-selected-loading: HorizontalLayout {
alignment: center;
padding-top: 30px;
LoadingSpinner { size: 28px; }
}
// No albums for this artist.
if !LocalLibraryState.artists-selected-loading
&& LocalLibraryState.artists-selected-albums.length == 0: VerticalLayout {
padding-top: 24px;
Text {
text: @tr("No albums found");
color: Theme.text-muted;
font-size: Typography.body;
horizontal-alignment: center;
}
}
// Album grid — self-scrolling.
if !LocalLibraryState.artists-selected-loading
&& LocalLibraryState.artists-selected-albums.length > 0: Rectangle {
vertical-stretch: 1;
artist-albums-flick := Flickable {
viewport-width: self.width;
viewport-height: artist-albums-page.preferred-height;
artist-albums-page := VerticalLayout {
width: parent.width;
padding-right: 20px;
padding-bottom: 100px;
alignment: start;
AlbumCollectionView {
albums: LocalLibraryState.artists-selected-albums;
is-grouped: false;
view-mode: "grid";
show-source: false;
show-favorite: false;
show-source-badge: true;
open-album(id) => {
LocalLibraryActions.open-album(id);
}
open-artist(id) => {
LocalLibraryActions.open-artist(id);
}
media-action(kind, id, action) => {
LocalLibraryActions.album-action(id, action);
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: artist-albums-flick.viewport-height;
visible-height: artist-albums-flick.height;
viewport-y <=> artist-albums-flick.viewport-y;
}
}
}
}
}
}
}
// -------- Other tabs — placeholder (all four now have bodies) -
if LocalLibraryState.active-tab != "albums"
&& LocalLibraryState.active-tab != "tracks"
&& LocalLibraryState.active-tab != "folders"
&& LocalLibraryState.active-tab != "artists": VerticalLayout {
alignment: center;
spacing: 6px;
Text {
text: @tr("Local Library port in progress");
horizontal-alignment: center;
color: Theme.text-secondary;
font-size: Typography.body;
font-weight: Typography.semibold;
}
Text {
text: @tr("This tab is coming next.");
horizontal-alignment: center;
color: Theme.text-muted;
font-size: Typography.legal;
}
}
}
}
// ===== Albums quality/format/source filter popup (overlay) =====
// Sibling of the content VerticalLayout (child of the view Rectangle), so
// it FLOATS over the content instead of taking a layout slot.
if LibAlbumFilterState.open: Rectangle {
// Click-out backdrop (covers the whole view).
TouchArea {
mouse-cursor: default;
clicked => {
LibAlbumFilterState.open = false;
}
}
Rectangle {
width: 372px;
height: filt-col.preferred-height;
x: parent.width - self.width - 36px;
y: 92px;
background: Theme.surface-card;
border-radius: 10px;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 28px;
drop-shadow-color: #00000080;
TouchArea { }
filt-col := VerticalLayout {
padding: 16px;
spacing: 10px;
HorizontalLayout {
Text {
text: @tr("Filter");
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.semibold;
horizontal-stretch: 1;
vertical-alignment: center;
}
if LibAlbumFilterState.count > 0: Rectangle {
width: clr.preferred-width + 18px;
height: 28px;
border-radius: 6px;
background: clr-ta.has-hover ? Theme.surface-hover : transparent;
clr := Text {
text: @tr("Clear");
color: Theme.accent;
font-size: Typography.legal;
font-weight: Typography.medium;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
clr-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
LocalLibraryActions.albums-clear-filter();
}
}
}
}
// Quality.
Text {
text: @tr("Quality");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
HorizontalLayout {
spacing: 8px;
FilterChip {
label: @tr("Hi-Res");
active: LibAlbumFilterState.hires;
toggled => {
LibAlbumFilterState.hires = !LibAlbumFilterState.hires;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: @tr("CD");
active: LibAlbumFilterState.cd;
toggled => {
LibAlbumFilterState.cd = !LibAlbumFilterState.cd;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: @tr("Lossy");
active: LibAlbumFilterState.lossy;
toggled => {
LibAlbumFilterState.lossy = !LibAlbumFilterState.lossy;
LocalLibraryActions.albums-filter-changed();
}
}
Rectangle { horizontal-stretch: 1; }
}
// Format.
Text {
text: @tr("Format");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
HorizontalLayout {
spacing: 8px;
FilterChip {
label: "FLAC";
active: LibAlbumFilterState.flac;
toggled => {
LibAlbumFilterState.flac = !LibAlbumFilterState.flac;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: "ALAC";
active: LibAlbumFilterState.alac;
toggled => {
LibAlbumFilterState.alac = !LibAlbumFilterState.alac;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: "APE";
active: LibAlbumFilterState.ape;
toggled => {
LibAlbumFilterState.ape = !LibAlbumFilterState.ape;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: "WAV";
active: LibAlbumFilterState.wav;
toggled => {
LibAlbumFilterState.wav = !LibAlbumFilterState.wav;
LocalLibraryActions.albums-filter-changed();
}
}
Rectangle { horizontal-stretch: 1; }
}
HorizontalLayout {
spacing: 8px;
FilterChip {
label: "MP3";
active: LibAlbumFilterState.mp3;
toggled => {
LibAlbumFilterState.mp3 = !LibAlbumFilterState.mp3;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: "AAC";
active: LibAlbumFilterState.aac;
toggled => {
LibAlbumFilterState.aac = !LibAlbumFilterState.aac;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: @tr("Other");
active: LibAlbumFilterState.other;
toggled => {
LibAlbumFilterState.other = !LibAlbumFilterState.other;
LocalLibraryActions.albums-filter-changed();
}
}
Rectangle { horizontal-stretch: 1; }
}
// Source.
Text {
text: @tr("Source");
color: Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.semibold;
}
HorizontalLayout {
spacing: 8px;
FilterChip {
label: @tr("Local");
active: LibAlbumFilterState.local;
toggled => {
LibAlbumFilterState.local = !LibAlbumFilterState.local;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: @tr("Offline cache");
active: LibAlbumFilterState.offline;
toggled => {
LibAlbumFilterState.offline = !LibAlbumFilterState.offline;
LocalLibraryActions.albums-filter-changed();
}
}
FilterChip {
label: "Plex";
active: LibAlbumFilterState.plex;
toggled => {
LibAlbumFilterState.plex = !LibAlbumFilterState.plex;
LocalLibraryActions.albums-filter-changed();
}
}
Rectangle { horizontal-stretch: 1; }
}
}
}
}
}