Files
qbz/crates/qbz-slint/ui/primitives/TrackPlayCell.slint
T
vicrodh c7129de096 fix(slint): follow chip, playlist card size, track row hover, dedupe
- Follow button on the artist card is no longer a pill — Radius.sm
  rounded rectangle, matching the rest of the app (new ADR-008).
- PlaylistCard sized identically to AlbumCard (220x266) with the same
  220px artwork area; collage and the grid/carousel updated to match.
- Track row play overlay now appears anywhere over the row, not only
  when the pointer is directly over the cell. Slint does not propagate
  has-hover to ancestor TouchAreas, so each row OR-s its own ta with
  every nested TouchArea (play-cell, fav/dl/more) into row-hovered.
- EqualizerBars sit on a fixed 16px baseline so the animation stays
  perfectly centered while the bar heights oscillate.
- TrackPlayCell exposes out hovered so rows can OR it cleanly.
- Search dedupes a top-result artist that is also the first carousel
  entry, removing the redundant duplicate.

cc
2026-05-19 19:53:15 -06:00

172 lines
6.0 KiB
Plaintext

// Track-row leading cell — the play affordance shared by every track row.
//
// Two variants, chosen by `show-artwork`:
// - artwork: a cover thumbnail with a dark play/pause overlay;
// - number: a track-number cell that swaps to a play/pause glyph.
//
// States (ported 1:1 from the Tauri track rows):
// rest -> number / plain artwork
// row hovered (not current) -> play triangle
// current + playing -> animated equalizer bars
// current + playing + hover -> pause glyph
// current + paused -> play triangle
// Clicking a non-current cell plays the track; clicking the current cell
// toggles play/pause.
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { Radius } from "../foundation/radius.slint";
import { NowPlayingState } from "../state.slint";
import { QbzIcon } from "QbzIcon.slint";
// One equalizer bar — oscillates continuously while it is shown
// (`animation-tick()` drives the height; a 1s triangle wave).
component EqBar inherits Rectangle {
in property <length> base;
in property <float> delay; // 0..1 phase offset
width: 3px;
border-radius: 1.5px;
background: Theme.accent;
property <float> phase: Math.mod((animation-tick() / 1ms) / 1000.0 + root.delay, 1.0);
property <float> tri: 1.0 - Math.abs(root.phase - 0.5) * 2.0;
height: root.base * (0.5 + 0.5 * root.tri);
}
// Three-bar equalizer; bars grow from a fixed baseline so the block
// stays perfectly centered while the bar heights oscillate.
component EqualizerBars inherits Rectangle {
width: row.preferred-width;
height: 16px;
row := HorizontalLayout {
height: 16px;
spacing: 2px;
VerticalLayout {
height: 16px;
alignment: end;
EqBar { base: 10px; delay: 0.0; }
}
VerticalLayout {
height: 16px;
alignment: end;
EqBar { base: 14px; delay: 0.15; }
}
VerticalLayout {
height: 16px;
alignment: end;
EqBar { base: 8px; delay: 0.3; }
}
}
}
export component TrackPlayCell inherits Rectangle {
in property <string> track-id;
in property <string> number;
in property <image> artwork;
in property <bool> show-artwork: true;
in property <bool> row-hovered: false;
in property <length> art-size: 40px;
in property <length> number-width: 32px;
// True while the pointer is over the cell itself. The row OR's this
// with its other TouchAreas to drive `row-hovered` (Slint's has-hover
// does not propagate to ancestor TouchAreas).
out property <bool> hovered: ta.has-hover;
// Fired when a non-current track should start playing.
callback play();
property <bool> is-active:
NowPlayingState.has-track && NowPlayingState.track-id == root.track-id;
property <bool> is-playing: root.is-active && NowPlayingState.playing;
property <bool> show-overlay: root.row-hovered || root.is-active;
// The animated bars show only while playing and the row is at rest.
property <bool> show-bars: root.is-playing && !root.row-hovered;
width: root.show-artwork ? root.art-size : root.number-width;
height: root.show-artwork ? root.art-size : 40px;
// --- Artwork variant ------------------------------------------------
if root.show-artwork: Rectangle {
width: root.art-size;
height: root.art-size;
border-radius: Radius.sm;
clip: true;
background: Theme.surface-elevated;
QbzIcon {
source: @image-url("../assets/icons/music.svg");
width: root.art-size * 0.45;
height: root.art-size * 0.45;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: Theme.text-muted;
}
Image {
width: 100%;
height: 100%;
source: root.artwork;
image-fit: cover;
}
Rectangle {
width: 100%;
height: 100%;
background: #000000;
opacity: root.show-overlay ? (root.row-hovered ? 0.72 : 0.6) : 0.0;
animate opacity { duration: 150ms; }
}
if root.show-bars: EqualizerBars {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
if root.show-overlay && !root.show-bars: QbzIcon {
source: (root.is-playing && root.row-hovered)
? @image-url("../assets/icons/pause.svg")
: @image-url("../assets/icons/play-fill.svg");
width: 20px;
height: 20px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: #ffffff;
}
}
// --- Number variant -------------------------------------------------
if !root.show-artwork: Rectangle {
width: root.number-width;
height: 40px;
if !root.show-overlay: Text {
text: root.number;
color: Theme.text-muted;
font-size: Typography.body;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
if root.show-bars: EqualizerBars {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
}
if root.show-overlay && !root.show-bars: QbzIcon {
source: (root.is-playing && root.row-hovered)
? @image-url("../assets/icons/pause.svg")
: @image-url("../assets/icons/play-fill.svg");
width: 16px;
height: 16px;
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
tint: #ffffff;
}
}
ta := TouchArea {
mouse-cursor: pointer;
clicked => {
if (root.is-active) {
NowPlayingState.toggle-play();
} else {
root.play();
}
}
}
}