refactor(slint): one shared TrackRow + TrackItem across all surfaces
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
This commit is contained in:
14 files changed
+207
-464
No files matched your search
@@ -8,7 +8,7 @@
|
||||
// per-type tabs show the full loaded set as a card grid or row list with
|
||||
// a load-more button.
|
||||
|
||||
import { SearchState, SearchActions, SearchTrackItem, SearchPlaylistItem, AlbumCardItem, SlimItem } from "../state.slint";
|
||||
import { SearchState, SearchActions, TrackItem, SearchPlaylistItem, AlbumCardItem, SlimItem } from "../state.slint";
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
@@ -18,7 +18,7 @@ import { PlaylistCarousel } from "../discover/PlaylistCarousel.slint";
|
||||
import { AlbumCard } from "../discover/AlbumCard.slint";
|
||||
import { ArtistCard } from "../discover/ArtistCard.slint";
|
||||
import { PlaylistCard } from "../discover/PlaylistCard.slint";
|
||||
import { SearchTrackRow } from "SearchTrackRow.slint";
|
||||
import { TrackRow } from "../primitives/TrackRow.slint";
|
||||
import { NavButtons } from "../shell/NavButtons.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
||||
@@ -480,7 +480,7 @@ export component SearchResultsView inherits Rectangle {
|
||||
if SearchState.most-popular-kind == "track": Rectangle {
|
||||
width: 320px;
|
||||
height: 56px;
|
||||
SearchTrackRow {
|
||||
TrackRow {
|
||||
width: 320px;
|
||||
track: SearchState.most-popular-track;
|
||||
index: 0;
|
||||
@@ -542,7 +542,7 @@ export component SearchResultsView inherits Rectangle {
|
||||
show-more: SearchState.tab == 0 && SearchState.tracks-total > root.preview-cap;
|
||||
more-clicked => { SearchActions.tab-changed(2); }
|
||||
}
|
||||
for track[idx] in SearchState.tracks: SearchTrackRow {
|
||||
for track[idx] in SearchState.tracks: TrackRow {
|
||||
visible: SearchState.tab == 2 || idx < root.preview-cap;
|
||||
height: self.visible ? 56px : 0px;
|
||||
track: track;
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
// A track result row for search.
|
||||
//
|
||||
// Like the album-page track row, but the leading slot is a cover
|
||||
// thumbnail — search tracks come from many different albums, so a shared
|
||||
// album cover would be wrong. `show-artwork` keeps the slot configurable
|
||||
// for a future shared row component (album context would set it false).
|
||||
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Typography } from "../foundation/typography.slint";
|
||||
import { Radius } from "../foundation/radius.slint";
|
||||
import { SearchTrackItem } from "../state.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { TrackPlayCell } from "../primitives/TrackPlayCell.slint";
|
||||
import { TrackContextMenu } from "../primitives/TrackContextMenu.slint";
|
||||
|
||||
export component SearchTrackRow inherits Rectangle {
|
||||
in property <SearchTrackItem> track;
|
||||
in property <int> index;
|
||||
in property <bool> show-artwork: true;
|
||||
// Optional extra columns (mix / playlist-style rows): an album
|
||||
// column and a standalone favorite button. Off by default so the
|
||||
// search / favorites rows are unchanged.
|
||||
in property <bool> show-album: false;
|
||||
in property <bool> show-favorite: false;
|
||||
// kind ("track"), track id, action ("play" | "play-next" | "queue" | "favorite").
|
||||
callback media-action(string, string, string);
|
||||
|
||||
// Slint's has-hover does not propagate to ancestor TouchAreas, so the
|
||||
// row OR's its own ta with every nested TouchArea to drive the cell
|
||||
// play overlay reliably regardless of pointer position.
|
||||
property <bool> row-hovered:
|
||||
ta.has-hover || play-cell.hovered || more-ta.has-hover;
|
||||
|
||||
height: 56px;
|
||||
border-radius: 8px;
|
||||
// Use the OR'd row-hovered so hovering nested controls (cell / more)
|
||||
// does not break the row tint.
|
||||
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;
|
||||
|
||||
// Track position in the result list.
|
||||
Text {
|
||||
text: "\{root.index + 1}";
|
||||
width: 24px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
// Leading slot — cover thumbnail with the play / playing-state
|
||||
// affordance.
|
||||
VerticalLayout {
|
||||
alignment: center;
|
||||
play-cell := TrackPlayCell {
|
||||
track-id: root.track.id;
|
||||
artwork: root.track.artwork;
|
||||
show-artwork: true;
|
||||
row-hovered: root.row-hovered;
|
||||
play => {
|
||||
root.media-action("track", root.track.id, "play");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Text {
|
||||
text: root.track.artist;
|
||||
visible: root.track.artist != "";
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.link;
|
||||
overflow: elide;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional album column (mix / playlist rows).
|
||||
if root.show-album: Text {
|
||||
text: root.track.album;
|
||||
width: 220px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
vertical-alignment: center;
|
||||
overflow: elide;
|
||||
}
|
||||
|
||||
Text {
|
||||
text: root.track.duration;
|
||||
width: 70px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.body;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
text: root.track.quality-tier == "hires"
|
||||
? "Hi-Res"
|
||||
: (root.track.quality-tier == "cd" ? "CD" : "");
|
||||
width: 60px;
|
||||
color: Theme.text-muted;
|
||||
font-size: Typography.legal;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
|
||||
// Optional standalone favorite button.
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user