Files
qbz/crates/qbz-qt/qml/views/playlistmanager/PmMenuRow.qml
T
vicrodh a16cf56c86 feat(qt): playlist manager, folders, importer and the Library parity round
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
2026-07-31 22:10:19 -06:00

87 lines
3.1 KiB
QML

// PmMenuRow — ONE row of the Playlist Manager's three menus: the toolbar's
// filter menu, its sort menu (with the #657 direction caret) and the
// move-to-folder menu. The port of `PmMenuItem`
// (PlaylistManagerView.slint:113-158).
//
// 30 px, NOT the port's 33 px context-menu convention: rule 3 says take the
// reference's number. `QbzContextMenu`'s own header states its rows are
// per-site, so there is no shared row component to reuse
// (QbzContextMenu.qml:16-18); controls/CardMenu.qml was rejected as the base —
// it is a flat entries model with a leading icon, no `selected` state, no
// trailing caret and no collapse arm.
//
// The `shown` arm is the reference's collapse (a hidden row is height 0, not
// removed) — Slint 1.16 disallows `for x: if c:`, so the move-to-folder menu
// hides the playlist's CURRENT folder that way (`:857`). Kept 1:1 rather than
// filtering the model, so the two menus stay one component.
//
// The trailing glyph renders ONLY when `selected`, in the reference: an
// unselected row has no check and no caret (`:140-150`).
import QtQuick
import com.blitzfc.qbz
import "../../theme"
Rectangle {
id: root
property string label: ""
property bool selected: false
/// false => height 0 (the collapse arm). The row keeps its slot in the
/// Column but takes no space and no input.
property bool shown: true
/// `"check"` | `"caret"` | `"none"` — what the SELECTED row trails with.
property string trailing: "check"
/// Only read when `trailing === "caret"`: up = ascending, down = descending.
property bool sortAsc: true
/// The handler MUST end with the owning menu's `close()` — Slint's
/// PopupWindow closes on any inside click and Qt's CloseOnPressOutside
/// does not (contract D15).
signal activated()
QbzTheme { id: theme }
height: root.shown ? 30 : 0
visible: root.shown
radius: 5
color: rowArea.containsMouse ? theme.surfaceHover : "transparent"
Row {
anchors.fill: parent
anchors.leftMargin: 8
anchors.rightMargin: 8
spacing: 8
Text {
anchors.verticalCenter: parent.verticalCenter
width: Math.max(0, parent.width - (trailingGlyph.visible ? 13 + 8 : 0))
text: root.label
color: root.selected ? theme.accent : theme.textSecondary
font.pixelSize: 13
font.weight: root.selected ? theme.weightSemibold : theme.weightRegular
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
QbzIcon {
id: trailingGlyph
anchors.verticalCenter: parent.verticalCenter
visible: root.selected && root.trailing !== "none"
width: 13
height: 13
name: root.trailing === "caret"
? (root.sortAsc ? "chevron-up" : "chevron-down")
: "check"
tintName: "accent"
}
}
MouseArea {
id: rowArea
anchors.fill: parent
enabled: root.shown
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.activated()
}
}