Collapses the three near-identical track rows (AlbumView's inline
TrackRow, ArtistPage's inline TrackRow with multi-select, and the
shared SearchTrackRow) into a single primitives/TrackRow driven by
flags, backed by one TrackItem (superset). Foundation for the
upcoming playlist module, which renders the artwork variant heavily.
- New primitives/TrackRow.slint with: show-artwork (cover+number vs
number cell), show-album, show-favorite, show-download,
multi-select-mode (selection checkbox). One media-action wires
play / play-next / queue / favorite / download / toggle-select +
the TrackContextMenu.
- TrackItem is the unified model (superset of the old AlbumTrackItem
+ SearchTrackItem: number, album, selected, artwork all present;
unused fields stay empty per surface).
- Migrated AlbumView (show-artwork:false, show-download:true),
ArtistPage Popular (show-artwork:false, multi-select-mode),
Search, Favorites, Mix (show-artwork:true, +album/+favorite).
Deleted SearchTrackRow.slint and both inline TrackRows.
- Rust: AlbumTrackItem + SearchTrackItem -> TrackItem across
album/artist/search/favorites/mix/main; search's internal row
struct renamed TrackRow -> TrackRowData to free the TrackRow name
for the Slint component. 46 tests pass.
cc
276 lines
10 KiB
Plaintext
276 lines
10 KiB
Plaintext
// The single, shared track-list row. Every track surface (album,
|
|
// artist Popular, search, favorites, mix, playlist) renders this with
|
|
// flags rather than its own copy:
|
|
//
|
|
// show-artwork leading is a cover thumbnail + a position number
|
|
// (search / mix / playlist); false = a number cell
|
|
// that swaps to play on hover (album / artist).
|
|
// show-album an album-title column (mix / playlist).
|
|
// show-favorite a standalone heart button.
|
|
// show-download a standalone offline-cache button (album).
|
|
// multi-select-mode the leading slot becomes a selection checkbox
|
|
// (artist Popular, playlist editor).
|
|
//
|
|
// Everything routes through one media-action(kind, id, action):
|
|
// "play" | "play-next" | "queue" | "favorite" | "download" |
|
|
// "toggle-select" and the context-menu actions.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { TrackItem } from "../state.slint";
|
|
import { QbzIcon } from "QbzIcon.slint";
|
|
import { TrackPlayCell } from "TrackPlayCell.slint";
|
|
import { TrackContextMenu } from "TrackContextMenu.slint";
|
|
|
|
export component TrackRow inherits Rectangle {
|
|
in property <TrackItem> track;
|
|
in property <int> index;
|
|
in property <bool> show-artwork: true;
|
|
in property <bool> show-album: false;
|
|
in property <bool> show-favorite: false;
|
|
in property <bool> show-download: false;
|
|
in property <bool> multi-select-mode: false;
|
|
callback media-action(string, string, string);
|
|
|
|
// Explicit track number, else the 1-based list position.
|
|
property <string> number-text:
|
|
root.track.number != "" ? root.track.number : "\{root.index + 1}";
|
|
property <bool> row-hovered:
|
|
ta.has-hover || play-cell.hovered || more-ta.has-hover;
|
|
|
|
height: 56px;
|
|
border-radius: 8px;
|
|
background: root.row-hovered
|
|
? #ffffff16
|
|
: (Math.mod(root.index, 2) == 1 ? #ffffff07 : transparent);
|
|
|
|
ta := TouchArea {
|
|
mouse-cursor: default;
|
|
}
|
|
|
|
HorizontalLayout {
|
|
padding-left: 12px;
|
|
padding-right: 12px;
|
|
spacing: 14px;
|
|
|
|
// Position number — only for artwork rows (the cover doesn't
|
|
// carry the number; non-artwork rows show it in the play cell).
|
|
if root.show-artwork && !root.multi-select-mode: VerticalLayout {
|
|
alignment: center;
|
|
Text {
|
|
text: root.number-text;
|
|
width: 20px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
}
|
|
}
|
|
|
|
// Leading cell — cover/number play affordance, swapped for a
|
|
// selection checkbox in multi-select mode. The play-cell stays
|
|
// in the tree (just invisible) so row-hovered can read it.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: root.show-artwork ? 40px : 28px;
|
|
height: root.show-artwork ? 40px : 28px;
|
|
play-cell := TrackPlayCell {
|
|
track-id: root.track.id;
|
|
number: root.number-text;
|
|
artwork: root.track.artwork;
|
|
show-artwork: root.show-artwork;
|
|
number-width: 28px;
|
|
row-hovered: root.row-hovered && !root.multi-select-mode;
|
|
visible: !root.multi-select-mode;
|
|
play => {
|
|
root.media-action("track", root.track.id, "play");
|
|
}
|
|
}
|
|
if root.multi-select-mode: Rectangle {
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
width: 22px;
|
|
height: 22px;
|
|
border-radius: 11px;
|
|
background: root.track.selected ? Theme.accent : transparent;
|
|
border-width: root.track.selected ? 0px : 1.5px;
|
|
border-color: sel-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
animate background { duration: 120ms; }
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/check.svg");
|
|
width: 14px;
|
|
height: 14px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: #ffffff;
|
|
visible: root.track.selected;
|
|
}
|
|
sel-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.media-action("track", root.track.id, "toggle-select");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Title + artist.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
horizontal-stretch: 1;
|
|
spacing: 2px;
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
spacing: 6px;
|
|
Text {
|
|
text: root.track.title;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
if root.track.explicit: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 3px;
|
|
background: Theme.surface-elevated;
|
|
Text {
|
|
text: "E";
|
|
color: Theme.text-muted;
|
|
font-size: 9px;
|
|
font-weight: Typography.semibold;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if root.track.artist != "": Text {
|
|
text: root.track.artist;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.link;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
|
|
// Album column.
|
|
if root.show-album: Text {
|
|
text: root.track.album;
|
|
width: 220px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
vertical-alignment: center;
|
|
overflow: elide;
|
|
}
|
|
|
|
// Duration.
|
|
Text {
|
|
text: root.track.duration;
|
|
width: 70px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
// Quality.
|
|
Text {
|
|
text: root.track.quality-tier == "hires"
|
|
? "Hi-Res"
|
|
: (root.track.quality-tier == "cd" ? "CD" : "");
|
|
width: 70px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
}
|
|
|
|
// Favorite.
|
|
if root.show-favorite: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: Radius.sm;
|
|
background: fav-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/heart.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: fav-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
fav-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.media-action("track", root.track.id, "favorite");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Offline cache.
|
|
if root.show-download: VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: Radius.sm;
|
|
background: dl-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
QbzIcon {
|
|
source: @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;
|
|
}
|
|
dl-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.media-action("track", root.track.id, "download");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Context menu.
|
|
VerticalLayout {
|
|
alignment: center;
|
|
Rectangle {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: Radius.sm;
|
|
background: more-ta.has-hover ? Theme.surface-elevated : transparent;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/ellipsis.svg");
|
|
width: 16px;
|
|
height: 16px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: more-ta.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
more-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
track-menu.show();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
track-menu := TrackContextMenu {
|
|
x: parent.width - self.width - 12px;
|
|
y: 48px;
|
|
track-id: root.track.id;
|
|
media-action(k, i, a) => {
|
|
root.media-action(k, i, a);
|
|
}
|
|
}
|
|
}
|