- Sidebar + playlist manager sorts gain both directions with a caret on the active row (re-select flips); 'recent' keeps the API order (newest-first — the old reverse was the #657 bug). - Import now inserts the new playlist optimistically and bounded-retries until the API lists it (read-after-write lag forced a manual refresh). - Play later: tracks land at the end of the manual block (after every play-next/later, before the source resumes); Play next and Add to queue are untouched. The queue sidebar shows the section divider (Next in queue / Next up) with drag-math compensation, and the context menu shows the landing position of each queue action. km
This commit is contained in:
11 files changed
+380
-40
No files matched your search
@@ -113,6 +113,9 @@ component PmSearch inherits Rectangle {
|
||||
component PmMenuItem inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> selected: false;
|
||||
// Sort rows: "up" | "down" draws the direction chevron in place of the
|
||||
// plain check on the active row (#657 caret).
|
||||
in property <string> trailing-icon: "";
|
||||
// Collapse property — `for x: if c: El` is disallowed in Slint 1.16, so
|
||||
// a hidden item collapses to zero height instead (the sidebar pattern).
|
||||
in property <bool> shown: true;
|
||||
@@ -137,7 +140,9 @@ component PmMenuItem inherits Rectangle {
|
||||
if root.selected: VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/check.svg");
|
||||
source: root.trailing-icon == "up" ? @image-url("../assets/icons/chevron-up.svg")
|
||||
: root.trailing-icon == "down" ? @image-url("../assets/icons/chevron-down.svg")
|
||||
: @image-url("../assets/icons/check.svg");
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
tint: Theme.accent;
|
||||
@@ -1193,26 +1198,36 @@ export component PlaylistManagerView inherits Rectangle {
|
||||
PmMenuItem {
|
||||
label: @tr("Name A-Z");
|
||||
selected: PlaylistManagerState.sort == "name";
|
||||
trailing-icon: PlaylistManagerState.sort == "name"
|
||||
? (PlaylistManagerState.sort-asc ? "up" : "down") : "";
|
||||
activated => { PlaylistManagerActions.set-sort("name"); }
|
||||
}
|
||||
PmMenuItem {
|
||||
label: @tr("Recent");
|
||||
selected: PlaylistManagerState.sort == "recent";
|
||||
trailing-icon: PlaylistManagerState.sort == "recent"
|
||||
? (PlaylistManagerState.sort-asc ? "up" : "down") : "";
|
||||
activated => { PlaylistManagerActions.set-sort("recent"); }
|
||||
}
|
||||
PmMenuItem {
|
||||
label: @tr("Play Count");
|
||||
selected: PlaylistManagerState.sort == "playcount";
|
||||
trailing-icon: PlaylistManagerState.sort == "playcount"
|
||||
? (PlaylistManagerState.sort-asc ? "up" : "down") : "";
|
||||
activated => { PlaylistManagerActions.set-sort("playcount"); }
|
||||
}
|
||||
PmMenuItem {
|
||||
label: @tr("Track Count");
|
||||
selected: PlaylistManagerState.sort == "tracks";
|
||||
trailing-icon: PlaylistManagerState.sort == "tracks"
|
||||
? (PlaylistManagerState.sort-asc ? "up" : "down") : "";
|
||||
activated => { PlaylistManagerActions.set-sort("tracks"); }
|
||||
}
|
||||
PmMenuItem {
|
||||
label: @tr("Custom");
|
||||
selected: PlaylistManagerState.sort == "custom";
|
||||
trailing-icon: PlaylistManagerState.sort == "custom"
|
||||
? (PlaylistManagerState.sort-asc ? "up" : "down") : "";
|
||||
activated => { PlaylistManagerActions.set-sort("custom"); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { ContextMenu } from "ContextMenu.slint";
|
||||
import { ContextMenuItem } from "ContextMenuItem.slint";
|
||||
import { QueueState } from "../state.slint";
|
||||
|
||||
export component TrackContextMenu inherits PopupWindow {
|
||||
in property <string> track-id;
|
||||
@@ -61,14 +62,15 @@ export component TrackContextMenu inherits PopupWindow {
|
||||
|
||||
width: 224px;
|
||||
// Nav-only (blacklisted): only Go to album / Go to artist / Track info.
|
||||
// Otherwise: 3 transient + optional favorite/mixtape/add-to-playlist/remove
|
||||
// + optional go-to pair + the Qobuz block (4 fixed: QBZ radio, Qobuz
|
||||
// radio, 2 shares; + 1-or-2 offline entries + optional track info).
|
||||
// Otherwise: 4 transient (play/next/later/queue) + optional
|
||||
// favorite/mixtape/add-to-playlist/remove + optional go-to pair + the
|
||||
// Qobuz block (4 fixed: QBZ radio, Qobuz radio, 2 shares; + 1-or-2
|
||||
// offline entries + optional track info).
|
||||
height: (root.nav-only
|
||||
? ((root.go-album-action ? 1 : 0)
|
||||
+ (root.go-artist-action ? 1 : 0)
|
||||
+ (root.show-track-info ? 1 : 0))
|
||||
: (3
|
||||
: (4
|
||||
+ (root.favorite-action ? 1 : 0)
|
||||
+ (root.mixtape-action ? 1 : 0)
|
||||
+ (root.playlist-action ? 1 : 0)
|
||||
@@ -89,14 +91,31 @@ export component TrackContextMenu inherits PopupWindow {
|
||||
}
|
||||
if !root.nav-only: ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/list-plus.svg");
|
||||
label: @tr("Play next");
|
||||
// #442 hint: where the track lands (1-based position in the
|
||||
// queue) — only while a queue exists.
|
||||
label: QueueState.queue-length > 0
|
||||
? @tr("Play next") + " · #\{QueueState.queue-current-index + 2}"
|
||||
: @tr("Play next");
|
||||
clicked => {
|
||||
root.media-action("track", root.track-id, "play-next");
|
||||
}
|
||||
}
|
||||
// #442 "Play later" — the end of the manual block (after every
|
||||
// play-next / play-later already queued, before the source resumes).
|
||||
if !root.nav-only: ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/list-plus.svg");
|
||||
label: QueueState.queue-length > 0
|
||||
? @tr("Play later") + " · #\{QueueState.queue-current-index + 2 + QueueState.manual-count}"
|
||||
: @tr("Play later");
|
||||
clicked => {
|
||||
root.media-action("track", root.track-id, "play-later");
|
||||
}
|
||||
}
|
||||
if !root.nav-only: ContextMenuItem {
|
||||
icon: @image-url("../assets/icons/list-end.svg");
|
||||
label: @tr("Add to queue");
|
||||
label: QueueState.queue-length > 0
|
||||
? @tr("Add to queue") + " · #\{QueueState.queue-length + 1}"
|
||||
: @tr("Add to queue");
|
||||
clicked => {
|
||||
root.media-action("track", root.track-id, "queue");
|
||||
}
|
||||
|
||||
@@ -382,9 +382,21 @@ export component QueueSidebar inherits Rectangle {
|
||||
// Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 44px stacked
|
||||
// with 0 spacing; the +0.5 row rounds the boundary to row centers (top half
|
||||
// of row k -> slot k, bottom half -> slot k+1).
|
||||
// #442: section headers (22px) above row/slot index i — every drag
|
||||
// computation below adds them to the uniform 44px strides.
|
||||
pure function headers-above(i: int) -> int {
|
||||
return (QueueState.page-manual-header && i >= 1 ? 1 : 0)
|
||||
+ (QueueState.page-manual-pos >= 0 && i > QueueState.page-manual-pos ? 1 : 0);
|
||||
}
|
||||
pure function slot-from-pointer(win-y: length) -> int {
|
||||
// One header-adjustment pass: with at most 2 headers and 44px rows,
|
||||
// adjusting by the headers below the RAW slot lands on the right gap
|
||||
// (boundary cases round to a row gap either way).
|
||||
return Math.max(0, Math.min(QueueState.upcoming-page.length,
|
||||
Math.floor((win-y - root.reorder-list-top) / 44px + 0.5)));
|
||||
Math.floor((win-y - root.reorder-list-top
|
||||
- root.headers-above(
|
||||
Math.floor((win-y - root.reorder-list-top) / 44px + 0.5)) * 22px)
|
||||
/ 44px + 0.5)));
|
||||
}
|
||||
function commit-reorder() {
|
||||
root.reorder-active = false;
|
||||
@@ -597,14 +609,31 @@ export component QueueSidebar inherits Rectangle {
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
// Page of upcoming rows + drop-indicator overlay. Explicit
|
||||
// height (rows are a fixed 44px, spacing 0) so the wrapping
|
||||
// Rectangle does not collapse — a VerticalLayout does not
|
||||
// auto-size its parent Rectangle.
|
||||
// height (rows are a fixed 44px, spacing 0; #442 section
|
||||
// headers are 22px each) so the wrapping Rectangle does
|
||||
// not collapse — a VerticalLayout does not auto-size its
|
||||
// parent Rectangle.
|
||||
Rectangle {
|
||||
height: QueueState.upcoming-page.length * 44px;
|
||||
height: QueueState.upcoming-page.length * 44px
|
||||
+ QueueState.page-section-count * 22px;
|
||||
VerticalLayout {
|
||||
spacing: 0px;
|
||||
for item[idx] in QueueState.upcoming-page: QueueRow {
|
||||
for item[idx] in QueueState.upcoming-page: VerticalLayout {
|
||||
spacing: 0px;
|
||||
// #442 section header above the row (only on
|
||||
// marked rows; "" everywhere else).
|
||||
if item.section != "": Text {
|
||||
text: item.section == "next-in-queue"
|
||||
? @tr("Next in queue")
|
||||
: @tr("Next up");
|
||||
color: Theme.text-muted;
|
||||
font-size: 11px;
|
||||
font-weight: Typography.semibold;
|
||||
letter-spacing: 0.5px;
|
||||
height: 22px;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
QueueRow {
|
||||
item: item;
|
||||
index: QueueState.page-start - 1 + idx;
|
||||
page-local-index: idx;
|
||||
@@ -627,7 +656,8 @@ export component QueueSidebar inherits Rectangle {
|
||||
root.reorder-active = true;
|
||||
root.reorder-from = local-idx;
|
||||
root.reorder-over = local-idx;
|
||||
root.reorder-list-top = row-top - local-idx * 44px;
|
||||
root.reorder-list-top = row-top
|
||||
- (local-idx * 44px + root.headers-above(local-idx) * 22px);
|
||||
}
|
||||
drag-update(pointer-y) => {
|
||||
root.reorder-over = root.slot-from-pointer(pointer-y);
|
||||
@@ -638,13 +668,15 @@ export component QueueSidebar inherits Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Drop indicator: a 2px accent line at the insertion
|
||||
// slot, hidden for the two no-op slots (from, from+1).
|
||||
if root.reorder-active
|
||||
&& root.reorder-over != root.reorder-from
|
||||
&& root.reorder-over != root.reorder-from + 1: Rectangle {
|
||||
x: 0;
|
||||
y: root.reorder-over * 44px - 1px;
|
||||
y: root.reorder-over * 44px
|
||||
+ root.headers-above(root.reorder-over) * 22px - 1px;
|
||||
width: parent.width;
|
||||
height: 2px;
|
||||
background: Theme.accent;
|
||||
|
||||
@@ -26,6 +26,9 @@ component MenuItem inherits Rectangle {
|
||||
in property <bool> selected: false;
|
||||
in property <image> icon;
|
||||
in property <bool> has-icon: false;
|
||||
// Sort rows: "up" | "down" draws the direction chevron in place of the
|
||||
// plain check on the active row (#657 caret).
|
||||
in property <string> trailing-icon: "";
|
||||
// When false the row is muted and ignores clicks (Import while
|
||||
// offline). Same pattern as ContextMenuItem's enabled.
|
||||
in property <bool> enabled: true;
|
||||
@@ -56,11 +59,13 @@ component MenuItem inherits Rectangle {
|
||||
horizontal-stretch: 1;
|
||||
overflow: elide;
|
||||
}
|
||||
// Selected check (sort options).
|
||||
// Selected check — or the direction chevron on a sort row.
|
||||
if root.selected: VerticalLayout {
|
||||
alignment: center;
|
||||
QbzIcon {
|
||||
source: @image-url("../assets/icons/check.svg");
|
||||
source: root.trailing-icon == "up" ? @image-url("../assets/icons/chevron-up.svg")
|
||||
: root.trailing-icon == "down" ? @image-url("../assets/icons/chevron-down.svg")
|
||||
: @image-url("../assets/icons/check.svg");
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
tint: Theme.accent;
|
||||
@@ -946,26 +951,36 @@ export component Sidebar inherits Rectangle {
|
||||
MenuItem {
|
||||
label: @tr("Name (A-Z)");
|
||||
selected: SidebarState.sort-option == "name";
|
||||
trailing-icon: SidebarState.sort-option == "name"
|
||||
? (SidebarState.sort-asc ? "up" : "down") : "";
|
||||
activated => { SidebarActions.set-sort("name"); }
|
||||
}
|
||||
MenuItem {
|
||||
label: @tr("Recent");
|
||||
selected: SidebarState.sort-option == "recent";
|
||||
trailing-icon: SidebarState.sort-option == "recent"
|
||||
? (SidebarState.sort-asc ? "up" : "down") : "";
|
||||
activated => { SidebarActions.set-sort("recent"); }
|
||||
}
|
||||
MenuItem {
|
||||
label: @tr("# of tracks");
|
||||
selected: SidebarState.sort-option == "tracks";
|
||||
trailing-icon: SidebarState.sort-option == "tracks"
|
||||
? (SidebarState.sort-asc ? "up" : "down") : "";
|
||||
activated => { SidebarActions.set-sort("tracks"); }
|
||||
}
|
||||
MenuItem {
|
||||
label: @tr("Play Count");
|
||||
selected: SidebarState.sort-option == "playcount";
|
||||
trailing-icon: SidebarState.sort-option == "playcount"
|
||||
? (SidebarState.sort-asc ? "up" : "down") : "";
|
||||
activated => { SidebarActions.set-sort("playcount"); }
|
||||
}
|
||||
MenuItem {
|
||||
label: @tr("Custom");
|
||||
selected: SidebarState.sort-option == "custom";
|
||||
trailing-icon: SidebarState.sort-option == "custom"
|
||||
? (SidebarState.sort-asc ? "up" : "down") : "";
|
||||
activated => { SidebarActions.set-sort("custom"); }
|
||||
}
|
||||
Rectangle {
|
||||
|
||||
Reference in new issue
Block a user