Files
qbz/crates/qbz-qt/qml/shell/ReportIssueModal.qml
T
vicrodh 8bd02e35d3 feat(qt): custom-theme editor, artist portrait, playlist rail, About/What's New and diagnostics
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
2026-08-15 00:52:25 -06:00

184 lines
6.5 KiB
QML

// Report an issue — the QML port of crates/qbz-ui/ui/shell/ReportIssueModal.slint,
// opened from the header hamburger menu.
//
// Structure, scrim, faked shadow and the self-gated mount follow
// LogViewerModal.qml; `z: 3000` explicitly, ADR-009 as this port spells it.
// Colour literals are `Qt.rgba(...)` floats: Slint writes #RRGGBBAA and Qt
// reads #AARRGGBB, and five modals in this tree shipped an invisible scrim or
// shadow from copying one verbatim (fixed 2026-08-14).
//
// The open state is LOCAL, not a bridge document: nothing in Rust needs to know
// this modal is up, and the two buttons call verbs that already exist
// (QbzShell.logOpen, QbzShell.reportIssueOpen). The header menu sets `open`.
//
// No pills (ADR-008): both buttons are bordered radiusSm rectangles.
import QtQuick
import com.blitzfc.qbz
import "../controls"
import "../theme"
Item {
id: root
QbzTheme { id: theme }
property bool open: false
anchors.fill: parent
z: 3000
visible: root.open
enabled: root.visible
function t(s) { return QbzSession.tr(s, QbzSession.trRev) }
FocusScope {
anchors.fill: parent
focus: root.visible
Keys.onEscapePressed: root.open = false
}
// Dimmed backdrop; a click closes.
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.75)
MouseArea {
anchors.fill: parent
onClicked: root.open = false
}
}
// Faked 32px drop shadow — the LogViewerModal/QbzConfirmModal precedent; a
// real blurred shadow owes its own parity pass.
Rectangle {
anchors.centerIn: panel
width: panel.width + 8
height: panel.height + 8
radius: theme.radiusMd
color: Qt.rgba(0, 0, 0, 0.5)
}
Rectangle {
id: panel
anchors.centerIn: parent
width: Math.min(root.width - 80, 520)
height: Math.min(root.height - 80, 300)
radius: theme.radiusMd
color: theme.surfaceCard
border.width: 1
border.color: theme.borderSubtle
// Swallow clicks so the scrim below does not close the modal.
MouseArea { anchors.fill: parent }
Column {
anchors.fill: parent
anchors.margins: 20
spacing: 14
// ---- Header: title + close X.
Item {
width: parent.width
height: 28
Text {
anchors.verticalCenter: parent.verticalCenter
text: root.t("Report an issue")
color: theme.textPrimary
font.pixelSize: theme.fontHeading
font.weight: theme.weightSemibold
}
Rectangle {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 28
height: 28
radius: theme.radiusSm
color: closeArea.containsMouse ? theme.surfaceHover : "transparent"
QbzIcon {
anchors.centerIn: parent
width: 17
height: 17
name: "x"
tintName: closeArea.containsMouse ? "primary" : "muted"
}
MouseArea {
id: closeArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.open = false
}
}
}
// ---- Intro: the manual, redacted log-sharing explanation.
Text {
width: parent.width
height: parent.height - 28 - 36 - 28
text: root.t("To make bug and issue resolution easier, it's recommended to share the application logs. We have been careful to mask the logs we send so no sensitive data is included. The process is manual, never automatic, so you can verify what is being shared.")
color: theme.textSecondary
font.pixelSize: theme.fontBody
wrapMode: Text.WordWrap
}
// ---- Actions: Go to logs (secondary) / Create issue (primary).
Row {
anchors.right: parent.right
spacing: 10
Rectangle {
width: goLabel.implicitWidth + 28
height: 36
radius: theme.radiusSm
border.width: 1
border.color: theme.borderSubtle
color: goArea.containsMouse ? theme.surfaceHover : theme.surfaceElevated
Text {
id: goLabel
anchors.centerIn: parent
text: root.t("Go to logs")
color: theme.textSecondary
font.pixelSize: theme.fontLink
font.weight: theme.weightMedium
}
MouseArea {
id: goArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.open = false
QbzShell.logOpen()
}
}
}
Rectangle {
width: createLabel.implicitWidth + 28
height: 36
radius: theme.radiusSm
color: createArea.containsMouse ? theme.accentHover : theme.accent
Text {
id: createLabel
anchors.centerIn: parent
text: root.t("Create issue report")
color: theme.accentText
font.pixelSize: theme.fontLink
font.weight: theme.weightMedium
}
MouseArea {
id: createArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.open = false
QbzShell.reportIssueOpen()
}
}
}
}
}
}
}