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
192 lines
7.6 KiB
QML
192 lines
7.6 KiB
QML
// TrackListHeader — the column header of the track table, the labelled twin
|
|
// of rows/TrackRow.qml. Both take their geometry from rows/TrackCols.qml, so
|
|
// a label sits over the column it names by construction rather than by two
|
|
// people typing the same numbers.
|
|
//
|
|
// --- THE CONTRACT (two lines, both checkable) ---------------------------
|
|
// 1. `width` MUST be the width the TrackRows below get. Not the page
|
|
// width — the ROW width. A list inset for a scrollbar gutter
|
|
// (PlaylistView's `anchors.rightMargin: 14`) or padded by its page
|
|
// (MixView's `parent.width - 64`) hands its rows a narrower width, and
|
|
// the header has to be narrowed the same way or every label after the
|
|
// stretch column slides right by the difference.
|
|
// 2. The five arms MUST be the arms the delegate sets. They gate BOTH the
|
|
// cell and its gap, so a header that reserves a column the row does not
|
|
// draw (or vice versa) shifts everything after it by width + 14.
|
|
//
|
|
// The owner's report was the second half of rule 2: the trailing unlabelled
|
|
// columns (heart, offline cloud, ⋯) were missing from some headers, so they
|
|
// took no space and every label was pushed right of its column. They are
|
|
// reserved here as empty slots — `favoriteGlyph` / `downloadGlyph` draw the
|
|
// 14px heart / cloud-download that AlbumPageView.slint:853-877 puts in the
|
|
// heads of those two columns, the only labelling Slint gives them.
|
|
//
|
|
// `trailingReserve` covers the one surface whose row draws controls OUTSIDE
|
|
// the shared TrackRow: views/local/LocalTrackRow.qml narrows the shared row
|
|
// and re-draws the ⋯ plus a source glyph in the gutter it freed. The header
|
|
// reserves the same slice so it stays the same width as the row it labels.
|
|
|
|
import QtQuick
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Item {
|
|
id: root
|
|
|
|
// --- Arms: MIRROR the TrackRow arms of the list underneath ----------
|
|
// Defaults are TrackRow's own defaults, so a host that sets nothing gets
|
|
// a header for a row that sets nothing.
|
|
property bool showArtwork: false
|
|
property bool showAlbum: false
|
|
property bool showFavorite: true
|
|
property bool showDownload: false
|
|
property bool showMenu: true
|
|
/// Leading reorder gutter (the row's up/down chevrons). MIRROR the
|
|
/// delegate's `showReorder`, or every label slides by 22 + 14 the moment
|
|
/// the user switches the list to a custom / reorderable order.
|
|
property bool showReorder: false
|
|
|
|
/// Draw the 14px heart / cloud-download in the head of those two
|
|
/// reserved columns (AlbumPageView.slint). Off = an empty reserved slot,
|
|
/// which is what PlaylistView.slint and MixView.slint do.
|
|
property bool favoriteGlyph: false
|
|
property bool downloadGlyph: false
|
|
|
|
/// Label band height. 0 = the label's own line height, which is what the
|
|
/// playlist / mix headers use (Slint gives those HorizontalLayouts no
|
|
/// height, so they take the text's — 15px at Typography.legal, and
|
|
/// hardcoding an 18 here would have added 3px to those pages' rhythm).
|
|
/// The album headers set an explicit 40px band (AlbumPageView.slint:812).
|
|
property int bandHeight: 0
|
|
/// `letter-spacing: 0.5px` on the album headers; the playlist / mix
|
|
/// headers have none.
|
|
property real labelSpacing: 0
|
|
/// Width the ROW draws outside the shared TrackRow (LocalTrackRow only).
|
|
property int trailingReserve: 0
|
|
|
|
TrackCols { id: cols }
|
|
QbzTheme { id: theme }
|
|
|
|
width: parent ? parent.width : 0
|
|
height: bandH
|
|
|
|
/// Resolved band height (see `bandHeight`). The metric below is the same
|
|
/// font the labels use, so "natural" tracks a font-size change.
|
|
readonly property int bandH: root.bandHeight > 0
|
|
? root.bandHeight : Math.ceil(metric.implicitHeight)
|
|
Text {
|
|
id: metric
|
|
visible: false
|
|
text: "Xg"
|
|
font.pixelSize: theme.fontLegal
|
|
}
|
|
|
|
/// The row's own outer width minus whatever it spends outside the shared
|
|
/// TrackRow — i.e. exactly the width the shared row lays its cells in.
|
|
readonly property int innerWidth: Math.max(0, root.width - root.trailingReserve)
|
|
|
|
component ColLabel: Text {
|
|
color: theme.textMuted
|
|
font.pixelSize: theme.fontLegal
|
|
font.letterSpacing: root.labelSpacing
|
|
height: root.bandH
|
|
verticalAlignment: Text.AlignVCenter
|
|
elide: Text.ElideRight
|
|
}
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: cols.padH
|
|
anchors.rightMargin: cols.padH
|
|
spacing: cols.gap
|
|
|
|
// Reorder gutter — unlabelled, reserved (the row draws two chevrons
|
|
// there; there is nothing to name).
|
|
Item {
|
|
visible: root.showReorder
|
|
width: cols.colReorder
|
|
height: root.bandH
|
|
}
|
|
// "#" — centred, because the cell it labels is TrackRow's play cell
|
|
// and its number is centred in the same 32px. (Slint's album headers
|
|
// left-align this one over a centred number; the playlist header
|
|
// centres it. Centred is the one that lines up.)
|
|
ColLabel {
|
|
width: cols.colNumber
|
|
text: "#"
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
// Artwork cell — unlabelled, reserved (PlaylistView.slint:990).
|
|
Item {
|
|
visible: root.showArtwork
|
|
width: cols.colArt
|
|
height: root.bandH
|
|
}
|
|
ColLabel {
|
|
width: cols.titleWidth(root.innerWidth, root.showArtwork, root.showAlbum,
|
|
root.showFavorite, root.showDownload, root.showMenu,
|
|
root.showReorder)
|
|
text: QbzSession.tr("Title", QbzSession.trRev)
|
|
}
|
|
ColLabel {
|
|
visible: root.showAlbum
|
|
width: cols.colAlbum
|
|
text: QbzSession.tr("Album", QbzSession.trRev)
|
|
}
|
|
ColLabel {
|
|
width: cols.colDuration
|
|
text: QbzSession.tr("Duration", QbzSession.trRev)
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
ColLabel {
|
|
width: cols.colQuality
|
|
text: QbzSession.tr("Quality", QbzSession.trRev)
|
|
horizontalAlignment: Text.AlignHCenter
|
|
}
|
|
// Heart column. `height: root.bandH` (not 1) so `centerIn`
|
|
// centres the glyph on the band instead of pinning it to the top —
|
|
// the bug AlbumView.qml carried and documented.
|
|
Item {
|
|
visible: root.showFavorite
|
|
width: cols.colFavorite
|
|
height: root.bandH
|
|
QbzIcon {
|
|
visible: root.favoriteGlyph
|
|
anchors.centerIn: parent
|
|
name: "heart"
|
|
width: 14
|
|
height: 14
|
|
tintName: "muted"
|
|
}
|
|
}
|
|
// Offline / cloud column.
|
|
Item {
|
|
visible: root.showDownload
|
|
width: cols.colDownload
|
|
height: root.bandH
|
|
QbzIcon {
|
|
visible: root.downloadGlyph
|
|
anchors.centerIn: parent
|
|
name: "cloud-download"
|
|
width: 14
|
|
height: 14
|
|
tintName: "muted"
|
|
}
|
|
}
|
|
// ⋯ column.
|
|
Item {
|
|
visible: root.showMenu
|
|
width: cols.colMenu
|
|
height: root.bandH
|
|
}
|
|
// Whatever the row draws outside the shared TrackRow.
|
|
Item {
|
|
visible: root.trailingReserve > 0
|
|
// The gap before it is already spent by the Row, so the slot
|
|
// itself is the reserve minus that gap.
|
|
width: Math.max(0, root.trailingReserve - cols.gap)
|
|
height: root.bandH
|
|
}
|
|
}
|
|
}
|