Files
qbz/crates/qbz-ui/ui/settings/BlacklistManagerView.slint
T
vicrodh 66889af421 feat(blacklist): album-block context menu + manager Albums tab; albumview polish
Phase 2 (Slint UI + wiring) for album-level blocking, plus the album-view
enhancements that share these files.

Album blacklist UI:
- "Block this album" in all three album menus: the page-header menu as a
  Block/Unblock toggle (gated to Qobuz via AlbumState), and the AlbumCard +
  AlbumListRow grid menus (one-way, drop-on-reload; local/Plex excluded).
  Menu heights bumped for the extra item.
- Blacklist Manager gains an Artists | Albums tab bar (segmented, ADR-008);
  an AlbumRow with a cover thumbnail (blind-eye fallback) + title + artist +
  "Added {date}"; tab-aware controls (shared enable toggle, per-tab search /
  Clear-All / count) and confirm modal. Settings row retitled "Blacklist",
  status shows both counts.
- state.slint: BlacklistedAlbumItem + album-items/album-count/active-tab on
  BlacklistState; set-tab/album-select/remove-album/clear-all-albums/
  block-album on BlacklistActions; AlbumState.is-album-blocked.
- main.rs: ("album","block"/"unblock") arm (header, resolves display fields
  from AlbumState); 5 album BlacklistActions callbacks; seed_blacklist_status
  pushes album_count.
- blacklist_manager.rs: album build/push + set_tab/block_album/remove_album/
  clear_all_albums; covers resolve async via a new ArtworkTarget::BlacklistAlbum
  through spawn_loads.

Album-view enhancements (same files):
- E1: full credited-artist header line with localized roles, each name a
  clickable artist link (Album.artists drove this; was interpreter-only).
- E3: classical work-header composer name is now a clickable artist link
  (work title + composer id/name split through TrackData/TrackItem).

i18n: es (Mexican) for all new strings; de/fr/pt/ru/ja fall back to English
pending a translator pass.

cargo check -p qbz green (Slint codegen + bin).

cc
2026-06-30 00:06:05 -06:00

