feat(slint): playlist detail polish, sort (incl. custom) + WIP track drag
Polish + consistency:
- Compact playlist header (cover 150px; nav->header spacing
homologated with AlbumView); description truncated to ~2 lines +
"Read more" modal; custom-artwork buttons only on cover hover.
- New Typography.section (18px) token for hero/section titles,
applied across Album / Artist / Mix / Playlist + Home carousels.
- Shorter track rows app-wide (50px row, 36px artwork, -1 fonts);
+3px column-header/divider gap; wider track-number column.
- Reusable ExpandableSearch (the JumpNavBar page-search pattern:
magnifier -> opens left -> X to close) in the playlist action row.
- Custom ListScrollbar (thicker, min thumb height, own gutter) since
Slint's ListView scrollbar can't be styled.
Sort:
- Default / Title / Artist / Album / Duration with asc/desc per
field, plus Custom (manual order persisted in library.db with
up/down reorder). Client-side search + sort combined.
WIP: track drag-and-drop onto sidebar playlists
- DragState/DragActions globals, drag source on the shared TrackRow,
follow-cursor DragGhost, sidebar playlist rows self-detect the drop
via absolute-position, multi-select group drag, add_tracks_to_playlist
on drop, Flickables freeze scroll during a drag.
- KNOWN ISSUE: the ghost is offset from the cursor (the row
absolute-position + pointer mapping is off, esp. inside Flickables).
Diagnostic logging is left in (TrackRow debug() + a start log) to
pin down the wrong term before the generic fix.
cc
This commit is contained in:
21 files changed
+1223
-65
No files matched your search
@@ -14,6 +14,7 @@ import { PlaylistPickerModal } from "../primitives/PlaylistPickerModal.slint";
|
||||
import { CreatePlaylistModal } from "../primitives/CreatePlaylistModal.slint";
|
||||
import { EditPlaylistModal } from "../primitives/EditPlaylistModal.slint";
|
||||
import { CreateFolderModal } from "../primitives/CreateFolderModal.slint";
|
||||
import { DragGhost } from "../primitives/DragGhost.slint";
|
||||
import { PlayerBar } from "PlayerBar.slint";
|
||||
import { QueueSidebar } from "QueueSidebar.slint";
|
||||
import { LyricsSidebar } from "LyricsSidebar.slint";
|
||||
@@ -237,4 +238,7 @@ export component AppShell inherits Rectangle {
|
||||
|
||||
// Global "Create folder" modal.
|
||||
CreateFolderModal { }
|
||||
|
||||
// Floating track-drag ghost (follows the cursor, on top of all).
|
||||
DragGhost { }
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
import { Theme } from "../foundation/semantic-colors.slint";
|
||||
import { Spacing } from "../foundation/spacing.slint";
|
||||
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
||||
import { ShellState, SidebarState, SidebarActions, SidebarEntry } from "../state.slint";
|
||||
import { ShellState, SidebarState, SidebarActions, SidebarEntry, DragState } from "../state.slint";
|
||||
import { ScrollView } from "std-widgets.slint";
|
||||
|
||||
// A single context-menu item row.
|
||||
@@ -55,10 +55,46 @@ component SidebarRow inherits Rectangle {
|
||||
callback delete-folder(string);
|
||||
|
||||
property <bool> is-folder: root.entry.kind == "folder";
|
||||
|
||||
// Track-drag drop target: a playlist row self-detects whether the
|
||||
// global drag pointer is within its bounds (the source grabs the
|
||||
// pointer, so normal hover doesn't fire during a drag). It claims
|
||||
// the drop target while hot, and releases it when the pointer
|
||||
// leaves.
|
||||
//
|
||||
// absolute-position must be read in an event handler (recompute),
|
||||
// NOT a declarative binding — reading it during the layout phase
|
||||
// is a recursion. So we mirror the drag pointer/active flag and
|
||||
// recompute on their change.
|
||||
property <length> drag-px: DragState.pointer-x;
|
||||
property <length> drag-py: DragState.pointer-y;
|
||||
property <bool> drag-on: DragState.active;
|
||||
property <bool> drop-hot: false;
|
||||
function recompute-drop() {
|
||||
root.drop-hot = root.drag-on
|
||||
&& !root.is-folder
|
||||
&& root.drag-px >= root.absolute-position.x
|
||||
&& root.drag-px <= root.absolute-position.x + root.width
|
||||
&& root.drag-py >= root.absolute-position.y
|
||||
&& root.drag-py <= root.absolute-position.y + root.height;
|
||||
if (root.drop-hot) {
|
||||
DragState.over-playlist-id = root.entry.id;
|
||||
} else if (DragState.over-playlist-id == root.entry.id) {
|
||||
DragState.over-playlist-id = "";
|
||||
}
|
||||
}
|
||||
changed drag-px => { root.recompute-drop(); }
|
||||
changed drag-py => { root.recompute-drop(); }
|
||||
changed drag-on => { root.recompute-drop(); }
|
||||
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
background: ta.has-hover || more-ta.has-hover || root.active
|
||||
? Theme.surface-hover : transparent;
|
||||
background: root.drop-hot
|
||||
? #22c55e26
|
||||
: (ta.has-hover || more-ta.has-hover || root.active
|
||||
? Theme.surface-hover : transparent);
|
||||
border-width: root.drop-hot ? 1px : 0px;
|
||||
border-color: #22c55e66;
|
||||
|
||||
HorizontalLayout {
|
||||
padding-left: root.entry.indent ? 24px : 8px;
|
||||
|
||||
Reference in new issue
Block a user