Five surfaces the Qt frontend was missing, landed together because they share
five wiring files (build.rs, main.rs, shell_bridge.rs, AppShell.qml,
HeaderBar.qml) and splitting them would produce commits that do not build.
ARTIST PAGE
- The Playlists section used a hand-rolled Rectangle delegate: a MouseArea with
no onClicked, and a RoundedImage with no fit:, so the default crop blew an
800x380 playlist banner up inside a 200x200 box and showed the middle 47% at
2.1x. Replaced by the real cards/PlaylistCard.qml — already live on five other
surfaces — mounted in a new views/PlaylistRail.qml modelled on SectionRail.
ArtistPlaylist publishes the five roles the card reads.
- The portrait gains what the album cover has: the right-click menu the Slint
reference already has (Add/Change/Remove image, Open in browser, Save as…) and
the lightbox this port invented for albums and playlists. Custom images are
keyed by artist NAME into the same custom_artwork.json the Slint build writes,
so they round-trip between the two.
- CoverLightbox is now aspect-aware. It forced a square box on the premise that
covers are square; artist portraits are not (1080x720 measured in the cache).
- Removing an override invalidates the pipeline's resolved path for that url:
artwork_qt::cached_path consults the override FIRST, so coverMap held the
custom file and the header kept showing the picture the user had just deleted.
- artwork_qt::local_path is the inverse of file_url. trim_start_matches("file://")
is not: it leaves the percent-escapes in place, so any path holding % # or ?
came back as a name that does not exist on disk.
- One new msgid, "View image", in the seven non-en locales — matching its sibling
"View cover", since en carries none of that family and falls back to the msgid.
CUSTOM THEME
- The editor, the colour picker and the swatch grid, ported 1:1 from
AppearanceSettings.slint and ColorPicker.slint. The 11 editable base tokens,
their expansion into the full 55-value palette and the on-disk format all stay
in the shared qbz-theme crate, so a theme authored in the shipping Slint build
already rendered here and now round-trips both ways.
- Hue is normalised 0..1 at every Qt.hsva boundary; the reference stores 0..360,
and porting the number instead of the domain would render every swatch red.
- The first-selection seed is captured BEFORE set_theme persists the slug,
because current_slug() re-reads it from disk — a seed taken afterwards would
resolve "custom", find no file and snapshot the default it was meant to replace.
- The in-memory base is dropped when the theme row enters "custom": it is
memoized for the life of the process and filled at bridge construction, so a
process that booted without the file would keep serving the OLED default even
after the shipping Slint build wrote the user's theme.
- The disk write is debounced to drag-end and atomic; the reference persists on
every drag frame with a truncating write, on a file the other build has open.
ABOUT + WHAT'S NEW
- Both modals, with their producers. render_markdown is transcribed line for line
from the reference: h1 and h2 collapse to one level, an indent-0 bullet becomes
a section plus a TOC entry, bold and backticks are stripped rather than styled.
Text.MarkdownText is a different subset and would have rendered a different
document from the same source. Pinned by tests, including the real 2.0.2 body.
- The codename is a single const, so the release checklist has one Qt-side place
to bump.
DIAGNOSTICS
- The panel in Settings > Developer, the Report an Issue modal and its menu row.
- The log bundle carried eight hand-built header fields where the reference
carries about seventy, so every issue filed from a Qt build arrived with no
kernel, no distro, no install method, no GPU, no exclusive-mode / DAC /
sample-rate state and no playback context. The report is injected INTO
qbz_log::bundle rather than replacing it, keeping the <details> wrapper that
makes a pasted bundle readable.
- The GTK / WebKit / GSK / GDK rows are CUT. In a Qt process those libraries are
not mapped, so their runtime column can only read "—", and their saved column
would come from a graphics.json this binary never reads or writes — foreign
state presented as this app's configuration.
SHELL
- The four always-on-top overlays (toast, tooltip, art preview, immersive) relied
on declaration order and carried no z, so any z-bearing modal buried them: a
toast raised by a background event behind an opaque panel is never seen. They
now sit in a band above the modals, keeping their relative order.
- build.rs asked cargo to watch ../../.git/HEAD, which does not exist in a git
worktree (.git is a file). Cargo treats a missing rerun-if-changed target as
always-dirty, so the whole build script — per-shader qsb, the recursive asset
sweep, cxx-qt/moc codegen — re-ran on every cargo invocation.
Verified: cargo test -p qbz-qt 343 passed, cargo test -p qbz-library 83 passed,
qml_singleton_xref and qml_resolution_audit clean over 35 bridges and 265 QML
files, and qmlcachegen compiles every one of them. Smoke-tested by hand.
Contracts, recon and adversarial reviews:
qbz-nix-docs/qt-frontend/2026-08-14-six-item-round/
cc
110 lines
4.2 KiB
QML
110 lines
4.2 KiB
QML
// Artwork lightbox — NEW in the Qt port (no Slint counterpart): the artwork
|
|
// at its best available quality, fitted to the window. Opened by left-click on
|
|
// the AlbumView cover, the PlaylistView cover or the ArtistView portrait, and
|
|
// by those menus' "View cover" / "View image" entries.
|
|
//
|
|
// Shape follows TrackInfoModal: a full-window Popup parented to
|
|
// Overlay.overlay, a dim scrim whose click closes, Escape closes. The image
|
|
// asks for a decode capped at the window size (sourceSize), so a mega
|
|
// variant does not cost a full-res decode to display at ~90% of the window.
|
|
//
|
|
// ASPECT-AWARE, and it has to be. This used to force a SQUARE box of
|
|
// `min(w,h) * 0.9` on the grounds that "covers are square". Album covers are;
|
|
// artist portraits frequently are NOT (1080x720 among the portraits measured
|
|
// in this machine's cache), and a landscape image squeezed into the min-axis
|
|
// square gave up roughly a third of the height available on a wide window.
|
|
// The box now takes the image's own ratio inside a 90% viewport, which lands
|
|
// on exactly the old square for a square source — nothing regresses.
|
|
|
|
import QtQuick
|
|
import QtQuick.Controls
|
|
import com.blitzfc.qbz
|
|
import "../theme"
|
|
|
|
Popup {
|
|
id: root
|
|
|
|
parent: Overlay.overlay
|
|
x: 0
|
|
y: 0
|
|
width: parent ? parent.width : 0
|
|
height: parent ? parent.height : 0
|
|
padding: 0
|
|
closePolicy: Popup.CloseOnEscape
|
|
|
|
// file:// (custom cover) or the remote best() URL. Set through openWith.
|
|
property string artSource: ""
|
|
|
|
function openWith(source) {
|
|
// Builders hand over remote URLs, file:// urls, and (for custom
|
|
// covers) bare absolute paths — normalize here, once.
|
|
if (source.charAt(0) === "/") source = "file://" + source
|
|
root.artSource = source
|
|
root.open()
|
|
}
|
|
|
|
QbzTheme { id: theme }
|
|
|
|
background: Rectangle {
|
|
color: "#bf000000"
|
|
}
|
|
|
|
contentItem: Item {
|
|
id: stage
|
|
// Scrim click closes; clicks on the image itself are swallowed so
|
|
// the lightbox stays up while the user inspects it.
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.close()
|
|
}
|
|
|
|
// The 90% viewport the artwork is fitted into, so it never touches
|
|
// the rim on either axis.
|
|
readonly property real maxW: root.width * 0.9
|
|
readonly property real maxH: root.height * 0.9
|
|
// The SOURCE ratio. `implicitWidth/Height` report the decoded size,
|
|
// which `sourceSize` has already scaled — but scaled preserving the
|
|
// aspect, so the ratio is the source's. 1.0 until the image reports,
|
|
// i.e. the square box this file used to force unconditionally.
|
|
readonly property real ratio: (art.implicitWidth > 0 && art.implicitHeight > 0)
|
|
? (art.implicitWidth / art.implicitHeight) : 1.0
|
|
readonly property real boxW: Math.min(stage.maxW, stage.maxH * stage.ratio)
|
|
readonly property real boxH: Math.min(stage.maxH, stage.maxW / stage.ratio)
|
|
|
|
Image {
|
|
id: art
|
|
anchors.centerIn: parent
|
|
width: stage.boxW
|
|
height: stage.boxH
|
|
source: root.artSource
|
|
asynchronous: true
|
|
fillMode: Image.PreserveAspectFit
|
|
// Capped off the VIEWPORT, not off boxW/boxH: the box is derived
|
|
// from implicitWidth/Height, which sourceSize itself determines —
|
|
// binding the cap to the box would be a loop. The viewport is the
|
|
// real display bound anyway, so nothing is under-decoded.
|
|
sourceSize: Qt.size(Math.round(stage.maxW), Math.round(stage.maxH))
|
|
smooth: true
|
|
visible: status === Image.Ready
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: {} // swallow — the scrim's close must not fire here
|
|
}
|
|
}
|
|
|
|
QbzSpinner {
|
|
anchors.centerIn: parent
|
|
visible: root.artSource !== "" && art.status === Image.Loading
|
|
}
|
|
|
|
Text {
|
|
anchors.centerIn: parent
|
|
visible: root.artSource !== "" && art.status === Image.Error
|
|
text: QbzSession.tr("Could not load the image", QbzSession.trRev)
|
|
color: theme.textSecondary
|
|
font.pixelSize: theme.fontBody
|
|
}
|
|
}
|
|
}
|