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
204 lines
7.7 KiB
QML
204 lines
7.7 KiB
QML
// PmGridCard — the GRID-mode playlist cell (PlaylistManagerView.slint:470-618).
|
||
// 160 wide; 220 tall, or 250 when the reorder header shows. r8, `surface-card`
|
||
// → `surface-hover` on BODY hover (the footer buttons are outside the body
|
||
// area, exactly like the reference's `ta`), 0.6 opacity while hidden.
|
||
//
|
||
// Affordances (contract D27): open (body), favourite, visibility,
|
||
// edit-playlist, add-to-mixtape, move-up, move-down. NO move-to-folder — that
|
||
// one exists only on the list row. The four footer buttons are ALWAYS visible;
|
||
// there is no hover reveal, no play affordance, no pin badge and no overlay row
|
||
// on this card (that is cards/PlaylistCard.qml, a different component).
|
||
//
|
||
// TWO local-row suppressions, both contract decisions:
|
||
// * D28 — the cassette is NOT RENDERED on a `local:` row. `accepts_item`
|
||
// answers `Accepts::NONE` for (Playlist, Local) and `qbz_mixtape::enqueue`
|
||
// hard-errors, so the row can never resolve; a dimmed control would promise
|
||
// "temporarily unavailable". `Row` skips invisible children, so the footer
|
||
// simply narrows.
|
||
// * D29 — the reorder chevrons are NOT RENDERED on a `local:` row (custom
|
||
// positions are u64-keyed and `reorder_step` drops locals), but the 22 px
|
||
// header IS still reserved: `canReorder` switches EVERY card 220 → 250
|
||
// globally, so hiding the header would leave a 30 px hole in this one card.
|
||
//
|
||
// The grip glyph between the chevrons is DECORATIVE — no MouseArea. The
|
||
// reference has no drag and drop anywhere in this view (contract D30) and the
|
||
// counter row says so in words ("Use the arrows to reorder playlists").
|
||
|
||
import QtQuick
|
||
import com.blitzfc.qbz
|
||
import "../../cards"
|
||
import "../../theme"
|
||
|
||
Rectangle {
|
||
id: root
|
||
|
||
/// One `PlaylistRow` (contract §4.3).
|
||
property var item: ({})
|
||
/// ABSOLUTE index in the visible order (the grid delegate is a ROW of
|
||
/// cards, so the Repeater's own index is the column — D1).
|
||
property int itemIndex: 0
|
||
/// Drives BOTH the 220 → 250 height and the reserved header (D29).
|
||
property bool canReorder: false
|
||
|
||
QbzTheme { id: theme }
|
||
|
||
readonly property string itemId: String(root.item.id || "")
|
||
readonly property bool isLocal: root.item.isLocal === true
|
||
|
||
width: 160
|
||
height: root.canReorder ? 250 : 220
|
||
radius: 8
|
||
color: bodyArea.containsMouse ? theme.surfaceHover : theme.surfaceCard
|
||
opacity: (root.item.isHidden === true) ? 0.6 : 1.0
|
||
|
||
Column {
|
||
x: 10
|
||
y: 10
|
||
width: 140
|
||
spacing: 8
|
||
|
||
// Reorder header — the SLOT is reserved whenever `canReorder`, the
|
||
// controls only render on a Qobuz row (D29).
|
||
Item {
|
||
visible: root.canReorder
|
||
width: 140
|
||
height: 22
|
||
|
||
PmActionButton {
|
||
visible: !root.isLocal
|
||
anchors.left: parent.left
|
||
name: "chevron-up"
|
||
btnSize: 22
|
||
glyph: 14
|
||
onClicked: QbzPlaylistManager.moveUp(root.itemId)
|
||
}
|
||
QbzIcon {
|
||
visible: !root.isLocal
|
||
anchors.centerIn: parent
|
||
name: "grip-vertical"
|
||
width: 13
|
||
height: 13
|
||
tintName: "muted"
|
||
}
|
||
PmActionButton {
|
||
visible: !root.isLocal
|
||
anchors.right: parent.right
|
||
name: "chevron-down"
|
||
btnSize: 22
|
||
glyph: 14
|
||
onClicked: QbzPlaylistManager.moveDown(root.itemId)
|
||
}
|
||
}
|
||
|
||
// Artwork + availability badge.
|
||
Item {
|
||
width: 140
|
||
height: 140
|
||
|
||
PlaylistCollage {
|
||
anchors.fill: parent
|
||
// The manager's collage law: 1-3 covers collapse to ONE
|
||
// full-bleed tile, 4+ go to a 2×2 with a 1 px seam.
|
||
layout: "pm"
|
||
urls: root.item.covers || []
|
||
radius: 8
|
||
}
|
||
PmLocalBadge {
|
||
x: parent.width - width - 6
|
||
y: 6
|
||
status: root.item.localStatus || ""
|
||
isLocal: root.isLocal
|
||
offlineOnly: root.item.offlineOnly === true
|
||
}
|
||
}
|
||
|
||
Text {
|
||
width: 140
|
||
height: 18
|
||
text: root.item.name || ""
|
||
color: theme.textPrimary
|
||
font.pixelSize: 13
|
||
font.weight: theme.weightMedium
|
||
verticalAlignment: Text.AlignVCenter
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
// Footer: meta on the left, the action strip on the right.
|
||
Item {
|
||
width: 140
|
||
height: 22
|
||
|
||
Text {
|
||
anchors.left: parent.left
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: Math.max(0, parent.width - actions.width)
|
||
// Pluralised in RUST (`qbz_i18n::tf`) — QML never pluralises.
|
||
text: root.item.tracksLine || ""
|
||
color: theme.textMuted
|
||
font.pixelSize: 11
|
||
elide: Text.ElideRight
|
||
}
|
||
Row {
|
||
id: actions
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
spacing: 0
|
||
|
||
PmActionButton {
|
||
name: root.item.isFavorite === true ? "heart-filled" : "heart"
|
||
filled: root.item.isFavorite === true
|
||
btnSize: 24
|
||
glyph: 14
|
||
onClicked: QbzPlaylistManager.toggleFavorite(root.itemId)
|
||
}
|
||
PmActionButton {
|
||
name: root.item.isHidden === true ? "eye-off" : "eye"
|
||
btnSize: 24
|
||
glyph: 14
|
||
onClicked: QbzPlaylistManager.toggleHidden(root.itemId)
|
||
}
|
||
PmActionButton {
|
||
name: "pen-line"
|
||
btnSize: 24
|
||
glyph: 14
|
||
// The SHARED editor singleton (contract D2/D20) — the id
|
||
// lives in Rust from here on, so a republish can never make
|
||
// the modal save under a different playlist.
|
||
onClicked: QbzPlaylistEdit.open(root.itemId)
|
||
}
|
||
PmActionButton {
|
||
visible: !root.isLocal
|
||
name: "cassette-tape"
|
||
btnSize: 24
|
||
glyph: 14
|
||
// The payload is assembled AT THE CALL SITE (D3) — the same
|
||
// seam every other surface uses. `source` is always "qobuz"
|
||
// because this button is absent on a local row (D28).
|
||
onClicked: QbzMyQbzAdd.open(JSON.stringify([{
|
||
"itemType": "playlist", "source": "qobuz",
|
||
"sourceItemId": root.itemId,
|
||
"title": root.item.name || "",
|
||
"subtitle": "", "artworkUrl": (root.item.covers || [])[0] || "",
|
||
"year": null, "trackCount": root.item.totalCount || 0
|
||
}]))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Body hit area: artwork + name only (pad 10 + collage 140 + gap 8 + name
|
||
// 18 = 176), below the reorder header and above the footer, so both keep
|
||
// their own clicks (`:605-617`).
|
||
MouseArea {
|
||
id: bodyArea
|
||
y: root.canReorder ? 30 : 0
|
||
width: parent.width
|
||
height: 176
|
||
hoverEnabled: true
|
||
cursorShape: Qt.PointingHandCursor
|
||
// Routes `local:` ids to the local loader and works OFFLINE
|
||
// (main.rs:926-955). There is no manager-local twin (D3).
|
||
onClicked: QbzBridge.openPlaylist(root.itemId)
|
||
}
|
||
}
|