diff --git a/crates/qbz-audio/src/alsa_direct.rs b/crates/qbz-audio/src/alsa_direct.rs index dc46323f0..3baa7bcd6 100644 --- a/crates/qbz-audio/src/alsa_direct.rs +++ b/crates/qbz-audio/src/alsa_direct.rs @@ -144,13 +144,8 @@ impl AlsaDirectStream { // immediately before a real `PCM::new()` and held for as long as that // PCM is open. // TODO(Task 5): replace second arg with user-facing DAC name from settings. - let reservation = - crate::DeviceReservation::acquire(device_id, device_id).map_err(|e| { - format!( - "Cannot acquire exclusive device '{}': {}", - device_id, e - ) - })?; + let reservation = crate::DeviceReservation::acquire(device_id, device_id) + .map_err(|e| format!("Cannot acquire exclusive device '{}': {}", device_id, e))?; // Defensive margin only matters when the reservation actually displaced // a holder (or could have). On the degraded D-Bus path the bus name is @@ -380,9 +375,7 @@ impl AlsaDirectStream { } } let Some((format, le)) = selected else { - return Err( - "Device has no native DSD format (kernel quirk missing?)".to_string() - ); + return Err("Device has no native DSD format (kernel quirk missing?)".to_string()); }; hwp.set_channels(channels as u32) .map_err(|e| format!("Failed to set channels: {}", e))?; @@ -441,7 +434,11 @@ impl AlsaDirectStream { match io.writei(samples) { Ok(written) => { if written != frames { - log::warn!("[ALSA Direct] Partial DoP write: {} / {} frames", written, frames); + log::warn!( + "[ALSA Direct] Partial DoP write: {} / {} frames", + written, + frames + ); } Ok(()) } @@ -852,7 +849,10 @@ impl AlsaDirectStream { // libc::EBADFD — the PCM was not in a running-ish state. const EBADFD: i32 = 77; if e.errno() as i32 != EBADFD { - log::warn!("[ALSA Direct] drop on stop failed (continuing to prepare): {}", e); + log::warn!( + "[ALSA Direct] drop on stop failed (continuing to prepare): {}", + e + ); } } pcm.prepare() diff --git a/crates/qbz-audio/src/backend.rs b/crates/qbz-audio/src/backend.rs index fa894e0cc..ab0ef7121 100644 --- a/crates/qbz-audio/src/backend.rs +++ b/crates/qbz-audio/src/backend.rs @@ -390,7 +390,10 @@ impl BackendManager { // (e.g. a non-default socket name, or Flatpak where the socket path // differs but the pulse shim is bridged). if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") { - if std::path::Path::new(&runtime_dir).join("pipewire-0").exists() { + if std::path::Path::new(&runtime_dir) + .join("pipewire-0") + .exists() + { return true; } } diff --git a/crates/qbz-audio/src/dac_capabilities.rs b/crates/qbz-audio/src/dac_capabilities.rs index 358e40c2e..1491ac04d 100644 --- a/crates/qbz-audio/src/dac_capabilities.rs +++ b/crates/qbz-audio/src/dac_capabilities.rs @@ -34,7 +34,11 @@ const FALLBACK_RATES: [u32; 6] = [44100, 48000, 88200, 96000, 176400, 192000]; /// clean per-node format capability here, so we report the common set honestly /// as nominal (matches the prior Tauri behavior; do not present it as measured). fn nominal_formats() -> Vec { - vec!["S16LE".to_string(), "S24LE".to_string(), "F32LE".to_string()] + vec![ + "S16LE".to_string(), + "S24LE".to_string(), + "F32LE".to_string(), + ] } /// Pure assembly (testable): combine detected rates + description into the DTO, @@ -64,16 +68,15 @@ fn assemble( /// Read-only — reads `/proc/asound` and runs `pw-dump`; never opens a stream. pub fn query_dac_capabilities(node_name: &str) -> DacCapabilities { // Pretty description from robust (pw-dump-backed, Slice 0) enumeration. - let description = crate::backend::BackendManager::create_backend( - crate::backend::AudioBackendType::PipeWire, - ) - .ok() - .and_then(|b| b.enumerate_devices().ok()) - .and_then(|devs| { - devs.into_iter() - .find(|d| d.id == node_name || d.name == node_name) - }) - .map(|d| d.description.unwrap_or(d.name)); + let description = + crate::backend::BackendManager::create_backend(crate::backend::AudioBackendType::PipeWire) + .ok() + .and_then(|b| b.enumerate_devices().ok()) + .and_then(|devs| { + devs.into_iter() + .find(|d| d.id == node_name || d.name == node_name) + }) + .map(|d| d.description.unwrap_or(d.name)); // Real sample rates: PipeWire sink -> ALSA card -> /proc/asound, with an // ALSA-direct fallback. Hardware-only; non-Linux gets the fallback set. @@ -92,7 +95,11 @@ mod tests { #[test] fn uses_detected_rates_when_present() { - let caps = assemble("alsa_output.usb-x", Some(vec![44100, 96000, 192000]), Some("My DAC".into())); + let caps = assemble( + "alsa_output.usb-x", + Some(vec![44100, 96000, 192000]), + Some("My DAC".into()), + ); assert_eq!(caps.sample_rates, vec![44100, 96000, 192000]); assert_eq!(caps.description.as_deref(), Some("My DAC")); assert_eq!(caps.channels, Some(2)); diff --git a/crates/qbz-audio/src/device_filter.rs b/crates/qbz-audio/src/device_filter.rs index b6f6d920b..517a33016 100644 --- a/crates/qbz-audio/src/device_filter.rs +++ b/crates/qbz-audio/src/device_filter.rs @@ -40,7 +40,11 @@ pub fn is_discard_sink(display: &str) -> bool { /// description collapses the wrappers and keeps the genuinely distinct /// outputs. fn dedup_key(display: &str) -> String { - display.split_whitespace().collect::>().join(" ").to_ascii_lowercase() + display + .split_whitespace() + .collect::>() + .join(" ") + .to_ascii_lowercase() } /// Preference within a display group (lower = kept). When several PCM ids @@ -147,8 +151,14 @@ mod tests { )); // null appears FIRST in the raw list but must be emitted last. let out = run(&[ - ("null", "Discard all samples (playback) or generate zero samples (capture)"), - ("default", "Default ALSA Output (currently PipeWire Media Server)"), + ( + "null", + "Discard all samples (playback) or generate zero samples (capture)", + ), + ( + "default", + "Default ALSA Output (currently PipeWire Media Server)", + ), ("front:CARD=PCH,DEV=0", "HDA Intel PCH, ALC3254 Analog"), ]); let ids: Vec<&str> = out.iter().map(|r| r.0.as_str()).collect(); @@ -173,12 +183,21 @@ mod tests { #[test] fn keeps_genuinely_distinct_outputs() { let out = run(&[ - ("default", "Default ALSA Output (currently PipeWire Media Server)"), + ( + "default", + "Default ALSA Output (currently PipeWire Media Server)", + ), ("front:CARD=PCH,DEV=0", "HDA Intel PCH, ALC3254 Analog"), ("iec958:CARD=PCH,DEV=1", "HDA Intel PCH, ALC3254 Digital"), ("hdmi:CARD=PCH,DEV=3", "HDA Intel PCH, HDMI 0"), - ("front:CARD=C20,DEV=0", "Cambridge Audio USB Audio 2.0, USB Audio"), - ("surround40:CARD=C20,DEV=0", "Cambridge Audio USB Audio 2.0, USB Audio"), + ( + "front:CARD=C20,DEV=0", + "Cambridge Audio USB Audio 2.0, USB Audio", + ), + ( + "surround40:CARD=C20,DEV=0", + "Cambridge Audio USB Audio 2.0, USB Audio", + ), ]); let ids: Vec<&str> = out.iter().map(|r| r.0.as_str()).collect(); assert_eq!( @@ -197,7 +216,10 @@ mod tests { fn passes_pipewire_node_names_through() { let out = run(&[ ("alsa_output.usb-Cambridge", "alsa_output.usb-Cambridge"), - ("alsa_output.pci-0000_00_1f.3", "alsa_output.pci-0000_00_1f.3"), + ( + "alsa_output.pci-0000_00_1f.3", + "alsa_output.pci-0000_00_1f.3", + ), ]); assert_eq!(out.len(), 2); } @@ -205,11 +227,17 @@ mod tests { #[test] fn first_seen_order_is_preserved() { let out = run(&[ - ("hw:CARD=C20,DEV=0", "Cambridge Audio USB Audio 2.0, USB Audio"), + ( + "hw:CARD=C20,DEV=0", + "Cambridge Audio USB Audio 2.0, USB Audio", + ), ("default", "Default ALSA Output"), // Better-ranked id for Cambridge appears later; it wins the group // but the group keeps its first-seen position (before Default). - ("front:CARD=C20,DEV=0", "Cambridge Audio USB Audio 2.0, USB Audio"), + ( + "front:CARD=C20,DEV=0", + "Cambridge Audio USB Audio 2.0, USB Audio", + ), ]); let ids: Vec<&str> = out.iter().map(|r| r.0.as_str()).collect(); assert_eq!(ids, vec!["front:CARD=C20,DEV=0", "default"]); diff --git a/crates/qbz-audio/src/device_reservation/linux.rs b/crates/qbz-audio/src/device_reservation/linux.rs index 58f2a1b57..c4fa5fa29 100644 --- a/crates/qbz-audio/src/device_reservation/linux.rs +++ b/crates/qbz-audio/src/device_reservation/linux.rs @@ -311,9 +311,8 @@ fn try_acquire_name( bus_name: &str, replace: bool, ) -> Result { - let proxy = DBusProxy::new(conn).map_err(|e| { - ReservationError::DbusError(format!("DBusProxy::new failed: {}", e)) - })?; + let proxy = DBusProxy::new(conn) + .map_err(|e| ReservationError::DbusError(format!("DBusProxy::new failed: {}", e)))?; let well_known: WellKnownName<'_> = bus_name.try_into().map_err(|e| { ReservationError::DbusError(format!("invalid bus name '{}': {}", bus_name, e)) })?; @@ -352,9 +351,8 @@ fn resolve_contention( // it twice for back-to-back property reads has been observed to cost an // extra GetAll round trip on some bus daemons; one proxy is both faster // and clearer. - let holder_proxy = open_holder_proxy(conn, bus_name, object_path).map_err(|e| { - ReservationError::DbusError(format!("Proxy::new for holder failed: {}", e)) - })?; + let holder_proxy = open_holder_proxy(conn, bus_name, object_path) + .map_err(|e| ReservationError::DbusError(format!("Proxy::new for holder failed: {}", e)))?; // Default to 0 if the holder is uncooperative or doesn't expose Priority. // Rationale: PulseAudio/PipeWire are the most common holders and run at @@ -681,10 +679,7 @@ mod tests { ] { match parse_card_index(s) { Ok(_) | Err(ReservationError::InvalidDevice(_)) => {} - Err(other) => panic!( - "unexpected error variant for {:?}: {:?}", - s, other - ), + Err(other) => panic!("unexpected error variant for {:?}: {:?}", s, other), } } } diff --git a/crates/qbz-audio/src/health.rs b/crates/qbz-audio/src/health.rs index a67f34f1a..e1de1b85f 100644 --- a/crates/qbz-audio/src/health.rs +++ b/crates/qbz-audio/src/health.rs @@ -258,9 +258,15 @@ fn parse_distro(os_release: &str) -> Distro { // os-release values may be bare, double-quoted, or single-quoted // (Gentoo uses single quotes), so strip both quote styles. if let Some(v) = line.strip_prefix("ID=") { - id = v.trim().trim_matches(|c| c == '"' || c == '\'').to_lowercase(); + id = v + .trim() + .trim_matches(|c| c == '"' || c == '\'') + .to_lowercase(); } else if let Some(v) = line.strip_prefix("ID_LIKE=") { - id_like = v.trim().trim_matches(|c| c == '"' || c == '\'').to_lowercase(); + id_like = v + .trim() + .trim_matches(|c| c == '"' || c == '\'') + .to_lowercase(); } } let hay = format!("{} {}", id, id_like); @@ -315,7 +321,10 @@ mod tests { fn classifies_fedora_arch_suse_gentoo_void_other() { assert_eq!(parse_distro("ID=fedora\n"), Distro::Fedora); assert_eq!(parse_distro("ID=arch\n"), Distro::Arch); - assert_eq!(parse_distro("ID=opensuse-tumbleweed\nID_LIKE=\"suse opensuse\"\n"), Distro::OpenSuse); + assert_eq!( + parse_distro("ID=opensuse-tumbleweed\nID_LIKE=\"suse opensuse\"\n"), + Distro::OpenSuse + ); assert_eq!(parse_distro("ID=gentoo\n"), Distro::Gentoo); // Gentoo's real os-release single-quotes the value. assert_eq!(parse_distro("ID='gentoo'\n"), Distro::Gentoo); diff --git a/crates/qbz-audio/src/jack_backend.rs b/crates/qbz-audio/src/jack_backend.rs index 150b8bb22..941e9b0e4 100644 --- a/crates/qbz-audio/src/jack_backend.rs +++ b/crates/qbz-audio/src/jack_backend.rs @@ -80,8 +80,10 @@ impl JackStream { /// Open the JACK client, register stable stereo ports, activate, and /// auto-connect to the system's physical playback. pub fn new(channels: u16) -> Result { - let (client, _status) = Client::new("qbz", ClientOptions::NO_START_SERVER) - .map_err(|e| format!("JACK client open failed (is a JACK/pipewire-jack server running?): {e}"))?; + let (client, _status) = + Client::new("qbz", ClientOptions::NO_START_SERVER).map_err(|e| { + format!("JACK client open failed (is a JACK/pipewire-jack server running?): {e}") + })?; let sample_rate = client.sample_rate() as u32; @@ -123,7 +125,8 @@ impl JackStream { } log::info!( "[JACK] auto-connected qbz:out_FL/FR -> {} / {}", - playback[0], playback[1] + playback[0], + playback[1] ); } else { log::warn!( diff --git a/crates/qbz-audio/src/lib.rs b/crates/qbz-audio/src/lib.rs index 834d85d9d..abaa61a17 100644 --- a/crates/qbz-audio/src/lib.rs +++ b/crates/qbz-audio/src/lib.rs @@ -26,35 +26,35 @@ //! └───────────────┘ //! ``` -pub mod backend; -#[cfg(target_os = "linux")] -pub mod pipewire_backend; #[cfg(target_os = "linux")] pub mod alsa_backend; +pub mod alsa_direct; #[cfg(target_os = "linux")] pub mod alsa_error_handler; -#[cfg(target_os = "linux")] -pub mod pulse_backend; -#[cfg(target_os = "linux")] -pub mod jack_backend; -pub mod alsa_direct; +pub mod analysis; +pub mod analyzer_tap; +pub mod backend; pub mod coreaudio_direct; pub mod dac_capabilities; pub mod dac_probe; -pub mod health; -pub mod analysis; -pub mod analyzer_tap; pub mod device_filter; pub mod device_reservation; pub mod diagnostic; pub mod dynamic_amplify; +pub mod health; +#[cfg(target_os = "linux")] +pub mod jack_backend; pub mod loudness; pub mod loudness_analyzer; pub mod loudness_cache; pub mod network_throttle; pub mod output_sinks; -pub mod settings; +#[cfg(target_os = "linux")] +pub mod pipewire_backend; +#[cfg(target_os = "linux")] +pub mod pulse_backend; pub mod seek_waveform; +pub mod settings; pub mod visualizer; // Re-export commonly used types @@ -64,8 +64,6 @@ pub use alsa_backend::{ resolve_stable_to_current_hw, }; pub use alsa_direct::AlsaDirectStream; -#[cfg(target_os = "linux")] -pub use jack_backend::JackStream; pub use analysis::SpectralAnalyzer; pub use analyzer_tap::{AnalyzerMessage, AnalyzerTap, AnalyzerWaveformTrack}; pub use backend::{ @@ -75,36 +73,46 @@ pub use backend::{ pub use coreaudio_direct::CoreAudioExclusiveGuard; pub use dac_capabilities::{query_dac_capabilities, DacCapabilities}; pub use dac_probe::{negotiated_active_rate, negotiated_stream_rate, NegotiatedRate}; +pub use device_reservation::{DeviceReservation, ReservationError}; +pub use diagnostic::{AudioDiagnostic, BitDepthResult, DiagnosticSource}; +pub use dynamic_amplify::DynamicAmplify; pub use health::{ audio_stack_health, detect_distro, detect_init, detect_sandbox, AudioStackHealth, Distro, InitSystem, Sandbox, }; -pub use device_reservation::{DeviceReservation, ReservationError}; -pub use diagnostic::{AudioDiagnostic, BitDepthResult, DiagnosticSource}; -pub use dynamic_amplify::DynamicAmplify; +#[cfg(target_os = "linux")] +pub use jack_backend::JackStream; pub use loudness::{calculate_gain_factor, db_to_linear, extract_replaygain, ReplayGainData}; pub use loudness_analyzer::LoudnessAnalyzer; pub use loudness_cache::LoudnessCache; pub use output_sinks::{list_output_sinks, OutputSinkInfo}; -pub use settings::AudioSettings; pub use seek_waveform::{ register_seek_waveform_key, seek_waveform_content_key, seek_waveform_snapshot, set_seek_waveform_enabled, SeekWaveformSnapshot, SEEK_WAVEFORM_BINS, }; +pub use settings::AudioSettings; pub use visualizer::{RingBuffer, TappedSource, VisualizerTap}; /// Stub: returns the ID unchanged on non-Linux (no ALSA normalization needed). #[cfg(not(target_os = "linux"))] -pub fn normalize_device_id_to_stable(id: &str) -> String { id.to_string() } +pub fn normalize_device_id_to_stable(id: &str) -> String { + id.to_string() +} /// Stub: no ALSA device resolution on non-Linux. #[cfg(not(target_os = "linux"))] -pub fn resolve_stable_to_current_hw(_stable: &str) -> Option { None } +pub fn resolve_stable_to_current_hw(_stable: &str) -> Option { + None +} /// Stub: no ALSA sample rate probing on non-Linux. #[cfg(not(target_os = "linux"))] -pub fn device_supports_sample_rate(_device_id: &str, _sample_rate: u32) -> Option { None } +pub fn device_supports_sample_rate(_device_id: &str, _sample_rate: u32) -> Option { + None +} /// Stub: no ALSA rate enumeration on non-Linux. #[cfg(not(target_os = "linux"))] -pub fn get_device_supported_rates(_device_id: &str) -> Option> { None } +pub fn get_device_supported_rates(_device_id: &str) -> Option> { + None +} diff --git a/crates/qbz-audio/src/output_sinks.rs b/crates/qbz-audio/src/output_sinks.rs index fcf7cac43..7229d10c0 100644 --- a/crates/qbz-audio/src/output_sinks.rs +++ b/crates/qbz-audio/src/output_sinks.rs @@ -64,10 +64,7 @@ pub fn list_output_sinks() -> Result, String> { .default_output_device() .and_then(|d| cpal_device_name(&d)); - log::debug!( - "[qbz-audio] CPAL default device: {:?}", - default_device_name - ); + log::debug!("[qbz-audio] CPAL default device: {:?}", default_device_name); let sinks: Vec = host .output_devices() diff --git a/crates/qbz-audio/src/pipewire_backend.rs b/crates/qbz-audio/src/pipewire_backend.rs index 6e1f84d9e..005b86e3f 100644 --- a/crates/qbz-audio/src/pipewire_backend.rs +++ b/crates/qbz-audio/src/pipewire_backend.rs @@ -379,7 +379,12 @@ impl PipeWireBackend { for (idx, dev) in devices.iter().enumerate() { log::info!( " [{}] {} (id: {}, bus: {:?}, hw: {}, default: {})", - idx, dev.name, dev.id, dev.device_bus, dev.is_hardware, dev.is_default + idx, + dev.name, + dev.id, + dev.device_bus, + dev.is_hardware, + dev.is_default ); } Some(devices) @@ -950,10 +955,7 @@ impl AudioBackend for PipeWireBackend { } } } else { - log::info!( - "[PipeWire Backend] Rate verified: {}Hz", - actual_rate - ); + log::info!("[PipeWire Backend] Rate verified: {}Hz", actual_rate); } } } @@ -972,7 +974,10 @@ impl AudioBackend for PipeWireBackend { // Subprocess probes are time-bounded so a wedged daemon can never // stall stream init on the audio thread. if let Some(runtime_dir) = std::env::var_os("XDG_RUNTIME_DIR") { - if std::path::Path::new(&runtime_dir).join("pipewire-0").exists() { + if std::path::Path::new(&runtime_dir) + .join("pipewire-0") + .exists() + { return true; } } @@ -1046,7 +1051,11 @@ mod pw_dump_tests { fn parses_sinks_only_with_node_names_bus_and_default() { let devs = parse_pw_dump_sinks(FIXTURE); // The Audio/Source must be excluded. - assert_eq!(devs.len(), 2, "should parse exactly the two Audio/Sink nodes"); + assert_eq!( + devs.len(), + 2, + "should parse exactly the two Audio/Sink nodes" + ); let usb = devs .iter() @@ -1056,7 +1065,10 @@ mod pw_dump_tests { assert_eq!(usb.device_bus.as_deref(), Some("usb")); // read from node props assert!(usb.is_hardware); assert!(usb.is_default, "usb sink is the default per Metadata"); - assert!(usb.max_sample_rate.is_none(), "rate comes from the capability probe, not pw-dump"); + assert!( + usb.max_sample_rate.is_none(), + "rate comes from the capability probe, not pw-dump" + ); let pci = devs .iter() diff --git a/crates/qbz-audio/src/settings.rs b/crates/qbz-audio/src/settings.rs index d2193fdc6..099465791 100644 --- a/crates/qbz-audio/src/settings.rs +++ b/crates/qbz-audio/src/settings.rs @@ -106,14 +106,14 @@ impl Default for AudioSettings { // fixes, and "System" is the app-like default audiophiles override. backend_type: Some(AudioBackendType::default()), alsa_plugin: Some(AlsaPlugin::Hw), // Default to hw (bit-perfect) - alsa_hardware_volume: false, // Disabled by default (maximum compatibility) - stream_first_track: true, // On by default (opt-out) - stream_buffer_seconds: 2, // 2 seconds initial buffer + alsa_hardware_volume: false, // Disabled by default (maximum compatibility) + stream_first_track: true, // On by default (opt-out) + stream_buffer_seconds: 2, // 2 seconds initial buffer streaming_only: false, // Disabled by default (cache tracks for instant replay) limit_quality_to_device: false, // Opt-in. Off since 1.1.9 (#45); wired to the read-only probe in #638 fix 3 - device_max_sample_rate: None, // Set when device is selected + device_max_sample_rate: None, // Set when device is selected device_sample_rate_limits: HashMap::new(), // Per-device limits (empty = no limit) - normalization_enabled: false, // Off by default — preserves bit-perfect pipeline + normalization_enabled: false, // Off by default — preserves bit-perfect pipeline normalization_target_lufs: -14.0, // Spotify/YouTube standard gapless_enabled: true, // On by default — works for same-format tracks on all backends pw_force_bitperfect: false, // Off by default — experimental PipeWire feature @@ -122,7 +122,7 @@ impl Default for AudioSettings { skip_sink_switch: false, // Off by default — only for JACK/DAW routing setups allow_quality_fallback: false, // Off by default — fail rather than silently downgrade reserve_dac_while_running: false, // Off by default — opt-in DAC reservation (Lifetime B) - dsd_mode: default_dsd_mode(), // "convert" — safe on every DAC + dsd_mode: default_dsd_mode(), // "convert" — safe on every DAC } } } @@ -909,11 +909,12 @@ mod tests { fn stream_buffer_seconds_clamps_to_valid_range() { let (dir, store) = fresh_store("stream-buffer-clamp"); - store - .set_stream_buffer_seconds(0) - .expect("set low buffer"); + store.set_stream_buffer_seconds(0).expect("set low buffer"); assert_eq!( - store.get_settings().expect("get settings").stream_buffer_seconds, + store + .get_settings() + .expect("get settings") + .stream_buffer_seconds, 1 ); @@ -921,7 +922,10 @@ mod tests { .set_stream_buffer_seconds(99) .expect("set high buffer"); assert_eq!( - store.get_settings().expect("get settings").stream_buffer_seconds, + store + .get_settings() + .expect("get settings") + .stream_buffer_seconds, 10 ); let _ = std::fs::remove_dir_all(dir); diff --git a/crates/qbz-qt/src/about_bridge.rs b/crates/qbz-qt/src/about_bridge.rs index f50b256c6..d46f363de 100644 --- a/crates/qbz-qt/src/about_bridge.rs +++ b/crates/qbz-qt/src/about_bridge.rs @@ -65,7 +65,6 @@ pub mod qbz_about_bridge { /// `hasBody`, the TOC and the flat block model. Produced by /// `whats_new_qt.rs`. Same `"{}"` default, same reason. #[qproperty(QString, whats_new_json)] - type QbzAbout = super::QbzAboutRust; /// Registers this object's Qt-thread hop. Harmless to call twice, and diff --git a/crates/qbz-qt/src/about_qt.rs b/crates/qbz-qt/src/about_qt.rs index 71674c492..71e4ee376 100644 --- a/crates/qbz-qt/src/about_qt.rs +++ b/crates/qbz-qt/src/about_qt.rs @@ -231,7 +231,10 @@ async fn ensure_avatar(client: &reqwest::Client, handle: &str) -> Option let out = dir.join(format!("{handle}.png")); // A zero-byte file is a failed write that would otherwise count as a hit // forever (the artwork cache's documented trap) — treat it as a miss. - if std::fs::metadata(&out).map(|m| m.len() > 0).unwrap_or(false) { + if std::fs::metadata(&out) + .map(|m| m.len() > 0) + .unwrap_or(false) + { return Some(crate::artwork_qt::file_url(&out.to_string_lossy())); } std::fs::create_dir_all(&dir).ok()?; @@ -318,7 +321,10 @@ mod tests { let doc = snapshot_doc(); assert_eq!( doc.release_url, - format!("https://github.com/vicrodh/qbz/releases/tag/v{}", app_version()) + format!( + "https://github.com/vicrodh/qbz/releases/tag/v{}", + app_version() + ) ); } diff --git a/crates/qbz-qt/src/album_bridge.rs b/crates/qbz-qt/src/album_bridge.rs index dd54ed522..5e4c06f1b 100644 --- a/crates/qbz-qt/src/album_bridge.rs +++ b/crates/qbz-qt/src/album_bridge.rs @@ -48,7 +48,6 @@ pub mod qbz_album_bridge { #[qproperty(bool, album_info_loading)] #[qproperty(QString, album_info_json)] #[qproperty(QString, album_info_error)] - type QbzAlbum = super::QbzAlbumRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -78,7 +77,12 @@ pub mod qbz_album_bridge { #[qinvokable] fn cover_remove_custom(self: Pin<&mut QbzAlbum>, album_id: QString); #[qinvokable] - fn cover_save_as(self: Pin<&mut QbzAlbum>, album_id: QString, title: QString, artwork_url: QString); + fn cover_save_as( + self: Pin<&mut QbzAlbum>, + album_id: QString, + title: QString, + artwork_url: QString, + ); /// ⋯ menu Share rows (share_qt.rs): copy the Qobuz link, or resolve /// UPC -> Deezer -> Album.link and copy that (async, toasts). #[qinvokable] @@ -98,7 +102,12 @@ pub mod qbz_album_bridge { /// Multi-select bulk bar (album_qt::bulk_action): ids in visible /// order, action id from the bar's vocabulary. #[qinvokable] - fn album_bulk_action(self: Pin<&mut QbzAlbum>, album_id: QString, ids_json: QString, action: QString); + fn album_bulk_action( + self: Pin<&mut QbzAlbum>, + album_id: QString, + ids_json: QString, + action: QString, + ); } impl cxx_qt::Threading for QbzAlbum {} @@ -170,7 +179,12 @@ impl qbz_album_bridge::QbzAlbum { pub fn cover_remove_custom(self: Pin<&mut Self>, album_id: QString) { crate::cover_artwork_qt::remove_custom_cover(album_id.to_string()); } - pub fn cover_save_as(self: Pin<&mut Self>, album_id: QString, title: QString, artwork_url: QString) { + pub fn cover_save_as( + self: Pin<&mut Self>, + album_id: QString, + title: QString, + artwork_url: QString, + ) { crate::cover_artwork_qt::save_cover_as( album_id.to_string(), title.to_string(), @@ -194,7 +208,16 @@ impl qbz_album_bridge::QbzAlbum { // not just the failed rows (Slint main.rs:12204). crate::offline_cache_qt::redownload_album(album_id.to_string(), false); } - pub fn album_bulk_action(self: Pin<&mut Self>, album_id: QString, ids_json: QString, action: QString) { - crate::album_qt::bulk_action(album_id.to_string(), ids_json.to_string(), action.to_string()); + pub fn album_bulk_action( + self: Pin<&mut Self>, + album_id: QString, + ids_json: QString, + action: QString, + ) { + crate::album_qt::bulk_action( + album_id.to_string(), + ids_json.to_string(), + action.to_string(), + ); } } diff --git a/crates/qbz-qt/src/album_info_qt.rs b/crates/qbz-qt/src/album_info_qt.rs index 7066810de..5f4c2dce6 100644 --- a/crates/qbz-qt/src/album_info_qt.rs +++ b/crates/qbz-qt/src/album_info_qt.rs @@ -118,18 +118,19 @@ fn roles_suffix(roles: &[String]) -> String { } fn map_track(index: usize, t: &Track, album_artist: &str) -> InfoTrack { - let performers: Vec = parse_performers(t.performers.as_deref().unwrap_or_default()) - .into_iter() - .map(|p| PerformerRow { - roles: roles_suffix(&p.roles), - primary_role: p - .roles - .first() - .cloned() - .unwrap_or_else(|| qbz_i18n::t("Performer")), - name: p.name, - }) - .collect(); + let performers: Vec = + parse_performers(t.performers.as_deref().unwrap_or_default()) + .into_iter() + .map(|p| PerformerRow { + roles: roles_suffix(&p.roles), + primary_role: p + .roles + .first() + .cloned() + .unwrap_or_else(|| qbz_i18n::t("Performer")), + name: p.name, + }) + .collect(); let copyright = t .copyright .as_deref() @@ -195,7 +196,12 @@ fn map(album: Album, requested_id: &str) -> AlbumInfoDoc { let meta_line = match album.genre.as_ref().filter(|g| !g.name.is_empty()) { Some(g) => [ g.name.clone(), - qbz_i18n::tf("{} track", "{} tracks", track_count as i64, &[&track_count.to_string()]), + qbz_i18n::tf( + "{} track", + "{} tracks", + track_count as i64, + &[&track_count.to_string()], + ), album_duration(album.duration.unwrap_or(0)), ] .join(" · "), diff --git a/crates/qbz-qt/src/album_qt.rs b/crates/qbz-qt/src/album_qt.rs index 9884b8331..cdb1491e0 100644 --- a/crates/qbz-qt/src/album_qt.rs +++ b/crates/qbz-qt/src/album_qt.rs @@ -356,12 +356,11 @@ fn spawn_header_color(generation: u64, artwork_url: String) { .await .ok() .flatten(); - let atmo = tokio::task::spawn_blocking(move || { - crate::atmosphere_qt::for_cover_blocking(&p2) - }) - .await - .ok() - .flatten(); + let atmo = + tokio::task::spawn_blocking(move || crate::atmosphere_qt::for_cover_blocking(&p2)) + .await + .ok() + .flatten(); if hex.is_some() || atmo.is_some() { publish_patch(generation, move |doc| { if let Some(hex) = hex { @@ -456,24 +455,24 @@ fn credit_role(roles: Option<&Vec>) -> String { /// album.rs `build_credits` (releaseArtistsMapper parity, minus the /// "VARIOUS"-composer drop it also applies). fn build_credits(album: &Album) -> Vec<(String, String, String)> { - let mut credits: Vec<(String, String, String)> = match album.artists.as_ref().filter(|v| !v.is_empty()) - { - Some(list) => list - .iter() - .map(|a| { - ( - a.name.clone(), - a.id.to_string(), - credit_role(a.roles.as_ref()), - ) - }) - .collect(), - None => vec![( - album.artist.name.clone(), - album.artist.id.to_string(), - String::new(), - )], - }; + let mut credits: Vec<(String, String, String)> = + match album.artists.as_ref().filter(|v| !v.is_empty()) { + Some(list) => list + .iter() + .map(|a| { + ( + a.name.clone(), + a.id.to_string(), + credit_role(a.roles.as_ref()), + ) + }) + .collect(), + None => vec![( + album.artist.name.clone(), + album.artist.id.to_string(), + String::new(), + )], + }; // Album-level composer appended last, unless it's the localized // "Various Composers" placeholder (releaseArtistsMapper's VARIOUS drop). if let Some(composer) = album.composer.as_ref().filter(|c| !c.name.is_empty()) { @@ -552,7 +551,10 @@ fn map_track(track: &Track) -> TrackRow { } /// Fetch + map the album detail (album.rs `map_album` port). -pub async fn load_album(runtime: &Arc>, album_id: &str) -> Result { +pub async fn load_album( + runtime: &Arc>, + album_id: &str, +) -> Result { let album = runtime .core() .get_album(album_id) @@ -578,9 +580,9 @@ pub async fn load_album(runtime: &Arc>, album_id: &st .as_ref() .filter(|g| !g.name.is_empty()) .map(|g| g.name.clone()); - let tracks_str = album.tracks_count.map(|count| { - qbz_i18n::tf("{} track", "{} tracks", count as i64, &[&count.to_string()]) - }); + let tracks_str = album + .tracks_count + .map(|count| qbz_i18n::tf("{} track", "{} tracks", count as i64, &[&count.to_string()])); let duration_str = album.duration.map(format_duration); let mut pre_parts: Vec = Vec::new(); @@ -892,10 +894,7 @@ fn pick_booklet_url(album: &Album) -> String { /// `booklet::download_booklet`): fetch with a 30s client, then a native /// save-as dialog seeded `{title}.pdf`. No-op when the open album has none. pub fn download_booklet() { - let (title, url) = BOOKLET_STASH - .lock() - .map(|s| s.clone()) - .unwrap_or_default(); + let (title, url) = BOOKLET_STASH.lock().map(|s| s.clone()).unwrap_or_default(); if url.is_empty() { return; } @@ -1038,7 +1037,10 @@ fn map_release_card(release: &qbz_models::PageArtistRelease) -> AlbumCardData { .and_then(|list| list.first().map(|a| a.name.clone())) }) .unwrap_or_default(); - let bit_depth = release.audio_info.as_ref().and_then(|a| a.maximum_bit_depth); + let bit_depth = release + .audio_info + .as_ref() + .and_then(|a| a.maximum_bit_depth); let sample_rate = release .audio_info .as_ref() @@ -1115,7 +1117,8 @@ pub async fn load_more_from_artist( /// One Last.fm recommendation as the card shape the two Qobuz rows already use, /// so the third row reuses their delegate instead of a fourth card variant. fn reco_to_card(r: qbz_external_reco::AlbumReco) -> AlbumCardData { - let art_url = crate::cover_artwork_qt::prefer_album_cover(&r.qobuz_album_id, r.artwork_url.clone()); + let art_url = + crate::cover_artwork_qt::prefer_album_cover(&r.qobuz_album_id, r.artwork_url.clone()); AlbumCardData { is_pinned: crate::sidebar_qt::is_pinned("album", &r.qobuz_album_id), is_favorite: crate::fav_cache_qt::is_album_favorite(&r.qobuz_album_id), @@ -1161,7 +1164,12 @@ pub async fn load_suggestions( let date = a .dates .as_ref() - .and_then(|d| d.original.clone().or(d.download.clone()).or(d.stream.clone())) + .and_then(|d| { + d.original + .clone() + .or(d.download.clone()) + .or(d.stream.clone()) + }) .or(a.release_date_original.clone()); let (artist, card_artist_id) = card_artist(a); AlbumCardData { @@ -1339,7 +1347,12 @@ fn spawn_deferred_rows( }, async { let suggestions = load_suggestions(&runtime, &album_id).await; - publish_row(generation, "suggestions", "suggestionsLoading", &suggestions); + publish_row( + generation, + "suggestions", + "suggestionsLoading", + &suggestions, + ); suggestions }, ); diff --git a/crates/qbz-qt/src/ambient_qt.rs b/crates/qbz-qt/src/ambient_qt.rs index b6726abb8..264fc5dd5 100644 --- a/crates/qbz-qt/src/ambient_qt.rs +++ b/crates/qbz-qt/src/ambient_qt.rs @@ -114,9 +114,7 @@ fn dominant_hue_bin(img: &image::RgbaImage) -> (usize, f32, u32) { hist[bin] += 1.0; chromatic += 1; } - let score_at = |i: usize| { - hist[i] + 0.5 * (hist[(i + BINS - 1) % BINS] + hist[(i + 1) % BINS]) - }; + let score_at = |i: usize| hist[i] + 0.5 * (hist[(i + BINS - 1) % BINS] + hist[(i + 1) % BINS]); let mut best_i = 0usize; let mut best = -1.0f32; for (i, _) in hist.iter().enumerate() { @@ -150,9 +148,7 @@ fn spectrum_colors(img: &image::RgbaImage) -> ((u8, u8, u8), (u8, u8, u8)) { let bin = ((h / 360.0 * BINS as f32) as usize).min(BINS - 1); hist[bin] += 1.0; } - let score_at = |i: usize| { - hist[i] + 0.5 * (hist[(i + BINS - 1) % BINS] + hist[(i + 1) % BINS]) - }; + let score_at = |i: usize| hist[i] + 0.5 * (hist[(i + BINS - 1) % BINS] + hist[(i + 1) % BINS]); let mut sec_i: Option = None; let mut sec_best = 0.0f32; for i in 0..BINS { @@ -253,9 +249,7 @@ mod tests { fn dominant_hue_wins_over_speck() { // 3/4 red-ish + 1/4 blue-ish: the coverage-weighted primary must be // the red hue (not the more saturated speck). - let img = img_from(&[ - (200, 30, 30), (200, 30, 30), (200, 30, 30), (30, 30, 220), - ]); + let img = img_from(&[(200, 30, 30), (200, 30, 30), (200, 30, 30), (30, 30, 220)]); let (primary, _secondary) = spectrum_colors(&img); let (h, _s, _l) = rgb_to_hsl(primary.0, primary.1, primary.2); assert!(h < 15.0 || h > 345.0, "primary hue {h} should be red-ish"); @@ -275,7 +269,10 @@ mod tests { let img = img_from(&[(200, 30, 30), (200, 30, 30), (200, 30, 30), (210, 40, 40)]); let accent = lyrics_accent_color(&img); let (h, _s, _l) = rgb_to_hsl(accent.0, accent.1, accent.2); - assert!((170.0..=190.0).contains(&h), "accent hue {h} should be cyan-ish"); + assert!( + (170.0..=190.0).contains(&h), + "accent hue {h} should be cyan-ish" + ); } #[test] @@ -287,7 +284,10 @@ mod tests { let (ph, _, _) = rgb_to_hsl(primary.0, primary.1, primary.2); let (sh, _, sl) = rgb_to_hsl(secondary.0, secondary.1, secondary.2); let dist = (ph - sh).abs().min(360.0 - (ph - sh).abs()); - assert!(dist < 30.0, "mono cover: secondary hue {sh} vs primary {ph}"); + assert!( + dist < 30.0, + "mono cover: secondary hue {sh} vs primary {ph}" + ); assert!(sl < 0.45, "mono cover: secondary should be deeper (L={sl})"); } @@ -340,7 +340,6 @@ mod tests { } } - /// Recompute the ambient triad from the current track's artwork and publish /// it to the bridge (ambientPrimary/Secondary/Accent). Called on track /// change from playback_qt's now-playing refresh. A cover that is not on @@ -365,24 +364,15 @@ pub fn update_for_artwork(artwork_url: &str) { let bytes = std::fs::read(&path).ok()?; let img = image::load_from_memory(&bytes).ok()?; let rgba = img.to_rgba8(); - let tiny = image::imageops::resize( - &rgba, - 16, - 16, - image::imageops::FilterType::Triangle, - ); + let tiny = + image::imageops::resize(&rgba, 16, 16, image::imageops::FilterType::Triangle); let (primary, secondary) = spectrum_colors(&tiny); let accent = lyrics_accent_color(&tiny); // Immersive (contract §4.3): the SAME decode feeds the 8x8 glow // sample and the atmosphere asset — both inside this // spawn_blocking, never on the Qt thread // (atmosphere_qt.rs:126-127). - let tiny8 = image::imageops::resize( - &rgba, - 8, - 8, - image::imageops::FilterType::Triangle, - ); + let tiny8 = image::imageops::resize(&rgba, 8, 8, image::imageops::FilterType::Triangle); let glow = glow_hex_qt(glow_color(&tiny8)); let atmosphere = crate::atmosphere_qt::for_cover_blocking(&path); // B1: the SAME decode feeds the Tunnel Flow palette — the Tauri @@ -397,7 +387,14 @@ pub fn update_for_artwork(artwork_url: &str) { let tunnel = crate::tunnelflow_qt::line_palette_json( &crate::tunnelflow_qt::line_palette(&tiny36), ); - Some((hex(primary), hex(secondary), hex(accent), glow, atmosphere, tunnel)) + Some(( + hex(primary), + hex(secondary), + hex(accent), + glow, + atmosphere, + tunnel, + )) }) .await .ok() @@ -405,10 +402,12 @@ pub fn update_for_artwork(artwork_url: &str) { if let Some((primary, secondary, accent, glow, atmosphere, tunnel)) = triad { log::info!("[qbz-qt] ambient palette: {primary} / {secondary} / {accent}"); crate::shell_bridge::ui(move |mut b| { - b.as_mut().set_ambient_primary(QString::from(primary.as_str())); + b.as_mut() + .set_ambient_primary(QString::from(primary.as_str())); b.as_mut() .set_ambient_secondary(QString::from(secondary.as_str())); - b.as_mut().set_ambient_accent(QString::from(accent.as_str())); + b.as_mut() + .set_ambient_accent(QString::from(accent.as_str())); }); // Publish ONLY on success; on failure (or a missing atmosphere) // the previous values stay — never cleared on track change (the diff --git a/crates/qbz-qt/src/artist_blacklist.rs b/crates/qbz-qt/src/artist_blacklist.rs index 5a06082c1..3a8fcaff8 100644 --- a/crates/qbz-qt/src/artist_blacklist.rs +++ b/crates/qbz-qt/src/artist_blacklist.rs @@ -336,8 +336,14 @@ mod tests { add(42, "X", None).expect("add succeeds with a bound store"); assert!(is_blacklisted(42), "added id is blacklisted"); assert!(is_blacklisted_id_str("42"), "string-id check matches"); - assert!(!is_blacklisted_id_str("not-a-number"), "local ids never match"); - assert!(ids_snapshot().contains(&42), "snapshot contains the added id"); + assert!( + !is_blacklisted_id_str("not-a-number"), + "local ids never match" + ); + assert!( + ids_snapshot().contains(&42), + "snapshot contains the added id" + ); assert_eq!(count(), 1); // Album axis: orthogonal, String-keyed, shares the enabled flag. @@ -351,7 +357,10 @@ mod tests { ); assert_eq!(album_count(), 1); // A blocked album drops via the shared stamp predicate, artist-independent. - assert!(stamp_row("qobuz", &[], Some("zzz")), "album-blocked row drops"); + assert!( + stamp_row("qobuz", &[], Some("zzz")), + "album-blocked row drops" + ); assert!( !stamp_row("local", &[], Some("zzz")), "non-qobuz row is protected even when album-blocked" @@ -366,13 +375,19 @@ mod tests { "non-qobuz track is protected" ); assert!(card_blacklisted("zzz", "1"), "card drops on the album axis"); - assert!(card_blacklisted("aaa", "42"), "card drops on the artist axis"); + assert!( + card_blacklisted("aaa", "42"), + "card drops on the artist axis" + ); assert_eq!(count(), 1, "album add did not touch the artist count"); teardown(); assert!(!is_blacklisted(42), "fail-open after teardown"); assert!(ids_snapshot().is_empty(), "empty snapshot after teardown"); - assert!(!is_album_blacklisted("zzz"), "album fail-open after teardown"); + assert!( + !is_album_blacklisted("zzz"), + "album fail-open after teardown" + ); assert!(album_ids_snapshot().is_empty(), "empty album snapshot"); assert_eq!(count(), 0); assert_eq!(album_count(), 0); diff --git a/crates/qbz-qt/src/artist_bridge.rs b/crates/qbz-qt/src/artist_bridge.rs index bb11afb9b..b6856033d 100644 --- a/crates/qbz-qt/src/artist_bridge.rs +++ b/crates/qbz-qt/src/artist_bridge.rs @@ -53,7 +53,6 @@ pub mod qbz_artist_bridge { // `loading` rides INSIDE the document rather than as a second // property — the reason is written down on that struct. #[qproperty(QString, artist_releases_json)] - type QbzArtist = super::QbzArtistRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -100,7 +99,12 @@ pub mod qbz_artist_bridge { /// ArtistView per-section "Load more" — the next releases page. #[qinvokable] - fn load_release_section(self: Pin<&mut QbzArtist>, artist_id: QString, release_type: QString, offset: i32); + fn load_release_section( + self: Pin<&mut QbzArtist>, + artist_id: QString, + release_type: QString, + offset: i32, + ); /// ArtistView per-section sort — `ReleaseGrid.slint:26` /// `set-section-sort(release-type, sort)`, fired by that view's @@ -161,7 +165,12 @@ pub mod qbz_artist_bridge { /// Emitted with the next page of a releases bucket. #[qsignal] - fn release_section_ready(self: Pin<&mut QbzArtist>, release_type: QString, cards_json: QString, has_more: bool); + fn release_section_ready( + self: Pin<&mut QbzArtist>, + release_type: QString, + cards_json: QString, + has_more: bool, + ); } impl cxx_qt::Threading for QbzArtist {} @@ -213,7 +222,12 @@ impl qbz_artist_bridge::QbzArtist { crate::open_artist(artist_id.to_string()); } - pub fn load_release_section(self: Pin<&mut Self>, artist_id: QString, release_type: QString, offset: i32) { + pub fn load_release_section( + self: Pin<&mut Self>, + artist_id: QString, + release_type: QString, + offset: i32, + ) { crate::load_release_section(artist_id.to_string(), release_type.to_string(), offset); } /// Straight through to `artist_qt`, with no `crate::` forwarder in main.rs: diff --git a/crates/qbz-qt/src/artist_qt.rs b/crates/qbz-qt/src/artist_qt.rs index 8b6326197..6b8570dce 100644 --- a/crates/qbz-qt/src/artist_qt.rs +++ b/crates/qbz-qt/src/artist_qt.rs @@ -413,7 +413,10 @@ pub(crate) fn map_release(release: &PageArtistRelease) -> AlbumCardData { .as_ref() .map(|a| a.id.to_string()) .unwrap_or_default(); - let bit_depth = release.audio_info.as_ref().and_then(|a| a.maximum_bit_depth); + let bit_depth = release + .audio_info + .as_ref() + .and_then(|a| a.maximum_bit_depth); let sample_rate = release .audio_info .as_ref() @@ -538,12 +541,18 @@ fn map_track(index: usize, track: PageArtistTrack) -> TrackRow { let (album_id, album, artwork_url) = track .album .map(|a| { - let url = a.image.and_then(|img| img.smallest().cloned()).unwrap_or_default(); + let url = a + .image + .and_then(|img| img.smallest().cloned()) + .unwrap_or_default(); (a.id, a.title, url) }) .unwrap_or_default(); let bit_depth = track.audio_info.as_ref().and_then(|a| a.maximum_bit_depth); - let sample_rate = track.audio_info.as_ref().and_then(|a| a.maximum_sampling_rate); + let sample_rate = track + .audio_info + .as_ref() + .and_then(|a| a.maximum_sampling_rate); // /artist/page reports availability on the nested `rights` object, not as a // flat key — the same read `playback_qt::artist_top_queue_tracks` (:731) // already does when it builds the queue row from this payload. `unwrap_or @@ -557,7 +566,11 @@ fn map_track(index: usize, track: PageArtistTrack) -> TrackRow { TrackRow { is_favorite: crate::fav_cache_qt::contains_track(track.id), id: track.id.to_string(), - cache_status: if crate::offline_qt::is_cached(&track.id.to_string()) { 3 } else { 0 }, + cache_status: if crate::offline_qt::is_cached(&track.id.to_string()) { + 3 + } else { + 0 + }, number: (index + 1).to_string(), title, artist, @@ -881,8 +894,15 @@ static TOP_QUEUE: std::sync::Mutex> = std::sync::Mutex::new(Vec: fn track_row_to_queue(row: &TrackRow) -> QueueTrack { let duration_secs = { let mut parts = row.duration.split(':'); - parts.next().and_then(|m| m.parse::().ok()).unwrap_or(0) * 60 - + parts.next().and_then(|s| s.parse::().ok()).unwrap_or(0) + parts + .next() + .and_then(|m| m.parse::().ok()) + .unwrap_or(0) + * 60 + + parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) }; QueueTrack { id: row.id.parse().unwrap_or(0), @@ -967,7 +987,13 @@ pub async fn load_release_page( .map_err(|_| format!("invalid artist id: {artist_id}"))?; let resp = runtime .core() - .get_releases_grid(id, release_type, RELEASE_PAGE_SIZE, offset, Some("release_date")) + .get_releases_grid( + id, + release_type, + RELEASE_PAGE_SIZE, + offset, + Some("release_date"), + ) .await .map_err(|e| e.to_string())?; let has_more = resp.has_more; @@ -1006,9 +1032,10 @@ fn merge_release_page( else { return; }; - let Some(section) = sections.iter_mut().find(|s| { - s.get("releaseType").and_then(|v| v.as_str()) == Some(release_type) - }) else { + let Some(section) = sections + .iter_mut() + .find(|s| s.get("releaseType").and_then(|v| v.as_str()) == Some(release_type)) + else { return; }; section["hasMore"] = json!(has_more); @@ -1110,12 +1137,16 @@ pub(crate) fn resort_section(release_type: &str, sort: &str) { let release_type = release_type.to_string(); let sort = sort.to_string(); publish_patch(generation, move |doc| { - let Some(sections) = doc.get_mut("releaseSections").and_then(|v| v.as_array_mut()) else { + let Some(sections) = doc + .get_mut("releaseSections") + .and_then(|v| v.as_array_mut()) + else { return; }; - let Some(section) = sections.iter_mut().find(|s| { - s.get("releaseType").and_then(|v| v.as_str()) == Some(release_type.as_str()) - }) else { + let Some(section) = sections + .iter_mut() + .find(|s| s.get("releaseType").and_then(|v| v.as_str()) == Some(release_type.as_str())) + else { return; }; // Stamp the key BEFORE the cards: `sortBy` is what seats the picker @@ -1186,8 +1217,11 @@ pub async fn load_artist_view( spawn_header_color(generation, header_art); - let similar_names: Vec = - data.similar_artists.iter().map(|s| s.name.clone()).collect(); + let similar_names: Vec = data + .similar_artists + .iter() + .map(|s| s.name.clone()) + .collect(); spawn_enrichment( runtime.clone(), generation, @@ -1332,12 +1366,11 @@ fn spawn_header_color(generation: u64, artwork_url: String) { .await .ok() .flatten(); - let atmo = tokio::task::spawn_blocking(move || { - crate::atmosphere_qt::for_cover_blocking(&p2) - }) - .await - .ok() - .flatten(); + let atmo = + tokio::task::spawn_blocking(move || crate::atmosphere_qt::for_cover_blocking(&p2)) + .await + .ok() + .flatten(); if hex.is_some() || atmo.is_some() { publish_patch(generation, move |doc| { if let Some(hex) = hex { @@ -1390,8 +1423,7 @@ fn spawn_enrichment( crate::spawn(async move { let stories = load_stories(&runtime, &artist_id).await; publish_patch(generation, move |doc| { - doc["stories"] = - serde_json::to_value(&stories).unwrap_or_else(|_| json!([])); + doc["stories"] = serde_json::to_value(&stories).unwrap_or_else(|_| json!([])); doc["storiesLoading"] = json!(false); }); }); @@ -1499,10 +1531,7 @@ fn map_story(item: ArtistStoryItem) -> StoryRow { /// The artist's Magazine stories (limit 2, like the official client). Any /// failure yields an empty list and the tab shows "No stories". -async fn load_stories( - runtime: &Arc>, - artist_id: &str, -) -> Vec { +async fn load_stories(runtime: &Arc>, artist_id: &str) -> Vec { let Ok(id) = artist_id.parse::() else { return Vec::new(); }; @@ -1573,8 +1602,7 @@ fn map_origin(meta: &qbz_integrations::musicbrainz::ArtistMetadata) -> MbOriginJ .as_ref() .map(|loc| loc.display_name.clone()) .unwrap_or_default(); - let has_data = - !begin_date.is_empty() || !end_date.is_empty() || !location_display.is_empty(); + let has_data = !begin_date.is_empty() || !end_date.is_empty() || !location_display.is_empty(); // ---- Artist Scene payload, from the SAME metadata this call already has. // No extra fetch: `meta` carries the whole location and the affinity @@ -1590,9 +1618,8 @@ fn map_origin(meta: &qbz_integrations::musicbrainz::ArtistMetadata) -> MbOriginJ .unwrap_or("") .to_string(); // The INTENDED form of Tauri's guard — see the field's doc. - let location_clickable = loc.is_some_and(|l| { - !matches!(l.precision, LocationPrecision::Country) || l.city.is_some() - }); + let location_clickable = + loc.is_some_and(|l| !matches!(l.precision, LocationPrecision::Country) || l.city.is_some()); MbOriginJson { is_person, diff --git a/crates/qbz-qt/src/artist_releases_qt.rs b/crates/qbz-qt/src/artist_releases_qt.rs index e94b1514a..6f47f22be 100644 --- a/crates/qbz-qt/src/artist_releases_qt.rs +++ b/crates/qbz-qt/src/artist_releases_qt.rs @@ -330,11 +330,7 @@ pub fn retry() { if s.artist_id.is_empty() { return None; } - Some(( - s.artist_id.clone(), - s.name.clone(), - s.release_type.clone(), - )) + Some((s.artist_id.clone(), s.name.clone(), s.release_type.clone())) }) else { return; }; @@ -397,7 +393,9 @@ fn fetch(generation: u64, artist_id: String, release_type: String, offset: u32, { Ok(resp) => resp, Err(e) => { - log::warn!("[qbz-qt] artist releases page failed ({artist_id}/{release_type}): {e}"); + log::warn!( + "[qbz-qt] artist releases page failed ({artist_id}/{release_type}): {e}" + ); fail(generation, first); return; } diff --git a/crates/qbz-qt/src/artist_scene_qt.rs b/crates/qbz-qt/src/artist_scene_qt.rs index 90d9c68da..d8024260e 100644 --- a/crates/qbz-qt/src/artist_scene_qt.rs +++ b/crates/qbz-qt/src/artist_scene_qt.rs @@ -903,7 +903,11 @@ fn apply_page( s.error.clear(); s.progress_phase = "done"; s.progress_pct = 100; - s.view_state = if s.artists.is_empty() { "empty" } else { "ready" }; + s.view_state = if s.artists.is_empty() { + "empty" + } else { + "ready" + }; true }); if changed { @@ -1416,10 +1420,7 @@ fn is_live_album(album: &Album) -> bool { return true; } - let studio_variant = has_any_word( - &title, - &["remaster", "deluxe", "anniversary", "expanded"], - ); + let studio_variant = has_any_word(&title, &["remaster", "deluxe", "anniversary", "expanded"]); // `\b(tour|on tour)\b(?!.*\bedition\b)` — a lookahead over the REST of the // string, which is why "edition" is tested against the whole title here. if has_word(&title, "tour") && !has_word(&title, "edition") && !studio_variant { @@ -1434,15 +1435,72 @@ fn is_live_album(album: &Album) -> bool { /// The city list from `qobuzAdapters.ts:222`, verbatim and in order. const LIVE_CITIES: &[&str] = &[ - "seattle", "tokyo", "london", "paris", "new york", "los angeles", "chicago", "dallas", - "amsterdam", "berlin", "sydney", "melbourne", "montreal", "toronto", "rio", "sao paulo", - "mexico city", "denver", "boston", "philadelphia", "san francisco", "cleveland", "detroit", - "atlanta", "miami", "phoenix", "houston", "minneapolis", "st. louis", "st louis", - "kansas city", "nashville", "memphis", "austin", "portland", "oakland", "stockholm", "oslo", - "dublin", "manchester", "birmingham", "glasgow", "brussels", "madrid", "barcelona", "rome", - "milan", "vienna", "munich", "hamburg", "zurich", "prague", "warsaw", "moscow", "seoul", - "osaka", "singapore", "hong kong", "taipei", "manila", "jakarta", "mumbai", "delhi", - "bangalore", "cape town", "johannesburg", + "seattle", + "tokyo", + "london", + "paris", + "new york", + "los angeles", + "chicago", + "dallas", + "amsterdam", + "berlin", + "sydney", + "melbourne", + "montreal", + "toronto", + "rio", + "sao paulo", + "mexico city", + "denver", + "boston", + "philadelphia", + "san francisco", + "cleveland", + "detroit", + "atlanta", + "miami", + "phoenix", + "houston", + "minneapolis", + "st. louis", + "st louis", + "kansas city", + "nashville", + "memphis", + "austin", + "portland", + "oakland", + "stockholm", + "oslo", + "dublin", + "manchester", + "birmingham", + "glasgow", + "brussels", + "madrid", + "barcelona", + "rome", + "milan", + "vienna", + "munich", + "hamburg", + "zurich", + "prague", + "warsaw", + "moscow", + "seoul", + "osaka", + "singapore", + "hong kong", + "taipei", + "manila", + "jakarta", + "mumbai", + "delhi", + "bangalore", + "cape town", + "johannesburg", ]; /// `\b(19[6-9]\d|20[0-2]\d)\b` — a standalone 4-digit token in 1960..=2029. diff --git a/crates/qbz-qt/src/atmosphere_qt.rs b/crates/qbz-qt/src/atmosphere_qt.rs index bdd9e43fa..64bf0186f 100644 --- a/crates/qbz-qt/src/atmosphere_qt.rs +++ b/crates/qbz-qt/src/atmosphere_qt.rs @@ -172,7 +172,10 @@ mod tests { let centre = out.get_pixel(64, 64); let corner = out.get_pixel(2, 2); let lum = |p: &Rgba| p[0] as u32 + p[1] as u32 + p[2] as u32; - assert!(lum(corner) < lum(centre), "corner {corner:?} vs centre {centre:?}"); + assert!( + lum(corner) < lum(centre), + "corner {corner:?} vs centre {centre:?}" + ); } #[test] diff --git a/crates/qbz-qt/src/auth_qt.rs b/crates/qbz-qt/src/auth_qt.rs index a17913db8..123190e35 100644 --- a/crates/qbz-qt/src/auth_qt.rs +++ b/crates/qbz-qt/src/auth_qt.rs @@ -347,7 +347,7 @@ where // Phase 15: intelligent-search service (cortinilla // cache+ranking). crate::search_qt::init(&dir, crate::search_qt::intelligent_search_pref()); - crate::search_cache_qt::init(); + crate::search_cache_qt::init(); // Phase 17: playlist is_owner math. crate::playlist_qt::set_user_id(user_id); bind_per_user_stores(&dir, user_id).await; @@ -600,7 +600,10 @@ mod tests { query_param("code_autorisation=abc123", "code_autorisation"), Some("abc123".to_string()) ); - assert_eq!(query_param("a=1&code=x%2Fy", "code"), Some("x/y".to_string())); + assert_eq!( + query_param("a=1&code=x%2Fy", "code"), + Some("x/y".to_string()) + ); assert_eq!(query_param("other=1", "code"), None); } diff --git a/crates/qbz-qt/src/award_qt.rs b/crates/qbz-qt/src/award_qt.rs index 737cf2ed6..b74892395 100644 --- a/crates/qbz-qt/src/award_qt.rs +++ b/crates/qbz-qt/src/award_qt.rs @@ -679,8 +679,7 @@ async fn favorite_award_ids() -> HashSet { } pub fn toggle_follow() { - let (id, following, busy) = - with_state(|s| (s.id.clone(), s.is_following, s.follow_toggling)); + let (id, following, busy) = with_state(|s| (s.id.clone(), s.is_following, s.follow_toggling)); if id.is_empty() || busy { return; } diff --git a/crates/qbz-qt/src/blacklist_bridge.rs b/crates/qbz-qt/src/blacklist_bridge.rs index 63e668540..0dcc9082d 100644 --- a/crates/qbz-qt/src/blacklist_bridge.rs +++ b/crates/qbz-qt/src/blacklist_bridge.rs @@ -46,7 +46,6 @@ pub mod qbz_blacklist_bridge { // artists") before the spinner. Slint's BlacklistState.loading has the // same `: true` default for the same reason. #[qproperty(bool, blacklist_loading)] - type QbzBlacklist = super::QbzBlacklistRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/blacklist_qt.rs b/crates/qbz-qt/src/blacklist_qt.rs index 082bae9df..30616f6e3 100644 --- a/crates/qbz-qt/src/blacklist_qt.rs +++ b/crates/qbz-qt/src/blacklist_qt.rs @@ -327,8 +327,7 @@ pub fn publish() { }; let json = serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string()); crate::blacklist_bridge::ui(move |mut b| { - b.as_mut() - .set_blacklist_json(QString::from(json.as_str())); + b.as_mut().set_blacklist_json(QString::from(json.as_str())); }); } @@ -687,9 +686,7 @@ pub fn dismiss_artist(artist_id: String, artist_name: String, image_url: String) let (name, image) = match runtime.core().get_artist(id).await { Ok(a) => ( a.name, - a.image - .and_then(|i| i.best().cloned()) - .unwrap_or_default(), + a.image.and_then(|i| i.best().cloned()).unwrap_or_default(), ), Err(e) => { log::warn!("[qbz-qt] reco dismiss name resolve failed for {id}: {e}"); diff --git a/crates/qbz-qt/src/browse_qt.rs b/crates/qbz-qt/src/browse_qt.rs index e46fde956..014bbbddf 100644 --- a/crates/qbz-qt/src/browse_qt.rs +++ b/crates/qbz-qt/src/browse_qt.rs @@ -266,8 +266,11 @@ fn fetch_discover(generation: u64) { Ok(data) => { let has_more = data.has_more; let fetched = data.items.len() as u32; - let mut cards: Vec = - data.items.into_iter().map(crate::home_qt::map_album).collect(); + let mut cards: Vec = data + .items + .into_iter() + .map(crate::home_qt::map_album) + .collect(); // The return is the MISSING list, and nobody wants it any more — // see the note at the publish below. This call is kept purely // for its disk-cache side effect. @@ -696,7 +699,8 @@ fn publish_history() { (doc, s.loading) }; crate::home_bridge::ui(move |mut b| { - b.as_mut().set_play_history_json(QString::from(json.as_str())); + b.as_mut() + .set_play_history_json(QString::from(json.as_str())); b.as_mut().set_play_history_loading(loading); }); } @@ -909,7 +913,10 @@ mod tests { #[test] fn visible_json_filters() { - let cards = vec![card("Blue Train", "Coltrane"), card("Giant Steps", "Coltrane")]; + let cards = vec![ + card("Blue Train", "Coltrane"), + card("Giant Steps", "Coltrane"), + ]; assert!(visible_json(&cards, "blue").contains("Blue Train")); assert!(!visible_json(&cards, "blue").contains("Giant Steps")); assert!(visible_json(&cards, "").contains("Giant Steps")); diff --git a/crates/qbz-qt/src/cast_bridge.rs b/crates/qbz-qt/src/cast_bridge.rs index 5dbb924c4..cd8ef439a 100644 --- a/crates/qbz-qt/src/cast_bridge.rs +++ b/crates/qbz-qt/src/cast_bridge.rs @@ -104,7 +104,6 @@ pub mod qbz_cast_bridge { // Raw last-error for the picker's error line (empty = none), same // as the Slint `CastState.error`. #[qproperty(QString, error)] - type QbzCast = super::QbzCastRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY diff --git a/crates/qbz-qt/src/cast_qt.rs b/crates/qbz-qt/src/cast_qt.rs index c2ec82f98..a8466fc49 100644 --- a/crates/qbz-qt/src/cast_qt.rs +++ b/crates/qbz-qt/src/cast_qt.rs @@ -510,11 +510,9 @@ impl CastService { cast_bridge::ui(|mut b| { b.as_mut().set_quality_limit_cause(0); b.as_mut().set_quality_over_cap(false); - b.as_mut() - .set_quality_origin(cxx_qt_lib::QString::from("")); + b.as_mut().set_quality_origin(cxx_qt_lib::QString::from("")); b.as_mut().set_device_cap_available(false); - b.as_mut() - .set_device_cap_key(cxx_qt_lib::QString::from("")); + b.as_mut().set_device_cap_key(cxx_qt_lib::QString::from("")); b.as_mut().set_device_cap_index(0); }); self.push_connection_state().await; @@ -668,10 +666,7 @@ impl CastService { "FLAC" } .to_string(), - crate::quality_state::detail( - Some(p.bits_per_sample), - Some(p.sample_rate as f64), - ), + crate::quality_state::detail(Some(p.bits_per_sample), Some(p.sample_rate as f64)), ), None => quality_label_from_track(track), }; @@ -1073,8 +1068,7 @@ impl CastService { // The guard only makes sense when the position signal is // usable: renderers whose RelTime never moves honor // STOPPED like pre-guard behavior. - let position_reliable = - inner.cast_max_position > CAST_POSITION_SIGNAL_MIN_SECS; + let position_reliable = inner.cast_max_position > CAST_POSITION_SIGNAL_MIN_SECS; let near_end = duration <= 0.0 || !position_reliable || max_position >= duration - CAST_END_GUARD_SECS; @@ -1116,13 +1110,7 @@ impl CastService { crate::viz_qt::set_paused(!playing); // The cast poll drives the bar while connected. - crate::now_playing::set_position( - position as i32, - duration as i32, - playing, - 0.0, - true, - ); + crate::now_playing::set_position(position as i32, duration as i32, playing, 0.0, true); push_position(position as f32, duration as f32, playing); if ended { @@ -1264,12 +1252,7 @@ impl CastService { // requested-mp3 shortcut is for the LOCAL engine path; here a // successful STREAMINFO parse proves the bytes are FLAC, never MP3. let effective_tier = if delivered.downgraded && info.probe.is_some() { - if eff_bits >= 24 { - "hires" - } else { - "cd" - } - .to_string() + if eff_bits >= 24 { "hires" } else { "cd" }.to_string() } else { delivered.effective_tier.clone() }; diff --git a/crates/qbz-qt/src/cdda_qt.rs b/crates/qbz-qt/src/cdda_qt.rs index bf22e6df4..162140f8b 100644 --- a/crates/qbz-qt/src/cdda_qt.rs +++ b/crates/qbz-qt/src/cdda_qt.rs @@ -125,7 +125,11 @@ async fn lookup_musicbrainz(disc_id: &str) -> Option { if status.as_u16() == 503 { log::warn!( "[qbz-qt] cd: MusicBrainz is rate-limiting (503){}", - if attempt == 0 { ", retrying once" } else { ", giving up" } + if attempt == 0 { + ", retrying once" + } else { + ", giving up" + } ); continue; } @@ -185,7 +189,10 @@ async fn lookup_musicbrainz(disc_id: &str) -> Option { .and_then(|m| m.as_array()) .and_then(|m| m.iter().find(|m| m.get("tracks").is_some())); let Some(media) = media else { - log::warn!("[qbz-qt] cd: release {:?} carries no track list", release.get("title")); + log::warn!( + "[qbz-qt] cd: release {:?} carries no track list", + release.get("title") + ); return None; }; @@ -212,10 +219,8 @@ async fn lookup_musicbrainz(disc_id: &str) -> Option { .iter() .filter_map(|c| { let name = c.get("name").and_then(|n| n.as_str())?; - let join = c - .get("joinphrase") - .and_then(|j| j.as_str()) - .unwrap_or(""); + let join = + c.get("joinphrase").and_then(|j| j.as_str()).unwrap_or(""); Some(format!("{name}{join}")) }) .collect::() @@ -406,7 +411,11 @@ pub async fn open_disc() -> Result { log::info!( "[qbz-qt] cd: remembered as {:?} ({})", m.album, - if m.edited { "corrected by hand" } else { "from a previous lookup" } + if m.edited { + "corrected by hand" + } else { + "from a previous lookup" + } ); meta_from_memory(m) } diff --git a/crates/qbz-qt/src/cover_artwork_qt.rs b/crates/qbz-qt/src/cover_artwork_qt.rs index bd32ff11c..5783d9887 100644 --- a/crates/qbz-qt/src/cover_artwork_qt.rs +++ b/crates/qbz-qt/src/cover_artwork_qt.rs @@ -166,14 +166,16 @@ pub fn save_cover_as(album_id: String, title: String, artwork_url: String) { let dest_path = dest.path().to_path_buf(); // Local sources first: the override file, then the artwork cache. - let local = album_cover(&album_id).filter(|p| !p.is_empty()).or_else(|| { - let cached = crate::artwork_qt::cached_path(&artwork_url); - if cached.is_empty() { - None - } else { - Some(cached) - } - }); + let local = album_cover(&album_id) + .filter(|p| !p.is_empty()) + .or_else(|| { + let cached = crate::artwork_qt::cached_path(&artwork_url); + if cached.is_empty() { + None + } else { + Some(cached) + } + }); if let Some(src) = local { if let Err(e) = tokio::fs::copy(&src, &dest_path).await { log::warn!("[qbz-qt] cover save-as copy failed: {e}"); @@ -193,7 +195,12 @@ pub fn save_cover_as(album_id: String, title: String, artwork_url: String) { return; } }; - match client.get(&artwork_url).send().await.and_then(|r| r.error_for_status()) { + match client + .get(&artwork_url) + .send() + .await + .and_then(|r| r.error_for_status()) + { Ok(resp) => match resp.bytes().await { Ok(bytes) => { if let Err(e) = tokio::fs::write(&dest_path, &bytes).await { @@ -374,7 +381,12 @@ pub fn save_artist_image_as(artist_name: String, artwork_url: String) { return; } }; - match client.get(&artwork_url).send().await.and_then(|r| r.error_for_status()) { + match client + .get(&artwork_url) + .send() + .await + .and_then(|r| r.error_for_status()) + { Ok(resp) => match resp.bytes().await { Ok(bytes) => { if let Err(e) = tokio::fs::write(&dest_path, &bytes).await { @@ -403,7 +415,11 @@ struct PlaylistStore { } fn playlist_store_path() -> Option { - Some(dirs::data_dir()?.join("qbz").join("custom_playlist_covers.json")) + Some( + dirs::data_dir()? + .join("qbz") + .join("custom_playlist_covers.json"), + ) } fn load_playlist_store() -> PlaylistStore { @@ -647,8 +663,14 @@ mod tests { // The whole point of scoping the widening: an arbitrary URL must not // start colliding with another one on its bare filename. assert_eq!(art_key("https://example.com/thing/photo.jpg"), None); - assert_eq!(art_key("https://static.qobuz.com/images/covers/tr/9l/abc123.jpg"), None); + assert_eq!( + art_key("https://static.qobuz.com/images/covers/tr/9l/abc123.jpg"), + None + ); assert_eq!(art_key(""), None); - assert_eq!(art_key("https://static.qobuz.com/images/artists/covers/large/"), None); + assert_eq!( + art_key("https://static.qobuz.com/images/artists/covers/large/"), + None + ); } } diff --git a/crates/qbz-qt/src/dac_wizard_bridge.rs b/crates/qbz-qt/src/dac_wizard_bridge.rs index 22a491101..5748a430b 100644 --- a/crates/qbz-qt/src/dac_wizard_bridge.rs +++ b/crates/qbz-qt/src/dac_wizard_bridge.rs @@ -36,7 +36,6 @@ pub mod qbz_dac_wizard_bridge { /// The whole wizard state (see `dac_wizard_qt::WizardDoc`). Parseable /// default so QML's `JSON.parse` never throws on the pre-publish frame. #[qproperty(QString, wizard_json)] - type QbzDacWizard = super::QbzDacWizardRust; /// Registers this object's Qt-thread hop. Without it every publish diff --git a/crates/qbz-qt/src/dac_wizard_qt.rs b/crates/qbz-qt/src/dac_wizard_qt.rs index 15d7ee8bd..0b46a49da 100644 --- a/crates/qbz-qt/src/dac_wizard_qt.rs +++ b/crates/qbz-qt/src/dac_wizard_qt.rs @@ -281,7 +281,12 @@ pub fn set_init(index: i32) { fn recompute_check() { with_state(|s| { let health = s.last_health.unwrap_or_else(core::probe_health); - let view = core::check_view(health, s.doc.distro_index, s.doc.init_index, s.doc.sandboxed); + let view = core::check_view( + health, + s.doc.distro_index, + s.doc.init_index, + s.doc.sandboxed, + ); s.doc.health_ok = view.health_ok; s.doc.health_summary = view.summary; s.doc.remediations = view @@ -535,9 +540,8 @@ pub fn verify_own() { s.doc.test_playing = false; s.doc.test_rate_matched = false; s.doc.test_negotiated_label = String::new(); - s.doc.test_requested_label = qbz_i18n::t( - "Your queue is empty — add some tracks first, or press Play test.", - ); + s.doc.test_requested_label = + qbz_i18n::t("Your queue is empty — add some tracks first, or press Play test."); }); publish(); return; diff --git a/crates/qbz-qt/src/diagnostics_qt.rs b/crates/qbz-qt/src/diagnostics_qt.rs index 955c01495..34ca7e4f5 100644 --- a/crates/qbz-qt/src/diagnostics_qt.rs +++ b/crates/qbz-qt/src/diagnostics_qt.rs @@ -162,7 +162,10 @@ fn snapshot_doc() -> Doc { fn publish_now() { let json = serde_json::to_string(&snapshot_doc()).unwrap_or_else(|_| "{}".into()); - ui(move |mut b| b.as_mut().set_diagnostics_json(QString::from(json.as_str()))); + ui(move |mut b| { + b.as_mut() + .set_diagnostics_json(QString::from(json.as_str())) + }); } /// Publish the document — but only while the panel is expanded. A collapsed @@ -253,13 +256,14 @@ fn gather_blocking() -> Blocking { let graphics = qbz_app::settings::graphics::GraphicsSettings::default(); let developer = qbz_app::settings::developer::DeveloperSettings::default(); let gfx = qbz_app::diagnostics::detect_graphics_runtime(&graphics, false); - let diag = qbz_app::diagnostics::runtime_diagnostics(&qbz_app::diagnostics::DiagnosticsInputs { - audio: &audio, - graphics: &graphics, - developer: &developer, - gfx, - app_version: env!("CARGO_PKG_VERSION"), - }); + let diag = + qbz_app::diagnostics::runtime_diagnostics(&qbz_app::diagnostics::DiagnosticsInputs { + audio: &audio, + graphics: &graphics, + developer: &developer, + gfx, + app_version: env!("CARGO_PKG_VERSION"), + }); let sys = qbz_app::diagnostics::system_info(); let (active_output, available_outputs, active_fmt) = collect_output_sinks(); @@ -378,9 +382,10 @@ fn gather_catalog_diagnostics() -> CatalogDiagnostics { reason: String::new(), generation: Some(manifest.active_generation), tracks: stats.as_ref().ok().map(|value| value.track_count), - bytes: stats.as_ref().ok().map(|value| { - value.page_size_bytes.saturating_mul(value.page_count) - }), + bytes: stats + .as_ref() + .ok() + .map(|value| value.page_size_bytes.saturating_mul(value.page_count)), runtime_check, runtime_ok, fts_activation_verified: true, @@ -672,7 +677,10 @@ fn build_system_rows( rows.push(row( "Local Catalog Size", "—", - &catalog.bytes.map(format_bytes).unwrap_or_else(|| "—".to_string()), + &catalog + .bytes + .map(format_bytes) + .unwrap_or_else(|| "—".to_string()), 0, )); rows.push(row( @@ -689,7 +697,11 @@ fn build_system_rows( } else { "not active" }, - if catalog.fts_activation_verified { 1 } else { 0 }, + if catalog.fts_activation_verified { + 1 + } else { + 0 + }, )); rows.push(row("Local Library Routes", "—", &catalog.routes, 0)); rows @@ -741,7 +753,12 @@ fn build_audio_rows(g: &Gathered) -> Vec { .unwrap_or("—"); vec![ - row("Output Device", &saved_output, output_runtime, output_status), + row( + "Output Device", + &saved_output, + output_runtime, + output_status, + ), row("Backend", &opt(&d.audio_backend_type), "—", 0), row("Exclusive Mode", yn(d.audio_exclusive_mode), "—", 0), row("DAC Passthrough", yn(d.audio_dac_passthrough), "—", 0), @@ -757,7 +774,12 @@ fn build_audio_rows(g: &Gathered) -> Vec { 0, ), row("Gapless", yn(d.audio_gapless_enabled), "—", 0), - row("PW Force Bitperfect", yn(d.audio_pw_force_bitperfect), "—", 0), + row( + "PW Force Bitperfect", + yn(d.audio_pw_force_bitperfect), + "—", + 0, + ), row( "Stream Buffer", &format!("{}s", d.audio_stream_buffer_seconds), @@ -775,12 +797,7 @@ fn build_graphics_rows(g: &Gathered) -> Vec { // The Qt analogue of the reference's "Renderer (Slint)": the saved // Settings > Appearance preference vs the RHI api that actually // resolved at startup (`renderer_qt::active_api`). - row( - "Renderer (Qt)", - &g.renderer_saved, - &g.renderer_runtime, - 0, - ), + row("Renderer (Qt)", &g.renderer_saved, &g.renderer_runtime, 0), // Stands where the reference's `renderer_decision_summary()` adapter // string does. NOT byte-identical: this is the Vulkan enumeration in // `QT_VK_PHYSICAL_DEVICE_INDEX` order, not wgpu's adapter list. @@ -807,13 +824,21 @@ fn build_graphics_rows(g: &Gathered) -> Vec { row( "GPU: NVIDIA", "—", - if d.runtime_has_nvidia { "Detected" } else { "No" }, + if d.runtime_has_nvidia { + "Detected" + } else { + "No" + }, 0, ), row( "GPU: Intel", "—", - if d.runtime_has_intel { "Detected" } else { "No" }, + if d.runtime_has_intel { + "Detected" + } else { + "No" + }, 0, ), row( @@ -900,7 +925,12 @@ fn build_playback_rows( ), row("Has Track", "—", yn(track.is_some()), 0), row("Track Title", "—", &opt(&track.map(|t| t.title.clone())), 0), - row("Track Artist", "—", &opt(&track.map(|t| t.artist.clone())), 0), + row( + "Track Artist", + "—", + &opt(&track.map(|t| t.artist.clone())), + 0, + ), row("Track Album", "—", &opt(&track.map(|t| t.album.clone())), 0), row( "Track Source", diff --git a/crates/qbz-qt/src/disc_meta_bridge.rs b/crates/qbz-qt/src/disc_meta_bridge.rs index 0d0f166fb..d36e1130d 100644 --- a/crates/qbz-qt/src/disc_meta_bridge.rs +++ b/crates/qbz-qt/src/disc_meta_bridge.rs @@ -32,7 +32,6 @@ pub mod qbz_disc_meta_bridge { /// The whole modal in one document. Parseable while closed so a /// binding reading `doc.open` on frame 1 cannot throw. #[qproperty(QString, meta_json)] - type QbzDiscMeta = super::QbzDiscMetaRust; /// Registers the Qt-thread hop. Without it every publish from Rust is diff --git a/crates/qbz-qt/src/disc_meta_qt.rs b/crates/qbz-qt/src/disc_meta_qt.rs index 41276e76b..76fa8ad70 100644 --- a/crates/qbz-qt/src/disc_meta_qt.rs +++ b/crates/qbz-qt/src/disc_meta_qt.rs @@ -219,7 +219,9 @@ pub fn search(query: &str) { d.preview = None; d.selected_id.clear(); ( - d.provider.parse::().unwrap_or(RemoteProvider::MusicBrainz), + d.provider + .parse::() + .unwrap_or(RemoteProvider::MusicBrainz), // The album artist as the app currently knows it, which is what // lets the orchestration split "artist album" back apart. crate::local_ephemeral::tracks_snapshot() @@ -243,7 +245,11 @@ pub fn search(query: &str) { "[qbz-qt] disc meta: {} returned {} result(s){}", provider_word(provider), response.results.len(), - if response.rate_limited { " (rate limited)" } else { "" } + if response.rate_limited { + " (rate limited)" + } else { + "" + } ); with(|d| { // A late answer from a provider the user has since switched away @@ -292,7 +298,9 @@ pub fn select(provider_id: &str) { d.selected_id = id.clone(); d.loading_id = id.clone(); d.preview = None; - d.provider.parse::().unwrap_or(RemoteProvider::MusicBrainz) + d.provider + .parse::() + .unwrap_or(RemoteProvider::MusicBrainz) }); publish(); @@ -375,7 +383,11 @@ pub fn apply() { tracks: (0..disc_tracks) .map(|i| qbz_disc::store::TrackMemory { number: i as u32 + 1, - title: meta.tracks.get(i).map(|t| t.title.clone()).unwrap_or_default(), + title: meta + .tracks + .get(i) + .map(|t| t.title.clone()) + .unwrap_or_default(), artist: meta.artist.clone(), }) .collect(), diff --git a/crates/qbz-qt/src/disco_bridge.rs b/crates/qbz-qt/src/disco_bridge.rs index 84d617825..536d878ce 100644 --- a/crates/qbz-qt/src/disco_bridge.rs +++ b/crates/qbz-qt/src/disco_bridge.rs @@ -48,7 +48,6 @@ pub mod qbz_disco_bridge { // loading/error, order, tri-state header flags, footer counts and the // grouped candidates with their alternates). #[qproperty(QString, disco_json)] - type QbzDisco = super::QbzDiscoRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain @@ -95,11 +94,7 @@ pub mod qbz_disco_bridge { /// Clears one override (back to the classifier's verdict). #[qinvokable] - fn reset_type_override( - self: Pin<&mut QbzDisco>, - source: QString, - source_item_id: QString, - ); + fn reset_type_override(self: Pin<&mut QbzDisco>, source: QString, source_item_id: QString); /// Create the artist_collection: trimmed-empty name ⇒ the raw literal /// "Artist Collection" (untranslated, 1:1 with myqbz_builder.rs:858), diff --git a/crates/qbz-qt/src/discover_config_qt.rs b/crates/qbz-qt/src/discover_config_qt.rs index 42a3e8183..55408b619 100644 --- a/crates/qbz-qt/src/discover_config_qt.rs +++ b/crates/qbz-qt/src/discover_config_qt.rs @@ -26,11 +26,11 @@ //! is out of scope. The toolbar gear is DISABLED on that tab instead of //! opening an empty modal (HomeView.qml). +use cxx_qt_lib::QString; use qbz_app::settings::discover_prefs::{ DiscoverPrefs, DiscoverPrefsStore, DiscoverySectionId, DiscoveryTab, DEFAULT_RAIL_SIZE, RAIL_SIZE_PRESETS, }; -use cxx_qt_lib::QString; use serde::Serialize; /// id -> label. Verbatim copy of `discover_prefs::label_for` in the Slint @@ -93,8 +93,7 @@ struct ConfigDoc { /// per-user prefs file as the section order, and `recommendations_qt` writes it /// — one accessor beats two copies of the user-dir lookup drifting apart. pub(crate) fn store() -> Option { - crate::sidebar_qt::user_dir() - .and_then(|dir| DiscoverPrefsStore::new_at(&dir).ok()) + crate::sidebar_qt::user_dir().and_then(|dir| DiscoverPrefsStore::new_at(&dir).ok()) } // --------------------------------------------------------------------------- diff --git a/crates/qbz-qt/src/fav_cache_qt.rs b/crates/qbz-qt/src/fav_cache_qt.rs index ef3f6abac..2375cf4ec 100644 --- a/crates/qbz-qt/src/fav_cache_qt.rs +++ b/crates/qbz-qt/src/fav_cache_qt.rs @@ -77,7 +77,10 @@ pub fn init_for_user(base_dir: &Path) { match store.get_favorite_album_ids() { Ok(ids) => { let set: HashSet = ids.into_iter().collect(); - log::info!("[qbz-qt] favorites cache: {} album ids seeded from disk", set.len()); + log::info!( + "[qbz-qt] favorites cache: {} album ids seeded from disk", + set.len() + ); if let Ok(mut guard) = FAV_ALBUMS.write() { *guard = set; } @@ -116,8 +119,14 @@ pub fn init_for_user(base_dir: &Path) { /// Seed one numeric set from the store's `i64` rows (the three of them read /// identically; the album set is the odd one out because its ids are text). fn write_u64(cell: &RwLock>, ids: Vec, what: &str) { - let set: HashSet = ids.into_iter().filter_map(|id| u64::try_from(id).ok()).collect(); - log::info!("[qbz-qt] favorites cache: {} {what} ids seeded from disk", set.len()); + let set: HashSet = ids + .into_iter() + .filter_map(|id| u64::try_from(id).ok()) + .collect(); + log::info!( + "[qbz-qt] favorites cache: {} {what} ids seeded from disk", + set.len() + ); if let Ok(mut guard) = cell.write() { *guard = set; } @@ -177,7 +186,10 @@ pub fn is_favorite(kind: &str, id: &str) -> bool { } pub fn contains_track(track_id: u64) -> bool { - FAV_TRACKS.read().map(|g| g.contains(&track_id)).unwrap_or(false) + FAV_TRACKS + .read() + .map(|g| g.contains(&track_id)) + .unwrap_or(false) } /// Snapshot of the whole favourite-track set — the shape the queue publisher @@ -187,15 +199,24 @@ pub fn all_tracks() -> HashSet { } pub fn is_album_favorite(album_id: &str) -> bool { - FAV_ALBUMS.read().map(|g| g.contains(album_id)).unwrap_or(false) + FAV_ALBUMS + .read() + .map(|g| g.contains(album_id)) + .unwrap_or(false) } pub fn is_artist_favorite(artist_id: u64) -> bool { - FAV_ARTISTS.read().map(|g| g.contains(&artist_id)).unwrap_or(false) + FAV_ARTISTS + .read() + .map(|g| g.contains(&artist_id)) + .unwrap_or(false) } pub fn is_label_favorite(label_id: u64) -> bool { - FAV_LABELS.read().map(|g| g.contains(&label_id)).unwrap_or(false) + FAV_LABELS + .read() + .map(|g| g.contains(&label_id)) + .unwrap_or(false) } /// Snapshot of the followed-label id set — the label page seeds its header @@ -208,7 +229,10 @@ pub fn all_labels() -> HashSet { /// authority `library_qt::toggle_playlist_favorite` reads to pick its /// direction, so the drawn heart and the click cannot disagree. pub fn is_playlist_favorite(playlist_id: u64) -> bool { - FAV_PLAYLISTS.read().map(|g| g.contains(&playlist_id)).unwrap_or(false) + FAV_PLAYLISTS + .read() + .map(|g| g.contains(&playlist_id)) + .unwrap_or(false) } // --------------------------------------------------------------------------- diff --git a/crates/qbz-qt/src/folder_edit_bridge.rs b/crates/qbz-qt/src/folder_edit_bridge.rs index f531fde21..957ff199c 100644 --- a/crates/qbz-qt/src/folder_edit_bridge.rs +++ b/crates/qbz-qt/src/folder_edit_bridge.rs @@ -61,7 +61,6 @@ pub mod qbz_folder_edit_bridge { // The full folder editor (§4.5). Parseable default so QML's // JSON.parse never throws on the pre-publish frame. #[qproperty(QString, edit_json)] - type QbzFolderEdit = super::QbzFolderEditRust; /// Registers this object's Qt-thread hop. Without it every `ui()` diff --git a/crates/qbz-qt/src/folder_edit_qt.rs b/crates/qbz-qt/src/folder_edit_qt.rs index 560b696b9..1bab8296f 100644 --- a/crates/qbz-qt/src/folder_edit_qt.rs +++ b/crates/qbz-qt/src/folder_edit_qt.rs @@ -602,8 +602,8 @@ pub(crate) fn set_hidden(folder_id: &str, hidden: bool) { return; } crate::spawn(async move { - let _ = tokio::task::spawn_blocking(move || folders_qt::set_folder_hidden(&id, hidden)) - .await; + let _ = + tokio::task::spawn_blocking(move || folders_qt::set_folder_hidden(&id, hidden)).await; after_write(); }); } @@ -625,7 +625,15 @@ mod tests { fn the_preset_list_is_the_references_seven_in_order() { assert_eq!( PRESETS, - ["heart", "star", "music", "folder", "disc", "library", "headphones"] + [ + "heart", + "star", + "music", + "folder", + "disc", + "library", + "headphones" + ] ); } @@ -639,6 +647,9 @@ mod tests { assert_eq!(v["presets"].as_array().map(|a| a.len()), Some(7)); assert_eq!(v["swatches"].as_array().map(|a| a.len()), Some(11)); assert_eq!(v["swatches"][0]["isAccent"], serde_json::Value::Bool(true)); - assert_eq!(v["customImagePath"], serde_json::Value::String(String::new())); + assert_eq!( + v["customImagePath"], + serde_json::Value::String(String::new()) + ); } } diff --git a/crates/qbz-qt/src/folders_qt.rs b/crates/qbz-qt/src/folders_qt.rs index 187f64be1..b4b558ff2 100644 --- a/crates/qbz-qt/src/folders_qt.rs +++ b/crates/qbz-qt/src/folders_qt.rs @@ -187,7 +187,9 @@ pub fn delete_folder(id: &str) -> bool { /// Move a playlist into `folder_id`, or to root when `None`. pub fn move_playlist(playlist_id: u64, folder_id: Option<&str>) { - with_db(true, |db| db.move_playlist_to_folder(playlist_id, folder_id)); + with_db(true, |db| { + db.move_playlist_to_folder(playlist_id, folder_id) + }); } /// Set a playlist's favorite flag. diff --git a/crates/qbz-qt/src/foryou_qt.rs b/crates/qbz-qt/src/foryou_qt.rs index 4ea5578d2..f41aa93e8 100644 --- a/crates/qbz-qt/src/foryou_qt.rs +++ b/crates/qbz-qt/src/foryou_qt.rs @@ -329,7 +329,10 @@ where }); } -async fn load_spotlight(runtime: &Arc>, favorites: &[Artist]) -> Option +async fn load_spotlight( + runtime: &Arc>, + favorites: &[Artist], +) -> Option where A: FrontendAdapter + Send + Sync + 'static, { @@ -413,7 +416,11 @@ where category: page.artist_category.clone().unwrap_or_default(), art_url, art_path: String::new(), - has_top_tracks: page.top_tracks.as_ref().map(|t| !t.is_empty()).unwrap_or(false), + has_top_tracks: page + .top_tracks + .as_ref() + .map(|t| !t.is_empty()) + .unwrap_or(false), albums, }) } @@ -865,7 +872,10 @@ where /// PRIMARY DailyQ / WeeklyQ path, Tauri `buildSeeds`): `get_track` each, /// extract `{track_id, artist_id, genre_id, label_id}` (artist = performer, /// else composer; missing ids default to 0), drop any with `artist_id == 0`. -async fn build_tracks_to_analyse(runtime: &Arc>, seeds: &[u64]) -> Vec +async fn build_tracks_to_analyse( + runtime: &Arc>, + seeds: &[u64], +) -> Vec where A: FrontendAdapter + Send + Sync + 'static, { @@ -1075,7 +1085,11 @@ fn to_row(track: &Track) -> TrackRow { .and_then(|a| a.image.best().cloned()) .unwrap_or_default(), is_favorite: crate::fav_cache_qt::is_favorite("track", &track.id.to_string()), - cache_status: if crate::offline_qt::is_cached(&track.id.to_string()) { 3 } else { 0 }, + cache_status: if crate::offline_qt::is_cached(&track.id.to_string()) { + 3 + } else { + 0 + }, // NOT covered by `..default()`: that would leave every For You / mix row // affirmatively claiming to be available, while `to_queue_track` two // functions down reads the real value — the row and the queue built from @@ -1188,7 +1202,10 @@ pub(crate) fn mix_play_track(track_id: String) { /// A row's ⋯ menu: "next" | "later" | "queue". pub(crate) fn mix_enqueue_track(track_id: String, mode: String) { - let Some(track) = mix_queue().into_iter().find(|t| t.id.to_string() == track_id) else { + let Some(track) = mix_queue() + .into_iter() + .find(|t| t.id.to_string() == track_id) + else { return; }; let runtime = crate::app(); @@ -1208,9 +1225,7 @@ fn play_queue_from(index: usize, shuffle: bool) { } let runtime = crate::app(); crate::spawn(async move { - if let Err(e) = - crate::playback_qt::play_track_list(&runtime, queue, index, shuffle).await - { + if let Err(e) = crate::playback_qt::play_track_list(&runtime, queue, index, shuffle).await { log::warn!("[qbz-qt] mix play failed: {e}"); } }); diff --git a/crates/qbz-qt/src/genre_filter_qt.rs b/crates/qbz-qt/src/genre_filter_qt.rs index 5af537145..d85a16415 100644 --- a/crates/qbz-qt/src/genre_filter_qt.rs +++ b/crates/qbz-qt/src/genre_filter_qt.rs @@ -393,7 +393,8 @@ pub(crate) fn current_json() -> String { fn publish() { let json = current_json(); crate::ui(move |mut b| { - b.as_mut().set_genre_filter_json(QString::from(json.as_str())); + b.as_mut() + .set_genre_filter_json(QString::from(json.as_str())); }); } @@ -455,7 +456,11 @@ async fn load_descendants(id: u64) { let kids: Vec = STATE .lock() .ok() - .and_then(|s| s.children.get(&id).map(|k| k.iter().map(|c| c.id).collect())) + .and_then(|s| { + s.children + .get(&id) + .map(|k| k.iter().map(|c| c.id).collect()) + }) .unwrap_or_default(); for kid in kids { load_children(kid).await; diff --git a/crates/qbz-qt/src/home_bridge.rs b/crates/qbz-qt/src/home_bridge.rs index 725ee2783..ac6d7d541 100644 --- a/crates/qbz-qt/src/home_bridge.rs +++ b/crates/qbz-qt/src/home_bridge.rs @@ -116,7 +116,6 @@ pub mod qbz_home { #[qproperty(bool, award_loading)] #[qproperty(QString, award_albums_json)] #[qproperty(bool, award_albums_loading)] - // --- Radio start-up indicator (src/foryou_qt.rs) ------------------- // "" while idle, else the KEY of the radio currently being built: // "album:" or "artist:". A radio is not one request — it is a @@ -136,7 +135,6 @@ pub mod qbz_home { // // READ + NOTIFY: only `foryou_qt` writes it, always in pairs. #[qproperty(QString, radio_pending, READ, NOTIFY)] - type QbzHome = super::QbzHomeRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -733,8 +731,7 @@ impl qbz_home::QbzHome { if patch.is_empty() { return; } - self.as_mut() - .reco_art_ready(QString::from(patch.as_str())); + self.as_mut().reco_art_ready(QString::from(patch.as_str())); } /// Synchronous, in-memory re-hand on HomeView mount. The history stores diff --git a/crates/qbz-qt/src/hotkeys_bridge.rs b/crates/qbz-qt/src/hotkeys_bridge.rs index 23c03bcf5..bf0355b82 100644 --- a/crates/qbz-qt/src/hotkeys_bridge.rs +++ b/crates/qbz-qt/src/hotkeys_bridge.rs @@ -39,9 +39,7 @@ use cxx_qt::CxxQtThread; use cxx_qt::Threading as _; use cxx_qt_lib::QString; -use crate::hotkeys_qt::{ - CaptureOutcome, EscapeState, EscapeTarget, QT_KEY_DOWN, QT_KEY_UP, -}; +use crate::hotkeys_qt::{CaptureOutcome, EscapeState, EscapeTarget, QT_KEY_DOWN, QT_KEY_UP}; /// Full-shape default for the groups document (trap 15): the QML columns read /// `.col1.length` in the pre-publish frame. `boot()` refreshes immediately. @@ -77,7 +75,6 @@ pub mod qbz_hotkeys { // Escape stack arms 2 and 3 read them). #[qproperty(bool, cheatsheet_open)] #[qproperty(bool, customize_open)] - type QbzHotkeys = super::QbzHotkeysRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -263,8 +260,10 @@ impl qbz_hotkeys::QbzHotkeys { // Live pending + the conflict label; recording STAYS so // the user can pick a different combo // (keybindings.rs:454-459). - self.as_mut().set_pending_display(QString::from(display.as_str())); - self.as_mut().set_conflict_label(QString::from(label.as_str())); + self.as_mut() + .set_pending_display(QString::from(display.as_str())); + self.as_mut() + .set_conflict_label(QString::from(label.as_str())); } CaptureOutcome::Bound { shortcut } => { crate::hotkeys_qt::set_binding(&recording, &shortcut); @@ -349,7 +348,9 @@ impl qbz_hotkeys::QbzHotkeys { publish_groups(self.as_mut()); } "ui.openLink" => { - log::debug!("[qbz-qt] hotkeys: ui.openLink — no-op seam, LinkResolver not ported (K3)"); + log::debug!( + "[qbz-qt] hotkeys: ui.openLink — no-op seam, LinkResolver not ported (K3)" + ); } // Shift+M — the reference's `toggle_miniplayer()` // (keybindings.rs:547-558). Its gamescope guard is NOT repeated diff --git a/crates/qbz-qt/src/hotkeys_qt.rs b/crates/qbz-qt/src/hotkeys_qt.rs index 3d2b30a90..1b96bae7b 100644 --- a/crates/qbz-qt/src/hotkeys_qt.rs +++ b/crates/qbz-qt/src/hotkeys_qt.rs @@ -135,27 +135,135 @@ pub struct ActionDef { /// (keybindings.rs:88-117; contract §2 verbatim). pub const ACTIONS: &[ActionDef] = &[ // Playback - ActionDef { id: "playback.toggle", label_en: "Play / Pause", category: Category::Playback, default: "Space", context: Context::None }, - ActionDef { id: "playback.next", label_en: "Next Track", category: Category::Playback, default: "Ctrl+ArrowRight", context: Context::None }, - ActionDef { id: "playback.prev", label_en: "Previous Track", category: Category::Playback, default: "Ctrl+ArrowLeft", context: Context::None }, + ActionDef { + id: "playback.toggle", + label_en: "Play / Pause", + category: Category::Playback, + default: "Space", + context: Context::None, + }, + ActionDef { + id: "playback.next", + label_en: "Next Track", + category: Category::Playback, + default: "Ctrl+ArrowRight", + context: Context::None, + }, + ActionDef { + id: "playback.prev", + label_en: "Previous Track", + category: Category::Playback, + default: "Ctrl+ArrowLeft", + context: Context::None, + }, // Navigation - ActionDef { id: "nav.back", label_en: "Go Back", category: Category::Navigation, default: "Alt+ArrowLeft", context: Context::None }, - ActionDef { id: "nav.forward", label_en: "Go Forward", category: Category::Navigation, default: "Alt+ArrowRight", context: Context::None }, - ActionDef { id: "nav.search", label_en: "Search", category: Category::Navigation, default: "Ctrl+f", context: Context::None }, - ActionDef { id: "nav.settings", label_en: "Settings", category: Category::Navigation, default: "Ctrl+,", context: Context::None }, + ActionDef { + id: "nav.back", + label_en: "Go Back", + category: Category::Navigation, + default: "Alt+ArrowLeft", + context: Context::None, + }, + ActionDef { + id: "nav.forward", + label_en: "Go Forward", + category: Category::Navigation, + default: "Alt+ArrowRight", + context: Context::None, + }, + ActionDef { + id: "nav.search", + label_en: "Search", + category: Category::Navigation, + default: "Ctrl+f", + context: Context::None, + }, + ActionDef { + id: "nav.settings", + label_en: "Settings", + category: Category::Navigation, + default: "Ctrl+,", + context: Context::None, + }, // Interface - ActionDef { id: "ui.sidebar", label_en: "Toggle Sidebar", category: Category::Ui, default: "Shift+S", context: Context::None }, - ActionDef { id: "ui.focusMode", label_en: "Immersive Mode", category: Category::Ui, default: "Shift+I", context: Context::None }, - ActionDef { id: "ui.queue", label_en: "Queue", category: Category::Ui, default: "q", context: Context::None }, - ActionDef { id: "ui.escape", label_en: "Close / Dismiss", category: Category::Ui, default: "Escape", context: Context::None }, - ActionDef { id: "ui.showShortcuts", label_en: "Show Shortcuts", category: Category::Ui, default: "?", context: Context::None }, - ActionDef { id: "ui.openLink", label_en: "Open Qobuz Link", category: Category::Ui, default: "Ctrl+l", context: Context::None }, - ActionDef { id: "ui.miniPlayer", label_en: "Toggle Mini Player", category: Category::Ui, default: "Shift+M", context: Context::None }, + ActionDef { + id: "ui.sidebar", + label_en: "Toggle Sidebar", + category: Category::Ui, + default: "Shift+S", + context: Context::None, + }, + ActionDef { + id: "ui.focusMode", + label_en: "Immersive Mode", + category: Category::Ui, + default: "Shift+I", + context: Context::None, + }, + ActionDef { + id: "ui.queue", + label_en: "Queue", + category: Category::Ui, + default: "q", + context: Context::None, + }, + ActionDef { + id: "ui.escape", + label_en: "Close / Dismiss", + category: Category::Ui, + default: "Escape", + context: Context::None, + }, + ActionDef { + id: "ui.showShortcuts", + label_en: "Show Shortcuts", + category: Category::Ui, + default: "?", + context: Context::None, + }, + ActionDef { + id: "ui.openLink", + label_en: "Open Qobuz Link", + category: Category::Ui, + default: "Ctrl+l", + context: Context::None, + }, + ActionDef { + id: "ui.miniPlayer", + label_en: "Toggle Mini Player", + category: Category::Ui, + default: "Shift+M", + context: Context::None, + }, // Immersive (contextual) - ActionDef { id: "focus.seekForward", label_en: "Seek Forward (5s)", category: Category::Immersive, default: "ArrowRight", context: Context::Immersive }, - ActionDef { id: "focus.seekBack", label_en: "Seek Back (5s)", category: Category::Immersive, default: "ArrowLeft", context: Context::Immersive }, - ActionDef { id: "focus.seekForwardLong", label_en: "Seek Forward (10s)", category: Category::Immersive, default: "Shift+ArrowRight", context: Context::Immersive }, - ActionDef { id: "focus.seekBackLong", label_en: "Seek Back (10s)", category: Category::Immersive, default: "Shift+ArrowLeft", context: Context::Immersive }, + ActionDef { + id: "focus.seekForward", + label_en: "Seek Forward (5s)", + category: Category::Immersive, + default: "ArrowRight", + context: Context::Immersive, + }, + ActionDef { + id: "focus.seekBack", + label_en: "Seek Back (5s)", + category: Category::Immersive, + default: "ArrowLeft", + context: Context::Immersive, + }, + ActionDef { + id: "focus.seekForwardLong", + label_en: "Seek Forward (10s)", + category: Category::Immersive, + default: "Shift+ArrowRight", + context: Context::Immersive, + }, + ActionDef { + id: "focus.seekBackLong", + label_en: "Seek Back (10s)", + category: Category::Immersive, + default: "Shift+ArrowLeft", + context: Context::Immersive, + }, // Mini Player (contextual — dispatched on the mini window) ActionDef { id: "mini.micro", @@ -646,8 +754,7 @@ pub fn is_ctrl_a(key: i32, modifiers: i32, text: &str) -> bool { if !ctrl { return false; } - token_from_qt_key(key, modifiers, text) - .is_some_and(|tok| tok.eq_ignore_ascii_case("a")) + token_from_qt_key(key, modifiers, text).is_some_and(|tok| tok.eq_ignore_ascii_case("a")) } // ============================================================================ @@ -740,14 +847,35 @@ mod tests { #[test] fn named_keys_come_from_the_key_code() { - assert_eq!(token_from_qt_key(QT_KEY_SPACE, 0, " "), Some("Space".into())); - assert_eq!(token_from_qt_key(QT_KEY_LEFT, 0, ""), Some("ArrowLeft".into())); - assert_eq!(token_from_qt_key(QT_KEY_RIGHT, 0, ""), Some("ArrowRight".into())); + assert_eq!( + token_from_qt_key(QT_KEY_SPACE, 0, " "), + Some("Space".into()) + ); + assert_eq!( + token_from_qt_key(QT_KEY_LEFT, 0, ""), + Some("ArrowLeft".into()) + ); + assert_eq!( + token_from_qt_key(QT_KEY_RIGHT, 0, ""), + Some("ArrowRight".into()) + ); assert_eq!(token_from_qt_key(QT_KEY_UP, 0, ""), Some("ArrowUp".into())); - assert_eq!(token_from_qt_key(QT_KEY_DOWN, 0, ""), Some("ArrowDown".into())); - assert_eq!(token_from_qt_key(QT_KEY_ESCAPE, 0, ""), Some("Escape".into())); - assert_eq!(token_from_qt_key(QT_KEY_RETURN, 0, "\r"), Some("Enter".into())); - assert_eq!(token_from_qt_key(QT_KEY_ENTER, 0, "\r"), Some("Enter".into())); + assert_eq!( + token_from_qt_key(QT_KEY_DOWN, 0, ""), + Some("ArrowDown".into()) + ); + assert_eq!( + token_from_qt_key(QT_KEY_ESCAPE, 0, ""), + Some("Escape".into()) + ); + assert_eq!( + token_from_qt_key(QT_KEY_RETURN, 0, "\r"), + Some("Enter".into()) + ); + assert_eq!( + token_from_qt_key(QT_KEY_ENTER, 0, "\r"), + Some("Enter".into()) + ); assert_eq!(token_from_qt_key(QT_KEY_TAB, 0, "\t"), Some("Tab".into())); assert_eq!( token_from_qt_key(QT_KEY_BACKSPACE, 0, "\u{8}"), @@ -765,7 +893,10 @@ mod tests { // is folded into the glyph — the stored string is "?". let token = token_from_qt_key(0x2f, QT_SHIFT, "?").unwrap(); assert_eq!(token, "?"); - assert_eq!(shortcut_from_parts(false, false, true, &token), Some("?".into())); + assert_eq!( + shortcut_from_parts(false, false, true, &token), + Some("?".into()) + ); } #[test] @@ -911,8 +1042,7 @@ mod tests { #[test] fn groups_split_round_robin_into_three_columns() { - let doc: serde_json::Value = - serde_json::from_str(&groups_json(&no_overrides())).unwrap(); + let doc: serde_json::Value = serde_json::from_str(&groups_json(&no_overrides())).unwrap(); let labels = |col: &str| -> Vec { doc[col] .as_array() @@ -928,8 +1058,7 @@ mod tests { #[test] fn groups_rows_carry_id_label_shortcut_modified_contextual() { - let doc: serde_json::Value = - serde_json::from_str(&groups_json(&no_overrides())).unwrap(); + let doc: serde_json::Value = serde_json::from_str(&groups_json(&no_overrides())).unwrap(); let row = &doc["col1"][0]["rows"][0]; assert_eq!(row["id"], "playback.toggle"); assert_eq!(row["label"], "Play / Pause"); // en fallback (msgid) @@ -973,18 +1102,48 @@ mod tests { } } // Everything open → the link-resolver arm wins (order position 1). - assert_eq!(escape_target(&s(true, true, true, true, true, true, true, true)), EscapeTarget::LinkResolver); + assert_eq!( + escape_target(&s(true, true, true, true, true, true, true, true)), + EscapeTarget::LinkResolver + ); // Then, in order, each surface beats every LATER one. - assert_eq!(escape_target(&s(false, true, true, true, true, true, true, true)), EscapeTarget::Customize); - assert_eq!(escape_target(&s(false, false, true, true, true, true, true, true)), EscapeTarget::Cheatsheet); - assert_eq!(escape_target(&s(false, false, false, true, true, true, true, true)), EscapeTarget::MusicianModal); - assert_eq!(escape_target(&s(false, false, false, false, true, true, true, true)), EscapeTarget::Cortinilla); - assert_eq!(escape_target(&s(false, false, false, false, false, true, true, true)), EscapeTarget::Immersive); - assert_eq!(escape_target(&s(false, false, false, false, false, false, true, true)), EscapeTarget::MultiSelect); - assert_eq!(escape_target(&s(false, false, false, false, false, false, false, true)), EscapeTarget::Queue); - assert_eq!(escape_target(&s(false, false, false, false, false, false, false, false)), EscapeTarget::None); + assert_eq!( + escape_target(&s(false, true, true, true, true, true, true, true)), + EscapeTarget::Customize + ); + assert_eq!( + escape_target(&s(false, false, true, true, true, true, true, true)), + EscapeTarget::Cheatsheet + ); + assert_eq!( + escape_target(&s(false, false, false, true, true, true, true, true)), + EscapeTarget::MusicianModal + ); + assert_eq!( + escape_target(&s(false, false, false, false, true, true, true, true)), + EscapeTarget::Cortinilla + ); + assert_eq!( + escape_target(&s(false, false, false, false, false, true, true, true)), + EscapeTarget::Immersive + ); + assert_eq!( + escape_target(&s(false, false, false, false, false, false, true, true)), + EscapeTarget::MultiSelect + ); + assert_eq!( + escape_target(&s(false, false, false, false, false, false, false, true)), + EscapeTarget::Queue + ); + assert_eq!( + escape_target(&s(false, false, false, false, false, false, false, false)), + EscapeTarget::None + ); // Multi-select beats the queue (order position 7 vs 8). - assert_eq!(escape_target(&s(false, false, false, false, false, false, true, true)), EscapeTarget::MultiSelect); + assert_eq!( + escape_target(&s(false, false, false, false, false, false, true, true)), + EscapeTarget::MultiSelect + ); } /// The musician modal is opened FROM the immersive Track-info panel @@ -1034,7 +1193,9 @@ mod tests { let key_w = QT_KEY_A + 22; assert_eq!( capture_step(&overrides, "ui.queue", key_w, 0, "w"), - CaptureOutcome::Bound { shortcut: "w".into() } + CaptureOutcome::Bound { + shortcut: "w".into() + } ); } diff --git a/crates/qbz-qt/src/immersive_bridge.rs b/crates/qbz-qt/src/immersive_bridge.rs index 2edf7d94f..d246dc0c8 100644 --- a/crates/qbz-qt/src/immersive_bridge.rs +++ b/crates/qbz-qt/src/immersive_bridge.rs @@ -87,7 +87,6 @@ pub mod qbz_immersive { #[qproperty(f64, imm_search_scroll_y)] // Two-way with the search field. #[qproperty(QString, search_input_text)] - type QbzImmersive = super::QbzImmersiveRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -288,7 +287,8 @@ fn apply_open(mut this: Pin<&mut QbzImmersive>, value: bool) { // numbers; pref_i32 is the compatible reader — delivery notes.) let vm = crate::settings_qt::pref_i32("immersive_last_view_mode", DEFAULT_VIEW_MODE); let m = crate::settings_qt::pref_i32("immersive_last_mode", DEFAULT_MODE); - let sp = crate::settings_qt::pref_i32("immersive_last_split_panel", DEFAULT_SPLIT_PANEL); + let sp = + crate::settings_qt::pref_i32("immersive_last_split_panel", DEFAULT_SPLIT_PANEL); this.as_mut().set_view_mode(vm); this.as_mut().set_mode(m); this.as_mut().set_split_panel(sp); @@ -428,7 +428,10 @@ mod tests { // Owner ruling 2026-08-15: with no remembered triple, the // remember-restore fallbacks AND the bridge construction default land // on viewMode=1 (SPLIT), splitPanel=0 (Lyrics) — never a FOCUS panel. - assert_eq!((DEFAULT_VIEW_MODE, DEFAULT_MODE, DEFAULT_SPLIT_PANEL), (1, 0, 0)); + assert_eq!( + (DEFAULT_VIEW_MODE, DEFAULT_MODE, DEFAULT_SPLIT_PANEL), + (1, 0, 0) + ); assert_eq!(QbzImmersiveRust::default().view_mode, 1); assert_eq!(QbzImmersiveRust::default().split_panel, 0); } diff --git a/crates/qbz-qt/src/integrations_qt.rs b/crates/qbz-qt/src/integrations_qt.rs index 5cbf9e85d..57a3bf065 100644 --- a/crates/qbz-qt/src/integrations_qt.rs +++ b/crates/qbz-qt/src/integrations_qt.rs @@ -69,8 +69,8 @@ static DISCOVER: OnceLock>> = OnceLock::new(); fn with_discover(f: impl FnOnce(&DiscoverPrefsStore) -> T) -> Option { let cell = DISCOVER.get_or_init(|| { - let store = crate::sidebar_qt::user_dir() - .and_then(|dir| DiscoverPrefsStore::new_at(&dir).ok()); + let store = + crate::sidebar_qt::user_dir().and_then(|dir| DiscoverPrefsStore::new_at(&dir).ok()); if store.is_none() { log::warn!("[qbz-qt] discover prefs store unavailable"); } @@ -407,7 +407,10 @@ pub async fn listenbrainz_set_token(token: &str) { }) .await; } - set_status(qbz_i18n::t_args("Connected as {}", &[info.user_name.as_str()]), 2); + set_status( + qbz_i18n::t_args("Connected as {}", &[info.user_name.as_str()]), + 2, + ); } Err(e) => set_status(qbz_i18n::t_args("Error: {}", &[&e.to_string()]), 3), } @@ -697,7 +700,12 @@ async fn send_now_playing(meta: &ScrobbleMeta, cfg: &ScrobblerSettings) { ) .await; if let Err(e) = client - .submit_playing_now(&meta.artist, &meta.track, album, lb_info(meta.duration_secs)) + .submit_playing_now( + &meta.artist, + &meta.track, + album, + lb_info(meta.duration_secs), + ) .await { log::debug!("[qbz-qt] ListenBrainz now-playing failed: {e}"); @@ -725,7 +733,11 @@ async fn send_scrobble(meta: &ScrobbleMeta) { .await { Ok(()) => { - log::info!("[qbz-qt] Last.fm scrobbled: {} - {}", meta.artist, meta.track); + log::info!( + "[qbz-qt] Last.fm scrobbled: {} - {}", + meta.artist, + meta.track + ); true } Err(e) => { diff --git a/crates/qbz-qt/src/kiosk_nav_bridge.rs b/crates/qbz-qt/src/kiosk_nav_bridge.rs index f4e35c3b7..93044794b 100644 --- a/crates/qbz-qt/src/kiosk_nav_bridge.rs +++ b/crates/qbz-qt/src/kiosk_nav_bridge.rs @@ -61,7 +61,6 @@ pub mod qbz_kiosk_nav_bridge { #[qproperty(i32, dir)] /// Enter pulse for conditionally-mounted content views. #[qproperty(i32, activate_seq)] - type QbzKioskNav = super::QbzKioskNavRust; /// Arrow keys, from the kiosk shell's root key handler. diff --git a/crates/qbz-qt/src/kiosk_nav_qt.rs b/crates/qbz-qt/src/kiosk_nav_qt.rs index 80c13a8c4..6affce88a 100644 --- a/crates/qbz-qt/src/kiosk_nav_qt.rs +++ b/crates/qbz-qt/src/kiosk_nav_qt.rs @@ -366,7 +366,11 @@ mod tests { for _ in 0..10 { m.right(); } - assert!(m.index < m.count, "never past the data ({before} -> {})", m.index); + assert!( + m.index < m.count, + "never past the data ({before} -> {})", + m.index + ); } #[test] @@ -387,7 +391,10 @@ mod tests { m.right(); } // index 10, column 5 m.up(); - assert_eq!(m.index, 4, "min(tabs - 1, column) with 5 tabs and 6 columns"); + assert_eq!( + m.index, 4, + "min(tabs - 1, column) with 5 tabs and 6 columns" + ); } #[test] diff --git a/crates/qbz-qt/src/label_qt.rs b/crates/qbz-qt/src/label_qt.rs index b18fd92f7..ebef64d2a 100644 --- a/crates/qbz-qt/src/label_qt.rs +++ b/crates/qbz-qt/src/label_qt.rs @@ -303,7 +303,9 @@ fn fetch_label(generation: u64, id: u64) { c.id.as_deref() .map(|id| { let id = id.to_lowercase(); - id.contains("award") || id.contains("critic") || id.contains("press") + id.contains("award") + || id.contains("critic") + || id.contains("press") }) .unwrap_or(false) }) @@ -632,9 +634,7 @@ pub fn enqueue_top(mode: String) { } let runtime = crate::app(); crate::spawn(async move { - if let Err(e) = - crate::playback_qt::enqueue_track_list_mode(&runtime, queue, &mode).await - { + if let Err(e) = crate::playback_qt::enqueue_track_list_mode(&runtime, queue, &mode).await { log::error!("[qbz-qt] label enqueue-top ({mode}) failed: {e}"); } }); @@ -1152,7 +1152,9 @@ fn parse_artist_image(raw: &Value) -> String { portrait.get("hash").and_then(|v| v.as_str()), portrait.get("format").and_then(|v| v.as_str()), ) { - return format!("https://static.qobuz.com/images/artists/covers/medium/{hash}.{format}"); + return format!( + "https://static.qobuz.com/images/artists/covers/medium/{hash}.{format}" + ); } } String::new() @@ -1289,8 +1291,15 @@ fn parse_top_track(index: usize, raw: &Value) -> TrackRow { }) .unwrap_or(true); TrackRow { - is_favorite: id.parse::().map(crate::fav_cache_qt::contains_track).unwrap_or(false), - cache_status: if crate::offline_qt::is_cached(&id) { 3 } else { 0 }, + is_favorite: id + .parse::() + .map(crate::fav_cache_qt::contains_track) + .unwrap_or(false), + cache_status: if crate::offline_qt::is_cached(&id) { + 3 + } else { + 0 + }, id, number: (index + 1).to_string(), title, @@ -1327,7 +1336,10 @@ fn parse_playlist(raw: &Value) -> HomeCard { .filter(|s| !s.is_empty()) .unwrap_or("Qobuz") .to_string(); - let track_count = raw.get("tracks_count").and_then(|v| v.as_u64()).unwrap_or(0); + let track_count = raw + .get("tracks_count") + .and_then(|v| v.as_u64()) + .unwrap_or(0); let id = raw.get("id").map(value_to_string).unwrap_or_default(); let pid = id.parse::().ok(); HomeCard { @@ -1339,7 +1351,9 @@ fn parse_playlist(raw: &Value) -> HomeCard { // raw JSON with no owner block worth trusting, so ownership is // resolved by ID against the user's own playlist list rather than by // comparing an owner field that may not be there. - is_favorite: pid.map(crate::fav_cache_qt::is_playlist_favorite).unwrap_or(false), + is_favorite: pid + .map(crate::fav_cache_qt::is_playlist_favorite) + .unwrap_or(false), playlist_owned: pid.map(crate::playlist_qt::is_owned).unwrap_or(false), playlist_following: pid.map(crate::playlist_qt::is_following).unwrap_or(false), id, @@ -1409,8 +1423,15 @@ fn parse_more_labels(resp: &qbz_models::LabelExploreResponse, current: u64) -> V fn track_row_to_queue(row: &TrackRow) -> QueueTrack { let duration_secs = { let mut parts = row.duration.split(':'); - parts.next().and_then(|m| m.parse::().ok()).unwrap_or(0) * 60 - + parts.next().and_then(|s| s.parse::().ok()).unwrap_or(0) + parts + .next() + .and_then(|m| m.parse::().ok()) + .unwrap_or(0) + * 60 + + parts + .next() + .and_then(|s| s.parse::().ok()) + .unwrap_or(0) }; QueueTrack { id: row.id.parse().unwrap_or(0), @@ -1525,10 +1546,7 @@ mod tests { #[test] fn sorting_uses_the_plain_year() { - let mut items = vec![ - card("A", "X", "1999", "cd"), - card("B", "Y", "2021", "cd"), - ]; + let mut items = vec![card("A", "X", "1999", "cd"), card("B", "Y", "2021", "cd")]; sort_cards(&mut items, "newest"); assert_eq!(items[0].year, "2021"); sort_cards(&mut items, "oldest"); diff --git a/crates/qbz-qt/src/library_bridge.rs b/crates/qbz-qt/src/library_bridge.rs index 9a8ba59e7..f91f4ebba 100644 --- a/crates/qbz-qt/src/library_bridge.rs +++ b/crates/qbz-qt/src/library_bridge.rs @@ -46,7 +46,6 @@ pub mod qbz_library_bridge { // (library_sidepanel.rs). Its own document, never folded into // `library_json`: a selection must not re-serialize a 10k-row feed. #[qproperty(QString, sidepanel_json)] - type QbzLibrary = super::QbzLibraryRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -127,7 +126,14 @@ pub mod qbz_library_bridge { /// AlbumCard pin badge: toggle pin (album/artist/playlist). #[qinvokable] - fn toggle_pin(self: Pin<&mut QbzLibrary>, kind: QString, id: QString, title: QString, subtitle: QString, artwork_url: QString); + fn toggle_pin( + self: Pin<&mut QbzLibrary>, + kind: QString, + id: QString, + title: QString, + subtitle: QString, + artwork_url: QString, + ); /// Emitted after a pin toggle (`{kind}:{id}` key like artKey). #[qsignal] fn pin_changed(self: Pin<&mut QbzLibrary>, key: QString, value: bool); @@ -250,7 +256,14 @@ impl qbz_library_bridge::QbzLibrary { crate::library_qt::play_visible_all(visible_ids_json.to_string(), shuffle); } - pub fn toggle_pin(self: Pin<&mut Self>, kind: QString, id: QString, title: QString, subtitle: QString, artwork_url: QString) { + pub fn toggle_pin( + self: Pin<&mut Self>, + kind: QString, + id: QString, + title: QString, + subtitle: QString, + artwork_url: QString, + ) { crate::toggle_pin( kind.to_string(), id.to_string(), diff --git a/crates/qbz-qt/src/local_bulk.rs b/crates/qbz-qt/src/local_bulk.rs index e8c42dfb0..894cd68e3 100644 --- a/crates/qbz-qt/src/local_bulk.rs +++ b/crates/qbz-qt/src/local_bulk.rs @@ -51,8 +51,7 @@ fn publish_selection() { let json = lib::to_json(&tree::tree_visible()); let count = tree::tree_selected_count(); ui(move |mut b| { - b.as_mut() - .set_local_tree_json(QString::from(json.as_str())); + b.as_mut().set_local_tree_json(QString::from(json.as_str())); b.as_mut().set_local_tree_selected_count(count); }); } @@ -71,7 +70,8 @@ pub fn set_select_mode(on: bool) { /// Folder checkbox: recursive, so it is a DB read on a blocking thread. pub fn toggle_folder_select(path: String) { crate::spawn(async move { - let _ = tokio::task::spawn_blocking(move || tree::toggle_folder_select_blocking(&path)).await; + let _ = + tokio::task::spawn_blocking(move || tree::toggle_folder_select_blocking(&path)).await; publish_selection(); }); } @@ -80,7 +80,8 @@ pub fn toggle_folder_select(path: String) { /// from the parent folder listing — blocking either way. pub fn toggle_track_select(path: String) { crate::spawn(async move { - let _ = tokio::task::spawn_blocking(move || tree::toggle_track_select_blocking(&path)).await; + let _ = + tokio::task::spawn_blocking(move || tree::toggle_track_select_blocking(&path)).await; publish_selection(); }); } @@ -152,7 +153,10 @@ pub fn bulk_action(scope: String, ids_json: String, action: String) { /// last resort for a local id that scrolled out of the cached page. fn resolve_blocking(scope: &str, ids: &[String]) -> Vec { if scope == "album" { - return ids.iter().flat_map(|key| fetch_album_tracks_blocking(key)).collect(); + return ids + .iter() + .flat_map(|key| fetch_album_tracks_blocking(key)) + .collect(); } let cached: HashMap = state(|s| { s.tracks_raw diff --git a/crates/qbz-qt/src/local_playlist_qt.rs b/crates/qbz-qt/src/local_playlist_qt.rs index a7da97c29..5178982c2 100644 --- a/crates/qbz-qt/src/local_playlist_qt.rs +++ b/crates/qbz-qt/src/local_playlist_qt.rs @@ -91,9 +91,11 @@ pub fn list_blocking() -> Vec { } pub fn get_blocking(id: &str) -> Option { - with_db(false, |db| Ok(db.with_connection(|conn| repo::get(conn, id)))) - .and_then(|r| r.ok()) - .flatten() + with_db(false, |db| { + Ok(db.with_connection(|conn| repo::get(conn, id))) + }) + .and_then(|r| r.ok()) + .flatten() } pub fn get_tracks_blocking(id: &str) -> Vec { @@ -104,7 +106,11 @@ pub fn get_tracks_blocking(id: &str) -> Vec { .unwrap_or_default() } -pub fn create_blocking(name: &str, description: Option<&str>, offline_only: bool) -> Option { +pub fn create_blocking( + name: &str, + description: Option<&str>, + offline_only: bool, +) -> Option { with_db(true, |db| { Ok(db.with_connection(|conn| repo::create(conn, name, description, offline_only))) }) @@ -392,10 +398,7 @@ async fn ready_cached_rows(track_ids: &[u64]) -> HashMap { let Ok(Some(info)) = db.get_track(*track_id) else { continue; }; - if !matches!( - info.status, - qbz_offline_cache::OfflineCacheStatus::Ready - ) { + if !matches!(info.status, qbz_offline_cache::OfflineCacheStatus::Ready) { continue; } let artwork_path = info.resolve_cover_path(&cache_path); @@ -695,15 +698,10 @@ pub(crate) fn row_to_display(item: &RowItem) -> (PlaylistTrackRow, Option (PlaylistTrackRow, Option (PlaylistTrackRow, Option ( - Vec, - Vec, - HashMap, -) { +) -> (Vec, Vec, HashMap) { let mut display = Vec::with_capacity(rows.len()); let mut queue = Vec::new(); let mut positions = HashMap::new(); @@ -1104,12 +1094,8 @@ pub async fn load(runtime: &Runtime, playlist_id: &str) -> bool { // The DB column survives as a READ fallback so a cover stored there by an // earlier build is not silently lost; `clear_custom_artwork_blocking` is // what lets Remove clear one of those. - let custom_cover = crate::cover_artwork_qt::playlist_cover(&id).or_else(|| { - header - .custom_artwork_path - .clone() - .filter(|p| !p.is_empty()) - }); + let custom_cover = crate::cover_artwork_qt::playlist_cover(&id) + .or_else(|| header.custom_artwork_path.clone().filter(|p| !p.is_empty())); let doc = PlaylistDoc { id: header.id.clone(), @@ -1265,10 +1251,7 @@ pub async fn play_shuffled(runtime: &Runtime) { async fn play_in(runtime: &Runtime, start_row_id: &str, shuffle: bool) { let (queue, offline_only, playlist_id) = { let Ok(q) = CURRENT_QUEUE.lock() else { return }; - let meta = CURRENT_META - .lock() - .ok() - .and_then(|m| m.clone()); + let meta = CURRENT_META.lock().ok().and_then(|m| m.clone()); let offline_only = meta.as_ref().map(|(_, o)| *o).unwrap_or(false); let playlist_id = meta.map(|(id, _)| id).unwrap_or_default(); (q.clone(), offline_only, playlist_id) @@ -1443,7 +1426,11 @@ pub async fn reorder_row(runtime: &Runtime, from_row: usize, to_slot: usize) { if from_row >= ids.len() || to_slot > ids.len() { return; } - let anchor = if to_slot > from_row { to_slot - 1 } else { to_slot }; + let anchor = if to_slot > from_row { + to_slot - 1 + } else { + to_slot + }; if anchor == from_row { return; } diff --git a/crates/qbz-qt/src/local_tree.rs b/crates/qbz-qt/src/local_tree.rs index 27678f27d..1acaa083f 100644 --- a/crates/qbz-qt/src/local_tree.rs +++ b/crates/qbz-qt/src/local_tree.rs @@ -20,7 +20,9 @@ use serde::Serialize; use qbz_source::SourceId; -use crate::local_rows::{art_token, basename, folder_key, map_track, FolderDetail, SubfolderRow, TrackRow, TreeNode}; +use crate::local_rows::{ + art_token, basename, folder_key, map_track, FolderDetail, SubfolderRow, TrackRow, TreeNode, +}; use crate::local_state::{state, with_art, with_db}; /// `list_folder_children` + the reference's on-disk cover fallback for the @@ -153,7 +155,12 @@ pub fn tree_collapse(path: &str) -> Vec { /// preload). pub fn tree_expand_blocking(path: &str) -> Vec { let children = folder_children_with_covers(path); - let nodes = state(|s| s.tree.iter().position(|n| n.path == path).map(|pos| (pos, s.tree[pos].depth))); + let nodes = state(|s| { + s.tree + .iter() + .position(|n| n.path == path) + .map(|pos| (pos, s.tree[pos].depth)) + }); let Some((pos, depth)) = nodes else { return state(|s| s.tree.clone()); }; @@ -204,7 +211,9 @@ fn visible_nodes() -> Vec { .iter() .filter(|n| { n.segment.to_lowercase().contains(q) - || matches.iter().any(|m| m.starts_with(&format!("{}/", n.path))) + || matches + .iter() + .any(|m| m.starts_with(&format!("{}/", n.path))) }) .cloned() .collect() @@ -413,10 +422,10 @@ pub fn load_folder_detail_blocking(path: &str) -> FolderDetail { let key = folder_key(path); if let Some(a) = artwork.as_ref().filter(|a| !a.is_empty()) { // The folder tree is `library.db`: every cover here is a - // local file. - if let Some(t) = art_token(Some("local"), a) { - art.insert(key.clone(), t); - } + // local file. + if let Some(t) = art_token(Some("local"), a) { + art.insert(key.clone(), t); + } } Some(SubfolderRow { path: path.clone(), diff --git a/crates/qbz-qt/src/log_viewer_qt.rs b/crates/qbz-qt/src/log_viewer_qt.rs index 783762103..1d60eaf12 100644 --- a/crates/qbz-qt/src/log_viewer_qt.rs +++ b/crates/qbz-qt/src/log_viewer_qt.rs @@ -126,7 +126,10 @@ fn snapshot_doc() -> Doc { auto_tail: AUTO_TAIL.load(Ordering::SeqCst), uploading: UPLOADING.load(Ordering::SeqCst), copied: COPIED.load(Ordering::SeqCst), - uploaded_url: UPLOADED_URL.lock().unwrap_or_else(|e| e.into_inner()).clone(), + uploaded_url: UPLOADED_URL + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(), } } @@ -292,7 +295,10 @@ pub async fn upload() { } pub fn copy_url() { - let url = UPLOADED_URL.lock().unwrap_or_else(|e| e.into_inner()).clone(); + let url = UPLOADED_URL + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(); if url.is_empty() { return; } diff --git a/crates/qbz-qt/src/lyrics_qt.rs b/crates/qbz-qt/src/lyrics_qt.rs index dc68cafdd..cfed8a602 100644 --- a/crates/qbz-qt/src/lyrics_qt.rs +++ b/crates/qbz-qt/src/lyrics_qt.rs @@ -100,7 +100,6 @@ pub mod qbz_lyrics_bridge { // Inline notice line (this shell has no toast host): translation // failures and the copy confirmation land here. "" = hidden. #[qproperty(QString, notice)] - // --- Display prefs (Slint LyricsState S5 block) ------------------- // Persisted per user in /qbz/users//lyrics_prefs.json — // the SAME file and field values the Slint build writes, so a user @@ -120,7 +119,6 @@ pub mod qbz_lyrics_bridge { // Active (karaoke fill) color: false = follow the theme accent. #[qproperty(bool, use_custom_color)] #[qproperty(QString, custom_color)] - type QbzLyrics = super::QbzLyricsRust; /// Registers this object's Qt-thread hop and seeds the persisted @@ -355,7 +353,9 @@ impl qbz_lyrics_bridge::QbzLyrics { // with the new language (the Slint build reaches the same result via // the next track change; here it is immediate). let language_index = prefs.translation_language_index(); - if translation_enabled() && language_index != LAST_LANGUAGE.swap(language_index, Ordering::SeqCst) { + if translation_enabled() + && language_index != LAST_LANGUAGE.swap(language_index, Ordering::SeqCst) + { self.as_mut().set_translation_busy(true); let pref = prefs.translation_language.clone(); crate::spawn(async move { @@ -740,7 +740,8 @@ fn publish(doc: LyricsDocJson, translation_available: bool, show_translation: bo // by the shell bridge; other surfaces may still bind it). let shell_json = json.clone(); crate::shell_bridge::ui(move |mut b| { - b.as_mut().set_lyrics_json(QString::from(shell_json.as_str())); + b.as_mut() + .set_lyrics_json(QString::from(shell_json.as_str())); }); ui(move |mut b| { b.as_mut().set_doc_json(QString::from(json.as_str())); @@ -1061,4 +1062,3 @@ async fn enable_translation(pref: String) { } } } - diff --git a/crates/qbz-qt/src/macos_chrome.rs b/crates/qbz-qt/src/macos_chrome.rs index f214d1d07..f156f9979 100644 --- a/crates/qbz-qt/src/macos_chrome.rs +++ b/crates/qbz-qt/src/macos_chrome.rs @@ -175,7 +175,11 @@ pub(crate) fn apply_and_center() -> bool { .unwrap_or(false); let original = container.frame(); let mut frame = original; - frame.origin.y += if parent_flipped { move_down } else { -move_down }; + frame.origin.y += if parent_flipped { + move_down + } else { + -move_down + }; unsafe { container.setFrame(frame) }; // Re-measure: if the shift landed FURTHER from target than where it diff --git a/crates/qbz-qt/src/mini_bridge.rs b/crates/qbz-qt/src/mini_bridge.rs index df4120b20..46f886751 100644 --- a/crates/qbz-qt/src/mini_bridge.rs +++ b/crates/qbz-qt/src/mini_bridge.rs @@ -72,7 +72,6 @@ pub mod qbz_mini { // binding and the first frame is already too late for a post-boot push. #[qproperty(f32, mini_width)] #[qproperty(f32, mini_height)] - type QbzMini = super::QbzMiniRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain @@ -101,7 +100,6 @@ pub mod qbz_mini { #[qinvokable] fn toggle_background_blur(self: Pin<&mut QbzMini>); - /// Persist the expanded window size (contract A-14). Qt has no /// `WindowEvent::Resized`, so `MiniWindow.qml`'s 400 ms debounce calls /// in. The §4.7 guards live in `mini_qt::geometry_to_persist` — the @@ -153,12 +151,7 @@ pub mod qbz_mini { /// `text_input_focused` argument** — the mini has no text fields, and /// `dispatch_mini` has no text-input guard either. `true` = consumed. #[qinvokable] - fn key_pressed( - self: Pin<&mut QbzMini>, - key: i32, - modifiers: i32, - text: QString, - ) -> bool; + fn key_pressed(self: Pin<&mut QbzMini>, key: i32, modifiers: i32, text: QString) -> bool; } // The custom WRITE targets. NOT qinvokables — QML reaches them by writing @@ -386,7 +379,6 @@ impl qbz_mini::QbzMini { crate::mini_qt::save_background_blur(value); } - /// Contract A-14 — the debounced resize persist. /// /// Three things happen here and the ORDER matters. The §4.7 guards decide @@ -484,8 +476,8 @@ impl qbz_mini::QbzMini { crate::spawn(async move { if let Some(svc) = crate::qconnect_qt::service() { match svc.play_remote_renderer_track_if_active(track_id).await { - Ok(true) => return, // handled by the peer - Ok(false) => {} // not applicable -> local path + Ok(true) => return, // handled by the peer + Ok(false) => {} // not applicable -> local path Err(e) => { log::warn!("[qbz-qt] mini: queue play remote handoff: {e}"); return; // connected but errored -> do NOT play locally @@ -529,12 +521,7 @@ impl qbz_mini::QbzMini { /// reads the user's ACTIVE bindings, so a rebound `mini.queue` works here /// exactly as the reference's `dispatch_mini` intends (`keybindings.rs:631`). /// A matched action consumes (`:647`); anything else propagates (`:645`). - pub fn key_pressed( - self: Pin<&mut Self>, - key: i32, - modifiers: i32, - text: QString, - ) -> bool { + pub fn key_pressed(self: Pin<&mut Self>, key: i32, modifiers: i32, text: QString) -> bool { let text = text.to_string(); let overrides = crate::hotkeys_qt::load_overrides(); let Some(action) = diff --git a/crates/qbz-qt/src/mini_qt.rs b/crates/qbz-qt/src/mini_qt.rs index 815815bd8..c536ef08f 100644 --- a/crates/qbz-qt/src/mini_qt.rs +++ b/crates/qbz-qt/src/mini_qt.rs @@ -230,7 +230,6 @@ pub(crate) fn initial_background_blur() -> bool { crate::settings_qt::pref_bool(KEY_BACKGROUND_BLUR, false) } - pub(crate) fn initial_width() -> f32 { crate::settings_qt::pref_f32(KEY_WIDTH, DEFAULT_WIDTH) } @@ -320,12 +319,11 @@ mod tests { let upcoming: Vec = (1..=60) .map(|i| track(100 + i, &format!("up-{i}"), None, 187, false)) .collect(); - let doc: serde_json::Value = - serde_json::from_str(&mini_queue_doc(&state( - Some(track(7, "Now", Some("Remastered"), 187, true)), - upcoming.clone(), - ))) - .unwrap(); + let doc: serde_json::Value = serde_json::from_str(&mini_queue_doc(&state( + Some(track(7, "Now", Some("Remastered"), 187, true)), + upcoming.clone(), + ))) + .unwrap(); let rows = doc["rows"].as_array().unwrap(); assert_eq!(rows.len(), 61); diff --git a/crates/qbz-qt/src/musician_bridge.rs b/crates/qbz-qt/src/musician_bridge.rs index 417f0acba..16554d6d2 100644 --- a/crates/qbz-qt/src/musician_bridge.rs +++ b/crates/qbz-qt/src/musician_bridge.rs @@ -56,7 +56,6 @@ pub mod qbz_musician_bridge { /// (resolving the same musician twice), the same trick /// `QbzSession.trRev` plays. #[qproperty(i32, musician_rev)] - type QbzMusician = super::QbzMusicianRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/musician_qt.rs b/crates/qbz-qt/src/musician_qt.rs index f4bb92fcc..ff436b22e 100644 --- a/crates/qbz-qt/src/musician_qt.rs +++ b/crates/qbz-qt/src/musician_qt.rs @@ -445,7 +445,9 @@ fn store_bands(mbid: &str, bands: &[BandRow]) { /// air for the same MBID. fn claim_inflight(mbid: &str) -> bool { let mut guard = BANDS_INFLIGHT.lock().unwrap(); - guard.get_or_insert_with(HashSet::new).insert(mbid.to_string()) + guard + .get_or_insert_with(HashSet::new) + .insert(mbid.to_string()) } fn release_inflight(mbid: &str) { @@ -735,9 +737,12 @@ pub fn open_band(mbid: String) { // since when it is open it is the one the user is clicking in. let name = { let guard = MODAL.lock().unwrap(); - guard - .as_ref() - .and_then(|m| m.bands.iter().find(|b| b.mbid == mbid).map(|b| b.name.clone())) + guard.as_ref().and_then(|m| { + m.bands + .iter() + .find(|b| b.mbid == mbid) + .map(|b| b.name.clone()) + }) } .or_else(|| { with_state(|s| { diff --git a/crates/qbz-qt/src/myqbz_add_bridge.rs b/crates/qbz-qt/src/myqbz_add_bridge.rs index c99c9f8b4..5036140fe 100644 --- a/crates/qbz-qt/src/myqbz_add_bridge.rs +++ b/crates/qbz-qt/src/myqbz_add_bridge.rs @@ -43,7 +43,6 @@ pub mod qbz_myqbz_add_bridge { // ONE JSON document (myqbz_add_qt.rs AddDoc: open/loading flags, header // strings, the create sub-panel and the collection rows). #[qproperty(QString, add_json)] - type QbzMyQbzAdd = super::QbzMyQbzAddRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/myqbz_add_qt.rs b/crates/qbz-qt/src/myqbz_add_qt.rs index 92a60f6fa..814f54b90 100644 --- a/crates/qbz-qt/src/myqbz_add_qt.rs +++ b/crates/qbz-qt/src/myqbz_add_qt.rs @@ -75,10 +75,7 @@ static PENDING: LazyLock>> = LazyLock::new(|| Mutex::new(Vec: /// Snapshot of the pending items (a clone — the store is cleared on close, not /// on read, so a failed write can be retried from the same open modal). fn pending_snapshot() -> Vec { - PENDING - .lock() - .unwrap_or_else(|e| e.into_inner()) - .clone() + PENDING.lock().unwrap_or_else(|e| e.into_inner()).clone() } fn item_type_from_str(s: &str) -> ItemType { @@ -381,8 +378,8 @@ pub struct LoadedRow { pub(crate) fn load_rows(accepts: Accepts, items: &[AddItem]) -> Vec { crate::library_db_qt::with_db(false, |db| { Ok(db.with_connection(|conn| { - let mut cols: Vec = - qbz_mixtape::repo::list_collections(conn, None).unwrap_or_else(|e| { + let mut cols: Vec = qbz_mixtape::repo::list_collections(conn, None) + .unwrap_or_else(|e| { log::warn!("[qbz-qt] myqbz_add list_collections failed: {e}"); Vec::new() }); @@ -612,7 +609,9 @@ pub(crate) fn show_create(kind: &str) { accepts.create_fallback() }; let Some(resolved) = resolved else { - log::warn!("[qbz-qt] myqbz_add: create '{kind}' refused — nothing is creatable for this payload"); + log::warn!( + "[qbz-qt] myqbz_add: create '{kind}' refused — nothing is creatable for this payload" + ); return; }; if resolved != wanted { diff --git a/crates/qbz-qt/src/myqbz_bridge.rs b/crates/qbz-qt/src/myqbz_bridge.rs index b86e0c286..529e90c6a 100644 --- a/crates/qbz-qt/src/myqbz_bridge.rs +++ b/crates/qbz-qt/src/myqbz_bridge.rs @@ -62,7 +62,6 @@ pub mod qbz_myqbz_bridge { #[qproperty(QString, mix_json)] // --- Nav branding (label + custom icon), seeded at construction ----- #[qproperty(QString, branding_json)] - type QbzMyQbz = super::QbzMyQbzRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/myqbz_builder_fetch_qt.rs b/crates/qbz-qt/src/myqbz_builder_fetch_qt.rs index fb9d6f839..9da6dc272 100644 --- a/crates/qbz-qt/src/myqbz_builder_fetch_qt.rs +++ b/crates/qbz-qt/src/myqbz_builder_fetch_qt.rs @@ -155,7 +155,8 @@ pub fn fetch_local_and_plex(artist_name: &str) -> Vec { // (qbz/src/local_library.rs). This port has no equivalent and every // other call site hardcodes `false` (local_albums.rs:42,70,94,173); // the builder must see the SAME set the Albums tab shows. - /* exclude_network_folders */ false, + /* exclude_network_folders */ + false, plex_path.as_deref(), remote_path.as_deref(), &remote_words, @@ -234,7 +235,11 @@ mod tests { #[test] fn matches_artist_is_case_insensitive_over_both_columns() { assert!(matches_artist("Radiohead", "", "radiohead")); - assert!(matches_artist("Various", "Foo, Radiohead , Bar", "RADIOHEAD")); + assert!(matches_artist( + "Various", + "Foo, Radiohead , Bar", + "RADIOHEAD" + )); assert!(!matches_artist("Radioheads", "", "radiohead")); assert!(!matches_artist("Radiohead", "", " ")); } diff --git a/crates/qbz-qt/src/myqbz_builder_qt.rs b/crates/qbz-qt/src/myqbz_builder_qt.rs index 97e848edf..c29d8552f 100644 --- a/crates/qbz-qt/src/myqbz_builder_qt.rs +++ b/crates/qbz-qt/src/myqbz_builder_qt.rs @@ -247,8 +247,7 @@ pub(crate) fn normalize_title(title: &str) -> String { let ch = bytes[i] as char; if ch == '(' || ch == '[' { let inner = lower[i + 1..].trim_start(); - let starts_kw = - KEYWORDS.iter().any(|k| inner.starts_with(k)) || starts_with_nth(inner); + let starts_kw = KEYWORDS.iter().any(|k| inner.starts_with(k)) || starts_with_nth(inner); if starts_kw { cut = i; break; @@ -294,8 +293,7 @@ pub(crate) fn classify_release( let l = title.to_lowercase(); let rt = qobuz_release_type.unwrap_or(""); let gt = qobuz_group_type.unwrap_or(""); - let type_is = - |needle: &str| rt.eq_ignore_ascii_case(needle) || gt.eq_ignore_ascii_case(needle); + let type_is = |needle: &str| rt.eq_ignore_ascii_case(needle) || gt.eq_ignore_ascii_case(needle); if rt.to_lowercase().contains("compilation") || gt.to_lowercase().contains("compilation") @@ -819,24 +817,20 @@ pub fn open(artist_id: String) { Ok((qobuz, artist_name, avatar_url)) => { // 2. Local + Plex by the resolved name (sequential, mandatory). let name_for_local = artist_name.clone(); - let mut local = tokio::task::spawn_blocking(move || { - fetch_local_and_plex(&name_for_local) - }) - .await - .unwrap_or_default(); + let mut local = + tokio::task::spawn_blocking(move || fetch_local_and_plex(&name_for_local)) + .await + .unwrap_or_default(); // 2b. Plex cold-start retry: if Plex is enabled and we got // nothing from the Plex source, wait 2s and refetch once. - if crate::local_plex::is_enabled() - && !local.iter().any(|c| c.source == "plex") - { + if crate::local_plex::is_enabled() && !local.iter().any(|c| c.source == "plex") { tokio::time::sleep(std::time::Duration::from_secs(2)).await; let name_retry = artist_name.clone(); - let retried = tokio::task::spawn_blocking(move || { - fetch_local_and_plex(&name_retry) - }) - .await - .unwrap_or_default(); + let retried = + tokio::task::spawn_blocking(move || fetch_local_and_plex(&name_retry)) + .await + .unwrap_or_default(); if retried.iter().any(|c| c.source == "plex") { local = retried; } @@ -850,8 +844,7 @@ pub fn open(artist_id: String) { // 4. Fetch the avatar if it was not already cached, then ONE // republish (T17 — never one per image). - if !avatar_url.is_empty() - && crate::artwork_qt::cached_path(&avatar_url).is_empty() + if !avatar_url.is_empty() && crate::artwork_qt::cached_path(&avatar_url).is_empty() { crate::artwork_qt::download_missing(vec![avatar_url.clone()]).await; let path = crate::artwork_qt::cached_path(&avatar_url); @@ -1114,12 +1107,18 @@ mod tests { #[test] fn normalize_title_strips_edition_suffixes() { - assert_eq!(normalize_title("OK Computer (Deluxe Edition)"), "ok computer"); + assert_eq!( + normalize_title("OK Computer (Deluxe Edition)"), + "ok computer" + ); assert_eq!(normalize_title("Nevermind [Remastered]"), "nevermind"); assert_eq!(normalize_title("Grace (20th Anniversary)"), "grace"); assert_eq!(normalize_title("Kid A (10th Anniversary)"), "kid a"); // A parenthetical that is NOT an edition marker is preserved. - assert_eq!(normalize_title("Song (Live at Wembley)"), "song (live at wembley)"); + assert_eq!( + normalize_title("Song (Live at Wembley)"), + "song (live at wembley)" + ); } /// The sharp edges of `normalize_title` that decide which releases land in @@ -1183,15 +1182,36 @@ mod tests { classify_release("Greatest Hits Live", Some(20), Some("album"), None, true), "compilation" ); - assert_eq!(classify_release("Unplugged", Some(12), None, None, false), "live"); - assert_eq!(classify_release("Some EP", Some(12), None, None, false), "ep"); - assert_eq!(classify_release("Whatever", Some(2), None, None, false), "single"); - assert_eq!(classify_release("Whatever", Some(5), None, None, false), "ep"); - assert_eq!(classify_release("Whatever", Some(12), None, None, false), "album"); + assert_eq!( + classify_release("Unplugged", Some(12), None, None, false), + "live" + ); + assert_eq!( + classify_release("Some EP", Some(12), None, None, false), + "ep" + ); + assert_eq!( + classify_release("Whatever", Some(2), None, None, false), + "single" + ); + assert_eq!( + classify_release("Whatever", Some(5), None, None, false), + "ep" + ); + assert_eq!( + classify_release("Whatever", Some(12), None, None, false), + "album" + ); // "epic" must NOT match the `ep` word test. - assert_eq!(classify_release("Epic", Some(12), None, None, false), "album"); + assert_eq!( + classify_release("Epic", Some(12), None, None, false), + "album" + ); // type_is matches either the item's or the group's type. - assert_eq!(classify_release("X", Some(12), None, Some("single"), false), "single"); + assert_eq!( + classify_release("X", Some(12), None, Some("single"), false), + "single" + ); } /// The arms `classify_precedence` does not reach. Each one changes the TYPE @@ -1210,10 +1230,19 @@ mod tests { "compilation" ); // live: the other two words, and punctuation counts as a boundary. - assert_eq!(classify_release("The Concert", Some(20), None, None, false), "live"); - assert_eq!(classify_release("Live!", Some(20), None, None, false), "live"); + assert_eq!( + classify_release("The Concert", Some(20), None, None, false), + "live" + ); + assert_eq!( + classify_release("Live!", Some(20), None, None, false), + "live" + ); // "alive" must NOT match the `live` word test. - assert_eq!(classify_release("Alive", Some(20), None, None, false), "album"); + assert_eq!( + classify_release("Alive", Some(20), None, None, false), + "album" + ); // The GROUP's type beats the item's for live (type_is is an OR). assert_eq!( classify_release("X", Some(20), Some("album"), Some("live"), false), @@ -1242,8 +1271,14 @@ mod tests { #[test] fn quality_score_ordering() { // bit depth dominates, then rate, then format bonus. - assert!(quality_score(Some(24), Some(96.0), "FLAC") > quality_score(Some(16), Some(192.0), "FLAC")); - assert!(quality_score(Some(16), Some(44.1), "FLAC") > quality_score(Some(16), Some(44.1), "MP3")); + assert!( + quality_score(Some(24), Some(96.0), "FLAC") + > quality_score(Some(16), Some(192.0), "FLAC") + ); + assert!( + quality_score(Some(16), Some(44.1), "FLAC") + > quality_score(Some(16), Some(44.1), "MP3") + ); // Unknown format sits between mp3 (0) and flac (1000). let unknown = quality_score(Some(16), Some(44.1), "OGG"); assert!(unknown > quality_score(Some(16), Some(44.1), "MP3")); @@ -1267,7 +1302,10 @@ mod tests { // "fix" in the fetchers is not mistaken for a change here. assert_eq!(quality_score(Some(16), Some(44100.0), "FLAC"), 204_101_000); // AIFF is lossless but not in the bonus list, so it scores BELOW FLAC. - assert!(quality_score(Some(24), Some(96.0), "AIFF") < quality_score(Some(24), Some(96.0), "FLAC")); + assert!( + quality_score(Some(24), Some(96.0), "AIFF") + < quality_score(Some(24), Some(96.0), "FLAC") + ); } #[test] @@ -1303,7 +1341,10 @@ mod tests { ); let by_title = ordered_groups(&groups, "title"); assert_eq!( - by_title.iter().map(|g| g.title.as_str()).collect::>(), + by_title + .iter() + .map(|g| g.title.as_str()) + .collect::>(), vec!["Aye", "Bee", "Cee"] ); let manual = ordered_groups(&groups, "manual"); @@ -1422,7 +1463,10 @@ mod tests { // Compilation-only selection: the header box is EMPTY (neither all nor // some) but the footer still counts 1 and Create is enabled. - s.checked.get_mut(&comp_key).unwrap().push(comp_primary.clone()); + s.checked + .get_mut(&comp_key) + .unwrap() + .push(comp_primary.clone()); s.checked.get_mut(&c_key).unwrap().clear(); assert!(!all_primaries_checked(&s)); assert!(!some_primaries_checked(&s)); @@ -1470,7 +1514,10 @@ mod tests { toggle_all(); session(|s| { assert!(!is_checked(s, &a_key, &a_primary)); - assert!(is_checked(s, &comp_key, &comp_primary), "compilation untouched"); + assert!( + is_checked(s, &comp_key, &comp_primary), + "compilation untouched" + ); assert!(is_checked(s, &a_key, "qobuz|alt"), "alternate untouched"); assert_eq!(selected_count(s), 2); }); diff --git a/crates/qbz-qt/src/myqbz_cover_qt.rs b/crates/qbz-qt/src/myqbz_cover_qt.rs index 82f43dde5..ec1269371 100644 --- a/crates/qbz-qt/src/myqbz_cover_qt.rs +++ b/crates/qbz-qt/src/myqbz_cover_qt.rs @@ -65,12 +65,10 @@ fn set_custom_artwork(id: &str, path: Option<&str>) -> bool { let id = id.to_string(); let path = path.map(|p| p.to_string()); crate::library_db_qt::with_db(true, move |db| { - db.with_connection(|conn| { - qbz_mixtape::repo::set_custom_artwork(conn, &id, path.as_deref()) - }) - .map_err(|e| { - qbz_library::LibraryError::Database(format!("set_custom_artwork failed: {e}")) - }) + db.with_connection(|conn| qbz_mixtape::repo::set_custom_artwork(conn, &id, path.as_deref())) + .map_err(|e| { + qbz_library::LibraryError::Database(format!("set_custom_artwork failed: {e}")) + }) }) .is_some() } diff --git a/crates/qbz-qt/src/myqbz_detail_qt.rs b/crates/qbz-qt/src/myqbz_detail_qt.rs index 178d93fd5..5f5c79a86 100644 --- a/crates/qbz-qt/src/myqbz_detail_qt.rs +++ b/crates/qbz-qt/src/myqbz_detail_qt.rs @@ -500,8 +500,7 @@ fn publish_rows(collection_id: &str, patches: Vec) { }) .unwrap_or_else(|_| "{}".into()); crate::myqbz_bridge::ui(move |mut b| { - b.as_mut() - .detail_rows_patched(QString::from(json.as_str())); + b.as_mut().detail_rows_patched(QString::from(json.as_str())); }); } @@ -918,10 +917,7 @@ fn track_to_item(track: &QueueTrack) -> InlineTrackRow { id: track.id.to_string(), title: inline_track_title(track), artist: track.artist.clone(), - artist_id: track - .artist_id - .map(|id| id.to_string()) - .unwrap_or_default(), + artist_id: track.artist_id.map(|id| id.to_string()).unwrap_or_default(), album: String::new(), album_id: track.album_id.clone().unwrap_or_default(), duration: track_duration_str(track.duration_secs), @@ -1036,10 +1032,8 @@ fn rebuild(d: &mut DetailDoc) { let source_count = i32::from(d.src_qobuz) + i32::from(d.src_plex) + i32::from(d.src_local); d.filter_count = source_count + i32::from(d.type_filter != "all"); - d.has_any_filter = d.type_filter != "all" - || source_count > 0 - || d.sort != "position" - || d.sort_dir == "desc"; + d.has_any_filter = + d.type_filter != "all" || source_count > 0 || d.sort != "position" || d.sort_dir == "desc"; } /// Re-derive, resolve the newly-visible rows' artwork, publish, and — while in @@ -1476,10 +1470,7 @@ fn resolve_from_tracks(item: &MixtapeCollectionItem, tracks: &[QueueTrack]) -> R /// same album twice hydrates both copies rather than only the first. fn apply_resolved(item: &MixtapeCollectionItem, resolved: ResolvedItem, collection_id: &str) { let key = cache_key(source_str(item.source), &item.source_item_id); - RESOLVE_CACHE - .lock() - .unwrap() - .insert(key, resolved.clone()); + RESOLVE_CACHE.lock().unwrap().insert(key, resolved.clone()); let stored_art_empty = item .artwork_url @@ -2242,7 +2233,12 @@ pub(crate) fn bulk_action(id: String) { mod tests { use super::*; - fn item(position: i32, title: &str, kind: ItemType, source: AlbumSource) -> MixtapeCollectionItem { + fn item( + position: i32, + title: &str, + kind: ItemType, + source: AlbumSource, + ) -> MixtapeCollectionItem { MixtapeCollectionItem { collection_id: "c".into(), position, diff --git a/crates/qbz-qt/src/myqbz_edit_qt.rs b/crates/qbz-qt/src/myqbz_edit_qt.rs index aba501036..40c34223c 100644 --- a/crates/qbz-qt/src/myqbz_edit_qt.rs +++ b/crates/qbz-qt/src/myqbz_edit_qt.rs @@ -256,10 +256,8 @@ pub(crate) fn toggle_play_mode() { let write_id = id.clone(); let result = tokio::task::spawn_blocking(move || { write_repo(|db| { - db.with_connection(|conn| { - qbz_mixtape::repo::set_play_mode(conn, &write_id, next) - }) - .map_err(|e| e.to_string()) + db.with_connection(|conn| qbz_mixtape::repo::set_play_mode(conn, &write_id, next)) + .map_err(|e| e.to_string()) }) }) .await diff --git a/crates/qbz-qt/src/myqbz_mix_qt.rs b/crates/qbz-qt/src/myqbz_mix_qt.rs index fa663abdf..6a1d60e13 100644 --- a/crates/qbz-qt/src/myqbz_mix_qt.rs +++ b/crates/qbz-qt/src/myqbz_mix_qt.rs @@ -200,9 +200,7 @@ pub(crate) fn open() { let unique = qbz_mixtape::shuffle::unique_track_count(&tracks) as i32; if unique <= 0 { close(); - crate::toast_qt::error(qbz_i18n::t( - "This collection resolved to 0 playable tracks", - )); + crate::toast_qt::error(qbz_i18n::t("This collection resolved to 0 playable tracks")); } else { apply_options(unique); } diff --git a/crates/qbz-qt/src/myqbz_play_qt.rs b/crates/qbz-qt/src/myqbz_play_qt.rs index 0d8cfb785..a4030b751 100644 --- a/crates/qbz-qt/src/myqbz_play_qt.rs +++ b/crates/qbz-qt/src/myqbz_play_qt.rs @@ -334,15 +334,12 @@ pub(crate) async fn play_all_tracks( tracks: Vec, ) { if tracks.is_empty() { - crate::toast_qt::error(qbz_i18n::t( - "This collection resolved to 0 playable tracks", - )); + crate::toast_qt::error(qbz_i18n::t("This collection resolved to 0 playable tracks")); return; } // Context `None` — `derive_context` infers album-or-artist (T16). // F1: the anchor comes back from the seam, never from `tracks[0]`. - let Some(anchor) = - crate::playback_qt::set_queue_stamped(runtime, tracks, Some(0), None).await + let Some(anchor) = crate::playback_qt::set_queue_stamped(runtime, tracks, Some(0), None).await else { // The seam already toasted the count and left the previous queue alone. // `set_queue_source_collection` and `touch_play` below are bookkeeping @@ -361,12 +358,7 @@ pub(crate) async fn play_all_tracks( // regardless of where the audio plays (the Slint collection-play flow // records it on the routed path too). if !crate::playback_qt::route_play_to_peer(runtime, first_id).await { - if let Err(e) = crate::playback_qt::play_resolved_offline_aware( - runtime, - first_id, - 0, - ) - .await + if let Err(e) = crate::playback_qt::play_resolved_offline_aware(runtime, first_id, 0).await { log::error!("[qbz-qt] myqbz_play: play_track {first_id} failed: {e}"); } @@ -429,16 +421,16 @@ async fn persist_album_shuffle(collection_id: &str) { db.with_connection(|conn| { qbz_mixtape::repo::set_play_mode(conn, &write_id, CollectionPlayMode::AlbumShuffle) }) - .map_err(|e| { - qbz_library::LibraryError::Database(format!("set_play_mode failed: {e}")) - }) + .map_err(|e| qbz_library::LibraryError::Database(format!("set_play_mode failed: {e}"))) }) }) .await; match persisted { Ok(Some(())) => {} Ok(None) => { - log::warn!("[qbz-qt] myqbz_play: persist album_shuffle({collection_id}): db write failed"); + log::warn!( + "[qbz-qt] myqbz_play: persist album_shuffle({collection_id}): db write failed" + ); return; } Err(e) => { @@ -542,12 +534,8 @@ pub(crate) fn item_action(source_item_id: String, action: String) { if crate::playback_qt::route_play_to_peer(&runtime, first_id).await { return; } - if let Err(e) = crate::playback_qt::play_resolved_offline_aware( - &runtime, - first_id, - 0, - ) - .await + if let Err(e) = + crate::playback_qt::play_resolved_offline_aware(&runtime, first_id, 0).await { log::error!("[qbz-qt] myqbz_play: play_track {first_id} failed: {e}"); } @@ -713,11 +701,7 @@ fn bulk_enqueue_inner(items: Vec, mode: BulkEnqueueMode) /// (`MyQbzDetailView.qml:628-630` sends it): the reference intercepts the same /// action string before the playback modes (`qbz/src/main.rs:6790-6811`) and /// opens the PARENT item with the parent's REAL type. -pub(crate) fn play_inline_track( - item_source_item_id: String, - track_id: String, - action: String, -) { +pub(crate) fn play_inline_track(item_source_item_id: String, track_id: String, action: String) { if action == "go-to-album" { // Route through the PARENT item, never the track's own ids, and with the // parent's real item_type — a playlist parent must reach the playlist @@ -780,12 +764,8 @@ pub(crate) fn play_inline_track( if crate::playback_qt::route_play_to_peer(&runtime, first_id).await { return; } - if let Err(e) = crate::playback_qt::play_resolved_offline_aware( - &runtime, - first_id, - 0, - ) - .await + if let Err(e) = + crate::playback_qt::play_resolved_offline_aware(&runtime, first_id, 0).await { log::error!("[qbz-qt] myqbz_play: play_track {first_id} failed: {e}"); } @@ -802,8 +782,9 @@ pub(crate) fn play_inline_track( if crate::playback_qt::route_track_to_peer(&track, "next").await { return; } - let added_castable = - crate::playback_qt::batch_all_qconnect_castable(std::slice::from_ref(&track)); + let added_castable = crate::playback_qt::batch_all_qconnect_castable( + std::slice::from_ref(&track), + ); runtime.core().add_track_next(track).await; crate::playback_qt::sync_qconnect_after_add(added_castable).await; } @@ -821,8 +802,9 @@ pub(crate) fn play_inline_track( if crate::playback_qt::route_track_to_peer(&track, "queue").await { return; } - let added_castable = - crate::playback_qt::batch_all_qconnect_castable(std::slice::from_ref(&track)); + let added_castable = crate::playback_qt::batch_all_qconnect_castable( + std::slice::from_ref(&track), + ); runtime.core().add_tracks(vec![track]).await; crate::playback_qt::sync_qconnect_after_add(added_castable).await; } @@ -840,9 +822,18 @@ mod tests { #[test] fn row_mode_accepts_both_spellings() { assert!(matches!(RowMode::parse("play"), Some(RowMode::Play))); - assert!(matches!(RowMode::parse("play_next"), Some(RowMode::PlayNext))); - assert!(matches!(RowMode::parse("play-later"), Some(RowMode::PlayLater))); - assert!(matches!(RowMode::parse("append"), Some(RowMode::AddToQueue))); + assert!(matches!( + RowMode::parse("play_next"), + Some(RowMode::PlayNext) + )); + assert!(matches!( + RowMode::parse("play-later"), + Some(RowMode::PlayLater) + )); + assert!(matches!( + RowMode::parse("append"), + Some(RowMode::AddToQueue) + )); assert!(RowMode::parse("nonsense").is_none()); } diff --git a/crates/qbz-qt/src/myqbz_prefs_qt.rs b/crates/qbz-qt/src/myqbz_prefs_qt.rs index dcefcce47..641604bdc 100644 --- a/crates/qbz-qt/src/myqbz_prefs_qt.rs +++ b/crates/qbz-qt/src/myqbz_prefs_qt.rs @@ -255,7 +255,10 @@ pub(crate) fn pick_icon() { crate::spawn(async move { let Some(file) = rfd::AsyncFileDialog::new() .set_title(&qbz_i18n::t_args("Choose a {} icon", &[DEFAULT_LABEL])) - .add_filter(&qbz_i18n::t("Image"), &["svg", "png", "jpg", "jpeg", "webp"]) + .add_filter( + &qbz_i18n::t("Image"), + &["svg", "png", "jpg", "jpeg", "webp"], + ) .pick_file() .await else { @@ -642,7 +645,10 @@ mod tests { r#"{"a":["qobuz|1","local|/x",""],"b":{"nope":true},"c":["qobuz|2",7],"d":null}"#, ) .expect("parses"); - assert_eq!(parse_open_rows_entry(doc.get("a")), vec!["qobuz|1", "local|/x"]); + assert_eq!( + parse_open_rows_entry(doc.get("a")), + vec!["qobuz|1", "local|/x"] + ); assert!(parse_open_rows_entry(doc.get("b")).is_empty()); assert_eq!(parse_open_rows_entry(doc.get("c")), vec!["qobuz|2"]); assert!(parse_open_rows_entry(doc.get("d")).is_empty()); diff --git a/crates/qbz-qt/src/myqbz_qt.rs b/crates/qbz-qt/src/myqbz_qt.rs index eea57b8cb..b3f4b3015 100644 --- a/crates/qbz-qt/src/myqbz_qt.rs +++ b/crates/qbz-qt/src/myqbz_qt.rs @@ -223,7 +223,8 @@ fn publish(grid: Grid) { b.as_mut().set_mixtapes_json(QString::from(json.as_str())); }), Grid::Collections => crate::myqbz_bridge::ui(move |mut b| { - b.as_mut().set_collections_json(QString::from(json.as_str())); + b.as_mut() + .set_collections_json(QString::from(json.as_str())); }), } } @@ -378,13 +379,7 @@ pub(crate) fn small_qobuz_url(url: &str, target: u32) -> String { return String::new(); } const TOKENS: [&str; 8] = [ - "_50.jpg", - "_100.jpg", - "_150.jpg", - "_230.jpg", - "_300.jpg", - "_600.jpg", - "_max.jpg", + "_50.jpg", "_100.jpg", "_150.jpg", "_230.jpg", "_300.jpg", "_600.jpg", "_max.jpg", "_org.jpg", ]; let lower = url.to_lowercase(); @@ -745,9 +740,7 @@ pub(crate) fn grid_set_kind_filter(kind: &str) { "collection" | "artist_collection" => kind, _ => "all", }; - with_grid(Grid::Collections, |g| { - g.doc.kind_filter = kind.to_string() - }); + with_grid(Grid::Collections, |g| g.doc.kind_filter = kind.to_string()); rebuild(Grid::Collections); publish(Grid::Collections); artwork_refresh(Grid::Collections); diff --git a/crates/qbz-qt/src/now_playing.rs b/crates/qbz-qt/src/now_playing.rs index 8750cbe97..4ff2f05b9 100644 --- a/crates/qbz-qt/src/now_playing.rs +++ b/crates/qbz-qt/src/now_playing.rs @@ -463,7 +463,11 @@ pub fn clear_track() { pub fn set_cast_session(active: bool, protocol: &str, target: &str) { mutate(|m| { m.cast_active = active; - m.cast_protocol = if active { protocol.to_string() } else { String::new() }; + m.cast_protocol = if active { + protocol.to_string() + } else { + String::new() + }; if active { m.cast_target = target.to_string(); } else if !m.is_remote { @@ -595,7 +599,8 @@ pub fn set_artwork_path_large(path: String) { *last = path.clone(); } crate::player_bridge::ui(move |mut b| { - b.as_mut().set_np_artwork_path_large(QString::from(path.as_str())) + b.as_mut() + .set_np_artwork_path_large(QString::from(path.as_str())) }); } diff --git a/crates/qbz-qt/src/offline_cache_qt.rs b/crates/qbz-qt/src/offline_cache_qt.rs index e30c94228..39e24d8f6 100644 --- a/crates/qbz-qt/src/offline_cache_qt.rs +++ b/crates/qbz-qt/src/offline_cache_qt.rs @@ -18,8 +18,8 @@ use std::sync::Arc; -use qbz_offline_cache::{CacheEvent, CacheEventSink, TrackCacheInfo}; use qbz_offline_cache::OfflineCacheState; +use qbz_offline_cache::{CacheEvent, CacheEventSink, TrackCacheInfo}; use crate::offline_qt; use crate::shell_bridge; @@ -31,11 +31,8 @@ use cxx_qt_lib::QString; fn push_status(track_id: u64, status: i32, progress: f64) { let id = track_id.to_string(); shell_bridge::ui(move |mut b| { - b.as_mut().track_cache_status_changed( - QString::from(id.as_str()), - status, - progress, - ); + b.as_mut() + .track_cache_status_changed(QString::from(id.as_str()), status, progress); }); } @@ -167,9 +164,9 @@ pub fn cache_track(id: u64) { let root = std::path::PathBuf::from(off.get_cache_path()); if let Err(e) = qbz_offline_cache::maintenance::check_cache_limit(db, &root, limit) { log::warn!("[qbz-qt] cache limit reached: {e}"); - crate::toast_qt::error( - qbz_i18n::t("Offline cache is full — free space or raise the limit"), - ); + crate::toast_qt::error(qbz_i18n::t( + "Offline cache is full — free space or raise the limit", + )); return; } if let Err(e) = db.insert_track(&info, &file_path_str) { @@ -214,9 +211,9 @@ pub fn cache_tracks_with_album( let root = std::path::PathBuf::from(off.get_cache_path()); if let Err(e) = qbz_offline_cache::maintenance::check_cache_limit(db, &root, limit) { log::warn!("[qbz-qt] batch cache limit reached: {e}"); - crate::toast_qt::error( - qbz_i18n::t("Offline cache is full — free space or raise the limit"), - ); + crate::toast_qt::error(qbz_i18n::t( + "Offline cache is full — free space or raise the limit", + )); return; } } @@ -260,10 +257,7 @@ pub fn cache_album(album_id: String) { return; } }; - let tracks: Vec = album - .tracks - .map(|c| c.items) - .unwrap_or_default(); + let tracks: Vec = album.tracks.map(|c| c.items).unwrap_or_default(); if tracks.is_empty() { crate::toast_qt::error(qbz_i18n::t("This album has no playable tracks")); return; diff --git a/crates/qbz-qt/src/offline_fwd.rs b/crates/qbz-qt/src/offline_fwd.rs index 541b8d829..e0c321f38 100644 --- a/crates/qbz-qt/src/offline_fwd.rs +++ b/crates/qbz-qt/src/offline_fwd.rs @@ -100,7 +100,7 @@ fn apply_status( mut b: core::pin::Pin<&mut crate::session_bridge::qbz_session::QbzSession>, status: OfflineStatus, ) { - b.as_mut().set_offline(status.is_offline()); + b.as_mut().set_offline(status.is_offline()); b.as_mut().set_offline_mode(match status.mode { OfflineMode::Online => 0, OfflineMode::RealOffline => 1, @@ -112,7 +112,8 @@ fn apply_status( Connectivity::Down => 2, }); b.as_mut().set_captive_portal(status.captive_portal); - b.as_mut().set_show_recovery_banner(status.show_recovery_banner()); + b.as_mut() + .set_show_recovery_banner(status.show_recovery_banner()); // The header badge's "Logged out" state needs the raw session flag — // show_recovery_banner() is false while connectivity is down. b.as_mut().set_offline_session(status.offline_session); diff --git a/crates/qbz-qt/src/offline_manager_bridge.rs b/crates/qbz-qt/src/offline_manager_bridge.rs index aff75bfbf..ce838e0a6 100644 --- a/crates/qbz-qt/src/offline_manager_bridge.rs +++ b/crates/qbz-qt/src/offline_manager_bridge.rs @@ -58,7 +58,6 @@ pub mod qbz_offline_manager_bridge { /// spinner — on the one screen whose whole job is to show you that you /// DO have downloads. #[qproperty(bool, manager_loading)] - type QbzOffline = super::QbzOfflineRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/offline_qt.rs b/crates/qbz-qt/src/offline_qt.rs index da381ebd3..777cfa234 100644 --- a/crates/qbz-qt/src/offline_qt.rs +++ b/crates/qbz-qt/src/offline_qt.rs @@ -126,7 +126,10 @@ fn cached_ids() -> &'static StdMutex> { /// this set with persisted playlist membership without holding the mutex over /// a database read. pub(crate) fn cached_ids_set() -> HashSet { - cached_ids().lock().map(|ids| ids.clone()).unwrap_or_default() + cached_ids() + .lock() + .map(|ids| ids.clone()) + .unwrap_or_default() } /// True if `track_id` has a ready offline copy. Used to seed each track row's @@ -181,7 +184,9 @@ async fn load_cached_ids() { .map(|tracks| { tracks .into_iter() - .filter(|t| matches!(t.status, qbz_offline_cache::OfflineCacheStatus::Ready)) + .filter(|t| { + matches!(t.status, qbz_offline_cache::OfflineCacheStatus::Ready) + }) .map(|t| t.track_id) .collect() }) diff --git a/crates/qbz-qt/src/output_labels.rs b/crates/qbz-qt/src/output_labels.rs index 38ed1cda9..cf4032e2c 100644 --- a/crates/qbz-qt/src/output_labels.rs +++ b/crates/qbz-qt/src/output_labels.rs @@ -126,7 +126,8 @@ pub fn publish(audio: &AudioSettings) { let l = output_labels(audio); let locked = volume_locked(audio); crate::player_bridge::ui(move |mut b| { - b.as_mut().set_np_output_backend_label(QString::from(l.backend)); + b.as_mut() + .set_np_output_backend_label(QString::from(l.backend)); b.as_mut().set_np_output_mode_label(QString::from(l.mode)); b.as_mut().set_np_output_backend_active(l.backend_active); b.as_mut().set_np_output_mode_active(l.mode_active); diff --git a/crates/qbz-qt/src/player_bridge.rs b/crates/qbz-qt/src/player_bridge.rs index 3f29ef6d9..06c821f24 100644 --- a/crates/qbz-qt/src/player_bridge.rs +++ b/crates/qbz-qt/src/player_bridge.rs @@ -90,7 +90,6 @@ pub mod qbz_player { // (ImmersiveSpectralOverlay.slint:45 parity). #[qproperty(i32, np_eff_rate_hz)] #[qproperty(i32, np_eff_bits)] - // --- Output LEDs (settings.rs `output_labels`) ------------------- // PIPEWIRE|ALSA|JACK|PULS|SYST|AUTO — *_active lights the LED. #[qproperty(QString, np_output_backend_label)] @@ -105,7 +104,6 @@ pub mod qbz_player { // disabled with a tooltip instead of pretending it works. No audio // behaviour hangs off this flag — it is a UI mirror only. #[qproperty(bool, np_volume_locked)] - // --- Cast / remote (Qobuz Connect + Chromecast/DLNA) ------------- // A peer Qobuz Connect renderer owns playback (transport is remote). #[qproperty(bool, np_is_remote)] @@ -150,7 +148,6 @@ pub mod qbz_player { // "Show track playing context" pref (Playback settings) — feeds the // SongCard layers icon. #[qproperty(bool, show_context_icon)] - type QbzPlayer = super::QbzPlayerRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -204,7 +201,12 @@ pub mod qbz_player { fn play_album_from(self: Pin<&mut QbzPlayer>, album_id: QString, track_id: QString); /// AlbumView row "Play next" ("next") / "Add to queue" ("later"). #[qinvokable] - fn enqueue_album_track(self: Pin<&mut QbzPlayer>, album_id: QString, track_id: QString, mode: QString); + fn enqueue_album_track( + self: Pin<&mut QbzPlayer>, + album_id: QString, + track_id: QString, + mode: QString, + ); /// AlbumCard ⋯ menu: Play next ("next") / Add to queue ("later"). #[qinvokable] fn enqueue_album(self: Pin<&mut QbzPlayer>, album_id: QString, mode: QString); @@ -220,7 +222,13 @@ pub mod qbz_player { /// (bulk_tracks_qt.rs): ids in visible order + the bar's action id + /// the queue context to stamp ("" = none). #[qinvokable] - fn bulk_tracks_action(self: Pin<&mut QbzPlayer>, ids_json: QString, action: QString, context_kind: QString, context_id: QString); + fn bulk_tracks_action( + self: Pin<&mut QbzPlayer>, + ids_json: QString, + action: QString, + context_kind: QString, + context_id: QString, + ); /// ArtistView Popular Tracks row play (whole list as the queue). #[qinvokable] fn play_artist_track(self: Pin<&mut QbzPlayer>, track_id: QString); @@ -471,7 +479,12 @@ impl qbz_player::QbzPlayer { } } - pub fn enqueue_album_track(self: Pin<&mut Self>, album_id: QString, track_id: QString, mode: QString) { + pub fn enqueue_album_track( + self: Pin<&mut Self>, + album_id: QString, + track_id: QString, + mode: QString, + ) { if let Ok(tid) = track_id.to_string().parse::() { crate::enqueue_album_track(album_id.to_string(), tid, mode.to_string()); } @@ -497,7 +510,13 @@ impl qbz_player::QbzPlayer { } } - pub fn bulk_tracks_action(self: Pin<&mut Self>, ids_json: QString, action: QString, context_kind: QString, context_id: QString) { + pub fn bulk_tracks_action( + self: Pin<&mut Self>, + ids_json: QString, + action: QString, + context_kind: QString, + context_id: QString, + ) { crate::bulk_tracks_qt::run( ids_json.to_string(), action.to_string(), diff --git a/crates/qbz-qt/src/playlist_create_qt.rs b/crates/qbz-qt/src/playlist_create_qt.rs index 51902dcd8..300853504 100644 --- a/crates/qbz-qt/src/playlist_create_qt.rs +++ b/crates/qbz-qt/src/playlist_create_qt.rs @@ -289,7 +289,11 @@ pub(crate) fn submit( }) .await; } - log::info!("[qbz-qt] playlist created: {} ({})", playlist.name, playlist.id); + log::info!( + "[qbz-qt] playlist created: {} ({})", + playlist.name, + playlist.id + ); settle_success(new_id, false); } Err(e) => { diff --git a/crates/qbz-qt/src/playlist_edit_bridge.rs b/crates/qbz-qt/src/playlist_edit_bridge.rs index 2922d78e9..80e26380e 100644 --- a/crates/qbz-qt/src/playlist_edit_bridge.rs +++ b/crates/qbz-qt/src/playlist_edit_bridge.rs @@ -88,7 +88,6 @@ pub mod qbz_playlist_edit_bridge { /// paid that bill once: the bridge header's own note about a missing /// `boot()` being "silently and forever" dropped is not hypothetical. #[qproperty(QString, create_json)] - type QbzPlaylistEdit = super::QbzPlaylistEditRust; /// Registers this object's Qt-thread hop. Without it every `ui()` @@ -213,17 +212,8 @@ impl qbz_playlist_edit_bridge::QbzPlaylistEdit { crate::playlist_edit_qt::open(&id.to_string()); } - pub fn save( - self: Pin<&mut Self>, - name: QString, - description: QString, - offline_only: bool, - ) { - crate::playlist_edit_qt::save( - &name.to_string(), - &description.to_string(), - offline_only, - ); + pub fn save(self: Pin<&mut Self>, name: QString, description: QString, offline_only: bool) { + crate::playlist_edit_qt::save(&name.to_string(), &description.to_string(), offline_only); } pub fn delete_playlist(self: Pin<&mut Self>) { diff --git a/crates/qbz-qt/src/playlist_edit_qt.rs b/crates/qbz-qt/src/playlist_edit_qt.rs index 5f4ea9ac6..513fdeddc 100644 --- a/crates/qbz-qt/src/playlist_edit_qt.rs +++ b/crates/qbz-qt/src/playlist_edit_qt.rs @@ -202,12 +202,11 @@ pub(crate) fn open(id: &str) { fn open_local(id: String) { crate::spawn(async move { let lookup = id.clone(); - let found = tokio::task::spawn_blocking(move || { - crate::local_playlist_qt::get_blocking(&lookup) - }) - .await - .ok() - .flatten(); + let found = + tokio::task::spawn_blocking(move || crate::local_playlist_qt::get_blocking(&lookup)) + .await + .ok() + .flatten(); let Some(p) = found else { // Deleted underneath us, or the DB is unreadable. Opening an @@ -333,12 +332,7 @@ pub(crate) fn save(name: &str, description: &str, offline_only: bool) { }); } -async fn save_local( - id: &str, - name: &str, - desc: Option, - offline_only: bool, -) -> bool { +async fn save_local(id: &str, name: &str, desc: Option, offline_only: bool) -> bool { let id = id.to_string(); let name = name.to_string(); tokio::task::spawn_blocking(move || { @@ -351,12 +345,7 @@ async fn save_local( Some(d) => Some(d), None => crate::local_playlist_qt::get_blocking(&id).and_then(|p| p.description), }; - crate::local_playlist_qt::update_blocking( - &id, - &name, - description.as_deref(), - offline_only, - ) + crate::local_playlist_qt::update_blocking(&id, &name, description.as_deref(), offline_only) }) .await .unwrap_or(false) diff --git a/crates/qbz-qt/src/playlist_import_bridge.rs b/crates/qbz-qt/src/playlist_import_bridge.rs index b94080ba0..732770b9d 100644 --- a/crates/qbz-qt/src/playlist_import_bridge.rs +++ b/crates/qbz-qt/src/playlist_import_bridge.rs @@ -40,7 +40,6 @@ pub mod qbz_playlist_import_bridge { #[qml_singleton] // ONE JSON document (playlist_import_qt.rs ImportDoc). #[qproperty(QString, import_json)] - type QbzPlaylistImport = super::QbzPlaylistImportRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/playlist_import_qt.rs b/crates/qbz-qt/src/playlist_import_qt.rs index 1b76aec70..3846efde4 100644 --- a/crates/qbz-qt/src/playlist_import_qt.rs +++ b/crates/qbz-qt/src/playlist_import_qt.rs @@ -946,8 +946,8 @@ fn build_source(st: &State) -> Option { let bytes = st.file_bytes.clone()?; // Detection runs HERE rather than at parse time so a wrong pick // fails before the modal flips into step B. - let format = qbz_playlist_import::sources::file::detect_format(&bytes, &st.file_name) - .ok()?; + let format = + qbz_playlist_import::sources::file::detect_format(&bytes, &st.file_name).ok()?; Some(PlaylistSource::File { format, bytes, @@ -1244,10 +1244,7 @@ fn apply_execute_ok(summary: &ImportSummary) { qbz_i18n::t_args("Skipped: {}", &[&summary.skipped_tracks.to_string()]); // Hidden when there were none, which is every well-formed source. st.summary_duplicates = if summary.duplicate_tracks > 0 { - qbz_i18n::t_args( - "Duplicates: {}", - &[&summary.duplicate_tracks.to_string()], - ) + qbz_i18n::t_args("Duplicates: {}", &[&summary.duplicate_tracks.to_string()]) } else { String::new() }; diff --git a/crates/qbz-qt/src/playlist_manager_bridge.rs b/crates/qbz-qt/src/playlist_manager_bridge.rs index 7e4c47aeb..703e9516c 100644 --- a/crates/qbz-qt/src/playlist_manager_bridge.rs +++ b/crates/qbz-qt/src/playlist_manager_bridge.rs @@ -59,7 +59,6 @@ pub mod qbz_playlist_manager_bridge { // touches it — see playlist_manager_qt's header for what breaks when // the two are conflated. #[qproperty(bool, pm_loading)] - type QbzPlaylistManager = super::QbzPlaylistManagerRust; /// Registers this object's Qt-thread hop. Until Main.qml calls it, diff --git a/crates/qbz-qt/src/playlist_manager_ops.rs b/crates/qbz-qt/src/playlist_manager_ops.rs index 81aac155f..ae5aee85f 100644 --- a/crates/qbz-qt/src/playlist_manager_ops.rs +++ b/crates/qbz-qt/src/playlist_manager_ops.rs @@ -56,22 +56,20 @@ use crate::playlist_manager_qt::{patch_cache, publish_document, visible_qobuz_id /// DB write below has to derive the value itself. fn patch_favorite(id: &str) -> Option { let mut out = None; - patch_cache(|data| { - match PlaylistRef::parse(id) { - Some(PlaylistRef::Local(local)) => { - if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { - p.is_favorite = !p.is_favorite; - out = Some(p.is_favorite); - } + patch_cache(|data| match PlaylistRef::parse(id) { + Some(PlaylistRef::Local(local)) => { + if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { + p.is_favorite = !p.is_favorite; + out = Some(p.is_favorite); } - Some(PlaylistRef::Qobuz(pid)) => { - if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { - p.is_favorite = !p.is_favorite; - out = Some(p.is_favorite); - } - } - None => {} } + Some(PlaylistRef::Qobuz(pid)) => { + if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { + p.is_favorite = !p.is_favorite; + out = Some(p.is_favorite); + } + } + None => {} }); out } @@ -89,27 +87,25 @@ pub(crate) fn toggle_favorite(id: &str) { } let id = id.to_string(); crate::spawn(async move { - let _ = tokio::task::spawn_blocking(move || { - match PlaylistRef::parse(&id) { - Some(PlaylistRef::Local(local)) => { - let value = optimistic.unwrap_or_else(|| { - !local_playlist_qt::get_blocking(&local) - .map(|p| p.favorite) - .unwrap_or(false) - }); - local_playlist_qt::set_favorite_blocking(&local, value); - } - Some(PlaylistRef::Qobuz(pid)) => { - let value = optimistic.unwrap_or_else(|| { - !folders_qt::playlist_settings_map() - .get(&pid) - .map(|s| s.is_favorite) - .unwrap_or(false) - }); - folders_qt::set_favorite(pid, value); - } - None => {} + let _ = tokio::task::spawn_blocking(move || match PlaylistRef::parse(&id) { + Some(PlaylistRef::Local(local)) => { + let value = optimistic.unwrap_or_else(|| { + !local_playlist_qt::get_blocking(&local) + .map(|p| p.favorite) + .unwrap_or(false) + }); + local_playlist_qt::set_favorite_blocking(&local, value); } + Some(PlaylistRef::Qobuz(pid)) => { + let value = optimistic.unwrap_or_else(|| { + !folders_qt::playlist_settings_map() + .get(&pid) + .map(|s| s.is_favorite) + .unwrap_or(false) + }); + folders_qt::set_favorite(pid, value); + } + None => {} }) .await; }); @@ -121,22 +117,20 @@ pub(crate) fn toggle_favorite(id: &str) { fn patch_hidden(id: &str) -> Option { let mut out = None; - patch_cache(|data| { - match PlaylistRef::parse(id) { - Some(PlaylistRef::Local(local)) => { - if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { - p.is_hidden = !p.is_hidden; - out = Some(p.is_hidden); - } + patch_cache(|data| match PlaylistRef::parse(id) { + Some(PlaylistRef::Local(local)) => { + if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { + p.is_hidden = !p.is_hidden; + out = Some(p.is_hidden); } - Some(PlaylistRef::Qobuz(pid)) => { - if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { - p.is_hidden = !p.is_hidden; - out = Some(p.is_hidden); - } - } - None => {} } + Some(PlaylistRef::Qobuz(pid)) => { + if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { + p.is_hidden = !p.is_hidden; + out = Some(p.is_hidden); + } + } + None => {} }); out } @@ -156,27 +150,25 @@ pub(crate) fn toggle_hidden(id: &str) { } let id = id.to_string(); crate::spawn(async move { - let _ = tokio::task::spawn_blocking(move || { - match PlaylistRef::parse(&id) { - Some(PlaylistRef::Local(local)) => { - let value = optimistic.unwrap_or_else(|| { - !local_playlist_qt::get_blocking(&local) - .map(|p| p.hidden) - .unwrap_or(false) - }); - local_playlist_qt::set_hidden_blocking(&local, value); - } - Some(PlaylistRef::Qobuz(pid)) => { - let value = optimistic.unwrap_or_else(|| { - !folders_qt::playlist_settings_map() - .get(&pid) - .map(|s| s.hidden) - .unwrap_or(false) - }); - folders_qt::set_hidden(pid, value); - } - None => {} + let _ = tokio::task::spawn_blocking(move || match PlaylistRef::parse(&id) { + Some(PlaylistRef::Local(local)) => { + let value = optimistic.unwrap_or_else(|| { + !local_playlist_qt::get_blocking(&local) + .map(|p| p.hidden) + .unwrap_or(false) + }); + local_playlist_qt::set_hidden_blocking(&local, value); } + Some(PlaylistRef::Qobuz(pid)) => { + let value = optimistic.unwrap_or_else(|| { + !folders_qt::playlist_settings_map() + .get(&pid) + .map(|s| s.hidden) + .unwrap_or(false) + }); + folders_qt::set_hidden(pid, value); + } + None => {} }) .await; // AFTER the write settles, inside the same task — otherwise the @@ -194,22 +186,20 @@ pub(crate) fn toggle_hidden(id: &str) { fn patch_folder(id: &str, folder_id: &str) -> bool { let mut patched = false; let target = (!folder_id.is_empty()).then(|| folder_id.to_string()); - patch_cache(|data| { - match PlaylistRef::parse(id) { - Some(PlaylistRef::Local(local)) => { - if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { - p.folder_id = target; - patched = true; - } + patch_cache(|data| match PlaylistRef::parse(id) { + Some(PlaylistRef::Local(local)) => { + if let Some(p) = data.locals.iter_mut().find(|p| p.id == local) { + p.folder_id = target; + patched = true; } - Some(PlaylistRef::Qobuz(pid)) => { - if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { - p.folder_id = target; - patched = true; - } - } - None => {} } + Some(PlaylistRef::Qobuz(pid)) => { + if let Some(p) = data.playlists.iter_mut().find(|p| p.id == pid) { + p.folder_id = target; + patched = true; + } + } + None => {} }); patched } diff --git a/crates/qbz-qt/src/playlist_manager_qt.rs b/crates/qbz-qt/src/playlist_manager_qt.rs index 24e9c4f07..fe2fd874a 100644 --- a/crates/qbz-qt/src/playlist_manager_qt.rs +++ b/crates/qbz-qt/src/playlist_manager_qt.rs @@ -72,14 +72,12 @@ static CACHE: Mutex> = Mutex::new(None); /// the DB and by [`build_document`] from a warm cache; it is what /// `foldersJson` always carries, so a cold-cache publish can never clobber it /// with `[]`. -static FOLDERS_CACHE: LazyLock>> = - LazyLock::new(|| Mutex::new(Vec::new())); +static FOLDERS_CACHE: LazyLock>> = LazyLock::new(|| Mutex::new(Vec::new())); /// Session tree-expand state. Never reset — only the latch below is, so a /// re-entry re-expands every folder on top of whatever the user collapsed. /// That is the reference behaviour (§5.5); keep it. -static EXPANDED: LazyLock>> = - LazyLock::new(|| Mutex::new(HashSet::new())); +static EXPANDED: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); /// True once the tree has auto-expanded folders since the last `navigate()`. static TREE_INIT: Mutex = Mutex::new(false); @@ -431,7 +429,11 @@ pub(crate) fn search_changed(query: &str) { pub(crate) fn set_filter(value: &str) { let valid = matches!(value, "all" | "visible" | "hidden"); - TOOLBAR.lock().unwrap().filter = if valid { value.to_string() } else { "all".into() }; + TOOLBAR.lock().unwrap().filter = if valid { + value.to_string() + } else { + "all".into() + }; publish_document(); } @@ -568,10 +570,14 @@ async fn load(runtime: &Runtime) -> PmData { let remote: Vec = if offline { Vec::new() } else { - runtime.core().get_user_playlists().await.unwrap_or_else(|e| { - log::warn!("[qbz-qt] playlist-manager playlists load failed: {e}"); - Vec::new() - }) + runtime + .core() + .get_user_playlists() + .await + .unwrap_or_else(|e| { + log::warn!("[qbz-qt] playlist-manager playlists load failed: {e}"); + Vec::new() + }) }; crate::playlist_snapshot_qt::record_names_detached( remote @@ -725,7 +731,10 @@ async fn load(runtime: &Runtime) -> PmData { // `folder_id.is_none()` test. let mut locals = locals; for p in &mut locals { - if p.folder_id.as_ref().is_some_and(|f| !folder_ids.contains(f)) { + if p.folder_id + .as_ref() + .is_some_and(|f| !folder_ids.contains(f)) + { p.folder_id = None; } } diff --git a/crates/qbz-qt/src/playlist_manager_rows.rs b/crates/qbz-qt/src/playlist_manager_rows.rs index 38ceb50bc..3b1b23fe5 100644 --- a/crates/qbz-qt/src/playlist_manager_rows.rs +++ b/crates/qbz-qt/src/playlist_manager_rows.rs @@ -257,12 +257,7 @@ pub(crate) fn playlist_item(p: &PmPlaylist) -> PlaylistRow { PlaylistRow { id: p.id.to_string(), name: p.name.clone(), - tracks_line: qbz_i18n::tf( - "{} track", - "{} tracks", - total as i64, - &[&total.to_string()], - ), + tracks_line: qbz_i18n::tf("{} track", "{} tracks", total as i64, &[&total.to_string()]), duration_line: format_duration(p.duration), local_line: if p.local_count > 0 { qbz_i18n::t_args("({} local)", &[&p.local_count.to_string()]) @@ -815,8 +810,7 @@ mod tests { let flat = visible_playlist_rows(&data, "", "all", "name", true, false, "grid", false); assert_eq!(ids(&flat).len(), 4); // Tree mode publishes the flat set unconditionally too (§4.2). - let tree_mode = - visible_playlist_rows(&data, "", "all", "name", true, true, "tree", false); + let tree_mode = visible_playlist_rows(&data, "", "all", "name", true, true, "tree", false); assert_eq!(ids(&tree_mode).len(), 4); } @@ -838,8 +832,7 @@ mod tests { let visible = visible_playlist_rows(&data, "", "visible", "name", true, false, "grid", false); assert_eq!(ids(&visible), vec!["local:l2", "2"]); - let hidden = - visible_playlist_rows(&data, "", "hidden", "name", true, false, "grid", false); + let hidden = visible_playlist_rows(&data, "", "hidden", "name", true, false, "grid", false); assert_eq!(ids(&hidden), vec!["local:l1", "1"]); } diff --git a/crates/qbz-qt/src/playlist_picker_bridge.rs b/crates/qbz-qt/src/playlist_picker_bridge.rs index ac38d7659..6063c4b10 100644 --- a/crates/qbz-qt/src/playlist_picker_bridge.rs +++ b/crates/qbz-qt/src/playlist_picker_bridge.rs @@ -49,7 +49,6 @@ pub mod qbz_playlist_picker_bridge { // creating flags, the playlist rows, the inline create panel and the // duplicate-confirm sub-modal). #[qproperty(QString, picker_json)] - type QbzPlaylistPicker = super::QbzPlaylistPickerRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/playlist_picker_qt.rs b/crates/qbz-qt/src/playlist_picker_qt.rs index 87a7507ca..e24825ee0 100644 --- a/crates/qbz-qt/src/playlist_picker_qt.rs +++ b/crates/qbz-qt/src/playlist_picker_qt.rs @@ -695,7 +695,12 @@ fn toast_added(n: usize, name: &str) { return; } let msg = if name.is_empty() { - qbz_i18n::tf("Added {} track", "Added {} tracks", n as i64, &[&n.to_string()]) + qbz_i18n::tf( + "Added {} track", + "Added {} tracks", + n as i64, + &[&n.to_string()], + ) } else { qbz_i18n::tf( "Added {} track to {}", diff --git a/crates/qbz-qt/src/playlist_qt.rs b/crates/qbz-qt/src/playlist_qt.rs index 8a0d3a3f8..48c73cd31 100644 --- a/crates/qbz-qt/src/playlist_qt.rs +++ b/crates/qbz-qt/src/playlist_qt.rs @@ -119,7 +119,11 @@ pub struct PlaylistTrackRow { pub unavailable: bool, /// The raw stored ref behind `unavailable`, shown so the user can see WHAT /// is broken rather than just that something is. - #[serde(default, rename = "unavailableRef", skip_serializing_if = "String::is_empty")] + #[serde( + default, + rename = "unavailableRef", + skip_serializing_if = "String::is_empty" + )] pub unavailable_ref: String, /// Qobuz PULLED this track: the catalog reports `streamable: false` for it /// (contract §5.1). DISTINCT from [`Self::unavailable`] above, and the two @@ -218,12 +222,20 @@ pub struct PlaylistDoc { /// This detail is a LOCAL playlist (`local:`), not a Qobuz one. The /// view drops every Qobuz-only affordance on it (follow, copy, share, the /// Qobuz heart) and offers the local ones instead. - #[serde(default, rename = "isLocalPlaylist", skip_serializing_if = "std::ops::Not::not")] + #[serde( + default, + rename = "isLocalPlaylist", + skip_serializing_if = "std::ops::Not::not" + )] pub is_local_playlist: bool, /// D8: an OFFLINE-ONLY local playlist. Nothing from one may ever reach /// Qobuz — the queue it builds is stamped so the QConnect push site skips /// the cloud entirely. - #[serde(default, rename = "offlineOnly", skip_serializing_if = "std::ops::Not::not")] + #[serde( + default, + rename = "offlineOnly", + skip_serializing_if = "std::ops::Not::not" + )] pub offline_only: bool, /// This QOBUZ detail carries SIDECAR rows — local files and/or Plex tracks /// added to it through `playlist_local_tracks` / `playlist_plex_tracks`. @@ -235,7 +247,11 @@ pub struct PlaylistDoc { /// those affordances. What it loses is drag/chevron reorder — see /// `apply_custom_order`, which keys its stored order by a `u64` catalog id /// and cannot tell one from a library row id. - #[serde(default, rename = "isMixed", skip_serializing_if = "std::ops::Not::not")] + #[serde( + default, + rename = "isMixed", + skip_serializing_if = "std::ops::Not::not" + )] pub is_mixed: bool, } @@ -293,10 +309,7 @@ pub(crate) fn mark_mixed() { /// A Plex row never rides its numeric id (the synthetic ids do not resolve): /// its rating key comes back from the open detail's queue snapshot through /// `local_picker_ref_for_row`, which returns it as `"plex:"`. -pub async fn remove_sidecar_row( - runtime: &Arc>, - row_id: &str, -) -> bool { +pub async fn remove_sidecar_row(runtime: &Arc>, row_id: &str) -> bool { let Some(playlist_id) = with_doc(|d| d.id.clone()).and_then(|id| id.parse::().ok()) else { return false; }; @@ -333,8 +346,12 @@ pub async fn remove_sidecar_row( let _ = tokio::task::spawn_blocking(move || { crate::library_db_qt::with_db(false, |db| { match &removal { - SidecarRemoval::Local(id) => db.remove_local_track_from_playlist(playlist_id, *id)?, - SidecarRemoval::Plex(key) => db.remove_plex_track_from_playlist(playlist_id, key)?, + SidecarRemoval::Local(id) => { + db.remove_local_track_from_playlist(playlist_id, *id)? + } + SidecarRemoval::Plex(key) => { + db.remove_plex_track_from_playlist(playlist_id, key)? + } } Ok(()) }) @@ -970,7 +987,10 @@ fn collage_urls(playlist: &Playlist, tracks: &[Track]) -> Vec { // Load // --------------------------------------------------------------------------- -pub async fn load(runtime: &Arc>, playlist_id: u64) -> Result<(), String> { +pub async fn load( + runtime: &Arc>, + playlist_id: u64, +) -> Result<(), String> { { let mut guard = PAGE.lock().unwrap(); let doc = &mut guard.get_or_insert_with(PageState::default).doc; @@ -1108,7 +1128,8 @@ pub async fn load(runtime: &Arc>, playlist_id: u64) - // library.db mirror in `fav_cache_qt` — the same authority the write reads // — and only falls back to the feed. let is_favorite = crate::library_qt::is_favorite("playlist", &playlist_id.to_string()); - let is_owner = pl.owner.id != 0 && pl.owner.id == USER_ID.load(std::sync::atomic::Ordering::SeqCst); + let is_owner = + pl.owner.id != 0 && pl.owner.id == USER_ID.load(std::sync::atomic::Ordering::SeqCst); // Following is only meaningful for a FOREIGN playlist, and its authority // is the user's own playlist list (the ownership snapshot). The feed stays // as the fallback for the window before the first snapshot lands. @@ -1118,13 +1139,9 @@ pub async fn load(runtime: &Arc>, playlist_id: u64) - let is_following = !is_owner && (self::is_following(playlist_id) || crate::library_qt::with_library(|d| { - d.feed - .iter() - .any(|i| { - i.kind == "playlist" - && i.id == playlist_id.to_string() - && i.playlist_following - }) + d.feed.iter().any(|i| { + i.kind == "playlist" && i.id == playlist_id.to_string() && i.playlist_following + }) }) .unwrap_or(false)); let pinned = crate::sidebar_qt::is_pinned("playlist", &playlist_id.to_string()); @@ -1136,7 +1153,8 @@ pub async fn load(runtime: &Arc>, playlist_id: u64) - let is_copied = !is_owner && crate::library_db_qt::is_playlist_copied(playlist_id); // Custom drag order (applied when sort == custom). - let sort_state = with_doc(|d| (d.sort_field.clone(), d.sort_asc)).unwrap_or(("default".into(), true)); + let sort_state = + with_doc(|d| (d.sort_field.clone(), d.sort_asc)).unwrap_or(("default".into(), true)); let track_count = rows.len() as i32; let total_duration = total_duration_label(&rows); @@ -1225,9 +1243,15 @@ fn apply_sort(doc: &mut PlaylistDoc, field: &str, asc: bool) { doc.sort_field = field.to_string(); doc.sort_asc = asc; match field { - "title" => doc.tracks.sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), - "artist" => doc.tracks.sort_by(|a, b| a.artist.to_lowercase().cmp(&b.artist.to_lowercase())), - "album" => doc.tracks.sort_by(|a, b| a.album.to_lowercase().cmp(&b.album.to_lowercase())), + "title" => doc + .tracks + .sort_by(|a, b| a.title.to_lowercase().cmp(&b.title.to_lowercase())), + "artist" => doc + .tracks + .sort_by(|a, b| a.artist.to_lowercase().cmp(&b.artist.to_lowercase())), + "album" => doc + .tracks + .sort_by(|a, b| a.album.to_lowercase().cmp(&b.album.to_lowercase())), "duration" => doc.tracks.sort_by_key(|t| t.duration_secs), // A MIXED playlist has no usable custom order, and forcing one would // scramble the merge that just ran: `apply_custom_order` keys its @@ -1271,7 +1295,11 @@ fn load_custom_orders(playlist_id: u64) -> Vec { v.get(playlist_id.to_string()).and_then(|ids| { ids.as_array().map(|a| { a.iter() - .filter_map(|id| id.as_str().and_then(|s| s.parse::().ok()).or(id.as_u64())) + .filter_map(|id| { + id.as_str() + .and_then(|s| s.parse::().ok()) + .or(id.as_u64()) + }) .collect() }) }) @@ -1572,7 +1600,8 @@ pub async fn copy_playlist(runtime: &Arc>) { } // Persist the copy against the SOURCE id (idempotent). rusqlite is // blocking, so it goes off the async path like every other db write here. - let _ = tokio::task::spawn_blocking(move || crate::library_db_qt::mark_playlist_copied(pid)).await; + let _ = + tokio::task::spawn_blocking(move || crate::library_db_qt::mark_playlist_copied(pid)).await; // Only the page that asked for the copy gets the flag: the user may have // navigated away while the create was in flight (reference `is_open`). let source_id = pid.to_string(); @@ -1586,7 +1615,10 @@ pub async fn copy_playlist(runtime: &Arc>) { if let Some(Some(doc)) = patched { publish(&doc); } - log::info!("[qbz-qt] playlist {pid} copied to library as {}", new_playlist.id); + log::info!( + "[qbz-qt] playlist {pid} copied to library as {}", + new_playlist.id + ); crate::toast_qt::success(qbz_i18n::t("Copied to your library")); // The copy is OWNED, so it joins the sidebar and the Library's favorites @@ -1785,7 +1817,8 @@ pub async fn remove_track(runtime: &Arc>, playlist_tr let Some(pid) = pid else { return }; // Optimistic removal; the API is the source of truth on failure. with_doc(|d| { - d.tracks.retain(|t| t.playlist_track_id != playlist_track_id); + d.tracks + .retain(|t| t.playlist_track_id != playlist_track_id); d.track_count = d.tracks.len() as i32; d.total_duration = total_duration_label(&d.tracks); let doc = d.clone(); @@ -1894,7 +1927,10 @@ pub fn move_row(row_id: &str, delta: i32) { /// Fetch a playlist and build its queue (the card-level actions: the /// LibPlaylistCard overlay Play and its menu's queueing — no open view /// needed). -async fn queue_for(runtime: &Arc>, playlist_id: u64) -> Result, String> { +async fn queue_for( + runtime: &Arc>, + playlist_id: u64, +) -> Result, String> { let pl = runtime .core() .get_playlist(playlist_id) @@ -2024,7 +2060,11 @@ pub async fn set_follow_by_id( /// Add tracks to a playlist by id (the DnD drop path): Qobuz catalog ids /// via the membership API, then refresh the sidebar + the open detail when /// it matches. -pub async fn add_tracks(runtime: &Arc>, playlist_id: u64, track_ids: &[u64]) { +pub async fn add_tracks( + runtime: &Arc>, + playlist_id: u64, + track_ids: &[u64], +) { if track_ids.is_empty() { return; } @@ -2034,7 +2074,10 @@ pub async fn add_tracks(runtime: &Arc>, playlist_id: .await { Ok(()) => { - log::info!("[qbz-qt] dropped {} track(s) onto playlist {playlist_id}", track_ids.len()); + log::info!( + "[qbz-qt] dropped {} track(s) onto playlist {playlist_id}", + track_ids.len() + ); } Err(e) => { log::error!("[qbz-qt] drop add to playlist {playlist_id} failed: {e}"); @@ -2099,13 +2142,9 @@ async fn play_queue_at( if crate::playback_qt::route_play_to_peer(runtime, first_id).await { return Ok(()); } - crate::playback_qt::play_resolved_offline_aware( - runtime, - first_id, - 0, - ) - .await - .map_err(|e| format!("play_track {first_id} failed: {e}"))?; + crate::playback_qt::play_resolved_offline_aware(runtime, first_id, 0) + .await + .map_err(|e| format!("play_track {first_id} failed: {e}"))?; crate::playback_qt::refresh_now_playing(runtime).await; Ok(()) } @@ -2135,7 +2174,10 @@ pub async fn play_shuffled(runtime: &Arc>) -> Result< } /// Row play: the playlist as the queue, starting at this track. -pub async fn play_track(runtime: &Arc>, track_id: &str) -> Result<(), String> { +pub async fn play_track( + runtime: &Arc>, + track_id: &str, +) -> Result<(), String> { let start = with_doc(|d| d.tracks.iter().position(|t| t.id == track_id)).flatten(); let tracks = current_queue(); play_queue_at(runtime, tracks, start.unwrap_or(0), open_context()).await diff --git a/crates/qbz-qt/src/plex_pin_qt.rs b/crates/qbz-qt/src/plex_pin_qt.rs index 61735a1fd..8e25f18c4 100644 --- a/crates/qbz-qt/src/plex_pin_qt.rs +++ b/crates/qbz-qt/src/plex_pin_qt.rs @@ -189,7 +189,11 @@ pub async fn generate_code(server_url: String) { /// Open the Plex sign-in page in the user's browser. pub async fn open_auth_url() { - let url = PIN.lock().unwrap_or_else(|e| e.into_inner()).auth_url.clone(); + let url = PIN + .lock() + .unwrap_or_else(|e| e.into_inner()) + .auth_url + .clone(); if url.is_empty() { return; } diff --git a/crates/qbz-qt/src/purchases_bridge.rs b/crates/qbz-qt/src/purchases_bridge.rs index f78c61d47..d83f1037e 100644 --- a/crates/qbz-qt/src/purchases_bridge.rs +++ b/crates/qbz-qt/src/purchases_bridge.rs @@ -60,7 +60,6 @@ pub mod qbz_purchases_bridge { /// (re-opening the same album, or a filter toggle that changes nothing /// visible), the same trick `QbzSession.trRev` plays. #[qproperty(i32, purchases_rev)] - type QbzPurchases = super::QbzPurchasesRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/purchases_qt.rs b/crates/qbz-qt/src/purchases_qt.rs index c9c66c1c8..7008f1c46 100644 --- a/crates/qbz-qt/src/purchases_qt.rs +++ b/crates/qbz-qt/src/purchases_qt.rs @@ -543,7 +543,11 @@ fn downloaded_for_format(downloaded_format_ids: &[u32], selected: Option) - /// album `formatId` is set and differs from the selected format reports NOTHING /// — that is what makes changing the dropdown hide the green checks. Every /// other status passes through unscoped. -fn status_scoped(state: Option<&AlbumDownload>, track_id: u64, selected: Option) -> Option { +fn status_scoped( + state: Option<&AlbumDownload>, + track_id: u64, + selected: Option, +) -> Option { let state = state?; let status = *state.statuses.get(&track_id)?; if status == DlStatus::Complete { @@ -838,7 +842,8 @@ async fn ensure_registry() -> Registry { // than that import. fn publish_list() { - let json = with_list(|s| serde_json::to_string(&build_list_doc(s)).unwrap_or_else(|_| "{}".into())); + let json = + with_list(|s| serde_json::to_string(&build_list_doc(s)).unwrap_or_else(|_| "{}".into())); crate::purchases_bridge::ui(move |mut b| { b.as_mut().set_list_json(QString::from(json.as_str())); let next = b.as_ref().purchases_rev() + 1; @@ -847,7 +852,8 @@ fn publish_list() { } fn publish_album() { - let json = with_detail(|s| serde_json::to_string(&build_album_doc(s)).unwrap_or_else(|_| "{}".into())); + let json = + with_detail(|s| serde_json::to_string(&build_album_doc(s)).unwrap_or_else(|_| "{}".into())); crate::purchases_bridge::ui(move |mut b| { b.as_mut().set_album_json(QString::from(json.as_str())); let next = b.as_ref().purchases_rev() + 1; @@ -874,7 +880,12 @@ fn build_list_doc(s: &ListState) -> ListDoc { .iter() .filter(|a| !s.hide_unavailable || a.downloadable) .filter(|a| { - matches_quality(filter, a.hires, a.maximum_bit_depth, a.maximum_sampling_rate) + matches_quality( + filter, + a.hires, + a.maximum_bit_depth, + a.maximum_sampling_rate, + ) }) .filter(|a| !s.hide_downloaded || !a.downloaded) .collect(); @@ -888,7 +899,12 @@ fn build_list_doc(s: &ListState) -> ListDoc { .iter() .filter(|t| !s.hide_downloaded || !t.downloaded) .filter(|t| { - matches_quality(filter, t.hires, t.maximum_bit_depth, t.maximum_sampling_rate) + matches_quality( + filter, + t.hires, + t.maximum_bit_depth, + t.maximum_sampling_rate, + ) }) .collect(); @@ -940,7 +956,13 @@ fn sort_albums(list: &mut [&PurchaseAlbum], key: &str, ascending: bool) { flip(ord) }), // "purchased" / "date" — the default. - _ => list.sort_by(|a, b| flip(a.purchased_at.unwrap_or(0).cmp(&b.purchased_at.unwrap_or(0)))), + _ => list.sort_by(|a, b| { + flip( + a.purchased_at + .unwrap_or(0) + .cmp(&b.purchased_at.unwrap_or(0)), + ) + }), } } @@ -1206,7 +1228,9 @@ fn build_album_doc(s: &DetailState) -> AlbumDoc { // a destination exists in the STORE — i.e. a download really happened, // not merely that the user picked a folder. add_to_library_visible: all_complete - && dl_ref.and_then(|d| d.resolved_album_folder.as_ref()).is_some(), + && dl_ref + .and_then(|d| d.resolved_album_folder.as_ref()) + .is_some(), } } @@ -1243,7 +1267,10 @@ async fn read_registry() -> Registry { }; let mut format_map: HashMap> = HashMap::new(); for (track_id, format_id) in formats { - format_map.entry(track_id).or_default().push(format_id as u32); + format_map + .entry(track_id) + .or_default() + .push(format_id as u32); } Registry { downloaded_ids: ids.into_iter().collect(), @@ -1283,7 +1310,11 @@ fn open_owned_library_db() -> Option { /// SHARED `apply_download_flags`, which carries the owner's §11-1 ruling (the /// album rule reads the local registry) — never a local re-derivation, so the /// list and the detail cannot disagree about the same album. -fn annotate(albums: Vec, tracks: Vec, reg: &Registry) -> (Vec, Vec) { +fn annotate( + albums: Vec, + tracks: Vec, + reg: &Registry, +) -> (Vec, Vec) { let mut response = PurchaseResponse { albums: SearchResultsPage { total: albums.len() as u32, @@ -1415,7 +1446,11 @@ fn load_tab(generation: u64, tab: String, force: bool) { if s.generation != generation { return false; } - let already = if tab == "tracks" { s.tracks_loaded } else { s.albums_loaded }; + let already = if tab == "tracks" { + s.tracks_loaded + } else { + s.albums_loaded + }; if already && !force { return false; } @@ -1479,7 +1514,11 @@ fn load_tab(generation: u64, tab: String, force: bool) { &mut s.counts.albums }; if *counter == 0 { - *counter = if server_total > 0 { server_total } else { loaded_len }; + *counter = if server_total > 0 { + server_total + } else { + loaded_len + }; } s.loading = false; @@ -1574,8 +1613,7 @@ pub fn search(query: String) { match result { Ok(response) => { let filtered = svc::filter_purchase_response(response, &query); - let (albums, tracks) = - annotate(filtered.albums.items, filtered.tracks.items, ®); + let (albums, tracks) = annotate(filtered.albums.items, filtered.tracks.items, ®); let applied = with_list(|s| { // Two guards: the visit, and the search token — a slow // earlier query must not overwrite a newer one's results. @@ -1628,7 +1666,13 @@ pub fn set_group(key: String) { } pub fn set_view_mode(mode: String) { - with_list(|s| s.view_mode = if mode == "list" { "list".into() } else { "grid".into() }); + with_list(|s| { + s.view_mode = if mode == "list" { + "list".into() + } else { + "grid".into() + } + }); publish_list(); } @@ -1745,7 +1789,8 @@ async fn load_album(generation: u64, album_id: String) { let reg = ensure_registry().await; - let album = svc::build_purchase_album(&catalog, &purchases, ®.downloaded_ids, ®.format_map); + let album = + svc::build_purchase_album(&catalog, &purchases, ®.downloaded_ids, ®.format_map); let formats = svc::synth_formats(&catalog); let cover_url = catalog.image.best().cloned().unwrap_or_default(); @@ -1812,7 +1857,10 @@ pub fn set_format(format_id: i32) { let changed = with_detail(|s| { let id = format_id as u32; if !s.formats.iter().any(|f| f.id == id) { - log::warn!("[qbz-qt] purchases: format {id} is not offered for album {}", s.album_id); + log::warn!( + "[qbz-qt] purchases: format {id} is not offered for album {}", + s.album_id + ); return false; } s.selected_format_id = Some(id); @@ -1971,7 +2019,12 @@ async fn ensure_destination(mut plan: DownloadPlan) -> Option { /// The album-level context every track is tagged from (§14): built ONCE before /// the loop, with the cover fetched once rather than per track. async fn build_context(catalog: &Album) -> svc::PurchaseAlbumContext { - let cover = match catalog.image.large.as_deref().or(catalog.image.best().map(String::as_str)) { + let cover = match catalog + .image + .large + .as_deref() + .or(catalog.image.best().map(String::as_str)) + { Some(url) if !url.is_empty() => svc::fetch_asset_bytes(url).await, _ => None, }; @@ -2019,17 +2072,20 @@ async fn run_album_download(plan: DownloadPlan) { // the tracks. The folder is the one the FIRST successful track produced — // the store's rewritten destination — falling back to the deterministic // derivation when nothing landed. - let folder = with_store(|st| st.get(&plan.album_id).and_then(|d| d.resolved_album_folder.clone())) - .filter(|d| d != &plan.destination) - .map(PathBuf::from) - .unwrap_or_else(|| { - album_dir_for( - &plan.destination, - &plan.artist, - &plan.album_title, - &plan.quality_dir, - ) - }); + let folder = with_store(|st| { + st.get(&plan.album_id) + .and_then(|d| d.resolved_album_folder.clone()) + }) + .filter(|d| d != &plan.destination) + .map(PathBuf::from) + .unwrap_or_else(|| { + album_dir_for( + &plan.destination, + &plan.artist, + &plan.album_title, + &plan.quality_dir, + ) + }); if cover.is_some() || back.is_some() { let _ = tokio::task::spawn_blocking(move || { svc::write_album_cover_files(&folder, cover.as_deref(), back.as_deref()); @@ -2058,7 +2114,10 @@ fn run_sequential( ctx: svc::PurchaseAlbumContext, cancellable: bool, ) { - let rt = match tokio::runtime::Builder::new_current_thread().enable_all().build() { + let rt = match tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + { Ok(rt) => rt, Err(e) => { log::error!("[qbz-qt] purchases: download runtime build failed: {e}"); @@ -2192,7 +2251,12 @@ async fn refresh_after_download() { let reg = read_registry().await; set_registry(reg.clone()); - let (albums, tracks) = with_list(|s| (std::mem::take(&mut s.albums_raw), std::mem::take(&mut s.tracks_raw))); + let (albums, tracks) = with_list(|s| { + ( + std::mem::take(&mut s.albums_raw), + std::mem::take(&mut s.tracks_raw), + ) + }); let (albums, tracks) = annotate(albums, tracks, ®); with_list(|s| { s.albums_raw = albums; @@ -2210,15 +2274,11 @@ async fn refresh_after_download() { for track in &mut page.items { let tid = track.id as i64; track.downloaded = reg.downloaded_ids.contains(&tid); - track.downloaded_format_ids = - reg.format_map.get(&tid).cloned().unwrap_or_default(); + track.downloaded_format_ids = reg.format_map.get(&tid).cloned().unwrap_or_default(); } } - album.downloaded = svc::album_downloaded_from_registry( - &album.id, - album.tracks_count, - ®.album_counts, - ); + album.downloaded = + svc::album_downloaded_from_registry(&album.id, album.tracks_count, ®.album_counts); true }); if refreshed { @@ -2275,17 +2335,20 @@ pub fn download_goodies() { // The album folder the tracks went into, or the one they WOULD go into // — a booklet is worth downloading before any audio is. - let folder = with_store(|st| st.get(&plan.album_id).and_then(|d| d.resolved_album_folder.clone())) - .filter(|d| d != &plan.destination) - .map(PathBuf::from) - .unwrap_or_else(|| { - album_dir_for( - &plan.destination, - &plan.artist, - &plan.album_title, - &plan.quality_dir, - ) - }); + let folder = with_store(|st| { + st.get(&plan.album_id) + .and_then(|d| d.resolved_album_folder.clone()) + }) + .filter(|d| d != &plan.destination) + .map(PathBuf::from) + .unwrap_or_else(|| { + album_dir_for( + &plan.destination, + &plan.artist, + &plan.album_title, + &plan.quality_dir, + ) + }); for goody in &goodies { // A goodie failure never fails anything: `download_goodie` returns @@ -2321,8 +2384,11 @@ pub fn download_goodies() { /// (`database.rs:1105-1120`) and the gold badge in the Local Library. pub fn add_to_library() { let (album_id, destination) = with_detail(|s| { - let dest = with_store(|st| st.get(&s.album_id).and_then(|d| d.resolved_album_folder.clone())) - .unwrap_or_else(|| s.destination.clone()); + let dest = with_store(|st| { + st.get(&s.album_id) + .and_then(|d| d.resolved_album_folder.clone()) + }) + .unwrap_or_else(|| s.destination.clone()); (s.album_id.clone(), dest) }); if album_id.is_empty() || destination.trim().is_empty() { @@ -2523,7 +2589,10 @@ mod tests { // hiding a failure behind a dropdown change would hide the retry // control with it. let failed = state_with(Some(27), &[(1, DlStatus::Failed)]); - assert_eq!(status_scoped(Some(&failed), 1, Some(7)), Some(DlStatus::Failed)); + assert_eq!( + status_scoped(Some(&failed), 1, Some(7)), + Some(DlStatus::Failed) + ); let running = state_with(Some(27), &[(1, DlStatus::Downloading)]); assert_eq!( @@ -2534,7 +2603,10 @@ mod tests { #[test] fn all_complete_is_format_scoped_and_gates_add_to_library() { - let state = state_with(Some(27), &[(1, DlStatus::Complete), (2, DlStatus::Complete)]); + let state = state_with( + Some(27), + &[(1, DlStatus::Complete), (2, DlStatus::Complete)], + ); assert!(all_complete_for_format(Some(&state), Some(27))); assert!(!all_complete_for_format(Some(&state), Some(7))); assert!(!all_complete_for_format(Some(&state), None)); @@ -2673,17 +2745,47 @@ mod tests { #[test] fn the_quality_filter_matches_the_reference_predicates() { assert!(matches_quality(QualityFilter::All, false, None, None)); - assert!(matches_quality(QualityFilter::Hires, true, Some(24), Some(96.0))); - assert!(!matches_quality(QualityFilter::Hires, false, Some(16), Some(44.1))); + assert!(matches_quality( + QualityFilter::Hires, + true, + Some(24), + Some(96.0) + )); + assert!(!matches_quality( + QualityFilter::Hires, + false, + Some(16), + Some(44.1) + )); // CD: not hires AND (16-bit OR nothing known at all). - assert!(matches_quality(QualityFilter::Cd, false, Some(16), Some(44.1))); + assert!(matches_quality( + QualityFilter::Cd, + false, + Some(16), + Some(44.1) + )); assert!(matches_quality(QualityFilter::Cd, false, None, None)); assert!(!matches_quality(QualityFilter::Cd, false, None, Some(44.1))); - assert!(!matches_quality(QualityFilter::Cd, true, Some(16), Some(44.1))); + assert!(!matches_quality( + QualityFilter::Cd, + true, + Some(16), + Some(44.1) + )); // Lossy: no bit depth, or under 16. - assert!(matches_quality(QualityFilter::Lossy, false, None, Some(44.1))); + assert!(matches_quality( + QualityFilter::Lossy, + false, + None, + Some(44.1) + )); assert!(matches_quality(QualityFilter::Lossy, false, Some(8), None)); - assert!(!matches_quality(QualityFilter::Lossy, false, Some(16), None)); + assert!(!matches_quality( + QualityFilter::Lossy, + false, + Some(16), + None + )); } #[test] @@ -2767,7 +2869,12 @@ mod tests { "So What", "flac", ); - let derived = album_dir_for("/music", "Miles Davis", "Kind of Blue", "[FLAC][24-bit,96kHz]"); + let derived = album_dir_for( + "/music", + "Miles Davis", + "Kind of Blue", + "[FLAC][24-bit,96kHz]", + ); assert_eq!(track.parent().unwrap(), derived.as_path()); assert_eq!( derived, diff --git a/crates/qbz-qt/src/qconnect_bridge.rs b/crates/qbz-qt/src/qconnect_bridge.rs index ae15a5c33..38c466c55 100644 --- a/crates/qbz-qt/src/qconnect_bridge.rs +++ b/crates/qbz-qt/src/qconnect_bridge.rs @@ -51,14 +51,12 @@ pub mod qbz_qconnect { // auto-connect success tail and the offline force-disconnect watcher — // the facade itself never writes it (1:1 with the reference). #[qproperty(bool, qconnect_connected)] - // --- Device picker ------------------------------------------------- // JSON array of device rows (see the module header for the exact // shape). Rebuilt on every inbound event by the sink. #[qproperty(QString, devices_json)] // -1 = none. #[qproperty(i32, active_renderer_id)] - // --- Diagnostics modal (DeveloperSettings > QOBUZ CONNECT) --------- // QML-driven open/close (DeveloperSettings sets it true; the modal's // own close sets it false). @@ -67,7 +65,6 @@ pub mod qbz_qconnect { #[qproperty(QString, diag_status)] // The rolling 150-line event log, newest first. #[qproperty(QString, diag_log_text)] - type QbzQConnect = super::QbzQConnectRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY diff --git a/crates/qbz-qt/src/qconnect_engine_qt.rs b/crates/qbz-qt/src/qconnect_engine_qt.rs index 46dfb1f83..7e57fc7d7 100644 --- a/crates/qbz-qt/src/qconnect_engine_qt.rs +++ b/crates/qbz-qt/src/qconnect_engine_qt.rs @@ -92,10 +92,14 @@ impl QconnectRendererEngine for QtRendererEngine { self.core().stop().map_err(|err| err.to_string()) } fn seek(&self, position_secs: u64) -> Result<(), String> { - self.core().seek(position_secs).map_err(|err| err.to_string()) + self.core() + .seek(position_secs) + .map_err(|err| err.to_string()) } fn set_volume(&self, fraction: f32) -> Result<(), String> { - self.core().set_volume(fraction).map_err(|err| err.to_string()) + self.core() + .set_volume(fraction) + .map_err(|err| err.to_string()) } fn get_playback_state(&self) -> PlaybackState { self.core().get_playback_state() @@ -220,14 +224,10 @@ impl QconnectRendererEngine for QtRendererEngine { fn current_output_format(&self) -> Option<(u32, u32)> { let player = self.core().player(); - Some(( - player.state.get_sample_rate(), - player.state.get_bit_depth(), - )) + Some((player.state.get_sample_rate(), player.state.get_bit_depth())) } } - async fn download_remote_audio(url: &str) -> Result, String> { let response = reqwest::Client::new() .get(url) @@ -358,7 +358,12 @@ async fn probe_remote_stream_info(url: &str) -> Result .header("User-Agent", "Mozilla/5.0") .send() .await - .map_err(|err| format!("probe HEAD request failed: {}", describe_reqwest_error(&err)))?; + .map_err(|err| { + format!( + "probe HEAD request failed: {}", + describe_reqwest_error(&err) + ) + })?; if !head_response.status().is_success() { return Err(format!( @@ -381,7 +386,12 @@ async fn probe_remote_stream_info(url: &str) -> Result .header("Range", "bytes=0-65535") .send() .await - .map_err(|err| format!("probe range request failed: {}", describe_reqwest_error(&err)))?; + .map_err(|err| { + format!( + "probe range request failed: {}", + describe_reqwest_error(&err) + ) + })?; if !range_response.status().is_success() { return Err(format!( @@ -486,8 +496,12 @@ async fn download_and_stream_remote_track( let mut last_log_time = Instant::now(); while let Some(chunk_result) = stream.next().await { - let chunk = chunk_result - .map_err(|err| format!("remote streaming chunk failed: {}", describe_reqwest_error(&err)))?; + let chunk = chunk_result.map_err(|err| { + format!( + "remote streaming chunk failed: {}", + describe_reqwest_error(&err) + ) + })?; bytes_received += chunk.len() as u64; if let Err(err) = writer.push_chunk(&chunk) { diff --git a/crates/qbz-qt/src/qconnect_event_sink_qt.rs b/crates/qbz-qt/src/qconnect_event_sink_qt.rs index 21cb77dd3..f8dfb72dc 100644 --- a/crates/qbz-qt/src/qconnect_event_sink_qt.rs +++ b/crates/qbz-qt/src/qconnect_event_sink_qt.rs @@ -203,7 +203,10 @@ impl QtQconnectEventSink { let is_remote = transport_connected && is_peer_renderer_active(session); let active = session.active_renderer_id; let active_info = active.and_then(|active_id| { - session.renderers.iter().find(|r| r.renderer_id == active_id) + session + .renderers + .iter() + .find(|r| r.renderer_id == active_id) }); let cast_target = if is_remote { active_info @@ -302,8 +305,8 @@ impl QtQconnectEventSink { .await; if let Some(loop_mode) = outcome.apply_loop_mode { - if let Err(err) = qconnect_app::renderer::apply_remote_loop_mode(&self.engine, loop_mode) - .await + if let Err(err) = + qconnect_app::renderer::apply_remote_loop_mode(&self.engine, loop_mode).await { log::warn!("[QConnect] Failed to apply remote loop mode: {err}"); } @@ -503,8 +506,7 @@ impl QconnectEventSink for QtQconnectEventSink { } else { log::info!("[QConnect] Renderer command applied: {:?}", command); } - let became_active = - matches!(command, RendererCommand::SetActive { active: true }); + let became_active = matches!(command, RendererCommand::SetActive { active: true }); if let Err(err) = qconnect_app::renderer::apply_renderer_command( &self.engine, &self.sync_state, @@ -560,9 +562,7 @@ impl QconnectEventSink for QtQconnectEventSink { // (Reference TODO(slint-qconnect-ui); kept unwired per §9 D6.) log::info!("[QConnect] Lifecycle -> {state:?} (UI badge TODO)"); } - QconnectAppEvent::Diagnostic { - channel, level, .. - } => { + QconnectAppEvent::Diagnostic { channel, level, .. } => { log::debug!("[QConnect] diagnostic {channel} [{level}]"); } _ => {} diff --git a/crates/qbz-qt/src/qconnect_qt.rs b/crates/qbz-qt/src/qconnect_qt.rs index 6f12c90d1..d6fec461a 100644 --- a/crates/qbz-qt/src/qconnect_qt.rs +++ b/crates/qbz-qt/src/qconnect_qt.rs @@ -53,16 +53,13 @@ use qconnect_app::queue_resolution::{ resolve_controller_queue_item_from_snapshots, resolve_queue_item_ids_from_queue_state, QconnectRemoteSkipDirection, }; -use qconnect_app::renderer::{ - PLAYING_STATE_PAUSED, PLAYING_STATE_PLAYING, PLAYING_STATE_STOPPED, -}; +use qconnect_app::renderer::{PLAYING_STATE_PAUSED, PLAYING_STATE_PLAYING, PLAYING_STATE_STOPPED}; use qconnect_app::{ build_effective_renderer_snapshot, ensure_session_renderer_state, is_local_renderer_active, - is_peer_renderer_active, renderer_allows_remote_volume, QConnectQueueState, - QConnectRendererState, QconnectApp, QconnectAppEvent, QconnectEventSink, - queue_item_snapshot_for_cursor, QconnectFileAudioQualitySnapshot, QconnectLifecycleState, - QconnectRemoteSyncState, QconnectSessionState, QueueCommandType, RendererReport, - RendererReportType, SessionLoopHost, + is_peer_renderer_active, queue_item_snapshot_for_cursor, renderer_allows_remote_volume, + QConnectQueueState, QConnectRendererState, QconnectApp, QconnectAppEvent, QconnectEventSink, + QconnectFileAudioQualitySnapshot, QconnectLifecycleState, QconnectRemoteSyncState, + QconnectSessionState, QueueCommandType, RendererReport, RendererReportType, SessionLoopHost, }; use qconnect_transport_ws::{NativeWsTransport, WsTransportConfig}; use serde_json::{json, Value}; @@ -75,8 +72,8 @@ use crate::qconnect_transport_qt::{ build_set_position_player_state_request, default_qconnect_device_info, default_qconnect_device_info_with_name, load_persisted_device_name, resolve_transport_config, QconnectJoinSessionRequest, QconnectMuteVolumeRequest, QconnectQueueVersionPayload, - QconnectSetPlayerStateQueueItemPayload, QconnectSetPlayerStateRequest, QconnectSetVolumeRequest, - AUDIO_QUALITY_HIRES_LEVEL2, BUFFER_STATE_OK, + QconnectSetPlayerStateQueueItemPayload, QconnectSetPlayerStateRequest, + QconnectSetVolumeRequest, AUDIO_QUALITY_HIRES_LEVEL2, BUFFER_STATE_OK, }; const QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS: u64 = 1_500; @@ -788,7 +785,11 @@ impl QtQconnectService { let active_id = session.active_renderer_id; let local_id = session.local_renderer_id; if let Some(active_id) = active_id { - if let Some(renderer) = session.renderers.iter().find(|r| r.renderer_id == active_id) { + if let Some(renderer) = session + .renderers + .iter() + .find(|r| r.renderer_id == active_id) + { snap.active_name = renderer.friendly_name.clone(); snap.active_brand = renderer.brand.clone(); snap.active_model = renderer.model.clone(); @@ -1215,8 +1216,11 @@ impl QtQconnectService { { let state = sync_state.lock().await; if let Some(applied) = &state.last_applied_queue_state { - let applied_ids: Vec = - applied.queue_items.iter().map(|item| item.track_id).collect(); + let applied_ids: Vec = applied + .queue_items + .iter() + .map(|item| item.track_id) + .collect(); if applied_ids == ordered_ids { return; } @@ -1261,7 +1265,9 @@ impl QtQconnectService { log::info!( "[QConnect] Pushed local queue to Connect ({count} tracks, start={start_index})" ); - dev_push_event(format!("-> QueueLoadTracks {count} tracks start={start_index}")); + dev_push_event(format!( + "-> QueueLoadTracks {count} tracks start={start_index}" + )); *self.last_pushed_queue_ids.lock().await = Some(ordered_ids); } Err(err) => log::warn!("[QConnect] Failed to push local queue: {err}"), @@ -1350,9 +1356,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_on_peer: pushed queue ({count} tracks, start={start_index})" ); - dev_push_event( - format!("-> play_on_peer QueueLoadTracks {count} start={start_index}"), - ); + dev_push_event(format!( + "-> play_on_peer QueueLoadTracks {count} start={start_index}" + )); *self.last_pushed_queue_ids.lock().await = Some(ordered_ids); } Err(err) => { @@ -1437,9 +1443,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_next_on_peer: inserted track {track_id} (after={insert_after:?})" ); - dev_push_event( - format!("-> play_next QueueInsertTracks {track_id} after={insert_after:?}"), - ); + dev_push_event(format!( + "-> play_next QueueInsertTracks {track_id} after={insert_after:?}" + )); self.note_controller_insert(insert_after, &[track_id]).await; } Err(err) => { @@ -1458,7 +1464,11 @@ impl QtQconnectService { /// Mirrors the webplayer `queue_add_tracks` path: a single append command with /// a fresh `context_uuid`, `autoplay_reset: false`, `autoplay_loading: false`. /// Same admission + echo handling as `play_next_on_peer_if_active`. - pub async fn add_to_queue_on_peer_if_active(&self, track_id: u64, source: Option<&str>) -> bool { + pub async fn add_to_queue_on_peer_if_active( + &self, + track_id: u64, + source: Option<&str>, + ) -> bool { let peer_active = self.is_peer_renderer_active().await; if !peer_active { return false; @@ -1469,7 +1479,9 @@ impl QtQconnectService { "[QConnect] add_to_queue_on_peer: track {track_id} not Qobuz-castable; refusing" ); crate::toast_qt::error(qbz_i18n::t("Track not castable to Qobuz Connect")); - dev_push_event(format!("-> add_to_queue REFUSED (non-Qobuz track {track_id})")); + dev_push_event(format!( + "-> add_to_queue REFUSED (non-Qobuz track {track_id})" + )); return true; } @@ -1622,9 +1634,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_next_batch_on_peer: inserted {count} tracks (after={insert_after:?})" ); - dev_push_event( - format!("-> play_next QueueInsertTracks {count} tracks after={insert_after:?}"), - ); + dev_push_event(format!( + "-> play_next QueueInsertTracks {count} tracks after={insert_after:?}" + )); self.note_controller_insert(insert_after, &ids_u64).await; } Err(err) => { @@ -1680,7 +1692,9 @@ impl QtQconnectService { "[QConnect] play_later_on_peer: track {track_id} not Qobuz-castable; refusing" ); crate::toast_qt::error(qbz_i18n::t("Track not castable to Qobuz Connect")); - dev_push_event(format!("-> play_later REFUSED (non-Qobuz track {track_id})")); + dev_push_event(format!( + "-> play_later REFUSED (non-Qobuz track {track_id})" + )); return true; } @@ -1704,9 +1718,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_later_on_peer: inserted track {track_id} (after={insert_after:?})" ); - dev_push_event( - format!("-> play_later QueueInsertTracks {track_id} after={insert_after:?}"), - ); + dev_push_event(format!( + "-> play_later QueueInsertTracks {track_id} after={insert_after:?}" + )); self.note_controller_insert(insert_after, &[track_id]).await; } Err(err) => { @@ -1750,11 +1764,11 @@ impl QtQconnectService { return false; } if !self.is_track_castable(track_id, source) { - log::info!( - "[QConnect] insert_at_slot: track {track_id} not Qobuz-castable; refusing" - ); + log::info!("[QConnect] insert_at_slot: track {track_id} not Qobuz-castable; refusing"); crate::toast_qt::error(qbz_i18n::t("Track not castable to Qobuz Connect")); - dev_push_event(format!("-> insert_at_slot REFUSED (non-Qobuz track {track_id})")); + dev_push_event(format!( + "-> insert_at_slot REFUSED (non-Qobuz track {track_id})" + )); return true; } @@ -1861,9 +1875,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_later_batch_on_peer: inserted {count} tracks (after={insert_after:?})" ); - dev_push_event( - format!("-> play_later QueueInsertTracks {count} tracks after={insert_after:?}"), - ); + dev_push_event(format!( + "-> play_later QueueInsertTracks {count} tracks after={insert_after:?}" + )); self.note_controller_insert(insert_after, &ids_u64).await; } Err(err) => { @@ -2091,8 +2105,11 @@ impl QtQconnectService { /// snapshot (`playing_state == PLAYING`). Title/artist/art come from the /// materialized local core queue, so only these three fields are needed here. pub async fn remote_now_playing(&self) -> Option { - let (renderer, queue, _session) = - self.effective_remote_renderer_snapshot().await.ok().flatten()?; + let (renderer, queue, _session) = self + .effective_remote_renderer_snapshot() + .await + .ok() + .flatten()?; Some(RemoteNowPlaying { position_ms: renderer.current_position_ms.unwrap_or(0), updated_at_ms: renderer.updated_at_ms, @@ -2212,21 +2229,23 @@ impl QtQconnectService { } }; log::info!("[QConnect] skip {direction_label} handoff skipped: {reason}"); - dev_push_event(format!("controller skip {direction_label}: local ({reason})")); + dev_push_event(format!( + "controller skip {direction_label}: local ({reason})" + )); return Ok(false); }; - let resolution = - resolve_controller_queue_item_from_snapshots(&queue, &renderer, direction); + let resolution = resolve_controller_queue_item_from_snapshots(&queue, &renderer, direction); let Some(target_queue_item_id) = resolution.target_queue_item_id else { log::warn!( "[QConnect] skip {direction_label} handoff: no target queue item resolved (strategy={})", resolution.strategy ); - dev_push_event( - format!("controller skip {direction_label}: NO TARGET ({})", resolution.strategy), - ); + dev_push_event(format!( + "controller skip {direction_label}: NO TARGET ({})", + resolution.strategy + )); return Err(format!( "remote renderer active but no {direction_label} target queue item could be resolved" )); @@ -2259,9 +2278,10 @@ impl QtQconnectService { "[QConnect] skip {direction_label} handoff -> queue_item {target_queue_item_id} (strategy={})", resolution.strategy ); - dev_push_event( - format!("controller skip {direction_label} -> qid {target_queue_item_id} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller skip {direction_label} -> qid {target_queue_item_id} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2319,9 +2339,10 @@ impl QtQconnectService { .await; log::info!("[QConnect] toggle_play handoff -> playing_state {next_playing_state}"); - dev_push_event( - format!("controller toggle_play -> {next_playing_state} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller toggle_play -> {next_playing_state} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2364,14 +2385,15 @@ impl QtQconnectService { state.last_load_attempt = Some((track_id, std::time::Instant::now())); } log::info!("[QConnect] play_track handoff skipped: {reason} (track {track_id})"); - dev_push_event(format!("controller play_track {track_id}: local ({reason})")); + dev_push_event(format!( + "controller play_track {track_id}: local ({reason})" + )); return Ok(false); } let deadline = tokio::time::Instant::now() + std::time::Duration::from_millis(QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS); - let poll_interval = - std::time::Duration::from_millis(QCONNECT_PLAY_TRACK_HANDOFF_POLL_MS); + let poll_interval = std::time::Duration::from_millis(QCONNECT_PLAY_TRACK_HANDOFF_POLL_MS); let mut attempts: u32 = 0; loop { attempts += 1; @@ -2412,9 +2434,9 @@ impl QtQconnectService { log::info!( "[QConnect] play_track handoff -> qid {target_queue_item_id} (track {track_id}, attempts={attempts})" ); - dev_push_event( - format!("controller play_track {track_id} -> qid {target_queue_item_id}"), - ); + dev_push_event(format!( + "controller play_track {track_id} -> qid {target_queue_item_id}" + )); return Ok(true); } @@ -2429,7 +2451,9 @@ impl QtQconnectService { log::warn!( "[QConnect] play_track handoff: track {track_id} not present in remote queue after {QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS}ms" ); - dev_push_event(format!("controller play_track {track_id}: NOT IN QUEUE (timeout)")); + dev_push_event(format!( + "controller play_track {track_id}: NOT IN QUEUE (timeout)" + )); Err(format!( "remote renderer active but track {track_id} was not present in qconnect queue after {QCONNECT_PLAY_TRACK_HANDOFF_WAIT_MS}ms" )) @@ -2513,10 +2537,14 @@ impl QtQconnectService { self.send_command(QueueCommandType::CtrlSrvrSetShuffleMode, payload) .await?; - log::info!("[QConnect] shuffle -> {next_shuffle} (cloud, active={:?})", session.active_renderer_id); - dev_push_event( - format!("shuffle -> {next_shuffle} (active={:?})", session.active_renderer_id), + log::info!( + "[QConnect] shuffle -> {next_shuffle} (cloud, active={:?})", + session.active_renderer_id ); + dev_push_event(format!( + "shuffle -> {next_shuffle} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2545,10 +2573,14 @@ impl QtQconnectService { self.send_command(QueueCommandType::CtrlSrvrSetLoopMode, payload) .await?; - log::info!("[QConnect] repeat -> {next_loop} (cloud, active={:?})", session.active_renderer_id); - dev_push_event( - format!("repeat -> {next_loop} (active={:?})", session.active_renderer_id), + log::info!( + "[QConnect] repeat -> {next_loop} (cloud, active={:?})", + session.active_renderer_id ); + dev_push_event(format!( + "repeat -> {next_loop} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2602,12 +2634,10 @@ impl QtQconnectService { "[QConnect] reorder upcoming {from_index} -> {to_index} (cloud, active={:?})", session.active_renderer_id ); - dev_push_event( - format!( - "reorder {from_index} -> {to_index} (active={:?})", - session.active_renderer_id - ), - ); + dev_push_event(format!( + "reorder {from_index} -> {to_index} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2650,9 +2680,10 @@ impl QtQconnectService { .await?; log::info!("[QConnect] set_volume handoff -> {volume}"); - dev_push_event( - format!("controller volume -> {volume} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller volume -> {volume} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2674,9 +2705,10 @@ impl QtQconnectService { .await?; log::info!("[QConnect] mute handoff -> {value}"); - dev_push_event( - format!("controller mute -> {value} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller mute -> {value} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2701,9 +2733,10 @@ impl QtQconnectService { .await?; log::info!("[QConnect] set_autoplay_mode handoff -> {enabled}"); - dev_push_event( - format!("controller autoplay -> {enabled} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller autoplay -> {enabled} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2733,9 +2766,10 @@ impl QtQconnectService { .await?; log::info!("[QConnect] autoplay_load_tracks handoff -> {track_count} tracks"); - dev_push_event( - format!("controller autoplay_load {track_count} (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller autoplay_load {track_count} (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2776,9 +2810,10 @@ impl QtQconnectService { .await; log::info!("[QConnect] stop handoff"); - dev_push_event( - format!("controller stop (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller stop (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2816,9 +2851,10 @@ impl QtQconnectService { } log::info!("[QConnect] set_position handoff -> {position_ms}ms"); - dev_push_event( - format!("controller seek -> {position_ms}ms (active={:?})", session.active_renderer_id), - ); + dev_push_event(format!( + "controller seek -> {position_ms}ms (active={:?})", + session.active_renderer_id + )); Ok(true) } @@ -2836,9 +2872,9 @@ impl QtQconnectService { .ok_or_else(|| "QConnect service is not running".to_string())? }; let handled = app.send_set_active_renderer(renderer_id).await?; - dev_push_event( - format!("controller set_active_renderer -> {renderer_id} (sent={handled})"), - ); + dev_push_event(format!( + "controller set_active_renderer -> {renderer_id} (sent={handled})" + )); Ok(handled) } } @@ -3277,7 +3313,10 @@ mod tests { #[test] fn qconnect_admits_catalog_and_offline_catalog_rows() { for source in ["qobuz", "qobuz_download", "qobuz_purchase", "offline"] { - assert!(is_qconnect_queue_track(&track(Some(source), true)), "{source}"); + assert!( + is_qconnect_queue_track(&track(Some(source), true)), + "{source}" + ); } assert!(is_qconnect_queue_track(&track(None, false))); } @@ -3285,7 +3324,10 @@ mod tests { #[test] fn qconnect_rejects_every_server_and_file_source() { for source in ["local", "plex", "jellyfin", "subsonic", "navidrome"] { - assert!(!is_qconnect_queue_track(&track(Some(source), true)), "{source}"); + assert!( + !is_qconnect_queue_track(&track(Some(source), true)), + "{source}" + ); } assert!(!is_qconnect_queue_track(&track(None, true))); } diff --git a/crates/qbz-qt/src/qconnect_transport_qt.rs b/crates/qbz-qt/src/qconnect_transport_qt.rs index 8aa2cc5d8..5c4305051 100644 --- a/crates/qbz-qt/src/qconnect_transport_qt.rs +++ b/crates/qbz-qt/src/qconnect_transport_qt.rs @@ -574,7 +574,10 @@ fn normalize_opt_string(value: Option) -> Option { } pub fn parse_subscribe_channels(items: Vec) -> Result>, String> { - items.into_iter().map(|item| decode_hex_channel(&item)).collect() + items + .into_iter() + .map(|item| decode_hex_channel(&item)) + .collect() } pub fn decode_hex_channel(raw: &str) -> Result, String> { diff --git a/crates/qbz-qt/src/quality_qt.rs b/crates/qbz-qt/src/quality_qt.rs index a5b1229d4..7198ad7aa 100644 --- a/crates/qbz-qt/src/quality_qt.rs +++ b/crates/qbz-qt/src/quality_qt.rs @@ -43,7 +43,10 @@ pub(crate) fn dsd_multiple_label(sample_rate: Option) -> String { Some(r) if r >= 1_000.0 => r * 1000.0, _ => return "DSD".to_string(), }; - format!("DSD{}", ((hz / 2_822_400.0).round() as u32).saturating_mul(64)) + format!( + "DSD{}", + ((hz / 2_822_400.0).round() as u32).saturating_mul(64) + ) } /// `(tier, detail)` for `controls/QualityBadgeFull.qml`. diff --git a/crates/qbz-qt/src/quality_state.rs b/crates/qbz-qt/src/quality_state.rs index 30180bd2c..21e8e9b18 100644 --- a/crates/qbz-qt/src/quality_state.rs +++ b/crates/qbz-qt/src/quality_state.rs @@ -162,7 +162,10 @@ pub fn dsd_multiple_label(sample_rate: Option) -> String { Some(r) if r >= 1_000.0 => r * 1000.0, _ => return "DSD".to_string(), }; - format!("DSD{}", ((hz / 2_822_400.0).round() as u32).saturating_mul(64)) + format!( + "DSD{}", + ((hz / 2_822_400.0).round() as u32).saturating_mul(64) + ) } // --------------------------------------------------------------------------- @@ -242,7 +245,10 @@ mod tests { QualityLimit::None as i32 ); // Ungoverned source (id 0) → none. - assert_eq!(classify_limit_cause(true, 0, 1, 16), QualityLimit::None as i32); + assert_eq!( + classify_limit_cause(true, 0, 1, 16), + QualityLimit::None as i32 + ); // Requested Hi-Res+ and still short → Qobuz had no more. assert_eq!( classify_limit_cause(true, Quality::UltraHiRes.id(), 0, 24), @@ -251,12 +257,22 @@ mod tests { // Requested Hi-Res, delivered 24-bit → the promise held, so the // request-time cause did this. assert_eq!( - classify_limit_cause(true, Quality::HiRes.id(), QualityLimit::Preference as i32, 24), + classify_limit_cause( + true, + Quality::HiRes.id(), + QualityLimit::Preference as i32, + 24 + ), QualityLimit::Preference as i32 ); // Requested Hi-Res, delivered 16-bit → the promise broke: catalog. assert_eq!( - classify_limit_cause(true, Quality::HiRes.id(), QualityLimit::Preference as i32, 16), + classify_limit_cause( + true, + Quality::HiRes.id(), + QualityLimit::Preference as i32, + 16 + ), QualityLimit::CatalogAvailability as i32 ); } diff --git a/crates/qbz-qt/src/queue_bridge.rs b/crates/qbz-qt/src/queue_bridge.rs index 303f685ee..cc7cf0abb 100644 --- a/crates/qbz-qt/src/queue_bridge.rs +++ b/crates/qbz-qt/src/queue_bridge.rs @@ -32,16 +32,13 @@ pub mod qbz_queue { // --- Queue panel ------------------------------------------------- // POC: empty until phase 4 (QML shows the empty states). #[qproperty(QList_QVariant, queue_model)] - // --- Queue panel (phase 9) ----------------------------------------- // One JSON document (queue_qt.rs QueueDoc: current/upcoming/history // + pagination + #442 section markers). Supersedes `queueModel`. #[qproperty(QString, queue_json)] - // Full chronological queue projection used only by QueueView. It is // opt-in so ordinary sidebar publishes stay paged and cheap. #[qproperty(QString, extended_queue_json)] - // --- Immersive coverflow (2026-08-02 immersive-port contract §4.4) -- // `{"index":i32,"tracks":[{id,title,artist,artUrl}]}` over the FULL // flat queue ([history oldest-first, NOW, upcoming]); `index` = flat @@ -50,7 +47,6 @@ pub mod qbz_queue { // immersive Queue badge + coverflow read THIS, never the paged // `queue_json`. Full-shape default, never "{}" (trap 15). #[qproperty(QString, coverflow_json)] - // --- Sleep timer (queue footer) ------------------------------------ // Kept OUT of `queue_json`: the countdown reformats every second and // the document is re-parsed by three QML files on every change, so @@ -71,7 +67,6 @@ pub mod qbz_queue { #[qproperty(bool, drop_play_prompt)] #[qproperty(bool, sleep_active)] #[qproperty(QString, sleep_remaining)] - type QbzQueue = super::QbzQueueRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY @@ -260,12 +255,7 @@ impl qbz_queue::QbzQueue { expected_id: QString, slot: i32, ) { - crate::queue_extended_drop( - phase.to_string(), - index, - expected_id.to_string(), - slot, - ); + crate::queue_extended_drop(phase.to_string(), index, expected_id.to_string(), slot); } pub fn queue_remove_upcoming_flat(self: Pin<&mut Self>, index: i32) { diff --git a/crates/qbz-qt/src/recently_qt.rs b/crates/qbz-qt/src/recently_qt.rs index b0287d3a3..bf09642bb 100644 --- a/crates/qbz-qt/src/recently_qt.rs +++ b/crates/qbz-qt/src/recently_qt.rs @@ -221,7 +221,9 @@ pub(crate) fn prune_albums(album_ids: &[String]) -> usize { store .tracks .retain(|t| !album_ids.iter().any(|k| k == &t.album_id)); - store.albums.retain(|a| !album_ids.iter().any(|k| k == &a.id)); + store + .albums + .retain(|a| !album_ids.iter().any(|k| k == &a.id)); let removed = tracks_before - store.tracks.len(); if removed > 0 || albums_before != store.albums.len() { write_store(&store); diff --git a/crates/qbz-qt/src/reco_dismiss_qt.rs b/crates/qbz-qt/src/reco_dismiss_qt.rs index 3efc8855f..b06fadf4f 100644 --- a/crates/qbz-qt/src/reco_dismiss_qt.rs +++ b/crates/qbz-qt/src/reco_dismiss_qt.rs @@ -223,7 +223,10 @@ mod tests { dismiss(9, "Lost", ""); // no-op while unbound; must not panic init_for_user(&dir); assert!(ids_snapshot().contains(&42), "rows survive a re-bind"); - assert!(!ids_snapshot().contains(&9), "unbound mutation did not persist"); + assert!( + !ids_snapshot().contains(&9), + "unbound mutation did not persist" + ); // Undo. remove(42); diff --git a/crates/qbz-qt/src/recommendations_qt.rs b/crates/qbz-qt/src/recommendations_qt.rs index 152eb8503..d07cdee1c 100644 --- a/crates/qbz-qt/src/recommendations_qt.rs +++ b/crates/qbz-qt/src/recommendations_qt.rs @@ -88,10 +88,7 @@ static LOADED: AtomicBool = AtomicBool::new(false); static IN_FLIGHT: AtomicBool = AtomicBool::new(false); fn rows_snapshot() -> Rows { - ROWS.lock() - .ok() - .and_then(|g| g.clone()) - .unwrap_or_default() + ROWS.lock().ok().and_then(|g| g.clone()).unwrap_or_default() } fn mutate(f: impl FnOnce(&mut Rows)) { @@ -294,7 +291,12 @@ fn publish_weeklies(download: bool) { "slimTracks", rows.weekly_exploration.clone(), ), - section(WEEKLY_JAMS, String::new(), "slimTracks", rows.weekly_jams.clone()), + section( + WEEKLY_JAMS, + String::new(), + "slimTracks", + rows.weekly_jams.clone(), + ), ] .into_iter() .flatten() @@ -310,7 +312,8 @@ fn publish_weeklies(download: bool) { } let json = serde_json::to_string(&doc).unwrap_or_else(|_| "{}".to_string()); crate::home_bridge::ui(move |mut b| { - b.as_mut().set_reco_weekly_json(QString::from(json.as_str())); + b.as_mut() + .set_reco_weekly_json(QString::from(json.as_str())); }); if download && !missing.is_empty() { @@ -742,9 +745,11 @@ async fn run(force: bool) { // ListenBrainz's own weekly cadence and have their own per-week cache, so // one transient empty build must not hide them for the whole window. if !force { - let cached = inputs - .cache - .and_then(|c| c.lock().ok().and_then(|g| g.get_results(&source_key, ttl_secs))); + let cached = inputs.cache.and_then(|c| { + c.lock() + .ok() + .and_then(|g| g.get_results(&source_key, ttl_secs)) + }); if let Some(json) = cached { if let Ok(result) = serde_json::from_str::(&json) { apply_all(result, &followed); @@ -848,7 +853,10 @@ async fn run(force: bool) { results-cache write (likely transient; next open re-fetches)" ); } else { - let json = collector.lock().ok().and_then(|g| serde_json::to_string(&*g).ok()); + let json = collector + .lock() + .ok() + .and_then(|g| serde_json::to_string(&*g).ok()); if let (Ok(guard), Some(json)) = (cache_mutex.lock(), json) { guard.put_results(&source_key, &json); } @@ -875,10 +883,7 @@ async fn build_and_apply_weeklies(inputs: &RecoInputs<'_>) { if inputs.listenbrainz.is_none() { return; } - let (explore, jams) = tokio::join!( - build_weekly_exploration(inputs), - build_weekly_jams(inputs) - ); + let (explore, jams) = tokio::join!(build_weekly_exploration(inputs), build_weekly_jams(inputs)); mutate(|rows| { rows.weekly_exploration = explore.iter().map(card_from_track).collect(); rows.weekly_jams = jams.iter().map(card_from_track).collect(); diff --git a/crates/qbz-qt/src/renderer_qt.rs b/crates/qbz-qt/src/renderer_qt.rs index df9a8e395..44a3e1567 100644 --- a/crates/qbz-qt/src/renderer_qt.rs +++ b/crates/qbz-qt/src/renderer_qt.rs @@ -449,8 +449,8 @@ pub fn apply_gpu_preference() { // A GPU pick needs Vulkan (see below), so it cannot also honour an explicit // `software`/`opengl` choice — the more specific, explicitly-chosen // RENDERER wins and this says so rather than quietly overriding it. - if let Some(forced) = std::env::var_os("QSG_RHI_BACKEND") - .or_else(|| std::env::var_os("QT_QUICK_BACKEND")) + if let Some(forced) = + std::env::var_os("QSG_RHI_BACKEND").or_else(|| std::env::var_os("QT_QUICK_BACKEND")) { log::info!( "[qbz-qt] gpu: the renderer row already forced {forced:?}; a GPU pick needs \ @@ -461,16 +461,17 @@ pub fn apply_gpu_preference() { let pref = crate::settings_qt::pref_str("gpu_power", "auto"); let Some(gpu) = resolve_gpu(&pref) else { if pref != "auto" && !pref.is_empty() { - log::warn!( - "[qbz-qt] gpu: '{pref}' matches no device present on this machine -> Auto" - ); + log::warn!("[qbz-qt] gpu: '{pref}' matches no device present on this machine -> Auto"); } return; }; if gpu.index == default_gpu_index() { // Already the default: selecting it needs no override, and forcing one // would buy the Vulkan coupling below for nothing. - log::info!("[qbz-qt] gpu: '{}' is device 0 (the default) -> no override", gpu.name); + log::info!( + "[qbz-qt] gpu: '{}' is device 0 (the default) -> no override", + gpu.name + ); return; } // THE VULKAN COUPLING, and why it is the only honest route. @@ -545,7 +546,10 @@ mod tests { assert!(!reduce_motion(false), "GPU + desktop = full motion"); assert!(reduce_motion(true), "kiosk forces it on regardless of tier"); GPU_TIER.store(false, Ordering::SeqCst); - assert!(reduce_motion(false), "a software tier forces it on by itself"); + assert!( + reduce_motion(false), + "a software tier forces it on by itself" + ); assert!(reduce_motion(true)); GPU_TIER.store(true, Ordering::SeqCst); } diff --git a/crates/qbz-qt/src/rip_qt.rs b/crates/qbz-qt/src/rip_qt.rs index 7086fac2a..ab18986bb 100644 --- a/crates/qbz-qt/src/rip_qt.rs +++ b/crates/qbz-qt/src/rip_qt.rs @@ -72,7 +72,10 @@ fn with_status(f: impl FnOnce(&mut Status) -> R) -> R { fn publish_status() { let json = with_status(|s| serde_json::to_string(s).unwrap_or_else(|_| "null".into())); - ui(move |mut b| b.as_mut().set_local_rip_status(QString::from(json.as_str()))); + ui(move |mut b| { + b.as_mut() + .set_local_rip_status(QString::from(json.as_str())) + }); } /// Show / hide the progress panel. The JOB is untouched either way — this @@ -207,7 +210,8 @@ pub fn run( (overall * 100.0) as u32 ); ui(move |mut b| { - b.as_mut().set_local_rip_progress(QString::from(text.as_str())); + b.as_mut() + .set_local_rip_progress(QString::from(text.as_str())); }); true }) @@ -281,7 +285,11 @@ fn build_plan( if rows.is_empty() { return None; } - let album = if album.is_empty() { rows[0].album.clone() } else { album.to_string() }; + let album = if album.is_empty() { + rows[0].album.clone() + } else { + album.to_string() + }; let album_artist = if album_artist.is_empty() { rows[0] .album_artist @@ -305,7 +313,11 @@ fn build_plan( let (title, artist) = named.get(i).cloned().unwrap_or_default(); Some(qbz_rip::RipTrack { number: t.track_number.unwrap_or(i as u32 + 1), - title: if title.is_empty() { t.title.clone() } else { title }, + title: if title.is_empty() { + t.title.clone() + } else { + title + }, artist: match artist { a if !a.is_empty() => a, _ if t.artist.is_empty() => album_artist.clone(), diff --git a/crates/qbz-qt/src/rip_wizard_qt.rs b/crates/qbz-qt/src/rip_wizard_qt.rs index 80cfd96a1..a27d3756b 100644 --- a/crates/qbz-qt/src/rip_wizard_qt.rs +++ b/crates/qbz-qt/src/rip_wizard_qt.rs @@ -365,7 +365,9 @@ pub async fn after_rip(action: &str, destination: &std::path::Path, folder_id: i Some(id) => { crate::settings_qt::library::scan(Some(id)); } - None => crate::toast_qt::error(qbz_i18n::t("Couldn't add that folder to the library")), + None => { + crate::toast_qt::error(qbz_i18n::t("Couldn't add that folder to the library")) + } } } _ => {} diff --git a/crates/qbz-qt/src/sacd_qt.rs b/crates/qbz-qt/src/sacd_qt.rs index 9d9677084..2efeb2ef1 100644 --- a/crates/qbz-qt/src/sacd_qt.rs +++ b/crates/qbz-qt/src/sacd_qt.rs @@ -75,8 +75,8 @@ pub fn open_image(path: &std::path::Path) -> Result { // folder uses), so this reuses it rather than growing a second rule about // where covers live. No cover is not an error — the pane draws its disc // glyph and the album plays. - let artwork = qbz_library::MetadataExtractor::find_folder_artwork(path, Some(&album)) - .and_then(|found| { + let artwork = + qbz_library::MetadataExtractor::find_folder_artwork(path, Some(&album)).and_then(|found| { qbz_library::MetadataExtractor::cache_artwork_file( std::path::Path::new(&found), &qbz_library::get_artwork_cache_dir(), diff --git a/crates/qbz-qt/src/scene_bridge.rs b/crates/qbz-qt/src/scene_bridge.rs index 5356c8700..521a16a0b 100644 --- a/crates/qbz-qt/src/scene_bridge.rs +++ b/crates/qbz-qt/src/scene_bridge.rs @@ -51,7 +51,6 @@ pub mod qbz_scene_bridge { // progress stream produces exactly that case (same phase, same pct, // same detail) more often than any other view in the tree. #[qproperty(i32, scene_rev)] - type QbzScene = super::QbzSceneRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/search_bridge.rs b/crates/qbz-qt/src/search_bridge.rs index 2a26b916f..e775623e6 100644 --- a/crates/qbz-qt/src/search_bridge.rs +++ b/crates/qbz-qt/src/search_bridge.rs @@ -59,10 +59,8 @@ pub mod qbz_search { #[qproperty(i32, cortinilla_selected_index)] /// Content-space top-y of the selected row, for scroll-into-view. #[qproperty(f64, cortinilla_scroll_y)] - // --- Results page -------------------------------------------------- #[qproperty(QString, search_json)] - // --- Intelligent Search kill switch -------------------------------- /// `ui_prefs.intelligent_search` mirror. Live-flippable, no restart. /// @@ -75,7 +73,6 @@ pub mod qbz_search { /// is the domain's canonical state; its `toggle` invokable was /// deleted (zero QML callers, contract DEAD-1). #[qproperty(bool, intelligent_search)] - type QbzSearch = super::QbzSearchRust; /// Registers this bridge's Qt-thread hop. MUST be called from @@ -180,10 +177,7 @@ pub(crate) fn set_cortinilla_query(q: String) { /// The live header query as last published. Empty before the first keystroke /// and after a clear. pub(crate) fn cortinilla_query() -> String { - CORT_QUERY - .lock() - .unwrap_or_else(|e| e.into_inner()) - .clone() + CORT_QUERY.lock().unwrap_or_else(|e| e.into_inner()).clone() } static QT_THREAD: OnceLock> = OnceLock::new(); diff --git a/crates/qbz-qt/src/search_cache_qt.rs b/crates/qbz-qt/src/search_cache_qt.rs index 155f5b283..6eeb5fe94 100644 --- a/crates/qbz-qt/src/search_cache_qt.rs +++ b/crates/qbz-qt/src/search_cache_qt.rs @@ -100,12 +100,7 @@ pub fn get(query: &str) -> Option { if !instant_paint_enabled() { return None; } - CACHE - .lock() - .ok()? - .as_ref()? - .get(query) - .cloned() + CACHE.lock().ok()?.as_ref()?.get(query).cloned() } /// Store a FINISHED payload. diff --git a/crates/qbz-qt/src/search_qt.rs b/crates/qbz-qt/src/search_qt.rs index 650df7611..e80a9a46e 100644 --- a/crates/qbz-qt/src/search_qt.rs +++ b/crates/qbz-qt/src/search_qt.rs @@ -379,7 +379,11 @@ fn map_track(track: &Track) -> TrackRow { .as_ref() .and_then(|a| a.image.best().cloned()) .unwrap_or_default(); - let album_id = track.album.as_ref().map(|a| a.id.clone()).unwrap_or_default(); + let album_id = track + .album + .as_ref() + .map(|a| a.id.clone()) + .unwrap_or_default(); let album = track .album .as_ref() @@ -459,7 +463,8 @@ fn map_playlist(playlist: &Playlist) -> PlaylistRow { let mut subtitle = playlist.owner.name.clone(); if playlist.tracks_count > 0 { let count = playlist.tracks_count; - let tracks_label = qbz_i18n::tf("{} track", "{} tracks", count as i64, &[&count.to_string()]); + let tracks_label = + qbz_i18n::tf("{} track", "{} tracks", count as i64, &[&count.to_string()]); subtitle = if subtitle.is_empty() { tracks_label } else { @@ -641,8 +646,12 @@ fn map_search_all_to_cortinilla(query: &str, results: &SearchAllResults) -> Cort let mut artist_rows: Vec = results.artists.items.iter().map(&to_artist_row).collect(); let mut album_rows: Vec = results.albums.items.iter().map(&to_album_row).collect(); let mut track_rows: Vec = results.tracks.items.iter().map(&to_track_row).collect(); - let mut playlist_rows: Vec = - results.playlists.items.iter().map(&to_playlist_row).collect(); + let mut playlist_rows: Vec = results + .playlists + .items + .iter() + .map(&to_playlist_row) + .collect(); rank_within(query, "artist", &mut artist_rows, |r| r.id.clone()); rank_within(query, "album", &mut album_rows, |r| r.id.clone()); @@ -675,8 +684,18 @@ fn map_search_all_to_cortinilla(query: &str, results: &SearchAllResults) -> Cort .iter() .find(|a| a.id.to_string() == id) .map(&to_artist_row), - "album" => results.albums.items.iter().find(|a| a.id == id).map(&to_album_row), - "track" => results.tracks.items.iter().find(|t| t.id.to_string() == id).map(&to_track_row), + "album" => results + .albums + .items + .iter() + .find(|a| a.id == id) + .map(&to_album_row), + "track" => results + .tracks + .items + .iter() + .find(|t| t.id.to_string() == id) + .map(&to_track_row), "playlist" => results .playlists .items @@ -706,10 +725,30 @@ fn map_search_all_to_cortinilla(query: &str, results: &SearchAllResults) -> Cort }); } }; - push_section(&qbz_i18n::t("Albums"), "album", album_rows, results.albums.total); - push_section(&qbz_i18n::t("Artists"), "artist", artist_rows, results.artists.total); - push_section(&qbz_i18n::t("Tracks"), "track", track_rows, results.tracks.total); - push_section(&qbz_i18n::t("Playlists"), "playlist", playlist_rows, results.playlists.total); + push_section( + &qbz_i18n::t("Albums"), + "album", + album_rows, + results.albums.total, + ); + push_section( + &qbz_i18n::t("Artists"), + "artist", + artist_rows, + results.artists.total, + ); + push_section( + &qbz_i18n::t("Tracks"), + "track", + track_rows, + results.tracks.total, + ); + push_section( + &qbz_i18n::t("Playlists"), + "playlist", + playlist_rows, + results.playlists.total, + ); let mut data = CortinillaData { query: query.to_string(), @@ -1144,8 +1183,7 @@ pub fn row_clicked(flat_index: i32) { "artist" => { crate::nav_qt::record("local"); crate::shell_bridge::ui(|mut b| { - b.as_mut() - .set_current_view(QString::from("local")) + b.as_mut().set_current_view(QString::from("local")) }); crate::local_album_actions::open_artist_by_name(row.title); } @@ -1354,8 +1392,7 @@ static IMM_SEL: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new /// directly. Read by the hotkeys pipeline's (B) dropdown-steal /// (2026-08-03 hotkeys-port contract §1.1(B): `QbzImmersive.open && /// QbzImmersive.immSearchOpen` wins over the desktop cortinilla). -static IMM_SEARCH_OPEN: std::sync::atomic::AtomicBool = - std::sync::atomic::AtomicBool::new(false); +static IMM_SEARCH_OPEN: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false); pub fn imm_search_open() -> bool { IMM_SEARCH_OPEN.load(Ordering::SeqCst) @@ -1418,8 +1455,12 @@ fn map_search_all_to_immersive(query: &str, results: &SearchAllResults) -> Corti let mut artist_rows: Vec = results.artists.items.iter().map(&to_artist_row).collect(); let mut album_rows: Vec = results.albums.items.iter().map(&to_album_row).collect(); - let mut playlist_rows: Vec = - results.playlists.items.iter().map(&to_playlist_row).collect(); + let mut playlist_rows: Vec = results + .playlists + .items + .iter() + .map(&to_playlist_row) + .collect(); // Intra-category order applies the learned ranking BEFORE truncation // (search.rs `take`); the caps themselves live in the assembly seam. rank_within(query, "artist", &mut artist_rows, |r| r.id.clone()); @@ -1431,7 +1472,11 @@ fn map_search_all_to_immersive(query: &str, results: &SearchAllResults) -> Corti artist_rows, album_rows, playlist_rows, - (results.artists.total, results.albums.total, results.playlists.total), + ( + results.artists.total, + results.albums.total, + results.playlists.total, + ), ) } @@ -1465,7 +1510,12 @@ fn assemble_immersive_sections( }; push(&qbz_i18n::t("Artists"), "artist", artist_rows, totals.0); push(&qbz_i18n::t("Albums"), "album", album_rows, totals.1); - push(&qbz_i18n::t("Playlists"), "playlist", playlist_rows, totals.2); + push( + &qbz_i18n::t("Playlists"), + "playlist", + playlist_rows, + totals.2, + ); let mut data = CortinillaData { query: query.to_string(), @@ -1833,7 +1883,9 @@ pub fn imm_row_activated(flat_index: i32) { if let Err(e) = crate::playlist_qt::enqueue_playlist_by_id(&runtime, pid, &mode).await { - log::error!("[qbz-qt] immersive search enqueue_playlist {pid} ({mode}): {e}"); + log::error!( + "[qbz-qt] immersive search enqueue_playlist {pid} ({mode}): {e}" + ); } }); } @@ -2114,21 +2166,59 @@ pub async fn submit(runtime: &Arc>, query: &str, tab: let mut albums: Vec = results.albums.items.iter().map(map_album).collect(); let mut tracks: Vec = results.tracks.items.iter().map(map_track).collect(); let mut artists: Vec = results.artists.items.iter().map(map_artist).collect(); - let mut playlists: Vec = results.playlists.items.iter().map(map_playlist).collect(); + let mut playlists: Vec = + results.playlists.items.iter().map(map_playlist).collect(); - let (mp_kind, mut mp_album, mut mp_artist, mut mp_track, mp_quality) = match &results.most_popular { - Some(MostPopularItem::Albums(a)) => ("album".to_string(), Some(map_album(a)), None, None, quality_label(a.maximum_bit_depth, a.maximum_sampling_rate)), - Some(MostPopularItem::Artists(a)) => ("artist".to_string(), None, Some(map_artist(a)), None, String::new()), - Some(MostPopularItem::Tracks(t)) => { - ("track".to_string(), None, None, Some(map_track(t)), quality_label(t.maximum_bit_depth, t.maximum_sampling_rate)) - } - None => (String::new(), None, None, None, String::new()), - }; + let (mp_kind, mut mp_album, mut mp_artist, mut mp_track, mp_quality) = + match &results.most_popular { + Some(MostPopularItem::Albums(a)) => ( + "album".to_string(), + Some(map_album(a)), + None, + None, + quality_label(a.maximum_bit_depth, a.maximum_sampling_rate), + ), + Some(MostPopularItem::Artists(a)) => ( + "artist".to_string(), + None, + Some(map_artist(a)), + None, + String::new(), + ), + Some(MostPopularItem::Tracks(t)) => ( + "track".to_string(), + None, + None, + Some(map_track(t)), + quality_label(t.maximum_bit_depth, t.maximum_sampling_rate), + ), + None => (String::new(), None, None, None, String::new()), + }; let mut missing: Vec = Vec::new(); - missing.extend(attach_urls(albums.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing.extend(attach_urls(tracks.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing.extend(attach_urls(artists.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing.extend(attach_urls(playlists.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); + missing.extend(attach_urls( + albums + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing.extend(attach_urls( + tracks + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing.extend(attach_urls( + artists + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing.extend(attach_urls( + playlists + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); if let Some(a) = &mut mp_album { missing.extend(attach_urls(vec![(a.art_url.clone(), &mut a.art_path)])); } @@ -2143,7 +2233,9 @@ pub async fn submit(runtime: &Arc>, query: &str, tab: // first list entry (apply_search dedupe) — derived AFTER artwork attach // so its rows carry the disk paths (they are clones of `artists` rows). let artists_carousel: Vec = match (&mp_kind, artists.first()) { - (kind, Some(first)) if kind == "artist" && mp_artist.as_ref().is_some_and(|m| m.id == first.id) => { + (kind, Some(first)) + if kind == "artist" && mp_artist.as_ref().is_some_and(|m| m.id == first.id) => + { artists[1..].to_vec() } _ => artists.clone(), @@ -2183,10 +2275,30 @@ pub async fn submit(runtime: &Arc>, query: &str, tab: let mut guard = PAGE.lock().unwrap(); let doc = &mut guard.get_or_insert_with(PageState::default).doc; let mut missing2 = Vec::new(); - missing2.extend(attach_urls(doc.albums.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing2.extend(attach_urls(doc.tracks.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing2.extend(attach_urls(doc.artists.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); - missing2.extend(attach_urls(doc.playlists.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect())); + missing2.extend(attach_urls( + doc.albums + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing2.extend(attach_urls( + doc.tracks + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing2.extend(attach_urls( + doc.artists + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); + missing2.extend(attach_urls( + doc.playlists + .iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + )); // The carousel shares rows with `artists`; artPath lives on // the ArtistRow structs, so refresh the carousel from the // updated list. @@ -2256,7 +2368,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { let (bl, abl) = blacklist_snapshots(); match tab { - 1 => match runtime.core().search_albums(&query, PAGE_SIZE, offset, search_type.as_deref()).await { + 1 => match runtime + .core() + .search_albums(&query, PAGE_SIZE, offset, search_type.as_deref()) + .await + { Ok(page) => { let mut rows: Vec = page .items @@ -2264,7 +2380,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { .filter(|a| !qbz_core::core::album_blacklisted(a, &bl, &abl)) .map(map_album) .collect(); - let missing = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let missing = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2280,7 +2400,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { } Err(e) => log::error!("[qbz-qt] search load-more albums failed: {e}"), }, - 2 => match runtime.core().search_tracks(&query, PAGE_SIZE, offset, search_type.as_deref()).await { + 2 => match runtime + .core() + .search_tracks(&query, PAGE_SIZE, offset, search_type.as_deref()) + .await + { Ok(page) => { let mut rows: Vec = page .items @@ -2288,7 +2412,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { .filter(|t| !qbz_core::core::track_blacklisted(t, &bl, &abl)) .map(map_track) .collect(); - let missing = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let missing = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2304,7 +2432,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { } Err(e) => log::error!("[qbz-qt] search load-more tracks failed: {e}"), }, - 3 => match runtime.core().search_artists(&query, PAGE_SIZE, offset, search_type.as_deref()).await { + 3 => match runtime + .core() + .search_artists(&query, PAGE_SIZE, offset, search_type.as_deref()) + .await + { Ok(page) => { // Artist axis ONLY — the reference filters this category on // `bl` alone (`search.rs:1655`); an artist has no album id. @@ -2314,7 +2446,11 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { .filter(|a| !bl.contains(&a.id)) .map(map_artist) .collect(); - let missing = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let missing = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2330,10 +2466,18 @@ pub async fn load_more(runtime: &Arc>, tab: i32) { } Err(e) => log::error!("[qbz-qt] search load-more artists failed: {e}"), }, - 4 => match runtime.core().search_playlists(&query, PAGE_SIZE, offset).await { + 4 => match runtime + .core() + .search_playlists(&query, PAGE_SIZE, offset) + .await + { Ok(page) => { let mut rows: Vec = page.items.iter().map(map_playlist).collect(); - let missing = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let missing = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2375,14 +2519,22 @@ pub async fn filter_changed(runtime: &Arc>, index: i3 let (bl, abl) = blacklist_snapshots(); // Albums. - if let Ok(page) = runtime.core().search_albums(&query, PAGE_SIZE, 0, search_type.as_deref()).await { + if let Ok(page) = runtime + .core() + .search_albums(&query, PAGE_SIZE, 0, search_type.as_deref()) + .await + { let mut rows: Vec = page .items .iter() .filter(|a| !qbz_core::core::album_blacklisted(a, &bl, &abl)) .map(map_album) .collect(); - let _ = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let _ = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2396,14 +2548,22 @@ pub async fn filter_changed(runtime: &Arc>, index: i3 } } // Tracks. - if let Ok(page) = runtime.core().search_tracks(&query, PAGE_SIZE, 0, search_type.as_deref()).await { + if let Ok(page) = runtime + .core() + .search_tracks(&query, PAGE_SIZE, 0, search_type.as_deref()) + .await + { let mut rows: Vec = page .items .iter() .filter(|t| !qbz_core::core::track_blacklisted(t, &bl, &abl)) .map(map_track) .collect(); - let _ = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let _ = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2417,14 +2577,22 @@ pub async fn filter_changed(runtime: &Arc>, index: i3 } } // Artists (the API takes the search_type too, 1:1 the Slint filter). - if let Ok(page) = runtime.core().search_artists(&query, PAGE_SIZE, 0, search_type.as_deref()).await { + if let Ok(page) = runtime + .core() + .search_artists(&query, PAGE_SIZE, 0, search_type.as_deref()) + .await + { let mut rows: Vec = page .items .iter() .filter(|a| !bl.contains(&a.id)) .map(map_artist) .collect(); - let _ = attach_urls(rows.iter_mut().map(|r| (r.art_url.clone(), &mut r.art_path)).collect()); + let _ = attach_urls( + rows.iter_mut() + .map(|r| (r.art_url.clone(), &mut r.art_path)) + .collect(), + ); let total = page.total as i32; let mut guard = PAGE.lock().unwrap(); if let Some(p) = guard.as_mut() { @@ -2463,8 +2631,16 @@ mod tests { kind: "album".into(), has_more: false, rows: vec![ - CortRow { kind: "album".into(), id: "1".into(), ..Default::default() }, - CortRow { kind: "album".into(), id: "2".into(), ..Default::default() }, + CortRow { + kind: "album".into(), + id: "1".into(), + ..Default::default() + }, + CortRow { + kind: "album".into(), + id: "2".into(), + ..Default::default() + }, ], }], }; @@ -2477,12 +2653,20 @@ mod tests { fn flat_indices_top_is_zero() { let mut data = CortinillaData { query: "q".into(), - top: Some(CortRow { kind: "artist".into(), id: "9".into(), ..Default::default() }), + top: Some(CortRow { + kind: "artist".into(), + id: "9".into(), + ..Default::default() + }), sections: vec![CortSection { title: "Albums".into(), kind: "album".into(), has_more: false, - rows: vec![CortRow { kind: "album".into(), id: "1".into(), ..Default::default() }], + rows: vec![CortRow { + kind: "album".into(), + id: "1".into(), + ..Default::default() + }], }], }; assign_flat_indices(&mut data); @@ -2492,7 +2676,10 @@ mod tests { #[test] fn quality_label_formats() { - assert_eq!(quality_label(Some(24), Some(96.0)), "Hi-Res 24-bit / 96 kHz"); + assert_eq!( + quality_label(Some(24), Some(96.0)), + "Hi-Res 24-bit / 96 kHz" + ); assert_eq!(quality_label(Some(16), None), "CD 16-bit / 44.1 kHz"); assert_eq!(quality_label(None, Some(192.0)), ""); assert_eq!(mmss(5), "0:05"); @@ -2520,18 +2707,34 @@ mod tests { // Contract §3.4 / search.rs:609-611,690-692: Artists, Albums, // Playlists IN THAT ORDER; caps 2/5/2; NO top result; NO track rows; // flat indices from 1 (main.rs:10491-10499). - let artists: Vec = (0..4).map(|i| imm_row("artist", &format!("ar{i}"), "qobuz")).collect(); - let albums: Vec = (0..7).map(|i| imm_row("album", &format!("al{i}"), "qobuz")).collect(); - let playlists: Vec = (0..3).map(|i| imm_row("playlist", &format!("pl{i}"), "qobuz")).collect(); + let artists: Vec = (0..4) + .map(|i| imm_row("artist", &format!("ar{i}"), "qobuz")) + .collect(); + let albums: Vec = (0..7) + .map(|i| imm_row("album", &format!("al{i}"), "qobuz")) + .collect(); + let playlists: Vec = (0..3) + .map(|i| imm_row("playlist", &format!("pl{i}"), "qobuz")) + .collect(); let data = assemble_immersive_sections("q", artists, albums, playlists, (4, 7, 3)); - assert!(data.top.is_none(), "the immersive payload has NO top result"); + assert!( + data.top.is_none(), + "the immersive payload has NO top result" + ); let kinds: Vec<&str> = data.sections.iter().map(|s| s.kind.as_str()).collect(); - assert_eq!(kinds, ["artist", "album", "playlist"], "section order is Artists/Albums/Playlists"); + assert_eq!( + kinds, + ["artist", "album", "playlist"], + "section order is Artists/Albums/Playlists" + ); assert_eq!(data.sections[0].rows.len(), 2, "artists cap 2"); assert_eq!(data.sections[1].rows.len(), 5, "albums cap 5"); assert_eq!(data.sections[2].rows.len(), 2, "playlists cap 2"); - assert!(data.sections.iter().all(|s| s.has_more), "totals exceed every cap"); + assert!( + data.sections.iter().all(|s| s.has_more), + "totals exceed every cap" + ); // Flat indices run from 1, contiguous across sections. let flats: Vec = data .sections @@ -2539,7 +2742,11 @@ mod tests { .flat_map(|s| s.rows.iter().map(|r| r.flat_index)) .collect(); assert_eq!(flats, (1..=9).collect::>()); - assert!(data.sections.iter().flat_map(|s| s.rows.iter()).all(|r| r.kind != "track")); + assert!(data + .sections + .iter() + .flat_map(|s| s.rows.iter()) + .all(|r| r.kind != "track")); } #[test] @@ -2562,20 +2769,56 @@ mod tests { // Contract §3.4 round-2-verified mapping (main.rs:10614-10749). // Local album rows branch BEFORE the Qobuz match (the id is a group // key, not a numeric Qobuz id). - assert_eq!(imm_dispatch("local", "album", "replace"), ImmDispatch::LocalPlay); - assert_eq!(imm_dispatch("local", "album", "next"), ImmDispatch::LocalEnqueue("next".into())); - assert_eq!(imm_dispatch("local", "album", "queue"), ImmDispatch::LocalEnqueue("queue".into())); + assert_eq!( + imm_dispatch("local", "album", "replace"), + ImmDispatch::LocalPlay + ); + assert_eq!( + imm_dispatch("local", "album", "next"), + ImmDispatch::LocalEnqueue("next".into()) + ); + assert_eq!( + imm_dispatch("local", "album", "queue"), + ImmDispatch::LocalEnqueue("queue".into()) + ); // Qobuz replace arms. - assert_eq!(imm_dispatch("qobuz", "album", "replace"), ImmDispatch::PlayAlbum); - assert_eq!(imm_dispatch("qobuz", "playlist", "replace"), ImmDispatch::PlayPlaylist); - assert_eq!(imm_dispatch("qobuz", "artist", "replace"), ImmDispatch::PlayArtist); + assert_eq!( + imm_dispatch("qobuz", "album", "replace"), + ImmDispatch::PlayAlbum + ); + assert_eq!( + imm_dispatch("qobuz", "playlist", "replace"), + ImmDispatch::PlayPlaylist + ); + assert_eq!( + imm_dispatch("qobuz", "artist", "replace"), + ImmDispatch::PlayArtist + ); // Qobuz next/queue arms (mode carried verbatim). - assert_eq!(imm_dispatch("qobuz", "album", "next"), ImmDispatch::EnqueueAlbum("next".into())); - assert_eq!(imm_dispatch("qobuz", "album", "queue"), ImmDispatch::EnqueueAlbum("queue".into())); - assert_eq!(imm_dispatch("qobuz", "playlist", "next"), ImmDispatch::EnqueuePlaylist("next".into())); - assert_eq!(imm_dispatch("qobuz", "playlist", "queue"), ImmDispatch::EnqueuePlaylist("queue".into())); - assert_eq!(imm_dispatch("qobuz", "artist", "next"), ImmDispatch::EnqueueArtistTop("next".into())); - assert_eq!(imm_dispatch("qobuz", "artist", "queue"), ImmDispatch::EnqueueArtistTop("queue".into())); + assert_eq!( + imm_dispatch("qobuz", "album", "next"), + ImmDispatch::EnqueueAlbum("next".into()) + ); + assert_eq!( + imm_dispatch("qobuz", "album", "queue"), + ImmDispatch::EnqueueAlbum("queue".into()) + ); + assert_eq!( + imm_dispatch("qobuz", "playlist", "next"), + ImmDispatch::EnqueuePlaylist("next".into()) + ); + assert_eq!( + imm_dispatch("qobuz", "playlist", "queue"), + ImmDispatch::EnqueuePlaylist("queue".into()) + ); + assert_eq!( + imm_dispatch("qobuz", "artist", "next"), + ImmDispatch::EnqueueArtistTop("next".into()) + ); + assert_eq!( + imm_dispatch("qobuz", "artist", "queue"), + ImmDispatch::EnqueueArtistTop("queue".into()) + ); // Unknown combinations are inert (no track rows exist in the payload; // an unknown action does nothing). assert_eq!(imm_dispatch("qobuz", "track", "replace"), ImmDispatch::None); @@ -2588,11 +2831,27 @@ mod tests { // main.rs:10524-10540: Down from -1 -> first row; Up from the first // row -> -1; both ends clamp (no wrap). let order = vec![1, 2, 3, 4]; - assert_eq!(imm_next_selection(&order, -1, 1), 1, "Down from -1 lands on the first row"); - assert_eq!(imm_next_selection(&order, -1, -1), -1, "Up from -1 stays at -1"); - assert_eq!(imm_next_selection(&order, 1, -1), -1, "Up from the first row returns to -1"); + assert_eq!( + imm_next_selection(&order, -1, 1), + 1, + "Down from -1 lands on the first row" + ); + assert_eq!( + imm_next_selection(&order, -1, -1), + -1, + "Up from -1 stays at -1" + ); + assert_eq!( + imm_next_selection(&order, 1, -1), + -1, + "Up from the first row returns to -1" + ); assert_eq!(imm_next_selection(&order, 1, 1), 2); - assert_eq!(imm_next_selection(&order, 4, 1), 4, "Down on the last row clamps (no wrap)"); + assert_eq!( + imm_next_selection(&order, 4, 1), + 4, + "Down on the last row clamps (no wrap)" + ); assert_eq!(imm_next_selection(&order, 3, -1), 2); } @@ -2613,7 +2872,10 @@ mod tests { title: "Albums".into(), kind: "album".into(), has_more: false, - rows: vec![imm_row("album", "1", "qobuz"), imm_row("album", "2", "qobuz")], + rows: vec![ + imm_row("album", "1", "qobuz"), + imm_row("album", "2", "qobuz"), + ], }, CortSection { title: "Artists".into(), @@ -2679,7 +2941,10 @@ mod tests { title: "Artists".into(), kind: "artist".into(), has_more: false, - rows: vec![imm_row("artist", "1", "qobuz"), imm_row("artist", "2", "qobuz")], + rows: vec![ + imm_row("artist", "1", "qobuz"), + imm_row("artist", "2", "qobuz"), + ], }, CortSection { title: "Albums".into(), @@ -2690,10 +2955,18 @@ mod tests { ], }; assign_flat_indices(&mut data); - assert_eq!(imm_scroll_y(&data, -1), 0.0, "no selection scrolls to the top"); + assert_eq!( + imm_scroll_y(&data, -1), + 0.0, + "no selection scrolls to the top" + ); assert_eq!(imm_scroll_y(&data, 1), 28.0, "first section header block"); assert_eq!(imm_scroll_y(&data, 2), 84.0, "28 + one 56px row"); - assert_eq!(imm_scroll_y(&data, 3), 168.0, "28 + 2*56 + the second section's 28"); + assert_eq!( + imm_scroll_y(&data, 3), + 168.0, + "28 + 2*56 + the second section's 28" + ); assert_eq!(imm_scroll_y(&data, 99), 0.0, "unknown index falls to 0"); } @@ -2701,8 +2974,22 @@ mod tests { fn local_caps_profiles_and_fetch_limit() { // search.rs:717-746: normal 3/2/3, expanded 8/4/8; fetch_limit = // max(albums, tracks) * 12 + 40. - assert_eq!(crate::search_local::LocalCaps::for_session(false), crate::search_local::LocalCaps { albums: 3, artists: 2, tracks: 3 }); - assert_eq!(crate::search_local::LocalCaps::for_session(true), crate::search_local::LocalCaps { albums: 8, artists: 4, tracks: 8 }); + assert_eq!( + crate::search_local::LocalCaps::for_session(false), + crate::search_local::LocalCaps { + albums: 3, + artists: 2, + tracks: 3 + } + ); + assert_eq!( + crate::search_local::LocalCaps::for_session(true), + crate::search_local::LocalCaps { + albums: 8, + artists: 4, + tracks: 8 + } + ); assert_eq!(crate::search_local::LocalCaps::NORMAL.fetch_limit(), 76); assert_eq!(crate::search_local::LocalCaps::EXPANDED.fetch_limit(), 136); } diff --git a/crates/qbz-qt/src/session_bridge.rs b/crates/qbz-qt/src/session_bridge.rs index eb04a865d..a03c8e058 100644 --- a/crates/qbz-qt/src/session_bridge.rs +++ b/crates/qbz-qt/src/session_bridge.rs @@ -58,7 +58,6 @@ pub mod qbz_session { // switch; every `QbzSession.tr(msgid, QbzSession.trRev)` binding // depends on it. #[qproperty(i32, tr_rev)] - type QbzSession = super::QbzSessionRust; /// The first invokable (Main.qml Component.onCompleted): register diff --git a/crates/qbz-qt/src/settings_qt/devtools.rs b/crates/qbz-qt/src/settings_qt/devtools.rs index 6ee967852..d9384b3f6 100644 --- a/crates/qbz-qt/src/settings_qt/devtools.rs +++ b/crates/qbz-qt/src/settings_qt/devtools.rs @@ -119,9 +119,7 @@ pub async fn export_settings(include_auth: bool) { }; let path = dir.join(bundle::default_filename()); match bundle::write_bundle_file(&path, &bundle) { - Ok(()) => { - qbz_i18n::t("Saved to {}").replacen("{}", &path.to_string_lossy(), 1) - } + Ok(()) => qbz_i18n::t("Saved to {}").replacen("{}", &path.to_string_lossy(), 1), Err(e) => { log::error!("[qbz-qt] settings bundle write failed: {e:?}"); qbz_i18n::t("Export failed.") diff --git a/crates/qbz-qt/src/settings_qt/library.rs b/crates/qbz-qt/src/settings_qt/library.rs index f603359e4..c35caf1ce 100644 --- a/crates/qbz-qt/src/settings_qt/library.rs +++ b/crates/qbz-qt/src/settings_qt/library.rs @@ -68,9 +68,7 @@ pub struct PlexFields { /// A password round-tripping through a QML string property would sit in the /// scene graph's memory and in any JSON the panel logged. /// Read one media server's persisted settings into the panel's shape. -fn media_fields( - kind: qbz_app::settings::media_servers::MediaServerKind, -) -> MediaServerFields { +fn media_fields(kind: qbz_app::settings::media_servers::MediaServerKind) -> MediaServerFields { let s = crate::media_servers_qt::get(kind); MediaServerFields { enabled: s.enabled, @@ -301,7 +299,10 @@ pub fn snapshot() -> Snapshot { scanning: SCANNING.load(Ordering::SeqCst), processed: PROCESSED.load(Ordering::SeqCst) as i32, total: TOTAL.load(Ordering::SeqCst) as i32, - file: CURRENT_FILE.lock().unwrap_or_else(|e| e.into_inner()).clone(), + file: CURRENT_FILE + .lock() + .unwrap_or_else(|e| e.into_inner()) + .clone(), cleaning: CLEANING.load(Ordering::SeqCst), cleanup_status: CLEANUP_STATUS .lock() @@ -309,12 +310,8 @@ pub fn snapshot() -> Snapshot { .clone(), clearing: CLEARING.load(Ordering::SeqCst), status: STATUS.lock().unwrap_or_else(|e| e.into_inner()).clone(), - jellyfin: media_fields( - qbz_app::settings::media_servers::MediaServerKind::Jellyfin, - ), - subsonic: media_fields( - qbz_app::settings::media_servers::MediaServerKind::Subsonic, - ), + jellyfin: media_fields(qbz_app::settings::media_servers::MediaServerKind::Jellyfin), + subsonic: media_fields(qbz_app::settings::media_servers::MediaServerKind::Subsonic), plex: PlexFields { has_token: !plex_cfg.token.trim().is_empty(), server_url: plex_cfg.base_url, @@ -779,13 +776,9 @@ pub fn scan(folder_id: Option) -> bool { } _ => {} }; - if let Err(e) = qbz_library::scan_with_progress( - &db, - ids.as_deref(), - &cache, - &CANCEL, - &on_event, - ) { + if let Err(e) = + qbz_library::scan_with_progress(&db, ids.as_deref(), &cache, &CANCEL, &on_event) + { log::error!("[qbz-qt] library scan failed: {e}"); if CLEANING.load(Ordering::SeqCst) { *CLEANUP_STATUS.lock().unwrap_or_else(|e| e.into_inner()) = @@ -826,8 +819,7 @@ pub async fn cleanup_missing() { qbz_i18n::t("Scanning track paths..."); if !scan(None) { CLEANING.store(false, Ordering::SeqCst); - *CLEANUP_STATUS.lock().unwrap_or_else(|e| e.into_inner()) = - qbz_i18n::t("Scan failed."); + *CLEANUP_STATUS.lock().unwrap_or_else(|e| e.into_inner()) = qbz_i18n::t("Scan failed."); } super::publish_snapshot().await; } diff --git a/crates/qbz-qt/src/shader_scene_bridge.rs b/crates/qbz-qt/src/shader_scene_bridge.rs index e0cbcdf4a..a6d3949cc 100644 --- a/crates/qbz-qt/src/shader_scene_bridge.rs +++ b/crates/qbz-qt/src/shader_scene_bridge.rs @@ -87,8 +87,7 @@ const PACK_EMPTY: &str = "{\"phase\":0,\"beat\":0,\"level\":0,\"levelSmooth\":0, /// B1: the Tunnel Flow palette default — the Tauri fallback /// (`DEFAULT_LINE_PALETTE`, TunnelFlowPanel.svelte:63-68), so the scene has /// a valid palette before the first artwork extraction lands. -const TUNNEL_PALETTE_DEFAULT: &str = - "[\"#ff6a6a\",\"#ffcd5c\",\"#68dcaa\",\"#6eb0ff\"]"; +const TUNNEL_PALETTE_DEFAULT: &str = "[\"#ff6a6a\",\"#ffcd5c\",\"#68dcaa\",\"#6eb0ff\"]"; #[cxx_qt::bridge] pub mod qbz_shader_scene { @@ -143,7 +142,6 @@ pub mod qbz_shader_scene { // TunnelFlowScene.qml stashes the parse and applies it on the pulse // edge. #[qproperty(QString, tunnel_palette_json, READ, NOTIFY)] - type QbzShaderScene = super::QbzShaderSceneRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY diff --git a/crates/qbz-qt/src/sidebar_qt.rs b/crates/qbz-qt/src/sidebar_qt.rs index 9d4da068b..2d4d8acfd 100644 --- a/crates/qbz-qt/src/sidebar_qt.rs +++ b/crates/qbz-qt/src/sidebar_qt.rs @@ -18,7 +18,7 @@ use std::collections::{HashMap, HashSet}; use std::path::Path; use std::sync::{Arc, Mutex, OnceLock}; -use qbz_app::settings::pinned_items::{PinnedItemsService, PinnedItem, DB_FILE_NAME}; +use qbz_app::settings::pinned_items::{PinnedItem, PinnedItemsService, DB_FILE_NAME}; use qbz_app::shell::AppRuntime; use qbz_app::user_data::UserDataPaths; use qbz_core::LoggingAdapter; @@ -71,7 +71,13 @@ pub fn user_dir() -> Option { /// Toggle the pin state of an album/artist/playlist. Returns the new /// state, or None with no store bound / on error. -pub fn toggle_pin(kind: &str, id: &str, title: &str, subtitle: &str, artwork_url: &str) -> Option { +pub fn toggle_pin( + kind: &str, + id: &str, + title: &str, + subtitle: &str, + artwork_url: &str, +) -> Option { let service = PINNED.get()?.lock().unwrap(); if service.is_pinned(kind, id) { service.unpin(kind, id).ok()?; @@ -110,7 +116,11 @@ pub struct SidebarEntry { /// A first-class LOCAL playlist (`local:`), not a Qobuz one. The row /// carries a hard-drive mark and routes to the local detail; its id is a /// string, so nothing may parse it as a catalog number. - #[serde(default, rename = "isLocal", skip_serializing_if = "std::ops::Not::not")] + #[serde( + default, + rename = "isLocal", + skip_serializing_if = "std::ops::Not::not" + )] pub is_local: bool, } @@ -478,9 +488,7 @@ pub fn rebuild() -> Vec { /// Locals sort among THEMSELVES by name, always — they have no /// track-count or custom-position stat to honour the toolbar's other /// sorts with, and inventing one would order them arbitrarily. - fn sorted_locals<'a>( - mut list: Vec<&'a SidebarLocal>, - ) -> Vec<&'a SidebarLocal> { + fn sorted_locals<'a>(mut list: Vec<&'a SidebarLocal>) -> Vec<&'a SidebarLocal> { list.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase())); list } @@ -489,7 +497,12 @@ pub fn rebuild() -> Vec { for (fid, fname) in &data.folders { let members: Vec<&SidebarPlaylist> = sorted .iter() - .filter(|p| data.folder_map.get(&p.id).map(|f| f == fid).unwrap_or(false)) + .filter(|p| { + data.folder_map + .get(&p.id) + .map(|f| f == fid) + .unwrap_or(false) + }) .filter(|p| matches(p)) .filter(|p| !data.hidden_playlists.contains(&p.id)) .collect(); @@ -634,7 +647,12 @@ pub fn folder_popup_rows(folder_id: &str) -> Vec { }; let mut rows: Vec = sort_playlists(&data.playlists) .into_iter() - .filter(|p| data.folder_map.get(&p.id).map(|f| f == folder_id).unwrap_or(false)) + .filter(|p| { + data.folder_map + .get(&p.id) + .map(|f| f == folder_id) + .unwrap_or(false) + }) .filter(|p| !data.hidden_playlists.contains(&p.id)) .map(|p| FolderPopupRow { id: p.id.to_string(), diff --git a/crates/qbz-qt/src/sleep_timer_qt.rs b/crates/qbz-qt/src/sleep_timer_qt.rs index 0d642ac74..a25f9d5e3 100644 --- a/crates/qbz-qt/src/sleep_timer_qt.rs +++ b/crates/qbz-qt/src/sleep_timer_qt.rs @@ -32,7 +32,8 @@ const MAX_MINUTES: i32 = 24 * 60; // 1440 fn push_state(active: bool, remaining: String) { crate::queue_bridge::ui(move |mut b| { b.as_mut().set_sleep_active(active); - b.as_mut().set_sleep_remaining(QString::from(remaining.as_str())); + b.as_mut() + .set_sleep_remaining(QString::from(remaining.as_str())); }); } diff --git a/crates/qbz-qt/src/source_wiring.rs b/crates/qbz-qt/src/source_wiring.rs index 2f0a64e6a..dd0891078 100644 --- a/crates/qbz-qt/src/source_wiring.rs +++ b/crates/qbz-qt/src/source_wiring.rs @@ -123,7 +123,9 @@ pub(crate) fn install() { registry.plex().set_creds(Some(Arc::new(PlexCredsGlue))); registry.jellyfin().set_creds(Some(Arc::new(JellyfinGlue))); registry.subsonic().set_creds(Some(Arc::new(SubsonicGlue))); - registry.local().set_ephemeral(Some(Arc::new(EphemeralGlue))); + registry + .local() + .set_ephemeral(Some(Arc::new(EphemeralGlue))); // Reads BEFORE a user is bound (splash, session restore) must resolve the // way `local_state::with_db` already resolves them — through // `load_last_user_id` — or the window between boot and @@ -231,7 +233,9 @@ pub(crate) fn teardown() { registry.plex().set_creds(Some(Arc::new(PlexCredsGlue))); registry.jellyfin().set_creds(Some(Arc::new(JellyfinGlue))); registry.subsonic().set_creds(Some(Arc::new(SubsonicGlue))); - registry.local().set_ephemeral(Some(Arc::new(EphemeralGlue))); + registry + .local() + .set_ephemeral(Some(Arc::new(EphemeralGlue))); registry .local() .set_unbound_fallback(Some(Box::new(crate::local_state::db_path))); diff --git a/crates/qbz-qt/src/suggestions_bridge.rs b/crates/qbz-qt/src/suggestions_bridge.rs index 4df5d78e9..d9e963d4c 100644 --- a/crates/qbz-qt/src/suggestions_bridge.rs +++ b/crates/qbz-qt/src/suggestions_bridge.rs @@ -46,7 +46,6 @@ pub mod qbz_suggestions { // The §4.5 document (see the file header). Written ONLY by // suggestions_qt through `ui()`. #[qproperty(QString, suggestions_json)] - type QbzSuggestions = super::QbzSuggestionsRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY diff --git a/crates/qbz-qt/src/suggestions_qt.rs b/crates/qbz-qt/src/suggestions_qt.rs index fab887b1a..8f4f0b5ed 100644 --- a/crates/qbz-qt/src/suggestions_qt.rs +++ b/crates/qbz-qt/src/suggestions_qt.rs @@ -133,7 +133,8 @@ fn publish(doc: &SuggestionsDoc) { r#"{"loading":false,"error":false,"cards":[],"tracks":[]}"#.to_string() }); crate::suggestions_bridge::ui(move |mut b| { - b.as_mut().set_suggestions_json(QString::from(json.as_str())); + b.as_mut() + .set_suggestions_json(QString::from(json.as_str())); }); } @@ -281,7 +282,11 @@ async fn load_suggestions( current_track_id: u64, seed_track_name: &str, ) -> Result { - let artist = match runtime.core().get_artist_detail(artist_id, None, None).await { + let artist = match runtime + .core() + .get_artist_detail(artist_id, None, None) + .await + { Ok(a) => a, Err(e) => { log::error!("[qbz-qt] suggestions get_artist_detail({artist_id}) failed: {e}"); @@ -374,7 +379,10 @@ async fn load_suggestions( } } Err(e) => { - log::warn!("[qbz-qt] suggestions get_playlist({}) failed: {e}", playlist.id); + log::warn!( + "[qbz-qt] suggestions get_playlist({}) failed: {e}", + playlist.id + ); } } // Fallback to the playlist's own images when no track covers found. @@ -424,10 +432,7 @@ async fn load_suggestions( /// a reload when the panel already shows this (artist, track). pub(crate) fn load(track_id: String) { let (artist_id, track_name) = crate::now_playing::seed_meta(); - let (Ok(aid), Ok(tid)) = ( - artist_id.parse::(), - track_id.parse::(), - ) else { + let (Ok(aid), Ok(tid)) = (artist_id.parse::(), track_id.parse::()) else { // No track / no artist -> reset to the empty state and stop // (`navigate_suggestions`'s empty-payload arm, :3662-3667). with_state(|s| { @@ -496,10 +501,7 @@ fn set_radio_loading(loading: bool) { /// track-list play seam. The radio-card spinner flips on optimistically and /// clears on completion (success OR failure, :16805). pub(crate) fn start_radio(seed_track_id: String, seed_track_name: String, seed_artist_id: String) { - let (Ok(tid), Ok(aid)) = ( - seed_track_id.parse::(), - seed_artist_id.parse::(), - ) else { + let (Ok(tid), Ok(aid)) = (seed_track_id.parse::(), seed_artist_id.parse::()) else { return; }; set_radio_loading(true); @@ -514,10 +516,11 @@ pub(crate) fn start_radio(seed_track_id: String, seed_track_name: String, seed_a Ok(tracks) if !tracks.is_empty() => { // The shared play_tracks seam: flat queue from the top, no // shuffle — `foryou_qt::play_flat`'s shape (:1144-1157). - let queue: Vec = - tracks.iter().map(crate::foryou_qt::to_queue_track).collect(); - if let Err(e) = - crate::playback_qt::play_track_list(&runtime, queue, 0, false).await + let queue: Vec = tracks + .iter() + .map(crate::foryou_qt::to_queue_track) + .collect(); + if let Err(e) = crate::playback_qt::play_track_list(&runtime, queue, 0, false).await { log::warn!("[qbz-qt] song radio play failed: {e}"); } @@ -564,9 +567,7 @@ pub(crate) fn queue_playlist(playlist_id: String) { }; let runtime = crate::app(); crate::spawn(async move { - if let Err(e) = - crate::playlist_qt::enqueue_playlist_by_id(&runtime, id, "queue").await - { + if let Err(e) = crate::playlist_qt::enqueue_playlist_by_id(&runtime, id, "queue").await { log::error!("[qbz-qt] suggestions queue-playlist {id} failed: {e}"); } }); @@ -580,9 +581,7 @@ pub(crate) fn play_next_playlist(playlist_id: String) { }; let runtime = crate::app(); crate::spawn(async move { - if let Err(e) = - crate::playlist_qt::enqueue_playlist_by_id(&runtime, id, "next").await - { + if let Err(e) = crate::playlist_qt::enqueue_playlist_by_id(&runtime, id, "next").await { log::error!("[qbz-qt] suggestions play-next-playlist {id} failed: {e}"); } }); @@ -689,15 +688,7 @@ mod tests { .collect(); assert_eq!( track_keys, - HashSet::from([ - "id", - "title", - "artist", - "artistId", - "duration", - "artUrl", - "explicit", - ]) + HashSet::from(["id", "title", "artist", "artistId", "duration", "artUrl", "explicit",]) ); assert_eq!(v["tracks"][0]["duration"], "3:45"); assert_eq!(v["tracks"][0]["explicit"], true); diff --git a/crates/qbz-qt/src/track_replace_bridge.rs b/crates/qbz-qt/src/track_replace_bridge.rs index 6bd8a92f2..110329730 100644 --- a/crates/qbz-qt/src/track_replace_bridge.rs +++ b/crates/qbz-qt/src/track_replace_bridge.rs @@ -40,7 +40,6 @@ pub mod qbz_track_replace_bridge { #[qml_singleton] // ONE JSON document (track_replace_qt.rs ReplaceDoc). #[qproperty(QString, replace_json)] - type QbzTrackReplace = super::QbzTrackReplaceRust; /// Registers this object's Qt-thread hop (Main.qml boots EVERY domain diff --git a/crates/qbz-qt/src/track_replace_qt.rs b/crates/qbz-qt/src/track_replace_qt.rs index a28d7a17c..7815f994b 100644 --- a/crates/qbz-qt/src/track_replace_qt.rs +++ b/crates/qbz-qt/src/track_replace_qt.rs @@ -95,7 +95,9 @@ pub(crate) mod session_unavailable { /// Remember that `track_id` failed terminally. Called by the reactive skip /// walk when it recognises a `TrackUnavailable`. pub(crate) fn mark(track_id: u64) { - IDS.lock().unwrap_or_else(|e| e.into_inner()).insert(track_id); + IDS.lock() + .unwrap_or_else(|e| e.into_inner()) + .insert(track_id); } /// Did this track already die under the player this run? The second half of @@ -111,7 +113,9 @@ pub(crate) mod session_unavailable { /// back (rights restored), a stale entry would keep marking it dead for the /// rest of the session. pub(crate) fn forget(track_id: u64) { - IDS.lock().unwrap_or_else(|e| e.into_inner()).remove(&track_id); + IDS.lock() + .unwrap_or_else(|e| e.into_inner()) + .remove(&track_id); } } diff --git a/crates/qbz-qt/src/tray_qt.rs b/crates/qbz-qt/src/tray_qt.rs index f0f4957a1..d93db23b3 100644 --- a/crates/qbz-qt/src/tray_qt.rs +++ b/crates/qbz-qt/src/tray_qt.rs @@ -224,7 +224,10 @@ pub(crate) fn set_mac_dock_hidden(hidden: bool) { } else { NSApplicationActivationPolicy::Regular }); - log::info!("[tray] dock icon {}", if hidden { "hidden" } else { "shown" }); + log::info!( + "[tray] dock icon {}", + if hidden { "hidden" } else { "shown" } + ); } #[cfg(not(target_os = "macos"))] diff --git a/crates/qbz-qt/src/tunnelflow_qt.rs b/crates/qbz-qt/src/tunnelflow_qt.rs index dcb4a9397..b56e3c888 100644 --- a/crates/qbz-qt/src/tunnelflow_qt.rs +++ b/crates/qbz-qt/src/tunnelflow_qt.rs @@ -186,12 +186,7 @@ mod tests { // 3/4 muted red + 1/4 vivid blue: score = count*(0.72+avgSat*1.28) — // the red bucket (count ~972, sat ~0.5) must outrank the blue speck // (count ~324, sat ~0.97). - let img = img_from(&[ - (140, 70, 70), - (140, 70, 70), - (140, 70, 70), - (30, 40, 235), - ]); + let img = img_from(&[(140, 70, 70), (140, 70, 70), (140, 70, 70), (30, 40, 235)]); let palette = line_palette(&img); assert_eq!(palette[0], (140, 70, 70)); assert_eq!(palette[1], (30, 40, 235)); @@ -219,12 +214,7 @@ mod tests { fn low_saturation_candidates_are_skipped_after_the_first_pick() { // Dominant vivid red + several grays: the grays (sat 0 < 0.12) may // never follow the first pick, so the palette pads by repeating. - let img = img_from(&[ - (220, 30, 30), - (220, 30, 30), - (220, 30, 30), - (120, 120, 120), - ]); + let img = img_from(&[(220, 30, 30), (220, 30, 30), (220, 30, 30), (120, 120, 120)]); let palette = line_palette(&img); assert_eq!(palette[0], (220, 30, 30)); assert!(palette.iter().all(|&c| c == palette[0])); @@ -249,8 +239,8 @@ mod tests { let mut img = image::RgbaImage::new(SAMPLE_SIZE, SAMPLE_SIZE); for (i, px) in img.pixels_mut().enumerate() { *px = match i % 3 { - 0 => image::Rgba([200, 30, 30, 60]), // alpha gate - 1 => image::Rgba([8, 8, 10, 255]), // luminance < 0.05 + 0 => image::Rgba([200, 30, 30, 60]), // alpha gate + 1 => image::Rgba([8, 8, 10, 255]), // luminance < 0.05 _ => image::Rgba([252, 252, 250, 255]), // luminance > 0.97 }; } diff --git a/crates/qbz-qt/src/viz_qt.rs b/crates/qbz-qt/src/viz_qt.rs index da6ad64a3..05f9c96ac 100644 --- a/crates/qbz-qt/src/viz_qt.rs +++ b/crates/qbz-qt/src/viz_qt.rs @@ -457,7 +457,12 @@ impl ShaderPackState { /// renders and hop them onto the Qt thread. Each cell's guard is released by /// the end of its `let` statement — the mutex is NEVER held across the /// `viz_bridge::ui` hop, so the producer never blocks on the drain. -fn publish_active(cells: &VizCells, pack: &mut ShaderPackState, linebed: &mut linebed_qt::LineBedState, ribbon: &mut ribbon_qt::RibbonState) { +fn publish_active( + cells: &VizCells, + pack: &mut ShaderPackState, + linebed: &mut linebed_qt::LineBedState, + ribbon: &mut ribbon_qt::RibbonState, +) { // §4.2b: immersive open -> publish ALL THREE streams each frame (the // immersive panels consume bars + energy + waveform simultaneously, like // the Slint immersive does). ACTIVE_MODE is irrelevant here. @@ -525,9 +530,7 @@ fn publish_active(cells: &VizCells, pack: &mut ShaderPackState, linebed: &mut li // (the C++ gap-fill cursor never moves backward on its own). if crate::shader_scene_bridge::ribbon_active() { if let Some(bins) = spectral.as_deref() { - if let Some(frame) = - ribbon.frame(bins, crate::now_playing::ribbon_cursor()) - { + if let Some(frame) = ribbon.frame(bins, crate::now_playing::ribbon_cursor()) { crate::shader_scene_bridge::publish_ribbon(frame); } } @@ -959,7 +962,7 @@ mod tests { let doc: serde_json::Value = serde_json::from_str(&p.tick(Some(0.5), None)).unwrap(); assert!((doc["beat"].as_f64().unwrap() - 0.44).abs() < 1e-6); // 0.5*0.88 assert!((doc["transient"].as_f64().unwrap() - 0.425).abs() < 1e-6); // 0.5*0.85 - // A weaker transient mid-decay does NOT lower the envelope (max-in). + // A weaker transient mid-decay does NOT lower the envelope (max-in). let doc: serde_json::Value = serde_json::from_str(&p.tick(Some(0.1), None)).unwrap(); assert!((doc["beat"].as_f64().unwrap() - 0.44 * 0.88).abs() < 1e-5); assert!((doc["transient"].as_f64().unwrap() - 0.425 * 0.85).abs() < 1e-5); @@ -985,8 +988,7 @@ mod tests { last_energy: [0.5; 5], ..Default::default() }; - let doc: serde_json::Value = - serde_json::from_str(&p.tick(Some(0.5), None)).unwrap(); + let doc: serde_json::Value = serde_json::from_str(&p.tick(Some(0.5), None)).unwrap(); let expected = 0.012 + 0.5 * 0.02 + 0.44 * 0.02; assert!((doc["phase"].as_f64().unwrap() - expected).abs() < 1e-4); } @@ -999,8 +1001,7 @@ mod tests { let mut p = ShaderPackState::default(); let mut bins = vec![0.0f32; 101]; bins[75] = 0.5; // highest active: 75/100 = 0.75 - let doc: serde_json::Value = - serde_json::from_str(&p.tick(None, Some(&bins))).unwrap(); + let doc: serde_json::Value = serde_json::from_str(&p.tick(None, Some(&bins))).unwrap(); assert!((doc["energyHi"][1].as_f64().unwrap() - 0.75 * 0.15).abs() < 1e-5); // Empty of signal: target 0, the EMA decays toward it. let doc: serde_json::Value = diff --git a/crates/qbz-qt/src/whats_new_qt.rs b/crates/qbz-qt/src/whats_new_qt.rs index 79fa3631f..8f4759c0a 100644 --- a/crates/qbz-qt/src/whats_new_qt.rs +++ b/crates/qbz-qt/src/whats_new_qt.rs @@ -280,7 +280,9 @@ fn format_release_date(iso: &str) -> String { /// Strip inline `**bold**` / `` `code` `` markers and reduce inline markdown /// links `[text](url)` to just their `text`. fn strip_inline(text: &str) -> String { - strip_markdown_links(text).replace("**", "").replace('`', "") + strip_markdown_links(text) + .replace("**", "") + .replace('`', "") } /// If `s[start..]` begins with a markdown link `[label](url)`, return @@ -514,8 +516,14 @@ mod tests { assert_eq!( toc, vec![ - TocEntry { id: "alpha".into(), label: "Alpha".into() }, - TocEntry { id: "beta".into(), label: "Beta".into() }, + TocEntry { + id: "alpha".into(), + label: "Alpha".into() + }, + TocEntry { + id: "beta".into(), + label: "Beta".into() + }, ] ); } @@ -581,22 +589,34 @@ mod tests { #[test] fn slugify_collapses_runs_and_drops_punctuation() { - assert_eq!(slugify("The headless daemon (qbzd)"), "the-headless-daemon-qbzd"); + assert_eq!( + slugify("The headless daemon (qbzd)"), + "the-headless-daemon-qbzd" + ); // Emphasis chars vanish; the comma and the `&` are dropped and the // whitespace run they leave behind collapses to ONE hyphen. - assert_eq!(slugify(" **Library, home & discovery** "), "library-home-discovery"); + assert_eq!( + slugify(" **Library, home & discovery** "), + "library-home-discovery" + ); assert_eq!(slugify("---"), ""); } #[test] fn release_dates_format_like_the_reference_and_fall_back_raw() { assert_eq!(format_release_date("2026-07-04T18:22:01Z"), "Jul 4, 2026"); - assert_eq!(format_release_date("2026-12-31T00:00:00+02:00"), "Dec 31, 2026"); + assert_eq!( + format_release_date("2026-12-31T00:00:00+02:00"), + "Dec 31, 2026" + ); assert_eq!(format_release_date("2026-01-09T00:00:00Z"), "Jan 9, 2026"); // Malformed input is echoed verbatim, never blanked. assert_eq!(format_release_date("not a date"), "not a date"); assert_eq!(format_release_date("2026/07/04"), "2026/07/04"); - assert_eq!(format_release_date("2026-13-04T00:00:00Z"), "2026-13-04T00:00:00Z"); + assert_eq!( + format_release_date("2026-13-04T00:00:00Z"), + "2026-13-04T00:00:00Z" + ); assert_eq!(format_release_date(""), ""); } @@ -639,11 +659,15 @@ mod tests { assert!(wiki.url.starts_with("https://"), "{wiki:?}"); // No block carries un-stripped inline markers. - assert!(blocks.iter().all(|b| !b.text.contains("**") && !b.text.contains('`'))); + assert!(blocks + .iter() + .all(|b| !b.text.contains("**") && !b.text.contains('`'))); // Nothing is a KIND_SECTION with a level above 1, and no bullet is // level 0 (that combination is what the indent-0 rule prevents). assert!(blocks.iter().all(|b| b.kind != KIND_BULLET || b.level >= 1)); - assert!(blocks.iter().all(|b| b.kind != KIND_SECTION || b.level <= 1)); + assert!(blocks + .iter() + .all(|b| b.kind != KIND_SECTION || b.level <= 1)); // Paragraphs exist (the intro prose, and the `---` separators the // reference deliberately does not special-case). assert!(blocks.iter().any(|b| b.kind == KIND_PARAGRAPH));