fix(playlist): reuse the shared drag ghost for custom-order reorder
The bespoke reorder ghost read a row's absolute-position inside a ListView, which double-counts the row's own offset, so it only sat at the cursor for row 0 and flew off for the rest. Drive the app-wide DragState directly from the reorder gesture so the shared DragGhost (positioned in AppShell at the proven window coords) renders the reorder ghost; a new DragState.reorder flag keeps the sidebar from claiming it as an add-to-playlist drop and keeps the Rust DragActions path out of it. Removed the buggy local ghost. Reorder logic + persistence unchanged. (state.slint also carries the quality-badge fields for the next commit.) cc
This commit is contained in:
4 files changed
+58
-44
No files matched your search
@@ -338,8 +338,10 @@ export component PlaylistView inherits Rectangle {
|
|||||||
: @tr("Default");
|
: @tr("Default");
|
||||||
|
|
||||||
// --- Custom-order drag-reorder state (issue #589) -------------------
|
// --- Custom-order drag-reorder state (issue #589) -------------------
|
||||||
// Component-LOCAL, mirroring QueueSidebar's UP NEXT drag — NOT the
|
// Slot bookkeeping is component-LOCAL; the floating ghost REUSES the
|
||||||
// DragState global (that one drives the track->sidebar-playlist ghost).
|
// app-wide DragGhost via the DragState global (with DragState.reorder
|
||||||
|
// set, so sidebar rows skip drop-target detection). DragActions is
|
||||||
|
// never called, so the Rust add-to-playlist drop path never runs.
|
||||||
// All indices are VISIBLE-list indices into PlaylistState.tracks.
|
// All indices are VISIBLE-list indices into PlaylistState.tracks.
|
||||||
property <bool> reorder-active: false;
|
property <bool> reorder-active: false;
|
||||||
property <int> reorder-from: -1; // visible index of the dragged row
|
property <int> reorder-from: -1; // visible index of the dragged row
|
||||||
@@ -349,8 +351,6 @@ export component PlaylistView inherits Rectangle {
|
|||||||
// list-top, the live viewport-y is folded back in below so the slot
|
// list-top, the live viewport-y is folded back in below so the slot
|
||||||
// math stays scroll-aware.
|
// math stays scroll-aware.
|
||||||
property <length> reorder-list-top;
|
property <length> reorder-list-top;
|
||||||
// Live pointer window-Y during the drag (positions the floating ghost).
|
|
||||||
property <length> reorder-pointer-y;
|
|
||||||
|
|
||||||
// Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 50px
|
// Pointer window-Y -> insertion slot [0..N]. Rows are a fixed 50px
|
||||||
// stacked with 0 spacing; the +0.5 rounds boundaries to row centers
|
// stacked with 0 spacing; the +0.5 rounds boundaries to row centers
|
||||||
@@ -361,6 +361,9 @@ export component PlaylistView inherits Rectangle {
|
|||||||
}
|
}
|
||||||
function commit-reorder() {
|
function commit-reorder() {
|
||||||
root.reorder-active = false;
|
root.reorder-active = false;
|
||||||
|
// Release the shared ghost.
|
||||||
|
DragState.active = false;
|
||||||
|
DragState.reorder = false;
|
||||||
// Fire only a real move. `to == from` and `to == from+1` both drop
|
// Fire only a real move. `to == from` and `to == from+1` both drop
|
||||||
// back onto the same gap (no-op).
|
// back onto the same gap (no-op).
|
||||||
if (root.reorder-over != root.reorder-from
|
if (root.reorder-over != root.reorder-from
|
||||||
@@ -1027,17 +1030,32 @@ export component PlaylistView inherits Rectangle {
|
|||||||
// reports the gesture; this view owns the state.
|
// reports the gesture; this view owns the state.
|
||||||
reorder-index: i;
|
reorder-index: i;
|
||||||
reorder-source: root.reorder-active && root.reorder-from == i;
|
reorder-source: root.reorder-active && root.reorder-from == i;
|
||||||
reorder-begin(idx, row-top) => {
|
reorder-begin(idx, row-top, gx, gy) => {
|
||||||
root.reorder-active = true;
|
root.reorder-active = true;
|
||||||
root.reorder-from = idx;
|
root.reorder-from = idx;
|
||||||
root.reorder-over = idx;
|
root.reorder-over = idx;
|
||||||
root.reorder-list-top =
|
root.reorder-list-top =
|
||||||
row-top - idx * 50px - track-list.viewport-y;
|
row-top - idx * 50px - track-list.viewport-y;
|
||||||
root.reorder-pointer-y = row-top;
|
// Reuse the app-wide DragGhost (proven
|
||||||
|
// window-coord positioning) for the floating
|
||||||
|
// pill; reorder=true keeps sidebar rows from
|
||||||
|
// claiming a drop target. Pointer first, then
|
||||||
|
// active, so the ghost never flashes at a
|
||||||
|
// stale position.
|
||||||
|
DragState.count = 1;
|
||||||
|
DragState.ghost-title = track.title;
|
||||||
|
DragState.ghost-subtitle = track.album != ""
|
||||||
|
? track.artist + " · " + track.album
|
||||||
|
: track.artist;
|
||||||
|
DragState.pointer-x = gx;
|
||||||
|
DragState.pointer-y = gy;
|
||||||
|
DragState.reorder = true;
|
||||||
|
DragState.active = true;
|
||||||
}
|
}
|
||||||
reorder-update(pointer-y) => {
|
reorder-update(gx, gy) => {
|
||||||
root.reorder-over = root.slot-from-pointer(pointer-y);
|
root.reorder-over = root.slot-from-pointer(gy);
|
||||||
root.reorder-pointer-y = pointer-y;
|
DragState.pointer-x = gx;
|
||||||
|
DragState.pointer-y = gy;
|
||||||
// TODO: auto-scroll during drag when the pointer
|
// TODO: auto-scroll during drag when the pointer
|
||||||
// nears the list's top/bottom edge (the drag
|
// nears the list's top/bottom edge (the drag
|
||||||
// currently reorders within the visible viewport).
|
// currently reorders within the visible viewport).
|
||||||
@@ -1429,36 +1447,8 @@ export component PlaylistView inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Floating drag ghost — a pill following the pointer with the dragged
|
// The floating reorder ghost is the app-wide DragGhost (AppShell),
|
||||||
// track's title (QueueSidebar parity). A free-positioned child of the
|
// driven through DragState by the reorder-begin/update handlers above —
|
||||||
// view ROOT (NOT a layout child), declared last so it renders above the
|
// no component-local ghost (its absolute-position math double-counted
|
||||||
// list. Converts the live pointer window-Y into this view's local space.
|
// the row offset inside the ListView and drifted off the cursor).
|
||||||
if root.reorder-active
|
|
||||||
&& root.reorder-from >= 0
|
|
||||||
&& root.reorder-from < PlaylistState.tracks.length: Rectangle {
|
|
||||||
x: 44px;
|
|
||||||
y: root.reorder-pointer-y - root.absolute-position.y - self.height / 2;
|
|
||||||
width: Math.min(360px, root.width - 88px);
|
|
||||||
height: 34px;
|
|
||||||
background: Theme.surface-elevated;
|
|
||||||
border-radius: Radius.sm;
|
|
||||||
border-width: 1px;
|
|
||||||
border-color: Theme.accent;
|
|
||||||
opacity: 0.95;
|
|
||||||
HorizontalLayout {
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-right: 10px;
|
|
||||||
VerticalLayout {
|
|
||||||
alignment: center;
|
|
||||||
horizontal-stretch: 1;
|
|
||||||
Text {
|
|
||||||
text: PlaylistState.tracks[root.reorder-from].title;
|
|
||||||
color: Theme.text-primary;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: Typography.medium;
|
|
||||||
overflow: elide;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -94,8 +94,9 @@ export component TrackRow inherits Rectangle {
|
|||||||
// playlists) — the handle has its OWN TouchArea.
|
// playlists) — the handle has its OWN TouchArea.
|
||||||
in property <int> reorder-index;
|
in property <int> reorder-index;
|
||||||
in property <bool> reorder-source: false; // this row is being dragged
|
in property <bool> reorder-source: false; // this row is being dragged
|
||||||
callback reorder-begin(int /* visible index */, length /* row window-top */);
|
callback reorder-begin(int /* visible index */, length /* row window-top */,
|
||||||
callback reorder-update(length /* pointer window-y */);
|
length /* pointer window-x */, length /* pointer window-y */);
|
||||||
|
callback reorder-update(length /* pointer window-x */, length /* pointer window-y */);
|
||||||
callback reorder-end();
|
callback reorder-end();
|
||||||
// Whether the row can be dragged onto sidebar playlists.
|
// Whether the row can be dragged onto sidebar playlists.
|
||||||
in property <bool> draggable: true;
|
in property <bool> draggable: true;
|
||||||
@@ -305,10 +306,13 @@ export component TrackRow inherits Rectangle {
|
|||||||
&& Math.abs(self.absolute-position.y - root.y + self.mouse-y - self.down-y) > 6px) {
|
&& Math.abs(self.absolute-position.y - root.y + self.mouse-y - self.down-y) > 6px) {
|
||||||
self.dragging = true;
|
self.dragging = true;
|
||||||
root.reorder-begin(root.reorder-index,
|
root.reorder-begin(root.reorder-index,
|
||||||
root.absolute-position.y - root.y);
|
root.absolute-position.y - root.y,
|
||||||
|
self.absolute-position.x - root.x + self.mouse-x,
|
||||||
|
self.absolute-position.y - root.y + self.mouse-y);
|
||||||
}
|
}
|
||||||
if (self.dragging) {
|
if (self.dragging) {
|
||||||
root.reorder-update(
|
root.reorder-update(
|
||||||
|
self.absolute-position.x - root.x + self.mouse-x,
|
||||||
self.absolute-position.y - root.y + self.mouse-y);
|
self.absolute-position.y - root.y + self.mouse-y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ component SidebarRow inherits Rectangle {
|
|||||||
// to get the true window bounds (the drag pointer in DragState is
|
// to get the true window bounds (the drag pointer in DragState is
|
||||||
// already in true window coords — TrackRow corrects it the same way).
|
// already in true window coords — TrackRow corrects it the same way).
|
||||||
root.drop-hot = root.drag-on
|
root.drop-hot = root.drag-on
|
||||||
|
// A within-list reorder drag (playlist custom order) reuses the
|
||||||
|
// DragState ghost but is NOT an add-to-playlist drop.
|
||||||
|
&& !DragState.reorder
|
||||||
&& !root.is-folder
|
&& !root.is-folder
|
||||||
&& root.drag-px >= root.absolute-position.x - root.x
|
&& root.drag-px >= root.absolute-position.x - root.x
|
||||||
&& root.drag-px <= root.absolute-position.x - root.x + root.width
|
&& root.drag-px <= root.absolute-position.x - root.x + root.width
|
||||||
|
|||||||
@@ -968,6 +968,12 @@ export global DragState {
|
|||||||
in-out property <length> pointer-y;
|
in-out property <length> pointer-y;
|
||||||
// The sidebar playlist id currently under the pointer (drop target).
|
// The sidebar playlist id currently under the pointer (drop target).
|
||||||
in-out property <string> over-playlist-id;
|
in-out property <string> over-playlist-id;
|
||||||
|
// WITHIN-LIST reorder drag (playlist custom order, issue #589): the
|
||||||
|
// gesture reuses this global purely for the floating ghost (DragGhost's
|
||||||
|
// window-coord positioning), driven from .slint only — DragActions is
|
||||||
|
// never called, so the Rust add-to-playlist drop path never runs.
|
||||||
|
// Sidebar rows skip drop-target detection while this is set.
|
||||||
|
in-out property <bool> reorder: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export global DragActions {
|
export global DragActions {
|
||||||
@@ -4054,6 +4060,17 @@ export global NowPlayingState {
|
|||||||
in property <int> sample-rate-hz: 0;
|
in property <int> sample-rate-hz: 0;
|
||||||
in property <int> bit-depth: 0;
|
in property <int> bit-depth: 0;
|
||||||
|
|
||||||
|
// EFFECTIVE (delivered) stream quality — what the engine is actually
|
||||||
|
// decoding after the streaming-quality setting downgrade (#590 follow-up).
|
||||||
|
// The badge keeps showing the catalog MAX (quality-tier/detail above);
|
||||||
|
// when the delivered stream is below it, `quality-downgraded` turns on
|
||||||
|
// the AudioStamp's downgrade arrow and `quality-true-detail` carries the
|
||||||
|
// true "16-bit / 44.1 kHz" line for the hover tooltip. 0 / "" = unknown.
|
||||||
|
in property <int> effective-sample-rate-hz: 0;
|
||||||
|
in property <int> effective-bit-depth: 0;
|
||||||
|
in property <bool> quality-downgraded: false;
|
||||||
|
in property <string> quality-true-detail;
|
||||||
|
|
||||||
// Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of
|
// Output backend + mode LEDs (Mode C "Small" bar reads these). Mirror of
|
||||||
// the same four on SettingsState (:2154-2157) — surfaced here so the
|
// the same four on SettingsState (:2154-2157) — surfaced here so the
|
||||||
// Small now-playing bar has ONE source for the song card + DAC/EXC
|
// Small now-playing bar has ONE source for the song card + DAC/EXC
|
||||||
|
|||||||
Reference in new issue
Block a user