CHECKPOINT — this batch is NOT yet acceptable; substantial feedback is pending. Committed so the work is preserved for rework next session, after a max-effort Tauri code review. Areas: TrackRow favorite toggle/state + clickable artist/album (TrackItem gains is-favorite/artist-id/album-id + fav_cache, populated across all builders); PlaylistView header buttons + cover-art hover overlay; LabelView full AlbumCard + grid/list toggle; vertical scrollbar moved to the right gutter across ~12 views. cc
780 lines
35 KiB
Plaintext
780 lines
35 KiB
Plaintext
// Playlist detail view. Same anatomy as AlbumView / MixView: a header
|
|
// (cover + "Playlist" label + name + description + "owner • N tracks •
|
|
// duration" info line + circular actions) over a column-headed track
|
|
// list of shared TrackRows (show-artwork + show-album).
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { PlaylistState, PlaylistActions, DragState } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
import { TrackRow } from "../primitives/TrackRow.slint";
|
|
import { ExpandableSearch } from "../primitives/ExpandableSearch.slint";
|
|
import { ListScrollbar } from "../primitives/ListScrollbar.slint";
|
|
import { LoadingSpinner } from "../primitives/LoadingSpinner.slint";
|
|
import { NavButtons } from "../shell/NavButtons.slint";
|
|
import { ListView } from "std-widgets.slint";
|
|
|
|
// Header action button — homologated 1:1 with AlbumPageView's
|
|
// CircleAction: a ghost circle (translucent fill + faint ring, never a
|
|
// solid accent fill) so the playlist transport row matches the album
|
|
// one. Adds `active` (accent-tinted glyph for toggled state, e.g.
|
|
// favorite / select mode) and `enabled` (dimmed + non-interactive),
|
|
// which the album header doesn't need but the playlist one does.
|
|
component CircleAction inherits Rectangle {
|
|
in property <image> icon;
|
|
in property <bool> primary: false;
|
|
in property <bool> active: false;
|
|
in property <length> diameter: 40px;
|
|
in property <bool> enabled: true;
|
|
callback clicked();
|
|
|
|
width: root.diameter;
|
|
height: root.diameter;
|
|
border-radius: root.diameter / 2;
|
|
opacity: root.enabled ? 1.0 : 0.4;
|
|
// Ghost circle — translucent fill with a faint ring (matches the
|
|
// Tauri transport controls + AlbumPageView).
|
|
background: ta.has-hover ? Theme.surface-hover : transparent;
|
|
border-width: 1px;
|
|
border-color: ta.has-hover ? Theme.text-disabled : Theme.border-subtle;
|
|
|
|
QbzIcon {
|
|
source: root.icon;
|
|
width: root.primary ? 19px : 17px;
|
|
height: root.primary ? 19px : 17px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: root.active
|
|
? Theme.accent
|
|
: (ta.has-hover ? Theme.text-primary : Theme.text-secondary);
|
|
}
|
|
ta := TouchArea {
|
|
mouse-cursor: root.enabled ? pointer : default;
|
|
clicked => {
|
|
if (root.enabled) {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// One row in the Sort menu. The active field shows its direction arrow
|
|
// (asc/desc); re-selecting it toggles direction.
|
|
component SortOption inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> active: false;
|
|
in property <bool> asc: true;
|
|
callback chosen();
|
|
height: 30px;
|
|
border-radius: 5px;
|
|
background: so-ta.has-hover ? Theme.surface-hover : transparent;
|
|
HorizontalLayout {
|
|
padding-left: 10px;
|
|
padding-right: 8px;
|
|
spacing: 6px;
|
|
Text {
|
|
text: root.label;
|
|
color: root.active ? Theme.text-primary : Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
font-weight: root.active ? Typography.semibold : Typography.regular;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
VerticalLayout {
|
|
alignment: center;
|
|
if root.active: QbzIcon {
|
|
source: root.asc
|
|
? @image-url("../assets/icons/chevron-up.svg")
|
|
: @image-url("../assets/icons/chevron-down.svg");
|
|
width: 13px;
|
|
height: 13px;
|
|
tint: Theme.accent;
|
|
}
|
|
}
|
|
}
|
|
so-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.chosen();
|
|
}
|
|
}
|
|
}
|
|
|
|
export component PlaylistView inherits Rectangle {
|
|
callback play-track(string);
|
|
callback play-all();
|
|
callback shuffle();
|
|
callback toggle-favorite();
|
|
callback edit();
|
|
callback media-action(string, string, string);
|
|
|
|
// Read-more description modal.
|
|
property <bool> show-description: false;
|
|
|
|
// Label for the active sort field.
|
|
property <string> sort-label:
|
|
PlaylistState.sort-field == "title" ? @tr("Title")
|
|
: PlaylistState.sort-field == "artist" ? @tr("Artist")
|
|
: PlaylistState.sort-field == "album" ? @tr("Album")
|
|
: PlaylistState.sort-field == "duration" ? @tr("Duration")
|
|
: PlaylistState.sort-field == "custom" ? @tr("Custom")
|
|
: @tr("Default");
|
|
|
|
background: Theme.surface-main;
|
|
|
|
page := VerticalLayout {
|
|
// Fill PlaylistView so the ListView's vertical-stretch has
|
|
// real height (otherwise it collapses to a single row).
|
|
width: 100%;
|
|
height: 100%;
|
|
padding-left: 32px;
|
|
// Smaller right padding so the track list + scrollbar use
|
|
// the space instead of leaving a dead band on the right.
|
|
padding-right: 16px;
|
|
padding-top: 11px;
|
|
padding-bottom: 16px;
|
|
spacing: 0px;
|
|
|
|
HorizontalLayout {
|
|
vertical-stretch: 0;
|
|
alignment: start;
|
|
NavButtons { }
|
|
}
|
|
|
|
// Same nav -> header separation as AlbumView (homologated).
|
|
Rectangle { height: 22px; }
|
|
|
|
// --- Header ------------------------------------------------
|
|
HorizontalLayout {
|
|
vertical-stretch: 0;
|
|
spacing: 24px;
|
|
alignment: start;
|
|
// Cover — custom artwork if set, else a 4-up collage of
|
|
// the first tracks' covers (Tauri parity), else the
|
|
// server playlist image, else the first track, else a
|
|
// placeholder.
|
|
cover-box := Rectangle {
|
|
width: 150px;
|
|
height: 150px;
|
|
border-radius: Radius.md;
|
|
background: Theme.surface-elevated;
|
|
clip: true;
|
|
|
|
// Hover state — drives the artwork overlay (only
|
|
// shown on mouse-over). OR'd with the overlay button so
|
|
// moving onto it keeps the overlay visible.
|
|
property <bool> hovered:
|
|
cover-hover.has-hover || cover-btn-ta.has-hover;
|
|
|
|
// Custom artwork / single server image.
|
|
property <bool> show-single:
|
|
PlaylistState.has-custom
|
|
|| (PlaylistState.track-count < 4 && PlaylistState.cover-url != "");
|
|
property <bool> show-collage:
|
|
!PlaylistState.has-custom && PlaylistState.track-count >= 4;
|
|
|
|
if cover-box.show-single: Image {
|
|
width: 100%;
|
|
height: 100%;
|
|
source: PlaylistState.cover;
|
|
image-fit: cover;
|
|
}
|
|
// 2x2 collage from the first four track covers.
|
|
if cover-box.show-collage: VerticalLayout {
|
|
spacing: 2px;
|
|
HorizontalLayout {
|
|
spacing: 2px;
|
|
Image {
|
|
width: (cover-box.width - 2px) / 2; height: (cover-box.width - 2px) / 2;
|
|
source: PlaylistState.tracks[0].artwork;
|
|
image-fit: cover;
|
|
}
|
|
Image {
|
|
width: (cover-box.width - 2px) / 2; height: (cover-box.width - 2px) / 2;
|
|
source: PlaylistState.tracks[1].artwork;
|
|
image-fit: cover;
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 2px;
|
|
Image {
|
|
width: (cover-box.width - 2px) / 2; height: (cover-box.width - 2px) / 2;
|
|
source: PlaylistState.tracks[2].artwork;
|
|
image-fit: cover;
|
|
}
|
|
Image {
|
|
width: (cover-box.width - 2px) / 2; height: (cover-box.width - 2px) / 2;
|
|
source: PlaylistState.tracks[3].artwork;
|
|
image-fit: cover;
|
|
}
|
|
}
|
|
}
|
|
// Single first-track cover when 1-3 tracks and no
|
|
// server image.
|
|
if !cover-box.show-single && !cover-box.show-collage
|
|
&& PlaylistState.track-count >= 1: Image {
|
|
width: 100%;
|
|
height: 100%;
|
|
source: PlaylistState.tracks[0].artwork;
|
|
image-fit: cover;
|
|
}
|
|
// Empty placeholder.
|
|
if PlaylistState.track-count == 0 && PlaylistState.cover-url == ""
|
|
&& !PlaylistState.has-custom: QbzIcon {
|
|
source: @image-url("../assets/icons/list-music.svg");
|
|
width: 56px;
|
|
height: 56px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: Theme.text-muted;
|
|
}
|
|
|
|
// Hover detector (below the overlay buttons so they
|
|
// stay clickable). Owner only.
|
|
cover-hover := TouchArea {
|
|
mouse-cursor: PlaylistState.is-owner ? pointer : default;
|
|
enabled: PlaylistState.is-owner;
|
|
}
|
|
|
|
// Hover overlay — a centered "change cover" affordance
|
|
// over the whole artwork, mirroring Tauri's
|
|
// `.artwork-overlay`: a dim scrim that fades in on
|
|
// hover with a single centered button. ImagePlus when
|
|
// no custom art is set (sets one); X when one is
|
|
// (clears it). Owner only. The button stays in the tree
|
|
// (its TouchArea name must be in scope for `hovered`),
|
|
// its glyph + action just switch on `has-custom`.
|
|
cover-overlay := Rectangle {
|
|
opacity: (PlaylistState.is-owner && cover-box.hovered) ? 1.0 : 0.0;
|
|
background: #00000066;
|
|
animate opacity { duration: 150ms; }
|
|
|
|
cover-btn := Rectangle {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 22px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
background: cover-btn-ta.has-hover
|
|
? (PlaylistState.has-custom ? #ef4444 : Theme.accent)
|
|
: #000000aa;
|
|
QbzIcon {
|
|
source: PlaylistState.has-custom
|
|
? @image-url("../assets/icons/x.svg")
|
|
: @image-url("../assets/icons/image-plus.svg");
|
|
width: PlaylistState.has-custom ? 20px : 22px;
|
|
height: PlaylistState.has-custom ? 20px : 22px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: #ffffff;
|
|
}
|
|
cover-btn-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
// Only interactive while the overlay is up
|
|
// (and the user owns the playlist).
|
|
enabled: PlaylistState.is-owner && cover-box.hovered;
|
|
clicked => {
|
|
if (PlaylistState.has-custom) {
|
|
root.media-action("playlist", "", "clear-artwork");
|
|
} else {
|
|
root.media-action("playlist", "", "set-artwork");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Metadata.
|
|
VerticalLayout {
|
|
alignment: start;
|
|
padding-top: 4px;
|
|
horizontal-stretch: 1;
|
|
Text {
|
|
text: @tr("Playlist");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
font-weight: Typography.semibold;
|
|
letter-spacing: 1.5px;
|
|
}
|
|
Rectangle { height: 4px; }
|
|
Text {
|
|
text: PlaylistState.name;
|
|
color: Theme.text-primary;
|
|
font-size: Typography.section;
|
|
font-weight: Typography.bold;
|
|
wrap: word-wrap;
|
|
}
|
|
if PlaylistState.description != "": Rectangle { height: 6px; }
|
|
if PlaylistState.description != "": Text {
|
|
width: root.width - 250px;
|
|
text: PlaylistState.description-short;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
wrap: word-wrap;
|
|
}
|
|
if PlaylistState.description != PlaylistState.description-short: Rectangle { height: 3px; }
|
|
if PlaylistState.description != PlaylistState.description-short: read-more := TouchArea {
|
|
width: rm-row.preferred-width;
|
|
height: 18px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-description = true;
|
|
}
|
|
rm-row := HorizontalLayout {
|
|
Text {
|
|
text: @tr("Read more");
|
|
color: read-more.has-hover ? Theme.accent-hover : Theme.accent;
|
|
font-size: Typography.legal;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
}
|
|
Rectangle { height: 8px; }
|
|
Text {
|
|
text: PlaylistState.owner
|
|
+ " • " + PlaylistState.track-count + " " + @tr("tracks")
|
|
+ " • " + PlaylistState.total-duration;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
}
|
|
Rectangle { height: 14px; }
|
|
// Actions — buttons left, search + sort floating right.
|
|
HorizontalLayout {
|
|
vertical-stretch: 0;
|
|
spacing: 12px;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/play-fill.svg");
|
|
primary: true;
|
|
diameter: 44px;
|
|
enabled: PlaylistState.tracks.length > 0;
|
|
clicked => {
|
|
root.play-all();
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/shuffle.svg");
|
|
enabled: PlaylistState.tracks.length > 0;
|
|
clicked => {
|
|
root.shuffle();
|
|
}
|
|
}
|
|
}
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/heart.svg");
|
|
active: PlaylistState.is-favorite;
|
|
clicked => {
|
|
root.toggle-favorite();
|
|
}
|
|
}
|
|
}
|
|
// Edit (rename / delete) — only the user's own.
|
|
if PlaylistState.is-owner: VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/pen-line.svg");
|
|
clicked => {
|
|
root.edit();
|
|
}
|
|
}
|
|
}
|
|
// Select tracks (multi-select edit mode).
|
|
if PlaylistState.is-owner && PlaylistState.tracks.length > 0: VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
CircleAction {
|
|
icon: @image-url("../assets/icons/square-check-big.svg");
|
|
active: PlaylistState.multi-select-mode;
|
|
clicked => {
|
|
root.media-action("playlist", "", "select-toggle");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spacer floats the search + sort to the right.
|
|
Rectangle { horizontal-stretch: 1; }
|
|
|
|
// In-playlist search (shared ExpandableSearch).
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
ExpandableSearch {
|
|
placeholder: @tr("Search tracks");
|
|
max-open-width: 220px;
|
|
edited(q) => {
|
|
PlaylistActions.search(q);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort dropdown — fixed width so it never grows
|
|
// when the active option changes.
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
sort-anchor := Rectangle {
|
|
height: 34px;
|
|
width: 132px;
|
|
border-radius: 6px;
|
|
background: sort-ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
sort-row := HorizontalLayout {
|
|
padding-left: 11px;
|
|
padding-right: 8px;
|
|
spacing: 5px;
|
|
Text {
|
|
text: @tr("Sort") + ": " + root.sort-label;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
VerticalLayout {
|
|
horizontal-stretch: 0;
|
|
alignment: center;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/chevron-down.svg");
|
|
width: 13px;
|
|
height: 13px;
|
|
tint: Theme.text-muted;
|
|
}
|
|
}
|
|
}
|
|
sort-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
sort-menu.show();
|
|
}
|
|
}
|
|
sort-menu := PopupWindow {
|
|
x: sort-anchor.width - 172px;
|
|
y: 38px;
|
|
width: 172px;
|
|
Rectangle {
|
|
background: Theme.surface-card;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
border-radius: 8px;
|
|
drop-shadow-blur: 20px;
|
|
drop-shadow-color: #00000080;
|
|
VerticalLayout {
|
|
padding: 6px;
|
|
spacing: 1px;
|
|
SortOption {
|
|
label: @tr("Default");
|
|
active: PlaylistState.sort-field == "default";
|
|
chosen => { PlaylistActions.set-sort("default"); }
|
|
}
|
|
SortOption {
|
|
label: @tr("Title");
|
|
active: PlaylistState.sort-field == "title";
|
|
asc: PlaylistState.sort-asc;
|
|
chosen => { PlaylistActions.set-sort("title"); }
|
|
}
|
|
SortOption {
|
|
label: @tr("Artist");
|
|
active: PlaylistState.sort-field == "artist";
|
|
asc: PlaylistState.sort-asc;
|
|
chosen => { PlaylistActions.set-sort("artist"); }
|
|
}
|
|
SortOption {
|
|
label: @tr("Album");
|
|
active: PlaylistState.sort-field == "album";
|
|
asc: PlaylistState.sort-asc;
|
|
chosen => { PlaylistActions.set-sort("album"); }
|
|
}
|
|
SortOption {
|
|
label: @tr("Duration");
|
|
active: PlaylistState.sort-field == "duration";
|
|
asc: PlaylistState.sort-asc;
|
|
chosen => { PlaylistActions.set-sort("duration"); }
|
|
}
|
|
// Manual order — enables the per-row
|
|
// up/down reorder controls.
|
|
SortOption {
|
|
label: @tr("Custom");
|
|
active: PlaylistState.sort-field == "custom";
|
|
chosen => { PlaylistActions.set-sort("custom"); }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { height: 18px; }
|
|
|
|
if PlaylistState.loading: HorizontalLayout {
|
|
vertical-stretch: 0;
|
|
alignment: center;
|
|
LoadingSpinner { size: 36px; }
|
|
}
|
|
if !PlaylistState.loading && PlaylistState.tracks.length == 0: Text {
|
|
text: @tr("This playlist is empty.");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.body;
|
|
}
|
|
|
|
// --- Track list column header ------------------------------
|
|
// Extra right padding (12 + 14px gutter) aligns the column
|
|
// labels with the rows, which are inset by the scrollbar.
|
|
if PlaylistState.tracks.length > 0: HorizontalLayout {
|
|
vertical-stretch: 0;
|
|
padding-left: 12px;
|
|
padding-right: 26px;
|
|
spacing: 14px;
|
|
Text {
|
|
text: "#";
|
|
width: 32px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
}
|
|
Rectangle { width: 40px; }
|
|
Text {
|
|
text: @tr("Title");
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-stretch: 1;
|
|
}
|
|
Text {
|
|
text: @tr("Album");
|
|
width: 220px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
}
|
|
Text {
|
|
text: @tr("Duration");
|
|
width: 70px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Quality");
|
|
width: 70px;
|
|
color: Theme.text-muted;
|
|
font-size: Typography.legal;
|
|
horizontal-alignment: center;
|
|
}
|
|
Rectangle { width: 28px; }
|
|
Rectangle { width: 32px; }
|
|
}
|
|
// Breathing room between the column labels and the divider.
|
|
if PlaylistState.tracks.length > 0: Rectangle { height: 3px; }
|
|
if PlaylistState.tracks.length > 0: Rectangle {
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
Rectangle { height: 6px; }
|
|
|
|
// Bulk action bar — visible in edit mode.
|
|
if PlaylistState.multi-select-mode: Rectangle {
|
|
height: 44px;
|
|
border-radius: 8px;
|
|
background: Theme.surface-elevated;
|
|
HorizontalLayout {
|
|
padding-left: 14px;
|
|
padding-right: 10px;
|
|
spacing: 12px;
|
|
Text {
|
|
text: PlaylistState.selected-count + " " + @tr("selected");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
pill-all := Rectangle {
|
|
width: all-label.preferred-width + 24px;
|
|
height: 30px;
|
|
border-radius: Radius.sm;
|
|
background: all-ta.has-hover ? Theme.surface-hover : transparent;
|
|
all-label := Text {
|
|
text: @tr("Select all");
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.legal;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
}
|
|
all-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.media-action("playlist", "", "select-all");
|
|
}
|
|
}
|
|
}
|
|
pill-remove := Rectangle {
|
|
width: rm-label.preferred-width + 24px;
|
|
height: 30px;
|
|
border-radius: Radius.sm;
|
|
opacity: PlaylistState.selected-count > 0 ? 1.0 : 0.5;
|
|
background: rm-ta.has-hover ? #ef444433 : transparent;
|
|
border-width: 1px;
|
|
border-color: #ef4444;
|
|
rm-label := Text {
|
|
text: @tr("Remove");
|
|
color: #ef4444;
|
|
font-size: Typography.legal;
|
|
font-weight: Typography.medium;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
}
|
|
rm-ta := TouchArea {
|
|
mouse-cursor: PlaylistState.selected-count > 0 ? pointer : default;
|
|
clicked => {
|
|
if (PlaylistState.selected-count > 0) {
|
|
root.media-action("playlist", "", "remove-selected");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if PlaylistState.multi-select-mode: Rectangle { height: 8px; }
|
|
|
|
// Virtualized track list (built-in scrollbar hidden — a
|
|
// custom one OVERLAYS its own gutter to the right — homologated
|
|
// with every other view (the scrollbar sits in the right gutter
|
|
// without shrinking the list).
|
|
Rectangle {
|
|
vertical-stretch: 1;
|
|
track-list := ListView {
|
|
width: 100%;
|
|
height: 100%;
|
|
vertical-scrollbar-policy: ScrollBarPolicy.always-off;
|
|
for track[i] in PlaylistState.tracks: TrackRow {
|
|
track: track;
|
|
index: i;
|
|
show-artwork: true;
|
|
show-album: true;
|
|
show-favorite: true;
|
|
show-reorder: PlaylistState.sort-field == "custom";
|
|
multi-select-mode: PlaylistState.multi-select-mode;
|
|
media-action(kind, id, action) => {
|
|
if (action == "play") {
|
|
root.play-track(id);
|
|
} else {
|
|
root.media-action(kind, id, action);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ListScrollbar {
|
|
x: parent.width - self.width - 4px;
|
|
width: 14px;
|
|
height: 100%;
|
|
viewport-height: track-list.viewport-height;
|
|
visible-height: track-list.visible-height;
|
|
viewport-y <=> track-list.viewport-y;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Back to top — appears once the list is scrolled past one
|
|
// viewport. A shared rule for big lists (playlists, library, ...).
|
|
if track-list.viewport-y < -track-list.visible-height: Rectangle {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 22px;
|
|
x: parent.width - self.width - 24px;
|
|
y: parent.height - self.height - 24px;
|
|
background: btt-ta.has-hover ? Theme.accent : Theme.surface-elevated;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
drop-shadow-blur: 16px;
|
|
drop-shadow-color: #00000066;
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/chevron-up.svg");
|
|
width: 20px;
|
|
height: 20px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: btt-ta.has-hover ? #ffffff : Theme.text-secondary;
|
|
}
|
|
btt-ta := TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
track-list.viewport-y = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read-more description modal (same treatment as Album / Artist).
|
|
if root.show-description: Rectangle {
|
|
background: #000000bf;
|
|
TouchArea {
|
|
mouse-cursor: default;
|
|
clicked => {
|
|
root.show-description = false;
|
|
}
|
|
}
|
|
Rectangle {
|
|
width: Math.min(root.width - 80px, 560px);
|
|
height: Math.min(root.height - 120px, 460px);
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
border-radius: Radius.md;
|
|
background: Theme.surface-card;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
drop-shadow-blur: 32px;
|
|
drop-shadow-color: #00000080;
|
|
TouchArea { }
|
|
VerticalLayout {
|
|
padding: 24px;
|
|
spacing: 14px;
|
|
HorizontalLayout {
|
|
Text {
|
|
text: @tr("About this playlist");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.section;
|
|
font-weight: Typography.semibold;
|
|
horizontal-stretch: 1;
|
|
vertical-alignment: center;
|
|
}
|
|
desc-close := TouchArea {
|
|
width: 28px;
|
|
height: 28px;
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.show-description = false;
|
|
}
|
|
QbzIcon {
|
|
source: @image-url("../assets/icons/x.svg");
|
|
width: 18px;
|
|
height: 18px;
|
|
x: (parent.width - self.width) / 2;
|
|
y: (parent.height - self.height) / 2;
|
|
tint: desc-close.has-hover ? Theme.text-primary : Theme.text-muted;
|
|
}
|
|
}
|
|
}
|
|
Flickable {
|
|
vertical-stretch: 1;
|
|
viewport-width: self.width;
|
|
viewport-height: desc-full.preferred-height;
|
|
desc-full := Text {
|
|
width: parent.width;
|
|
text: PlaylistState.description;
|
|
color: Theme.text-secondary;
|
|
font-size: Typography.body;
|
|
wrap: word-wrap;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|