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

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
</svg>

After

Width:  |  Height:  |  Size: 284 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
</svg>

After

Width:  |  Height:  |  Size: 285 B

+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. // Offline cache — a stateful affordance reflecting cache-status:
if root.show-download: VerticalLayout { // 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; alignment: center;
Rectangle { Rectangle {
width: 28px; width: 28px;
@@ -374,17 +409,30 @@ export component TrackRow inherits Rectangle {
border-radius: Radius.sm; border-radius: Radius.sm;
background: dl-ta.has-hover ? Theme.surface-elevated : transparent; background: dl-ta.has-hover ? Theme.surface-elevated : transparent;
QbzIcon { 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; width: 16px;
height: 16px; height: 16px;
x: (parent.width - self.width) / 2; x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 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 { dl-ta := TouchArea {
mouse-cursor: pointer; mouse-cursor: pointer;
clicked => { clicked => {
root.media-action("track", root.track.id, "download"); root.media-action("track", root.track.id,
root.track.cache-status == 3 ? "uncache" : "cache");
} }
} }
} }