feat(qconnect): restore the device-name setting from Tauri

Tauri let the user rename how QBZ announces itself to Qobuz Connect;
the Slint port lost the UI entirely while keeping the persistence and
the announce path (same device_name key in qconnect_settings.db, read
by connect() -> bootstrap_remote_presence). Restored 1:1: a text row
in Settings > Playback next to the QConnect startup select (LineEdit
mirroring the ListenBrainz token row, placeholder = the resolved
default 'Qbz - {hostname}'), a new settings-string callback channel
through the settings chain, and a live-service setter so a rename
takes effect on the next connection without an app restart — exactly
Tauri's semantics (no live re-announce; the row description says so).
Trim-only validation like Tauri; empty clears the override. Two new
msgids across the 8 locales.

cc
This commit is contained in:
vicrodh committed 2026-07-08 15:26:39 -06:00
1 parent 029bb0583f
commit 5d50158edb
16 files changed
+186 -1

No files matched your search

@@ -4482,3 +4482,10 @@ msgstr "Standardmäßig aus"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ hat sich von wiederholten Startabstürzen erholt — einige wiederhergestellte Zustände wurden zurückgesetzt"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Qobuz Connect-Gerätename"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "Der Name, den andere Qobuz Connect-Apps für dieses Gerät sehen. Wird bei der nächsten Verbindung übernommen."
@@ -251,3 +251,10 @@ msgstr "Remove album from collection"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ recovered from repeated startup crashes — some restored state was reset"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Qobuz Connect device name"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "The name other Qobuz Connect apps see for this device. Applies on the next connection."
@@ -4512,3 +4512,10 @@ msgstr "Desactivado de forma predeterminada"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ se recuperó de fallos repetidos al iniciar — se restableció parte del estado guardado"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Nombre del dispositivo en Qobuz Connect"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "El nombre que otras apps de Qobuz Connect ven para este dispositivo. Se aplica en la próxima conexión."
@@ -4482,3 +4482,10 @@ msgstr "Désactivé par défaut"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ s'est remis de plantages répétés au démarrage — une partie de l'état restauré a été réinitialisée"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Nom d'appareil Qobuz Connect"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "Le nom que les autres applications Qobuz Connect voient pour cet appareil. Appliqué à la prochaine connexion."
@@ -4471,3 +4471,10 @@ msgstr "デフォルトでオフ"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ は起動時の連続クラッシュから復旧しました — 復元された状態の一部がリセットされました"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Qobuz Connectのデバイス名"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "他のQobuz Connectアプリに表示されるこのデバイスの名前です。次回の接続時に適用されます。"
@@ -4494,3 +4494,10 @@ msgstr "Standaard uit"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ is hersteld van herhaalde crashes bij het opstarten — een deel van de herstelde staat is gereset"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Qobuz Connect-apparaatnaam"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "De naam die andere Qobuz Connect-apps voor dit apparaat zien. Wordt toegepast bij de volgende verbinding."
@@ -4482,3 +4482,10 @@ msgstr "Desligado por padrão"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "O QBZ se recuperou de falhas repetidas na inicialização — parte do estado restaurado foi redefinida"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Nome do dispositivo no Qobuz Connect"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "O nome que outros aplicativos Qobuz Connect veem para este dispositivo. Aplicado na próxima conexão."
@@ -4495,3 +4495,10 @@ msgstr "Выключено по умолчанию"
msgid "QBZ recovered from repeated startup crashes — some restored state was reset"
msgstr "QBZ восстановился после повторных сбоев при запуске — часть восстановленного состояния была сброшена"
# QConnect device name (Settings > Playback, ported from Tauri)
msgid "Qobuz Connect device name"
msgstr "Имя устройства в Qobuz Connect"
msgid "The name other Qobuz Connect apps see for this device. Applies on the next connection."
msgstr "Имя, под которым это устройство видят другие приложения Qobuz Connect. Применяется при следующем подключении."
+4
View File
@@ -98,6 +98,7 @@ export component AppWindow inherits Window {
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
callback settings-string(string, string);
callback settings-reset();
callback settings-release-device();
callback logout();
@@ -158,6 +159,9 @@ export component AppWindow inherits Window {
settings-slider(k, v) => {
root.settings-slider(k, v);
}
settings-string(k, s) => {
root.settings-string(k, s);
}
settings-reset => {
root.settings-reset();
}
@@ -3,9 +3,10 @@
// SettingsState global and persisted through the Rust settings
// controller.
import { LineEdit } from "std-widgets.slint";
import { Theme } from "../foundation/semantic-colors.slint";
import { Typography } from "../foundation/typography.slint";
import { SettingsState } from "../state.slint";
import { SettingsState, UiFocusState } from "../state.slint";
import { QbzToggle } from "../primitives/QbzToggle.slint";
import { QbzSelect } from "../primitives/QbzSelect.slint";
import { QbzSlider } from "../primitives/QbzSlider.slint";
@@ -27,6 +28,7 @@ export component PlaybackSettings inherits VerticalLayout {
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
callback settings-string(string, string);
spacing: 4px;
@@ -112,6 +114,37 @@ export component PlaybackSettings inherits VerticalLayout {
}
}
}
SettingRow {
label: @tr("Qobuz Connect device name");
description: @tr("The name other Qobuz Connect apps see for this device. Applies on the next connection.");
// Same fixed width as the select rows so the input right-aligns
// flush with the rest of the panel (alignment standard).
HorizontalLayout {
width: 240px;
VerticalLayout {
alignment: center;
horizontal-stretch: 1;
LineEdit {
text: SettingsState.qconnect-device-name;
placeholder-text: SettingsState.qconnect-device-name-default;
// Commit on focus loss too (mirrors the Tauri onchange
// semantics: blur or Enter), not only on Enter.
changed has-focus => {
UiFocusState.text-input-focused = self.has-focus;
if (!self.has-focus) {
root.settings-string("qconnect-device-name", self.text);
}
}
edited(s) => {
SettingsState.qconnect-device-name = s;
}
accepted(s) => {
root.settings-string("qconnect-device-name", s);
}
}
}
}
}
Rectangle { height: 12px; }
Divider { }
@@ -67,6 +67,7 @@ export component SettingsView inherits Rectangle {
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
callback settings-string(string, string);
callback settings-reset();
callback settings-release-device();
@@ -248,6 +249,9 @@ export component SettingsView inherits Rectangle {
settings-slider(k, v) => {
root.settings-slider(k, v);
}
settings-string(k, s) => {
root.settings-string(k, s);
}
}
// Appearance — a structural replica; rows bind to
// AppearanceState and emit its own callbacks, so no
+4
View File
@@ -94,6 +94,7 @@ export component AppShell inherits Rectangle {
callback settings-bool(string, bool);
callback settings-select(string, int);
callback settings-slider(string, int);
callback settings-string(string, string);
callback settings-reset();
callback settings-release-device();
callback logout();
@@ -502,6 +503,9 @@ export component AppShell inherits Rectangle {
settings-slider(k, v) => {
root.settings-slider(k, v);
}
settings-string(k, s) => {
root.settings-string(k, s);
}
settings-reset => {
root.settings-reset();
}
+6
View File
@@ -2891,6 +2891,12 @@ export global SettingsState {
// Rust controller, which owns the index -> mode mapping.
in-out property <[string]> qconnect-startup-modes: [];
in-out property <int> qconnect-startup-index: 0;
// Playback — Qobuz Connect device name. The persisted custom override
// ("" = unset) plus the effective default name shown as the input
// placeholder ("Qbz - {hostname}"). Backed by the QConnect settings DB
// via the settings-string callback; applies on the next connect.
in-out property <string> qconnect-device-name: "";
in-out property <string> qconnect-device-name-default: "";
// --- OFFLINE MODE --------------------------------------------------------
+16
View File
@@ -9029,6 +9029,22 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
// Settings — a text input committed (QConnect device name): persist it
// and refresh the live QConnect service cache; the new name is announced
// on the next connect.
{
let weak = window.as_weak();
let handle = tokio_rt.handle().clone();
window.on_settings_string(move |key, value| {
let weak = weak.clone();
let key = key.to_string();
let value = value.to_string();
handle.spawn(async move {
settings::handle_string(weak, key, value).await;
});
});
}
// Settings — Reset: restore Audio + Playback defaults, rebuild the
// snapshot, and re-apply the audio settings to the player.
{
+10
View File
@@ -457,6 +457,16 @@ impl SlintQconnectService {
self.inner.lock().await.runtime.is_some()
}
/// Update the cached custom device name (Settings > Playback). The name
/// is only announced by `bootstrap_remote_presence` during `connect()`,
/// so — mirroring the Tauri `v2_qconnect_set_device_name` — a rename
/// takes effect on the NEXT connect; the live session is untouched.
/// `None` clears the override (falls back to "Qbz - {hostname}").
/// Persistence is the caller's job (`qconnect_transport::persist_device_name`).
pub async fn set_custom_device_name(&self, name: Option<String>) {
*self.custom_device_name.write().await = name;
}
/// Pure read of the live session for the Diagnostics panel (Settings >
/// Developer). Mirrors the Tauri `snapshotQconnect`: reads `inner.last_error`,
/// the transport-connected flag (`app.state_handle().transport_connected`),
+52
View File
@@ -187,6 +187,10 @@ pub struct SettingsSnapshot {
retry_behavior_index: i32,
qconnect_startup_modes: Vec<String>,
qconnect_startup_index: i32,
// QConnect device name — persisted custom override ("" = unset) + the
// effective default used as the input's placeholder.
qconnect_device_name: String,
qconnect_device_name_default: String,
// Now-playing output indicators (backend + effective bit-perfect mode).
output_backend_label: String,
output_mode_label: String,
@@ -512,6 +516,13 @@ fn build_snapshot(
.iter()
.position(|(_, m)| *m == qconnect_startup_mode)
.unwrap_or(QCONNECT_STARTUP_MODES.len() - 1); // last entry = Off (default)
// QConnect device name — same DB (and same blocking-SQLite caveat) as the
// startup mode above. Empty = no custom override; the placeholder shows
// the name that will actually be announced (env var -> "Qbz - {hostname}").
let qconnect_device_name =
crate::qconnect_transport::load_persisted_device_name().unwrap_or_default();
let qconnect_device_name_default =
crate::qconnect_transport::resolve_qconnect_friendly_name(None);
let backend_is_alsa = active_backend == AudioBackendType::Alsa;
let backend_is_pipewire = active_backend == AudioBackendType::PipeWire;
@@ -583,6 +594,8 @@ fn build_snapshot(
.map(|(l, _)| qbz_i18n::t(l))
.collect(),
qconnect_startup_index: qconnect_startup_index as i32,
qconnect_device_name,
qconnect_device_name_default,
output_backend_label: out_backend_label,
output_mode_label: out_mode_label,
output_backend_active: out_backend_active,
@@ -673,6 +686,8 @@ pub fn apply_snapshot(window: &AppWindow, snap: SettingsSnapshot) {
st.set_retry_behavior_index(snap.retry_behavior_index);
st.set_qconnect_startup_modes(string_model(snap.qconnect_startup_modes));
st.set_qconnect_startup_index(snap.qconnect_startup_index);
st.set_qconnect_device_name(snap.qconnect_device_name.into());
st.set_qconnect_device_name_default(snap.qconnect_device_name_default.into());
st.set_loading(false);
}
@@ -1068,6 +1083,43 @@ pub fn handle_slider(
}
}
/// Handle a text-input commit (Enter or focus loss). Currently only the
/// QConnect device name — mirrors the Tauri `v2_qconnect_set_device_name`:
/// trim; empty clears the override so the announced name falls back to the
/// default ("Qbz - {hostname}"). Persisted in the QConnect settings DB and
/// pushed into the live service's cache; the name is only announced during
/// `connect()`, so a rename takes effect on the next connection.
pub async fn handle_string(weak: slint::Weak<AppWindow>, key: String, value: String) {
match key.as_str() {
"qconnect-device-name" => {
let trimmed = value.trim().to_string();
let stored = (!trimmed.is_empty()).then(|| trimmed.clone());
// Persist (blocking SQLite) off the async runtime.
let to_persist = stored.clone();
if let Err(e) = tokio::task::spawn_blocking(move || {
crate::qconnect_transport::persist_device_name(to_persist.as_deref())
})
.await
{
log::error!("[qbz-slint] persist qconnect device name failed: {e}");
}
// Update the live service cache so the next connect announces the
// new name without an app restart (it loads the DB only once, at
// construction).
if let Some(svc) = crate::qconnect_service::service() {
svc.set_custom_device_name(stored).await;
}
// Push the trimmed value back so a whitespace-only entry visibly
// resets the input to the placeholder/default state.
let _ = weak.upgrade_in_event_loop(move |w| {
w.global::<SettingsState>()
.set_qconnect_device_name(trimmed.into());
});
}
other => log::warn!("[qbz-slint] unknown settings string key: {other}"),
}
}
/// Handle a dropdown change: persist it, apply audio ones to the player,
/// and — for a backend switch — re-enumerate devices and recompute the
/// conditional flags into `SettingsState`.