Files
qbz/crates/qbz-qt/Cargo.toml
T
vicrodh bbce651d79 feat(qt): a Rip button on an open CD, and it asks where to put the album
The button lives on the ephemeral pane's header and appears only for a
PHYSICAL disc — an image is already a file, so offering to rip one would be
offering to copy something the user already has. Whether the session is a disc
is DERIVED from its rows rather than passed in: a flag somebody has to
remember to set is a flag that eventually is not.

The destination is always asked. The app knows several plausible music folders,
which is exactly why it must not pick one — a ripper that decides where your
music goes is a ripper you clean up after.

While a rip runs the button gives way to its progress ("3/7 · 45%") and a
spinner: one control, one state, rather than a button that looks pressable
during a job it cannot start twice. The bar is per-DISC and not per-track,
because a listener watching a 79-minute album wants to know how far the ALBUM
is, and a per-track bar that resets seven times reads like a stall.

Driven end to end on the owner's Fear Inoculum: seven tracks written into
"Tool - Fear Inoculum/", every one decoding clean and seekable, all tagged,
no `.part` left behind.

Four new strings in all eight locales.

cc
2026-08-21 02:27:40 -06:00

269 lines
14 KiB
TOML

[package]
name = "qbz-qt"
description = "Qt/QML frontend POC for QBZ (cxx-qt bridge over the UI-agnostic backend)"
version.workspace = true
edition.workspace = true
license.workspace = true
[[bin]]
# The SHIPPED name is `qbz` (owner, 2026-08-04). The package stays
# `qbz-qt`, so `cargo build -p qbz-qt` is unchanged; only the artifact
# moves to target/<profile>/qbz.
#
# ⚠ `crates/qbz` (the Slint binary) declares the SAME bin name and
# shares this workspace's target dir, so both write
# target/<profile>/qbz and the last build wins. That is survivable only
# because the Slint binary is never built in this worktree (the qbz-ui
# 20-30 GB wall). If it ever is, whoever built last owns the file — and
# a smoke against "the wrong qbz" is silent.
name = "qbz"
path = "src/main.rs"
[dependencies]
cxx = "1"
cxx-qt = "0.7"
cxx-qt-lib = { version = "0.7", features = ["qt_full"] }
tokio = { workspace = true }
log = { workspace = true }
# Hard-exit watchdog (main.rs arm_hard_exit_watchdog): `_exit` terminates
# without running the atexit chain — a hang inside Qt's atexit destructors is
# one of the wedges the watchdog exists to cut short. Already in the lock as
# a transitive dep of half the tree; this only makes it direct.
libc = "0.2"
qbz-disc = { path = "../qbz-disc" }
qbz-rip = { path = "../qbz-rip" }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
reqwest = { workspace = true }
urlencoding = "2"
open = "5"
dirs = "5"
# Share links (src/share_qt.rs, the port of crates/qbz/src/share.rs:32-70).
# Rust owns the clipboard on this side because the toast does: cxx-qt-lib 0.7
# ships no QClipboard binding, and QML cannot publish QbzShell.toastJson, so
# splitting the copy and its confirmation across the boundary would buy
# nothing. Same pin as the reference (crates/qbz/Cargo.toml:68), comment
# included:
#
# wayland-data-control: Wayland-native clipboard via the data-control
# protocols (GNOME/KDE/sway/niri/Hyprland), falling back to X11 when Wayland
# is absent (#514 — without it arboard is X11-only and every copy fails on
# Wayland-only sessions).
arboard = { version = "3", features = ["wayland-data-control"] }
qbz-core = { path = "../qbz-core" }
qbz-app = { path = "../qbz-app" }
qbz-audio = { path = "../qbz-audio" }
# The HiFi Wizard's frontend-agnostic core. NOT `qbz-dac-wizard` — that one is
# the Slint adapter and pulls in qbz-ui + slint, the ~1.6 M-line generated
# module this whole track exists to stay out of.
qbz-dac-wizard-core = { path = "../qbz-dac-wizard-core" }
# Vulkan enumeration for the Preferred-GPU row — real adapter NAMES, MODELS and
# types, in the exact order `QT_VK_PHYSICAL_DEVICE_INDEX` counts them (both are
# `vkEnumeratePhysicalDevices`). This is the Qt analogue of what the reference
# does with `wgpu::Instance::enumerate_adapters`.
#
# `ash` is a thin FFI binding that dlopens libvulkan at RUNTIME: no Vulkan SDK
# at build time, and a box without libvulkan simply enumerates nothing. It was
# already in the workspace lock (wgpu pulls it), so it costs no new compile.
ash = "0.38"
qbz-qobuz = { path = "../qbz-qobuz" }
qbz-models = { path = "../qbz-models" }
qbz-cache = { path = "../qbz-cache" }
qbz-offline-cache = { path = "../qbz-offline-cache" }
qbz-library = { path = "../qbz-library" }
# MyQBZ: Mixtapes & Collections backend (schema + migrations, repo, shuffle,
# enqueue). Headless and frontend-agnostic (ADR-006) — the same crate the
# Slint app uses (crates/qbz/Cargo.toml:96), so the two frontends share
# library.db's mixtape tables rather than each growing their own.
qbz-mixtape = { path = "../qbz-mixtape" }
qbz-source = { path = "../qbz-source" }
# The media-server protocol crates. `qbz-qt` needs them directly for the SYNC
# (which lives here, where the tokio runtime and the progress UI are) and for
# `Credentials`, which the settings glue derives.
qbz-jellyfin = { path = "../qbz-jellyfin" }
qbz-subsonic = { path = "../qbz-subsonic" }
qbz-media-cache = { path = "../qbz-media-cache" }
# Cast: Chromecast + DLNA discovery, the two device families and the local
# media server (src/cast_qt.rs drives it — nothing is reimplemented).
qbz-cast = { path = "../qbz-cast" }
qbz-plex = { path = "../qbz-plex" }
qbz-lyrics = { path = "../qbz-lyrics" }
# Playlist Importer: the provider scrapers (Spotify / Apple Music / Tidal /
# Deezer), the Qobuz matcher and the playlist creation. Headless and
# frontend-agnostic (ADR-006) — the same crate the Slint app uses
# (crates/qbz/Cargo.toml), so the two frontends share one importer instead of
# each growing its own. src/playlist_import_qt.rs drives it.
qbz-playlist-import = { path = "../qbz-playlist-import" }
qbz-text-utils = { path = "../qbz-text-utils" }
qbz-credentials = { path = "../qbz-credentials" }
qbz-i18n = { path = "../qbz-i18n" }
qbz-log = { path = "../qbz-log" }
qbz-theme = { path = "../qbz-theme" }
qbz-integrations = { path = "../qbz-integrations" }
# Last.fm "similar albums" row on the album page (external_reco_qt.rs).
qbz-external-reco = { path = "../qbz-external-reco" }
# System media controls — MPRIS on Linux (what makes the KDE Plasma / GNOME
# now-playing widget and the hardware media keys see QBZ), SMTC/MediaRemote
# elsewhere. Driven by src/media_controls_qt.rs.
#
# Plain, NOT target-gated, and with no features — the same line both other
# consumers use (crates/qbz/Cargo.toml:80, crates/qbzd/Cargo.toml:22). The
# crate carries its own cfg arms internally (qbz-media-controls/src/lib.rs:36-53)
# and is already a workspace member (crates/Cargo.toml:32), so this adds no
# lock entry.
#
# Its Linux backend rides mpris-server -> zbus 4 with its own current-thread
# tokio runtime, which is a DIFFERENT major from the zbus 5 that ksni / ashpd /
# rfd use with async-io. That is the only reason the two coexist in one
# process; the rule in the ksni comment below applies here too — never enable a
# `tokio` feature on any D-Bus dependency.
qbz-media-controls = { path = "../qbz-media-controls" }
# Qobuz Connect (2026-08-01 Qt-port contract): the shared session/renderer
# orchestration and the WS transport, consumed VERBATIM — backend untouchable,
# same edges as the Slint crate (crates/qbz/Cargo.toml:87-88). Driven by
# src/qconnect_transport_qt.rs + src/qconnect_engine_qt.rs (+ the B3/B4 facade
# and sink).
qconnect-app = { path = "../qconnect-app" }
qconnect-transport-ws = { path = "../qconnect-transport-ws" }
# Type-level edge only, sanctioned by contract §1.1: the QConnect renderer
# engine trait names `qbz_player::PlaybackState`, and the engine's feeder
# reaches the protected player seams THROUGH `runtime.core().player()` — the
# same edge the Slint crate has (crates/qbz/Cargo.toml:85). No
# qbz-player/qbz-audio internals are touched (TRACK-RULES §7).
qbz-player = { path = "../qbz-player" }
# QConnect: uuid v4 for controller action ids (the B3 facade / B4 sink — same
# as crates/qbz/Cargo.toml:90); futures-util for the engine's HTTP feeder
# (StreamExt over the response byte stream).
uuid = { version = "1", features = ["v4"] }
futures-util = { workspace = true }
async-trait = "0.1"
rusqlite = { version = "0.31", features = ["bundled"] }
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] }
# Native file picker, used by exactly two call sites — the MyQBZ collection
# cover upload (src/myqbz_cover_qt.rs) and the My-QBZ branding icon picker
# (src/myqbz_prefs_qt.rs).
#
# The feature set is NOT negotiable and is copied verbatim from the reference
# (crates/qbz/Cargo.toml:72): "async-std" is rfd's name for the
# async-io/ashpd backend. NEVER enable rfd's "tokio" feature — zbus 5's
# tokio feature unifies graph-wide and breaks the accesskit/atspi/ksni
# threads. `default-features = false` keeps the GTK3 backend out; the
# xdg-portal path is the Wayland-correct one and needs no extra system libs.
rfd = { version = "0.15", default-features = false, features = ["xdg-portal", "async-std"] }
# Linux system tray via StatusNotifierItem (src/tray_linux.rs). Linux only —
# owner ruling K2: `crates/qbz/src/tray/macos.rs` exists because Slint's winit
# backend already bundles `muda`; under Qt that premise is gone, so macOS would
# be a NEW implementation, not a port. Windows has no tray in either tree.
#
# The comment below is copied verbatim from the reference
# (crates/qbz/Cargo.toml:139-143) because the feature set is not negotiable:
#
# default-features = false: ksni's default is "tokio", which flips the shared
# zbus 5 executor to tokio graph-wide (breaks accesskit/atspi plain-thread
# connections — see the rfd comment above). We only use the blocking API on
# worker threads; async-io self-drives the connection from any context.
#
# ksni 0.3.4 is already in the workspace lock (pulled by crates/qbz), so this
# adds no new lock entry. Feature unification is per-crate-graph, not
# per-target: ANY dependency that turns on zbus/tokio poisons the whole binary.
[target.'cfg(target_os = "linux")'.dependencies]
ksni = { version = "0.3", default-features = false, features = ["blocking", "async-io"] }
[build-dependencies]
cxx-qt-build = "0.7"
# A4 (Line Bed QQuickRhiItem): the crate's first hand-written C++
# (cxx/linebed_item.cpp). `qt-build-utils` provides moc + the Qt include
# paths — it is cxx-qt-build's OWN engine, already in the lock at the same
# version, so this adds no new package. `cc` compiles the static lib
# (likewise already in the lock).
qt-build-utils = "0.7"
cc = "1"
# macOS only: the Dock-icon activation policy behind the "Hide Dock icon when
# closed to menu bar" setting (Spotify behaviour — close puts the app in the
# menu bar and drops the Dock icon; minimize still goes to the Dock).
#
# objc2 0.5 / objc2-app-kit 0.2 are pinned to the SAME generation the Slint
# crate uses (crates/qbz/Cargo.toml:166-172). That matters even though this
# crate has no winit: mixing objc2 generations in one binary registers
# duplicate objc classes, which is the failure the reference documents at
# length. Only the two items `set_mac_dock_hidden` names are enabled.
#
# ⚠ NOT COMPILED OR TESTED HERE. This tree builds on Linux; the macOS arm is
# behind cfg, so `cargo build` cannot see it. First real check is a macOS
# build — the Mac mini (192.168.0.69) is where that happens.
[target.'cfg(target_os = "macos")'.dependencies]
# DIRECT objc2, added 2026-08-05 for the menu-bar tray: `declare_class!`,
# `msg_send_id!`, `sel!`, `ClassType`, `DeclaredClass`, `mutability` and
# `rc::Retained` all live at the objc2 root. Pinned to 0.5, the SAME
# generation objc2-app-kit 0.2 targets — mixing generations in one binary
# registers duplicate objc classes, which is the failure the reference
# documents at length (qbz/Cargo.toml:150-156).
objc2 = "0.5"
# `MainThreadMarker` — in THIS generation it is exported by objc2-foundation,
# not by the objc2 root (objc2-foundation-0.2.2/src/lib.rs:152, ungated). The
# reference imports it from the same place. There is no direct `objc2`
# dependency because nothing here names a type from it.
objc2-foundation = { version = "0.2", default-features = false, features = [
"std",
# NSThread gates `MainThreadMarker::new()` itself
# (objc2-foundation-0.2.2/src/thread.rs:210-213) — without it the type
# exists but only the UNSAFE `new_unchecked` does, which is not what this
# wants: the whole point is to ASK whether we are on the AppKit main
# thread and no-op if not. Caught by the Mac build; invisible on Linux.
"NSThread",
] }
objc2-app-kit = { version = "0.2", default-features = false, features = [
"std",
# NSApplication: the class and `setActivationPolicy`.
"NSApplication",
# NSResponder: `sharedApplication` is behind `cfg(feature = "NSResponder")`
# (objc2-app-kit-0.2.2/src/generated/NSApplication.rs:326-329) — it is
# NSApplication's superclass, and the NSApplication feature does NOT pull
# it.
"NSResponder",
# NSRunningApplication: where `NSApplicationActivationPolicy` actually
# lives (…/src/generated/NSRunningApplication.rs:33-35), NOT in
# NSApplication.rs as the name suggests. Verified by reading the crate
# rather than by compiling — this arm cannot be built on Linux, and the
# reference's own proven set (crates/qbz/Cargo.toml) lists the same three.
"NSRunningApplication",
# --- macos_chrome.rs (traffic-light centring) -------------------------
# Every one of these was read out of objc2-app-kit-0.2.2's own sources
# before being added, because this arm cannot be compiled on Linux:
# NSWindow — `NSApplication::mainWindow` is gated on it
# (generated/NSApplication.rs:355), and it carries `frame` +
# `standardWindowButton`. The style-mask / titlebar setters it also
# carries are NO LONGER USED: expanding the client area is Qt's job
# via the 6.9 window flags (see Main.qml), because QCocoaWindow
# recomputes and reassigns the mask behind us.
# NSButton + NSControl + NSView — `standardWindowButton` is gated on
# ALL THREE at once (generated/NSWindow.rs:1181), not just NSButton;
# NSControl is NSButton's superclass and NSView is NSControl's.
# NSView also provides the geometry the centring measures with
# (bounds / frame / setFrame / superview / isFlipped /
# convertRect_toView).
"NSWindow",
"NSButton",
"NSControl",
"NSView",
# --- tray_macos.rs (the menu-bar NSStatusItem) ------------------------
# Exactly the set the reference proves on a Mac (qbz/Cargo.toml:172-188);
# NSStatusBarButton is separate from NSStatusItem, and NSEvent is what
# `handle_status_click` reads to tell a right-click from a left-click.
"NSStatusBar",
"NSStatusItem",
"NSStatusBarButton",
"NSMenu",
"NSMenuItem",
"NSImage",
"NSEvent",
] }