Sleep timer (queue footer): tokio monotonic deadline + 1Hz countdown that fires core().pause() at expiry; presets + custom minutes via a popover. Stop after this song: 4 QbzCore passthroughs + playback consume-before-advance seam and the load-bearing gapless-prefetch gate, plus the queue marker/menu so the Queue persists when playback halts. qbz-log crate: TeeLogger wrapping env_logger (stderr + 5000-line ring + ~/.local/share/qbz/logs/qbz.log) with write-time secret redaction (regex + live-secret registry) and a diagnostics bundle builder. Dev panel logs: in-app viewer that shows content the moment it opens, advanced controls behind a discreet [+], "Copy all" as the sole footer button, a copy button on the upload URL, and a redacted public paste upload. Diagnostics: qbz-app::diagnostics (extracted, src-tauri untouched) feeding a 7-section panel with the live active output device + sample rate/format (pactl) and a complete redacted report for the upload. Report an Issue: wires the header menu item to a 1:1 Tauri modal that opens the GitHub bug-report template. Appearance persistence: 6 toggles that were live-only now persist via ui_prefs + handler arm + startup seed (window-title-show, show-volume-steppers, sidebar-playlist-collage, local-library-track-artwork, in-app-toasts, theme-filter). Recommendations cache (per-tab only): TTL select 24/36/48/72h + Refresh now in ExternalRecoView; no change to the other Discover tabs. Bump workspace version 0.1.0 -> 1.2.15 so app_version is correct in the diagnostics report and the QConnect About string. i18n es/de/fr/pt for all new strings. cc
391 lines
14 KiB
Plaintext
391 lines
14 KiB
Plaintext
// Settings > Developer — inline Diagnostics panel (1:1 port of the Tauri
|
|
// DiagnosticsPanel.svelte). A master collapsible holding seven sub-sections
|
|
// (System / Playback / Qobuz Connect / Cast / Audio / Graphics / Environment),
|
|
// each a small saved-vs-runtime table. Five sections are 3-column
|
|
// (Setting | Runtime | glyph); Audio and Graphics are 4-column
|
|
// (Setting | Saved | Runtime | glyph) — gated by the per-section `show-saved`.
|
|
//
|
|
// The data model is the DiagnosticsState global + DiagRow struct (state.slint):
|
|
// seven `[DiagRow]` models (one per section, 1:1 with the Tauri row-builders),
|
|
// plus refresh()/export-clipboard()/cast-scan() callbacks driven from Rust.
|
|
// Section open/closed state is pure UI (private `open` per DiagSection); no
|
|
// Rust round-trip. Per-row labels are plain Rust strings (Tauri hardcodes them
|
|
// too); only section titles + column headers + buttons go through @tr.
|
|
|
|
import { Theme } from "../foundation/semantic-colors.slint";
|
|
import { Typography } from "../foundation/typography.slint";
|
|
import { Radius } from "../foundation/radius.slint";
|
|
import { DiagRow, DiagnosticsState } from "../state.slint";
|
|
import { QbzIcon } from "../primitives/QbzIcon.slint";
|
|
|
|
// One table row. 4-col when `show-saved`, else 3-col (Saved column omitted).
|
|
// The trailing glyph maps `status`: 0 info(·) / 1 match(✓) / 2 mismatch(✗).
|
|
component DiagRowView inherits Rectangle {
|
|
in property <DiagRow> entry;
|
|
in property <bool> show-saved;
|
|
height: 28px;
|
|
HorizontalLayout {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
spacing: 10px;
|
|
Text {
|
|
text: root.entry.label;
|
|
color: Theme.text-secondary;
|
|
font-size: 12px;
|
|
vertical-alignment: center;
|
|
width: 38%;
|
|
overflow: elide;
|
|
}
|
|
if root.show-saved: Text {
|
|
text: root.entry.saved;
|
|
color: Theme.text-primary;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
Text {
|
|
text: root.entry.runtime;
|
|
color: Theme.text-primary;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
Text {
|
|
width: 20px;
|
|
horizontal-alignment: center;
|
|
vertical-alignment: center;
|
|
font-weight: Typography.bold;
|
|
text: root.entry.status == 1 ? "✓" : root.entry.status == 2 ? "✗" : "·";
|
|
color: root.entry.status == 1 ? #4caf50 : root.entry.status == 2 ? #f44336 : Theme.text-muted;
|
|
}
|
|
}
|
|
Rectangle {
|
|
y: parent.height - 1px;
|
|
height: 1px;
|
|
background: Theme.border-subtle;
|
|
}
|
|
}
|
|
|
|
// Collapsible section: clickable header (chevron + title) toggles a private
|
|
// `open` (seeded from `default-open`); when open, a column header + the rows.
|
|
component DiagSection inherits VerticalLayout {
|
|
in property <string> title;
|
|
in property <[DiagRow]> rows;
|
|
in property <bool> show-saved: false;
|
|
in property <bool> default-open: true;
|
|
private property <bool> open: root.default-open;
|
|
spacing: 0px;
|
|
|
|
Rectangle {
|
|
height: 34px;
|
|
TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.open = !root.open;
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 6px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
width: 14px;
|
|
height: 14px;
|
|
tint: Theme.text-secondary;
|
|
source: root.open
|
|
? @image-url("../assets/icons/chevron-down.svg")
|
|
: @image-url("../assets/icons/chevron-right.svg");
|
|
}
|
|
}
|
|
Text {
|
|
text: root.title;
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
font-weight: Typography.semibold;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if root.open: VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle {
|
|
height: 26px;
|
|
HorizontalLayout {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
spacing: 10px;
|
|
Text {
|
|
text: @tr("Setting");
|
|
width: 38%;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
}
|
|
if root.show-saved: Text {
|
|
text: @tr("Saved");
|
|
horizontal-stretch: 1;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Runtime");
|
|
horizontal-stretch: 1;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle {
|
|
width: 20px;
|
|
}
|
|
}
|
|
}
|
|
for r in root.rows: DiagRowView {
|
|
entry: r;
|
|
show-saved: root.show-saved;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Action button — auto-width, 34px high, styled like DeveloperSettings'
|
|
// SecondaryButton. `enabled: false` dims it and swallows the click.
|
|
component DiagButton inherits Rectangle {
|
|
in property <string> label;
|
|
in property <bool> enabled: true;
|
|
callback clicked();
|
|
height: 34px;
|
|
width: btn-label.preferred-width + 28px;
|
|
border-radius: Radius.sm;
|
|
border-width: 1px;
|
|
border-color: Theme.border-subtle;
|
|
background: root.enabled && ta.has-hover ? Theme.surface-hover : Theme.surface-elevated;
|
|
HorizontalLayout {
|
|
alignment: center;
|
|
padding-left: 14px;
|
|
padding-right: 14px;
|
|
btn-label := Text {
|
|
text: root.label;
|
|
color: root.enabled ? Theme.text-secondary : Theme.text-muted;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.medium;
|
|
vertical-alignment: center;
|
|
}
|
|
}
|
|
ta := TouchArea {
|
|
mouse-cursor: root.enabled ? pointer : default;
|
|
clicked => {
|
|
if (root.enabled) {
|
|
root.clicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export component DiagnosticsPanel inherits VerticalLayout {
|
|
spacing: 4px;
|
|
private property <bool> panel-open: false;
|
|
private property <bool> cast-open: true;
|
|
|
|
// Master toggle (chevron + title + muted note). First expand triggers the
|
|
// initial load if it hasn't happened yet.
|
|
Rectangle {
|
|
height: 44px;
|
|
TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.panel-open = !root.panel-open;
|
|
if (root.panel-open && !DiagnosticsState.loaded) {
|
|
DiagnosticsState.refresh();
|
|
}
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
width: 16px;
|
|
height: 16px;
|
|
tint: Theme.text-secondary;
|
|
source: root.panel-open
|
|
? @image-url("../assets/icons/chevron-down.svg")
|
|
: @image-url("../assets/icons/chevron-right.svg");
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Diagnostics");
|
|
color: Theme.text-primary;
|
|
font-size: Typography.body;
|
|
font-weight: Typography.semibold;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Runtime vs saved configuration snapshot.");
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
overflow: elide;
|
|
}
|
|
}
|
|
}
|
|
|
|
if root.panel-open: VerticalLayout {
|
|
spacing: 4px;
|
|
|
|
// Actions: Refresh + Export-to-clipboard.
|
|
HorizontalLayout {
|
|
spacing: 8px;
|
|
alignment: start;
|
|
DiagButton {
|
|
label: @tr("Refresh");
|
|
clicked => {
|
|
DiagnosticsState.refresh();
|
|
}
|
|
}
|
|
DiagButton {
|
|
label: DiagnosticsState.copied ? @tr("Exported") : @tr("Export to clipboard");
|
|
enabled: DiagnosticsState.loaded;
|
|
clicked => {
|
|
DiagnosticsState.export-clipboard();
|
|
}
|
|
}
|
|
}
|
|
|
|
if DiagnosticsState.error != "": Text {
|
|
text: DiagnosticsState.error;
|
|
color: #f44336;
|
|
font-size: 12px;
|
|
wrap: word-wrap;
|
|
}
|
|
|
|
if DiagnosticsState.loaded: VerticalLayout {
|
|
spacing: 4px;
|
|
Text {
|
|
text: "QBZ v" + DiagnosticsState.app-version;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
}
|
|
|
|
DiagSection {
|
|
title: @tr("System");
|
|
rows: DiagnosticsState.system-rows;
|
|
default-open: true;
|
|
}
|
|
DiagSection {
|
|
title: @tr("Playback");
|
|
rows: DiagnosticsState.playback-rows;
|
|
default-open: true;
|
|
}
|
|
DiagSection {
|
|
title: @tr("Qobuz Connect");
|
|
rows: DiagnosticsState.qconnect-rows;
|
|
default-open: true;
|
|
}
|
|
|
|
// Cast — bespoke collapsible: needs its own on-demand Scan button.
|
|
VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle {
|
|
height: 34px;
|
|
TouchArea {
|
|
mouse-cursor: pointer;
|
|
clicked => {
|
|
root.cast-open = !root.cast-open;
|
|
}
|
|
}
|
|
HorizontalLayout {
|
|
spacing: 6px;
|
|
VerticalLayout {
|
|
alignment: center;
|
|
QbzIcon {
|
|
width: 14px;
|
|
height: 14px;
|
|
tint: Theme.text-secondary;
|
|
source: root.cast-open
|
|
? @image-url("../assets/icons/chevron-down.svg")
|
|
: @image-url("../assets/icons/chevron-right.svg");
|
|
}
|
|
}
|
|
Text {
|
|
text: @tr("Cast Discovery");
|
|
color: Theme.text-primary;
|
|
font-size: 13px;
|
|
font-weight: Typography.semibold;
|
|
vertical-alignment: center;
|
|
horizontal-stretch: 1;
|
|
}
|
|
}
|
|
}
|
|
if root.cast-open: VerticalLayout {
|
|
spacing: 4px;
|
|
HorizontalLayout {
|
|
alignment: start;
|
|
DiagButton {
|
|
label: DiagnosticsState.cast-scanning ? @tr("Scanning…") : @tr("Scan for devices");
|
|
enabled: !DiagnosticsState.cast-scanning;
|
|
clicked => {
|
|
DiagnosticsState.cast-scan();
|
|
}
|
|
}
|
|
}
|
|
if DiagnosticsState.cast-rows.length > 0: VerticalLayout {
|
|
spacing: 0px;
|
|
Rectangle {
|
|
height: 26px;
|
|
HorizontalLayout {
|
|
padding-left: 10px;
|
|
padding-right: 10px;
|
|
spacing: 10px;
|
|
Text {
|
|
text: @tr("Setting");
|
|
width: 38%;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
}
|
|
Text {
|
|
text: @tr("Runtime");
|
|
horizontal-stretch: 1;
|
|
color: Theme.text-muted;
|
|
font-size: 11px;
|
|
vertical-alignment: center;
|
|
}
|
|
Rectangle {
|
|
width: 20px;
|
|
}
|
|
}
|
|
}
|
|
for r in DiagnosticsState.cast-rows: DiagRowView {
|
|
entry: r;
|
|
show-saved: false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
DiagSection {
|
|
title: @tr("Audio");
|
|
rows: DiagnosticsState.audio-rows;
|
|
show-saved: true;
|
|
default-open: true;
|
|
}
|
|
DiagSection {
|
|
title: @tr("Graphics");
|
|
rows: DiagnosticsState.graphics-rows;
|
|
show-saved: true;
|
|
default-open: true;
|
|
}
|
|
DiagSection {
|
|
title: @tr("Environment");
|
|
rows: DiagnosticsState.env-rows;
|
|
default-open: false;
|
|
}
|
|
}
|
|
}
|
|
}
|