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");
|
||||
|
||||
// --- Custom-order drag-reorder state (issue #589) -------------------
|
||||
// Component-LOCAL, mirroring QueueSidebar's UP NEXT drag — NOT the
|
||||
// DragState global (that one drives the track->sidebar-playlist ghost).
|
||||
// Slot bookkeeping is component-LOCAL; the floating ghost REUSES the
|
||||
// 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.
|
||||
property <bool> reorder-active: false;
|
||||
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
|
||||
// math stays scroll-aware.
|
||||
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
|
||||
// stacked with 0 spacing; the +0.5 rounds boundaries to row centers
|
||||
@@ -361,6 +361,9 @@ export component PlaylistView inherits Rectangle {
|
||||
}
|
||||
function commit-reorder() {
|
||||
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
|
||||
// back onto the same gap (no-op).
|
||||
if (root.reorder-over != root.reorder-from
|
||||
@@ -1027,17 +1030,32 @@ export component PlaylistView inherits Rectangle {
|
||||
// reports the gesture; this view owns the state.
|
||||
reorder-index: 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-from = idx;
|
||||
root.reorder-over = idx;
|
||||
root.reorder-list-top =
|
||||
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) => {
|
||||
root.reorder-over = root.slot-from-pointer(pointer-y);
|
||||
root.reorder-pointer-y = pointer-y;
|
||||
reorder-update(gx, gy) => {
|
||||
root.reorder-over = root.slot-from-pointer(gy);
|
||||
DragState.pointer-x = gx;
|
||||
DragState.pointer-y = gy;
|
||||
// TODO: auto-scroll during drag when the pointer
|
||||
// nears the list's top/bottom edge (the drag
|
||||
// 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
|
||||
// track's title (QueueSidebar parity). A free-positioned child of the
|
||||
// view ROOT (NOT a layout child), declared last so it renders above the
|
||||
// list. Converts the live pointer window-Y into this view's local space.
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// The floating reorder ghost is the app-wide DragGhost (AppShell),
|
||||
// driven through DragState by the reorder-begin/update handlers above —
|
||||
// no component-local ghost (its absolute-position math double-counted
|
||||
// the row offset inside the ListView and drifted off the cursor).
|
||||
}
|
||||
@@ -94,8 +94,9 @@ export component TrackRow inherits Rectangle {
|
||||
// playlists) — the handle has its OWN TouchArea.
|
||||
in property <int> reorder-index;
|
||||
in property <bool> reorder-source: false; // this row is being dragged
|
||||
callback reorder-begin(int /* visible index */, length /* row window-top */);
|
||||
callback reorder-update(length /* pointer window-y */);
|
||||
callback reorder-begin(int /* visible index */, length /* row window-top */,
|
||||
length /* pointer window-x */, length /* pointer window-y */);
|
||||
callback reorder-update(length /* pointer window-x */, length /* pointer window-y */);
|
||||
callback reorder-end();
|
||||
// Whether the row can be dragged onto sidebar playlists.
|
||||
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) {
|
||||
self.dragging = true;
|
||||
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) {
|
||||
root.reorder-update(
|
||||
self.absolute-position.x - root.x + self.mouse-x,
|
||||
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
|
||||
// already in true window coords — TrackRow corrects it the same way).
|
||||
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.drag-px >= root.absolute-position.x - root.x
|
||||
&& root.drag-px <= root.absolute-position.x - root.x + root.width
|
||||
|
||||
@@ -968,6 +968,12 @@ export global DragState {
|
||||
in-out property <length> pointer-y;
|
||||
// The sidebar playlist id currently under the pointer (drop target).
|
||||
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 {
|
||||
@@ -4054,6 +4060,17 @@ export global NowPlayingState {
|
||||
in property <int> sample-rate-hz: 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
|
||||
// the same four on SettingsState (:2154-2157) — surfaced here so the
|
||||
// Small now-playing bar has ONE source for the song card + DAC/EXC
|
||||
|
||||
Reference in new issue
Block a user