903 lines
37 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Artist Blacklist Manager — a faithful Slint port of Tauri's
// BlacklistManagerView.svelte (consolidated review §7). The standalone
// manage-blacklist surface reached from the Settings content-filtering row:
// a header (Back + ban icon + title), a description line, a controls row
// (enable/disable toggle / search-as-you-type with × clear / Clear-All /
// total-count badge), an amber disabled-warning banner, and a 4-branch body
// (loading / empty / no-search-results / list), plus a Clear-All confirm
// modal overlaid above everything (ADR-009 — declared last so it stacks on
// top; the manager itself has no other overlay).
//
// State + actions live in BlacklistState / BlacklistActions; the controller
// (crate::blacklist_manager) precomputes the date string + has-notes flag and
// pushes ONLY the search-filtered rows into `items`, while `count` stays the
// FULL list count — so "empty" = count==0 and "no results" = count>0 with an
// empty `items`. Rows show a generic blind-eye glyph (no artist image exists),
// the name, "Added {date}", optional italic notes, and a per-row remove X
// (optimistic, no confirm). The blacklist visual across the Slint port is
// `blind-eye.svg` (TrackRow + ArtistPageView, T5/T9), used here too instead of
// Tauri's lucide `Ban`.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { BlacklistState, BlacklistActions, UiFocusState } from "../state.slint";
import { QbzIcon } from "../primitives/QbzIcon.slint";
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
import { NavButtons } from "../shell/NavButtons.slint";
// One blacklisted-artist row.
component ArtistRow inherits Rectangle {
in property <string> artist-name;
in property <string> added-display;
in property <string> notes;
in property <bool> has-notes;
property <bool> hovered: ta.has-hover || rm-ta.has-hover;
callback select();
callback remove();
callback hover-changed(bool);
height: 64px;
border-radius: Radius.md;
background: root.hovered ? Theme.surface-elevated : Theme.surface-card;
changed hovered => {
root.hover-changed(self.hovered);
}
HorizontalLayout {
padding-left: 16px;
padding-right: 8px;
spacing: 12px;
// Clickable artist-info region — opens the artist page.
Rectangle {
horizontal-stretch: 1;
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.select();
}
}
HorizontalLayout {
spacing: 12px;
// Generic blind-eye avatar (no artist image data exists).
VerticalLayout {
alignment: center;
Rectangle {
width: 44px;
height: 44px;
border-radius: 22px;
background: Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/blind-eye.svg");
width: 20px;
height: 20px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: Theme.text-muted;
}
}
}
// Name + meta (+ optional notes).
VerticalLayout {
alignment: center;
spacing: 2px;
Text {
text: root.artist-name;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.medium;
overflow: elide;
}
Text {
text: @tr("Added {}", root.added-display);
color: Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
if root.has-notes: Text {
text: root.notes;
color: Theme.text-muted;
font-size: Typography.legal;
font-italic: true;
overflow: elide;
}
}
}
}
// Per-row remove (optimistic, no confirm).
VerticalLayout {
alignment: center;
Rectangle {
width: 36px;
height: 36px;
border-radius: Radius.sm;
background: rm-ta.has-hover ? Theme.danger-bg : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 18px;
height: 18px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: rm-ta.has-hover ? Theme.danger : Theme.text-muted;
}
rm-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.remove();
}
}
}
}
}
}
// One blocked-album row (parallel to ArtistRow, with a cover thumbnail).
component AlbumRow inherits Rectangle {
in property <string> album-title;
in property <string> artist-name;
in property <string> added-display;
in property <image> cover-image;
property <bool> hovered: ta.has-hover || rm-ta.has-hover;
callback select();
callback remove();
callback hover-changed(bool);
height: 64px;
border-radius: Radius.md;
background: root.hovered ? Theme.surface-elevated : Theme.surface-card;
changed hovered => {
root.hover-changed(self.hovered);
}
HorizontalLayout {
padding-left: 16px;
padding-right: 8px;
spacing: 12px;
Rectangle {
horizontal-stretch: 1;
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.select();
}
}
HorizontalLayout {
spacing: 12px;
// 48x48 cover with a blind-eye fallback (shows through until the
// network cover resolves, or when none exists).
VerticalLayout {
alignment: center;
Rectangle {
width: 48px;
height: 48px;
border-radius: Radius.sm;
clip: true;
background: Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/blind-eye.svg");
width: 20px;
height: 20px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: Theme.text-muted;
}
Image {
width: 100%;
height: 100%;
source: root.cover-image;
image-fit: cover;
}
}
}
VerticalLayout {
alignment: center;
spacing: 2px;
Text {
text: root.album-title;
color: Theme.text-primary;
font-size: Typography.body;
font-weight: Typography.medium;
overflow: elide;
}
Text {
text: root.artist-name;
color: Theme.text-secondary;
font-size: Typography.legal;
overflow: elide;
}
Text {
text: @tr("Added {}", root.added-display);
color: Theme.text-muted;
font-size: Typography.legal;
overflow: elide;
}
}
}
}
VerticalLayout {
alignment: center;
Rectangle {
width: 36px;
height: 36px;
border-radius: Radius.sm;
background: rm-ta.has-hover ? Theme.danger-bg : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 18px;
height: 18px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: rm-ta.has-hover ? Theme.danger : Theme.text-muted;
}
rm-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.remove();
}
}
}
}
}
}
export component BlacklistManagerView inherits Rectangle {
background: Theme.surface-main;
// Local modal flag (Clear-All confirmation). Lives in the view; the
// confirm only fires BlacklistActions.clear-all on the danger button.
property <bool> confirm-clear-open: false;
property <int> list-hover-rows: 0;
VerticalLayout {
padding: 24px;
spacing: 14px;
// Top bar — Back (ADR-004 via NavButtons; routes through nav history).
HorizontalLayout {
vertical-stretch: 0;
NavButtons { }
}
// Header — blind-eye icon + title.
HorizontalLayout {
vertical-stretch: 0;
spacing: 12px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/blind-eye.svg");
width: 24px;
height: 24px;
tint: Theme.text-muted;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Blacklist");
color: Theme.text-primary;
font-size: Typography.section;
font-weight: Typography.bold;
}
}
}
// Tab bar — Artists | Albums (segmented, no pills per ADR-008).
HorizontalLayout {
vertical-stretch: 0;
spacing: 0;
alignment: start;
Rectangle {
width: tabs-row.preferred-width + 8px;
height: 36px;
border-radius: Radius.sm;
background: Theme.surface-elevated;
tabs-row := HorizontalLayout {
x: 4px;
width: self.preferred-width;
height: 100%;
padding: 4px;
spacing: 4px;
// Artists tab.
Rectangle {
width: artists-lbl.preferred-width + 28px;
border-radius: Radius.sm;
background: BlacklistState.active-tab == 0
? Theme.surface-card
: (artists-ta.has-hover ? Theme.surface-hover : transparent);
artists-lbl := Text {
text: @tr("Artists");
color: BlacklistState.active-tab == 0 ? Theme.text-primary : Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.medium;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
artists-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
BlacklistActions.set-tab(0);
}
}
}
// Albums tab.
Rectangle {
width: albums-lbl.preferred-width + 28px;
border-radius: Radius.sm;
background: BlacklistState.active-tab == 1
? Theme.surface-card
: (albums-ta.has-hover ? Theme.surface-hover : transparent);
albums-lbl := Text {
text: @tr("Albums");
color: BlacklistState.active-tab == 1 ? Theme.text-primary : Theme.text-muted;
font-size: Typography.legal;
font-weight: Typography.medium;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
albums-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
BlacklistActions.set-tab(1);
}
}
}
}
}
}
// Description (per tab).
Text {
vertical-stretch: 0;
text: BlacklistState.active-tab == 0
? @tr("Blacklisted artists are hidden from search results, radio, suggestions, and similar artists.")
: @tr("Blocked albums are hidden from search, discovery, and listings — even when their artist is allowed.");
color: Theme.text-muted;
font-size: Typography.body;
wrap: word-wrap;
}
// Controls row.
HorizontalLayout {
vertical-stretch: 0;
spacing: 12px;
// Enable/Disable toggle.
Rectangle {
width: toggle-row.preferred-width + 28px;
height: 34px;
border-radius: Radius.sm;
background: toggle-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
toggle-row := HorizontalLayout {
x: 14px;
width: self.preferred-width;
height: 100%;
spacing: 8px;
VerticalLayout {
alignment: center;
QbzIcon {
source: BlacklistState.enabled
? @image-url("../assets/icons/eye.svg")
: @image-url("../assets/icons/eye-off.svg");
width: 18px;
height: 18px;
tint: BlacklistState.enabled ? Theme.accent : Theme.text-muted;
}
}
VerticalLayout {
alignment: center;
Text {
text: BlacklistState.enabled ? @tr("Enabled") : @tr("Disabled");
color: BlacklistState.enabled ? Theme.accent : Theme.text-muted;
font-size: Typography.legal;
vertical-alignment: center;
}
}
}
toggle-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
BlacklistActions.toggle-enabled();
}
}
}
// Search input (search-as-you-type) + clear (×).
Rectangle {
width: 280px;
height: 34px;
border-radius: Radius.sm;
background: Theme.surface-elevated;
border-width: 1px;
border-color: search-in.has-focus ? Theme.accent : Theme.border-subtle;
HorizontalLayout {
padding-left: 10px;
padding-right: 6px;
spacing: 6px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 15px;
height: 15px;
tint: Theme.text-muted;
}
}
Rectangle {
horizontal-stretch: 1;
search-in := TextInput {
width: 100%;
height: 100%;
vertical-alignment: center;
color: Theme.text-primary;
font-size: Typography.legal;
text: BlacklistState.search-query;
changed has-focus => { UiFocusState.text-input-focused = self.has-focus; }
// Search-as-you-type: every edit re-pushes the
// controller-filtered rows. No debounce (Tauri parity).
edited => {
BlacklistActions.search-changed(self.text);
}
}
}
if BlacklistState.search-query != "": VerticalLayout {
alignment: center;
Rectangle {
width: 24px;
height: 24px;
border-radius: Radius.sm;
background: clear-ta.has-hover ? Theme.surface-hover : transparent;
QbzIcon {
source: @image-url("../assets/icons/x.svg");
width: 13px;
height: 13px;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
tint: clear-ta.has-hover ? Theme.text-primary : Theme.text-muted;
}
clear-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
search-in.text = "";
BlacklistActions.search-changed("");
}
}
}
}
}
}
// Clear-All — only when the active tab has something to clear.
if (BlacklistState.active-tab == 0 ? BlacklistState.count : BlacklistState.album-count) > 0: Rectangle {
width: clear-all-row.preferred-width + 28px;
height: 34px;
border-radius: Radius.sm;
background: clear-all-ta.has-hover ? Theme.danger-bg : Theme.surface-elevated;
clear-all-row := HorizontalLayout {
x: 14px;
width: self.preferred-width;
height: 100%;
spacing: 8px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/trash-2.svg");
width: 16px;
height: 16px;
tint: Theme.danger;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Clear All");
color: Theme.danger;
font-size: Typography.legal;
vertical-alignment: center;
}
}
}
clear-all-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.confirm-clear-open = true;
}
}
}
// Spacer + count badge (FULL count, right-aligned).
Rectangle {
horizontal-stretch: 1;
}
VerticalLayout {
alignment: center;
Text {
text: BlacklistState.active-tab == 0
? @tr("{} artists", BlacklistState.count)
: @tr("{} albums", BlacklistState.album-count);
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: right;
}
}
}
// Disabled warning banner.
if !BlacklistState.enabled: Rectangle {
vertical-stretch: 0;
height: warn-row.preferred-height + 24px;
border-radius: Radius.md;
background: Theme.warning-bg;
border-width: 1px;
border-color: Theme.warning-border;
warn-row := HorizontalLayout {
padding: 12px;
spacing: 10px;
VerticalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/circle-alert.svg");
width: 16px;
height: 16px;
tint: Theme.warning;
}
}
VerticalLayout {
alignment: center;
Text {
text: @tr("Blacklist filtering is disabled. Blacklisted artists are shown everywhere until you re-enable it.");
color: Theme.warning;
font-size: Typography.legal;
wrap: word-wrap;
}
}
}
}
// Body — 4 mutually-exclusive branches (order is load-bearing).
Rectangle {
vertical-stretch: 1;
// 1) Loading.
if BlacklistState.loading: VerticalLayout {
alignment: center;
HorizontalLayout {
alignment: center;
LoadingSpinner { }
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("Loading blacklist…");
color: Theme.text-muted;
font-size: Typography.body;
}
}
}
// 2) Empty blacklist (Artists tab).
if BlacklistState.active-tab == 0 && !BlacklistState.loading && BlacklistState.count == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/blind-eye.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No blacklisted artists");
color: Theme.text-muted;
font-size: Typography.heading;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("To hide an artist, go to their page and use the menu.");
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
}
}
}
// 3) No search results (Artists tab; full list non-empty, filtered empty).
if BlacklistState.active-tab == 0 && !BlacklistState.loading && BlacklistState.count > 0 && BlacklistState.items.length == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No results for \"{}\"", BlacklistState.search-query);
color: Theme.text-muted;
font-size: Typography.heading;
}
}
}
// 4) The list. Use Flickable + the shared custom scrollbar so the
// scrollbar can reveal on list/gutter hover; std ScrollView does
// not expose hover-controlled scrollbar visibility.
if BlacklistState.active-tab == 0 && !BlacklistState.loading && BlacklistState.items.length > 0: Rectangle {
list-flick := Flickable {
width: 100%;
height: 100%;
viewport-width: self.width;
viewport-height: list-content.preferred-height;
list-content := VerticalLayout {
width: parent.width - 18px;
spacing: 4px;
for item in BlacklistState.items: ArtistRow {
artist-name: item.artist-name;
added-display: item.added-display;
notes: item.notes;
has-notes: item.has-notes;
select => {
BlacklistActions.artist-select(item.artist-id);
}
remove => {
BlacklistActions.remove(item.artist-id);
}
hover-changed(h) => {
root.list-hover-rows = h ? root.list-hover-rows + 1 : Math.max(0, root.list-hover-rows - 1);
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: list-flick.viewport-height;
visible-height: list-flick.height;
viewport-y <=> list-flick.viewport-y;
auto-hide: true;
hover-active: root.list-hover-rows > 0;
}
}
// --- Albums tab branches -------------------------------------
// 2b) Empty (no blocked albums).
if BlacklistState.active-tab == 1 && !BlacklistState.loading && BlacklistState.album-count == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/blind-eye.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No blocked albums");
color: Theme.text-muted;
font-size: Typography.heading;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("To block an album, open its menu and choose Block this album.");
color: Theme.text-muted;
font-size: Typography.legal;
horizontal-alignment: center;
}
}
}
// 3b) No search results (albums).
if BlacklistState.active-tab == 1 && !BlacklistState.loading && BlacklistState.album-count > 0 && BlacklistState.album-items.length == 0: VerticalLayout {
alignment: center;
spacing: 12px;
HorizontalLayout {
alignment: center;
QbzIcon {
source: @image-url("../assets/icons/search.svg");
width: 48px;
height: 48px;
tint: Theme.text-muted;
opacity: 0.3;
}
}
HorizontalLayout {
alignment: center;
Text {
text: @tr("No results for \"{}\"", BlacklistState.search-query);
color: Theme.text-muted;
font-size: Typography.heading;
}
}
}
// 4b) The blocked-albums list.
if BlacklistState.active-tab == 1 && !BlacklistState.loading && BlacklistState.album-items.length > 0: Rectangle {
album-flick := Flickable {
width: 100%;
height: 100%;
viewport-width: self.width;
viewport-height: album-content.preferred-height;
album-content := VerticalLayout {
width: parent.width - 18px;
spacing: 4px;
for item in BlacklistState.album-items: AlbumRow {
album-title: item.album-title;
artist-name: item.artist-name;
added-display: item.added-display;
cover-image: item.cover;
select => {
BlacklistActions.album-select(item.album-id);
}
remove => {
BlacklistActions.remove-album(item.album-id);
}
hover-changed(h) => {
root.list-hover-rows = h ? root.list-hover-rows + 1 : Math.max(0, root.list-hover-rows - 1);
}
}
}
}
ListScrollbar {
x: parent.width - self.width - 4px;
width: 14px;
height: 100%;
viewport-height: album-flick.viewport-height;
visible-height: album-flick.height;
viewport-y <=> album-flick.viewport-y;
auto-hide: true;
hover-active: root.list-hover-rows > 0;
}
}
}
}
// Clear-All confirm modal — declared LAST so it stacks above the view
// (ADR-009 ≥ 3000 satisfied by declaration order; mirrors
// PlaylistDuplicateConfirmModal's scrim + centered card). Backdrop click
// dismisses; Esc dismisses via the focus-scope key handler.
if root.confirm-clear-open: Rectangle {
background: #000000bf;
// Scrim — click outside to dismiss.
TouchArea {
mouse-cursor: default;
clicked => {
root.confirm-clear-open = false;
}
}
// Esc dismiss. Grab focus on mount so the key handler is live.
esc-scope := FocusScope {
init => {
self.focus();
}
key-pressed(event) => {
if (event.text == Key.Escape) {
root.confirm-clear-open = false;
return accept;
}
return reject;
}
}
Rectangle {
width: Math.min(root.width - 80px, 400px);
height: panel.preferred-height;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
border-radius: Radius.md;
background: Theme.surface-main;
border-width: 1px;
border-color: Theme.border-subtle;
drop-shadow-blur: 32px;
drop-shadow-color: #00000080;
// Swallow clicks so they don't reach the scrim.
TouchArea { }
panel := VerticalLayout {
padding: 24px;
spacing: 16px;
Text {
text: @tr("Clear Blacklist?");
color: Theme.text-primary;
font-size: Typography.heading;
font-weight: Typography.semibold;
}
Text {
text: BlacklistState.active-tab == 0
? @tr("This will remove all {} blacklisted artists. This cannot be undone.", BlacklistState.count)
: @tr("This will remove all {} blocked albums. This cannot be undone.", BlacklistState.album-count);
color: Theme.text-secondary;
font-size: Typography.body;
wrap: word-wrap;
}
HorizontalLayout {
alignment: end;
spacing: 10px;
// Cancel.
Rectangle {
width: cancel-lbl.preferred-width + 32px;
height: 36px;
border-radius: Radius.sm;
background: cancel-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
cancel-lbl := Text {
text: @tr("Cancel");
color: Theme.text-primary;
font-size: Typography.body;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
cancel-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.confirm-clear-open = false;
}
}
}
// Danger Clear-All.
Rectangle {
width: danger-lbl.preferred-width + 32px;
height: 36px;
border-radius: Radius.sm;
// Solid-filled danger button: base is the theme's
// danger color, hover a derived darker solid shade.
// NOT Theme.danger-hover — that token is a translucent
// hover-tint for ghost/borderless danger surfaces (the
// trash buttons above); using it on a filled button
// would wash the fill out (and is LIGHTER in some a11y
// themes per the migration palette), regressing the
// white label's contrast. .darker() mirrors how
// accent-pressed is derived in theme.slint.
background: danger-ta.has-hover ? Theme.danger.darker(0.12) : Theme.danger;
danger-lbl := Text {
text: @tr("Clear All");
color: #ffffff;
font-size: Typography.body;
font-weight: Typography.medium;
x: Math.round((parent.width - self.width) / 2 / 1px) * 1px;
y: Math.round((parent.height - self.height) / 2 / 1px) * 1px;
}
danger-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if (BlacklistState.active-tab == 0) {
BlacklistActions.clear-all();
} else {
BlacklistActions.clear-all-albums();
}
root.confirm-clear-open = false;
}
}
}
}
}
}
}
}