Ports the whole playlist-organisation domain, which had no counterpart in this frontend, plus the Library work the owner asked not to defer. Playlist Manager. New controller (playlist_manager_qt, _rows, _ops) over the already-ported folders_qt repository, which until now had zero consumers. Grid, list and folder-tree modes; sort, filter and search; folder cards and chips; drag reorder and up/down arrows. Reached from the sidebar's "..." menu, whose New folder / Manage playlists / Import rows had been rendering dimmed and inert behind a comment saying the seam did not exist. Folders. QbzFolderEdit plus the create/edit modals and PmFolderIcon: preset or custom-image icons, colour, hidden flag, delete. Deleting a folder nulls its LOCAL members' folder_id explicitly, because the app keeps the foreign_keys pragma off, so the schema's ON DELETE SET NULL never fires for them. QbzPlaylistEdit. One shared rename/description/delete editor replacing the inline popup in PlaylistView. Three corrections the old path had wrong: the description is carried through a rename instead of being written as NULL, a FOLLOWED playlist routes to unsubscribe (playlist/delete returns 200 and silently no-ops on a playlist you do not own), and the post-delete back navigation is conditional on actually being on that playlist's page, so deleting from the manager no longer navigates you off it. Importer. Spotify / Apple Music / Tidal / Deezer, over the shared qbz-playlist-import crate. Picker local mode. The reference's four-case matrix — target local or Qobuz, payload local refs or Qobuz ids. Local playlists list unconditionally and Qobuz ones only when online. Local refs onto a Qobuz playlist write the library.db sidecar at next_playlist_sidecar_position(pid, qobuz_count); the 0-based enumerate the reference used before collided slots and lost rows. The carried payload can no longer be parsed as a catalog id when it is a library row id. Unblocks the local bulk add-to-playlist. Library. A-Z jump strip, Albums list mode, Artists side panel, per-tab persistence and multi-select. Preferences land in the same <data-dir>/qbz/favorites_ui.json the shipping build writes, so switching between the two frontends keeps them. LibraryView is split into views/library/ on the way past 1800 lines. Local library. Group-by now persists; playback queues the order on screen rather than the SQL page order; switching album-identity mode invalidates the Artists tab; artist matching folds diacritics and compares normalized credit parts, so "Air" stops listing "Airbourne" and "Beyonce" stops being two artists. Offline. activate_offline never persisted a last-user marker, so every per-user path resolved to None and all local reads came back empty; a session that started offline saw no local playlists at all. The sidebar's load-once latch was function-local and therefore unreachable from the logout reset, so a second login never rebuilt the tree. Both are session-scoped now, and the sidebar Refresh row runs the local-safe verb instead of being dimmed with no way to recover. Also fixes, found while verifying rather than reported: artwork derivatives were written straight to their final path, so a QML Image resolving one mid-encode opened a truncated png and Qt cached that failure against the URL for the rest of the session; Library's track group separators referenced a Component declared in another delegate's scope and could never render; the genre filter was drawn on Tracks and Albums but applied only on All; an All-feed row called a player method that does not exist; and the sidebar's drop-hot highlight used Slint's #RRGGBBAA literals in Qt's #AARRGGBB slot. 17 new msgids across all 8 locales. cc
96 lines
3.9 KiB
QML
96 lines
3.9 KiB
QML
// QbzContextMenu — the shared popup surface for the app's ⋯ / context
|
|
// menus, with CORRECT positioning (phase 12):
|
|
//
|
|
// - openAtCursor(item, localX, localY): context menus open AT THE
|
|
// POINTER, like the Slint unified TrackContextMenu (TrackRow.slint:
|
|
// `absolute-position - layout-offset + local-pointer`, clamped into
|
|
// the row/window). Coordinates are mapped to the window and the menu
|
|
// is parented to the Overlay, so a menu never renders inside the
|
|
// triggering card/row.
|
|
// - openBelowRight(item): button menus anchor under their trigger,
|
|
// right edge aligned (the QbzSelect / sort-menu convention).
|
|
// - Both clamp into the window with an 8px margin (Slint clamps the x
|
|
// to `width - menu-width - 12`) so a menu never opens off-screen —
|
|
// Popup does NOT do this on its own.
|
|
//
|
|
// The background matches ContextMenu.slint (surface-main, Radius.sm,
|
|
// 1px border-muted). Rows go in the default content (33px, icon 15 +
|
|
// label 13 per the existing per-site convention).
|
|
//
|
|
// Right-click parity: every ⋯ site also opens ITS menu on a right press
|
|
// through the same `openAtCursor(area, mouse.x, mouse.y)` — the button and
|
|
// the context gesture must never show different menus.
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Popup {
|
|
id: root
|
|
|
|
parent: Overlay.overlay
|
|
width: menuWidth
|
|
padding: 5
|
|
closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
|
|
|
|
property int menuWidth: 196
|
|
default property alias menuContent: col.data
|
|
/// Row gap inside the panel. Defaults to the Column's own 0 — every
|
|
/// existing call site is unaffected — and exists because a call site CANNOT
|
|
/// reach `col`: QML gives a component no access to another file's ids, so
|
|
/// `col.spacing: 1` inside a `QbzContextMenu { … }` block is a grouped
|
|
/// assignment against a property the type does not have and fails the WHOLE
|
|
/// consuming file at load. The Playlist Manager's three menus set 1, which
|
|
/// is the reference's `spacing: 1px`
|
|
/// (playlist/PlaylistManagerView.slint:1153, :1197, :816).
|
|
property alias contentSpacing: col.spacing
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
background: Rectangle {
|
|
color: theme.surfaceMain
|
|
radius: theme.radiusSm
|
|
border.width: 1
|
|
border.color: theme.borderMuted
|
|
}
|
|
contentItem: Column { id: col }
|
|
|
|
// Final placement is computed BEFORE open() — the popup's implicit
|
|
// height is already known (static content), and `Window.window` is
|
|
// only reliably attached on the SOURCE item, not on the popup.
|
|
function _place(gx, gy, win) {
|
|
if (win) {
|
|
x = Math.max(8, Math.min(gx, win.width - width - 8))
|
|
y = Math.max(8, Math.min(gy, win.height - implicitHeight - 8))
|
|
} else {
|
|
x = gx
|
|
y = gy
|
|
}
|
|
open()
|
|
}
|
|
|
|
/// Context menu at the pointer. `localX/localY` are the click coords
|
|
/// inside `sourceItem` (the MouseArea that caught the click).
|
|
function openAtCursor(sourceItem, localX, localY) {
|
|
var g = sourceItem.mapToItem(null, localX, localY)
|
|
_place(g.x, g.y, sourceItem.Window.window)
|
|
}
|
|
|
|
/// Button menu: below the trigger, right edges aligned.
|
|
function openBelowRight(sourceItem) {
|
|
var g = sourceItem.mapToItem(null, sourceItem.width - width, sourceItem.height + 4)
|
|
_place(g.x, g.y, sourceItem.Window.window)
|
|
}
|
|
|
|
/// Button menu: below the trigger, LEFT edges aligned. The mirror of
|
|
/// openBelowRight, for triggers that sit at the LEFT end of their row (the
|
|
/// MyQBZ hero overflow and the Disco builder's per-row type override), where
|
|
/// a right-aligned panel would hang off the trigger to the left and, on a
|
|
/// narrow window, get clamped by _place into visibly the wrong place.
|
|
function openBelowLeft(sourceItem) {
|
|
var g = sourceItem.mapToItem(null, 0, sourceItem.height + 4)
|
|
_place(g.x, g.y, sourceItem.Window.window)
|
|
}
|
|
}
|