Smoke round 2 (owner feedback: content area was an opaque black slab, motion
imperceptible, ambient≈blurred).
- Content-area sweep: 27 content-view roots go transparent when the app
background is active (was opaque Theme.surface-main covering the background) —
Home/Discover/Album/Artist/Search-adjacent/Favorites/Playlist/LocalLibrary/
MyQBZ/managers/etc. Content well (AppShell) transparent too.
- SongCard (NPB classic), search input box, cortinilla panel go translucent so
they stop reading as opaque slabs over the background.
- ambient.wgsl: much faster + bigger blob travel + stronger domain warp +
brighter/more saturated so the flow is obviously moving and clearly distinct
from the blurred-art mode.
- Defaults: dim 0.35, surface-alpha 0.5.
Checkpoint during smoke iteration (owner-requested). Base: 76ea5a82.
cc
903 lines
37 KiB
Plaintext
903 lines
37 KiB
Plaintext
// 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, ShellState } 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: ShellState.app-background-active ? transparent : 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;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|