fix(qt): table headers line up, the mini rail is centred, tooltips have a background
Three of the owner's visual reports, one theme: geometry that was derived in two places and drifted. The track table laid its header out from the LABELLED columns only, so the heart and the overflow menu took no space there and every label sat right of the column it names. Header and rows now come from one shared set of widths, including the responsive arms -- if the header hid columns on a different condition than the row, the drift would come back only on narrow windows. The collapsed rail goes back to Slint's 64px. It had been 50 by owner request, and the measurement showed why that was the actual defect: the rail centred its row at 25 while the header's own sidebar toggle, sitting directly above it, centres at 30. Symmetric padding inside a 50px rail still reads as misaligned because the eye compares the glyphs to the header control, not to the rail; chasing it from inside bought the alignment at the price of a visibly asymmetric hover pill. At 64 both references land 2px apart and the padding is symmetric again. The row stays a literal 34 rather than being derived from the width -- deriving it is what coupled the two tokens and would have silently grown every row to 48. Tooltips: the port had none, while Slint has a shell-level overlay. This adds the overlay rather than a Popup per call site. cc
This commit is contained in:
9 files changed
+877
-167
No files matched your search
@@ -143,6 +143,17 @@ Rectangle {
|
||||
m[key] = path
|
||||
root.coverMap = Object.assign({}, m)
|
||||
}
|
||||
// The SETTLED heart from Rust: the flipped value when the write
|
||||
// landed, the UNCHANGED one when it failed. The header click writes
|
||||
// its optimistic flip into localToggles and nothing used to correct
|
||||
// it, so a 404'd un-favorite stayed visibly un-favorited until the
|
||||
// user navigated away — LibraryView.qml was this signal's only
|
||||
// listener. Key shape is `library_qt::feed_key` (`{kind}:{id}`).
|
||||
function onLibraryFavoriteChanged(key, value) {
|
||||
var id = (header && header.id) ? header.id : ""
|
||||
if (id !== "" && key === "album:" + id)
|
||||
root.setToggleState("album", value)
|
||||
}
|
||||
}
|
||||
Component.onCompleted: { syncAlbumState(); dispatchCovers() }
|
||||
onAlbumChanged: { syncAlbumState(); dispatchCovers() }
|
||||
@@ -822,40 +833,31 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
|
||||
// Column header.
|
||||
Row {
|
||||
// Column header — rows/TrackListHeader.qml, i.e. the SAME
|
||||
// rows/TrackCols.qml geometry the TrackRows below use.
|
||||
//
|
||||
// What was here disagreed with the rows on every number:
|
||||
// spacing 16 vs the row's 14, Duration 80 vs 70, Quality
|
||||
// 80 vs 92, and a title width that counted five gaps
|
||||
// where the layout draws six. Those are Slint's own
|
||||
// header numbers (AlbumPageView.slint:811-880) and they
|
||||
// do NOT match primitives/TrackRow.slint — the reference
|
||||
// has the same defect the owner reported here, so the
|
||||
// port keeps the row's numbers and drops the second
|
||||
// hardcoded copy entirely.
|
||||
//
|
||||
// The heart / cloud glyphs stay (they are the only
|
||||
// labelling those two columns get) — and they still fill
|
||||
// the band so `centerIn` centres them, which is the fix
|
||||
// this block used to document at length.
|
||||
TrackListHeader {
|
||||
visible: !QbzAlbum.albumLoading
|
||||
width: parent.width
|
||||
height: 40
|
||||
leftPadding: 12
|
||||
rightPadding: 12
|
||||
spacing: 16
|
||||
Text { text: "#"; width: 32; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: QbzSession.tr("Title", QbzSession.trRev); width: parent.width - 32 - 80 - 80 - 28 - 28 - 32 - 5 * 16 - 24; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; anchors.verticalCenter: parent.verticalCenter; elide: Text.ElideRight }
|
||||
Text { text: QbzSession.tr("Duration", QbzSession.trRev); width: 80; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; horizontalAlignment: Text.AlignHCenter; anchors.verticalCenter: parent.verticalCenter }
|
||||
Text { text: QbzSession.tr("Quality", QbzSession.trRev); width: 80; color: theme.textMuted; font.pixelSize: 13; font.letterSpacing: 0.5; horizontalAlignment: Text.AlignHCenter; anchors.verticalCenter: parent.verticalCenter }
|
||||
// Heart / offline-cache column heads. AlbumPageView
|
||||
// .slint:854-877 declares these as `Rectangle { width:
|
||||
// 28px; }` with NO height — a box-layout child with no
|
||||
// fixed cross-axis size is STRETCHED to the row, and
|
||||
// the glyph is then centred inside it (`y: (parent
|
||||
// .height - self.height) / 2`). The port gave them
|
||||
// `height: 1`, so `centerIn` centred the glyph on a
|
||||
// 1px item pinned at the top of the 40px band: the two
|
||||
// icons floated above the text labels, which do centre.
|
||||
// Fill the band and centre, exactly as the .slint does.
|
||||
Item {
|
||||
width: 28
|
||||
height: parent.height
|
||||
QbzIcon { name: "heart"; width: 14; height: 14; anchors.centerIn: parent; tintName: "muted" }
|
||||
}
|
||||
Item {
|
||||
width: 28
|
||||
height: parent.height
|
||||
QbzIcon { name: "cloud-download"; width: 14; height: 14; anchors.centerIn: parent; tintName: "muted" }
|
||||
}
|
||||
// Aligns with the per-row more-button slot (.slint:878).
|
||||
Item { width: 32; height: parent.height }
|
||||
bandHeight: 40
|
||||
labelSpacing: 0.5
|
||||
showDownload: true
|
||||
favoriteGlyph: true
|
||||
downloadGlyph: true
|
||||
}
|
||||
|
||||
// Rows (with Disc / work headers).
|
||||
|
||||
@@ -32,6 +32,7 @@ import QtQuick.Window
|
||||
import com.blitzfc.qbz
|
||||
import "../cards"
|
||||
import "../controls"
|
||||
import "../rows"
|
||||
import "../theme"
|
||||
|
||||
Rectangle {
|
||||
@@ -289,6 +290,30 @@ Rectangle {
|
||||
root._coverInbox[key] = path
|
||||
if (!coverFlush.running) coverFlush.start()
|
||||
}
|
||||
// The SETTLED follow/heart state from Rust: the flipped value when the
|
||||
// write landed, the UNCHANGED one when it failed. Both the header
|
||||
// follow and the Popular Tracks / Appears On hearts write their
|
||||
// optimistic flip into localToggles and nothing used to correct it, so
|
||||
// a failed write stayed visibly wrong until the user left the page.
|
||||
// Key shape is `library_qt::feed_key` (`{kind}:{id}`) — which is
|
||||
// already the exact key the track rows use for their override.
|
||||
function onLibraryFavoriteChanged(key, value) {
|
||||
var aid = (artist && artist.id) ? artist.id : ""
|
||||
if (aid !== "" && key === "artist:" + aid)
|
||||
root.setToggleState("artist", value)
|
||||
else if (key.indexOf("track:") === 0)
|
||||
root.setToggleState(key, value)
|
||||
}
|
||||
// Header pin, same seam: the overflow-menu row flips `artistPin`
|
||||
// optimistically, and this settles it from the store (Slint does the
|
||||
// same for the open detail view: `st.set_pinned(pinned)` when its id
|
||||
// matches). The release CARDS need nothing here — each one listens to
|
||||
// this signal itself (cards/AlbumCard.qml).
|
||||
function onPinChanged(key, value) {
|
||||
var aid = (artist && artist.id) ? artist.id : ""
|
||||
if (aid !== "" && key === "artist:" + aid)
|
||||
root.setToggleState("artistPin", value)
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -431,11 +456,23 @@ Rectangle {
|
||||
|
||||
|
||||
// Popular Tracks row (TrackRow with artwork + album column).
|
||||
//
|
||||
// COLUMN GEOMETRY: rows/TrackCols.qml, the same object rows/TrackRow.qml
|
||||
// and rows/TrackListHeader.qml read. This component is a FORK of the
|
||||
// shared row (POC-NOTE, pre-existing) and it had the full column set
|
||||
// re-typed as literals; they were numerically right, but a fork with its
|
||||
// own copy of the numbers is precisely how a table's columns drift. The
|
||||
// artist page draws no column header (neither does
|
||||
// artist/ArtistPageView.slint), so nothing is misaligned today — this
|
||||
// keeps it that way if the widths ever move.
|
||||
component PopularTrackRow: Rectangle {
|
||||
id: popRow
|
||||
property var row: ({})
|
||||
property int rowIndex: 0
|
||||
property bool showAlbum: true
|
||||
|
||||
TrackCols { id: cols }
|
||||
|
||||
readonly property bool isActive: QbzPlayer.npTrackId !== "" && QbzPlayer.npTrackId === row.id
|
||||
readonly property bool hovered: trArea.containsMouse || favArea.containsMouse || moreArea.containsMouse
|
||||
|
||||
@@ -456,14 +493,14 @@ Rectangle {
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 12
|
||||
anchors.rightMargin: 12
|
||||
spacing: 14
|
||||
anchors.leftMargin: cols.padH
|
||||
anchors.rightMargin: cols.padH
|
||||
spacing: cols.gap
|
||||
|
||||
// Position number (artwork rows carry it separate from the cover).
|
||||
Text {
|
||||
visible: showAlbum
|
||||
width: 32
|
||||
width: cols.colNumber
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.number
|
||||
color: theme.textMuted
|
||||
@@ -473,8 +510,8 @@ Rectangle {
|
||||
}
|
||||
// Cover with hover play overlay.
|
||||
Rectangle {
|
||||
width: showAlbum ? 36 : 32
|
||||
height: showAlbum ? 36 : 28
|
||||
width: showAlbum ? cols.colArt : cols.colNumber
|
||||
height: showAlbum ? cols.colArt : 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
radius: theme.radiusSm
|
||||
color: theme.surfaceElevated
|
||||
@@ -515,8 +552,20 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
// Title + artist.
|
||||
//
|
||||
// This was `- 6 * 14` for the gaps in BOTH arms. With the album
|
||||
// column on there are nine visible cells, i.e. EIGHT gaps, so the
|
||||
// stretch column ran 28px long and dragged Album / Duration /
|
||||
// Quality / heart 28px right of where the shared TrackRow puts
|
||||
// them — the same class of defect as the header, inside a forked
|
||||
// row. `cols.titleWidth` counts the gaps from the arms.
|
||||
//
|
||||
// Arms: with the album column the leading cells are the 32px
|
||||
// number AND the 36px cover (artwork arm); without it the single
|
||||
// 32px cover IS the number cell.
|
||||
Column {
|
||||
width: parent.width - (showAlbum ? 32 + 36 : 32) - albumCell.width - 70 - 92 - 28 - 28 - 32 - 6 * 14
|
||||
width: cols.titleWidth(popRow.width, popRow.showAlbum, popRow.showAlbum,
|
||||
true, true, true)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 2
|
||||
Row {
|
||||
@@ -558,7 +607,7 @@ Rectangle {
|
||||
Text {
|
||||
id: albumCell
|
||||
visible: showAlbum
|
||||
width: showAlbum ? 220 : 0
|
||||
width: showAlbum ? cols.colAlbum : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.album
|
||||
color: row.albumId !== "" && albumLinkArea.containsMouse ? theme.textPrimary : theme.textMuted
|
||||
@@ -574,7 +623,7 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
Text {
|
||||
width: 70
|
||||
width: cols.colDuration
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.duration
|
||||
color: theme.textMuted
|
||||
@@ -582,7 +631,7 @@ Rectangle {
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
Text {
|
||||
width: 92
|
||||
width: cols.colQuality
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: row.qualityTier === "hires" ? "HI-RES" : (row.qualityTier === "cd" ? "CD" : "")
|
||||
color: theme.textMuted
|
||||
@@ -594,8 +643,8 @@ Rectangle {
|
||||
// survives a document republish (see root.localToggles).
|
||||
Rectangle {
|
||||
property bool favorite: root.toggleState("track:" + row.id, row.isFavorite)
|
||||
width: 28
|
||||
height: 28
|
||||
width: cols.colFavorite
|
||||
height: cols.colFavorite
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: favArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
@@ -621,16 +670,16 @@ Rectangle {
|
||||
}
|
||||
// Offline download — INERT stub.
|
||||
Rectangle {
|
||||
width: 28
|
||||
height: 28
|
||||
width: cols.colDownload
|
||||
height: cols.colDownload
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
QbzIcon { anchors.centerIn: parent; name: "cloud-download"; width: 16; height: 16; tintName: "muted" }
|
||||
}
|
||||
// ⋯ (play-only menu for the POC).
|
||||
Rectangle {
|
||||
width: 32
|
||||
height: 32
|
||||
width: cols.colMenu
|
||||
height: cols.colMenu
|
||||
radius: theme.radiusSm
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: moreArea.containsMouse ? theme.surfaceElevated : "transparent"
|
||||
@@ -923,7 +972,18 @@ Rectangle {
|
||||
year: modelData.year
|
||||
qualityTier: modelData.qualityTier
|
||||
artSource: root.coverMap[modelData.artUrl] || ""
|
||||
isFavorite: false
|
||||
// artist_qt `map_release` stamps the pin state on every
|
||||
// release row; SectionRail is the only other reader and
|
||||
// this page never mounts it, so the flag was published
|
||||
// and dropped on the floor — the glyph lied on all four
|
||||
// of this page's album grids. `artUrl` is the REMOTE url
|
||||
// (coverMap is keyed BY it), which is what the pin
|
||||
// payload must store.
|
||||
isPinned: modelData.isPinned === true
|
||||
artworkUrl: modelData.artUrl || ""
|
||||
// artist_qt::map_release stamps this the same way it
|
||||
// stamps the pin; false inverted the first click.
|
||||
isFavorite: modelData.isFavorite === true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1359,7 +1419,10 @@ Rectangle {
|
||||
year: artist.lastRelease ? artist.lastRelease.year : ""
|
||||
qualityTier: artist.lastRelease ? artist.lastRelease.qualityTier : ""
|
||||
artSource: artist.lastRelease ? (root.coverMap[artist.lastRelease.artUrl] || "") : ""
|
||||
isFavorite: false
|
||||
// Same row shape as the release grids (map_release).
|
||||
isPinned: artist.lastRelease ? artist.lastRelease.isPinned === true : false
|
||||
artworkUrl: artist.lastRelease ? (artist.lastRelease.artUrl || "") : ""
|
||||
isFavorite: artist.lastRelease ? artist.lastRelease.isFavorite === true : false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1540,7 +1603,9 @@ Rectangle {
|
||||
year: modelData.year
|
||||
qualityTier: modelData.qualityTier
|
||||
artSource: root.coverMap[modelData.artUrl] || ""
|
||||
isFavorite: false
|
||||
isPinned: modelData.isPinned === true
|
||||
artworkUrl: modelData.artUrl || ""
|
||||
isFavorite: modelData.isFavorite === true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1615,6 +1680,12 @@ Rectangle {
|
||||
year: modelData.year
|
||||
qualityTier: modelData.qualityTier
|
||||
artSource: root.coverMap[modelData.imageUrl] || ""
|
||||
// These rows are LIBRARY feed items (FeedItem),
|
||||
// not release cards: the remote url is
|
||||
// `imageUrl`, and the pin state rides the same
|
||||
// row (library_qt `map_album`).
|
||||
isPinned: modelData.isPinned === true
|
||||
artworkUrl: modelData.imageUrl || ""
|
||||
isFavorite: modelData.isFavorite
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,14 +174,24 @@ Rectangle {
|
||||
ribbonKind: crCell.modelData.ribbonKind || ""
|
||||
artSource: crCell.modelData.artPath || ""
|
||||
isPinned: crCell.modelData.isPinned === true
|
||||
isFavorite: false
|
||||
// The pin payload's display snapshot — the REMOTE
|
||||
// url (artPath is the local cache path). Without
|
||||
// it a label release pinned from here landed in
|
||||
// the Home Pinned rail as a grey placeholder.
|
||||
artworkUrl: crCell.modelData.artUrl || ""
|
||||
// Stamped on the row by label_qt; false made the
|
||||
// glyph lie and inverted the first click.
|
||||
isFavorite: crCell.modelData.isFavorite === true
|
||||
}
|
||||
}
|
||||
Component {
|
||||
id: crPlaylist
|
||||
PlaylistCard {
|
||||
// artworkUrl defaults to `item.artUrl` (the card's
|
||||
// own arm), so only the pin state is handed over.
|
||||
item: crCell.modelData
|
||||
artSource: crCell.modelData.artPath || ""
|
||||
isPinned: crCell.modelData.isPinned === true
|
||||
}
|
||||
}
|
||||
Component {
|
||||
@@ -189,9 +199,14 @@ Rectangle {
|
||||
ArtistCard {
|
||||
item: crCell.modelData
|
||||
artSource: crCell.modelData.artPath || ""
|
||||
// The .slint passes card-follow-mode "none" here;
|
||||
// ArtistCard's hasFollowSeam already gates the chip
|
||||
// off, so this is belt and braces.
|
||||
isPinned: crCell.modelData.isPinned === true
|
||||
artworkUrl: crCell.modelData.artUrl || ""
|
||||
// LabelPageView.slint:524 passes card-follow-mode
|
||||
// "none" here — and now that ArtistCard's follow
|
||||
// arm is LIVE (its `hasFollowSeam: false` was a
|
||||
// factual error: library_toggle_favorite routes
|
||||
// "artist"), this line is what actually keeps the
|
||||
// chip off this carousel instead of a constant.
|
||||
followMode: "none"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user