feat(slint): per-row offline affordance + the unlock padlock (Slice 2 UI)

TrackRow now renders the offline state the controller maintains:

- The offline button is stateful: cloud-download (none, click -> cache) ->
  loader-circle accent (queued/downloading) -> circle-check accent (ready,
  click -> uncache) -> triangle-alert red (failed, click -> retry). Shown on
  surfaces that enable it AND on any row with active cache state.
- The unlock PADLOCK (user-requested, back from Tauri): a pulsing lock glyph
  overlays the play cell while the track's CMAF bundle decrypts, driven by the
  `unlocking` flag the row_sink toggles on UnlockStart/UnlockEnd. New lucide
  lock.svg / lock-open.svg assets.

The context menu's "Make available offline" already routes to caching. Testable
loop: cache a track (album button or context menu) -> watch it download -> play
it offline -> padlock pulses during decrypt. qbz-slint compiles clean.

cc
This commit is contained in:
vicrodh committed 2026-05-27 22:05:37 -06:00
1 parent b65b402ff1
commit d5f23c38d8
3 files changed
+61 -5

No files matched your search

+53 -5
View File
@@ -220,6 +220,34 @@ export component TrackRow inherits Rectangle {
}
}
}
// Offline-decrypt padlock — overlays the play cell while the
// track's CMAF bundle is being decrypted for playback. Pulses
// to read as "unlocking" (mirrors Tauri's unlock glyph).
if root.track.unlocking: lock-overlay := Rectangle {
property <bool> pulse;
width: parent.width;
height: parent.height;
border-radius: 6px;
background: #000000aa;
QbzIcon {
source: @image-url("../assets/icons/lock.svg");
width: 15px;
height: 15px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.accent;
opacity: lock-overlay.pulse ? 1.0 : 0.4;
animate opacity { duration: 450ms; }
}
Timer {
interval: 450ms;
running: true;
triggered => {
lock-overlay.pulse = !lock-overlay.pulse;
}
}
}
}
}
@@ -365,8 +393,15 @@ export component TrackRow inherits Rectangle {
}
}
}
// Offline cache.
if root.show-download: VerticalLayout {
// Offline cache — a stateful affordance reflecting cache-status:
// 0 none cloud-download (click -> cache)
// 1/2 q'd/dl'ing loader (accent) (click -> cache; re-queues)
// 3 ready circle-check (click -> uncache)
// 4 failed triangle-alert (click -> cache; retries)
// Shown when the surface enables it OR the track has active state, so
// a downloading/ready copy surfaces its status even where the button
// isn't enabled by default.
if root.show-download || root.track.cache-status > 0: VerticalLayout {
alignment: center;
Rectangle {
width: 28px;
@@ -374,17 +409,30 @@ export component TrackRow inherits Rectangle {
border-radius: Radius.sm;
background: dl-ta.has-hover ? Theme.surface-elevated : transparent;
QbzIcon {
source: @image-url("../assets/icons/cloud-download.svg");
source: root.track.cache-status == 3
? @image-url("../assets/icons/circle-check-big.svg")
: (root.track.cache-status == 4
? @image-url("../assets/icons/triangle-alert.svg")
: ((root.track.cache-status == 1 || root.track.cache-status == 2)
? @image-url("../assets/icons/loader-circle.svg")
: @image-url("../assets/icons/cloud-download.svg")));
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: dl-ta.has-hover ? Theme.text-primary : Theme.text-muted;
tint: root.track.cache-status == 3
? Theme.accent
: (root.track.cache-status == 4
? #e0564f
: ((root.track.cache-status == 1 || root.track.cache-status == 2)
? Theme.accent
: (dl-ta.has-hover ? Theme.text-primary : Theme.text-muted)));
}
dl-ta := TouchArea {
mouse-cursor: pointer;
clicked => {
root.media-action("track", root.track.id, "download");
root.media-action("track", root.track.id,
root.track.cache-status == 3 ? "uncache" : "cache");
}
}
}