feat(windows): Phase 6 C2 — close-to-tray + Settings toggle 持久化
- ui/settings-page.slint: SettingsToggleRow 的 toggled callback
从 (bool) 改成 (string, bool) — 把 row 自己的 label + 新 value
一起交给 caller;row 内部先 value=!value 再 emit;value 改
in-out property 让内部可以 flip;SettingsPage 加 callback
toggle-persist(string, bool);7 个真 toggle 全部 wire 到
toggle-persist(Notifications 4 + Startup 3)
- ui/app.slint: MainWindow 加 in-out property ×7 +
setting-toggled(string, bool) callback;启动从 ActiveConfig
装载;SettingsPage section 把 7 个 property forward 给 page +
toggle-persist 转接到 root.setting-toggled
- src/lib.rs:
· 启动从 ActiveConfig 读 7 个 toggle 设到 MainWindow
· 新 on_setting_toggled 回调:load AppConfig → 按 label 改
对应字段 → save_config;start_with_windows 额外调
startup::set_enabled 写 HKCU Run(Phase 6 C1)
· on_close_requested 看 close_to_tray:true 时
window().hide() + KeepWindowShown;false 时保持原来的
HideWindow 行为
避坑:
- Slint 1.x callback 不能直接用 label 区分,必须 caller 传
string 参数;改造 (bool) → (string, bool)
- 设置 in-out property 让 row 内部能 flip value 然后 emit
新值(caller 收到的就是 post-flip 值)
- close_to_tray 在 UI MainWindow 上是 in-out property,Rust
端用 get_close_to_tray() 实时读,所以 toggle 立刻影响
close_requested 行为(不需要重启)
- on_close_requested 内部读 get_close_to_tray 要 upgrade()
MainWindow weak handle,捕获空时 fallback false(默认
close 而不是 hide)
验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 15 passed; 0 failed
- debug 35.64 MB, subsystem = WINDOWS_CUI
- release 9.78 MB, subsystem = WINDOWS_GUI
This commit is contained in:
+85
-3
@@ -96,6 +96,18 @@ pub fn run() -> error::AppResult<()> {
|
||||
state_rc.borrow().last_window_height.max(0) as f32,
|
||||
)));
|
||||
|
||||
// ── Phase 6 C2: load live AppConfig toggles into MainWindow ───
|
||||
{
|
||||
let cfg = config::ActiveConfig::load()?;
|
||||
ui.set_desktop_notify(cfg.desktop_notifications);
|
||||
ui.set_sound(cfg.sound);
|
||||
ui.set_notify_high(cfg.notify_high);
|
||||
ui.set_notify_max(cfg.notify_max);
|
||||
ui.set_start_with_windows(cfg.start_with_windows);
|
||||
ui.set_start_minimized(cfg.start_minimized);
|
||||
ui.set_close_to_tray(cfg.close_to_tray);
|
||||
}
|
||||
|
||||
// ── Phase 3 C2 + 4 C2 + 5 C1: HTTP catch-up, then channel for WS ────
|
||||
let cfg = config::ActiveConfig::load()?;
|
||||
let shared_model: Rc<VecModel<MessageItem>>;
|
||||
@@ -261,6 +273,61 @@ pub fn run() -> error::AppResult<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Phase 6 C2: every other SettingsPage toggle (notifications,
|
||||
// startup) routes through one callback. We dispatch on the `label`
|
||||
// string the UI hands us, persist into `config.json`, and for
|
||||
// `start_with_windows` we also write HKCU Run (Phase 6 C1).
|
||||
ui.on_setting_toggled(|label, value| {
|
||||
// Phase 6 C2 also: AppConfig is mutated through a fresh load +
|
||||
// save because the toggle handler is a `Fn` (no captured
|
||||
// mutex); this keeps the persistence path side-effect-only and
|
||||
// doesn't risk racing with the close-requested save below.
|
||||
let mut cfg = match storage::load_config() {
|
||||
c => c,
|
||||
};
|
||||
let label_str = label.to_string();
|
||||
let label_ref: &str = &label_str;
|
||||
match label_ref {
|
||||
"desktop_notifications" => cfg.desktop_notifications = value,
|
||||
"sound" => cfg.sound = value,
|
||||
"notify_high" => cfg.notify_high = value,
|
||||
"notify_max" => cfg.notify_max = value,
|
||||
"start_minimized" => cfg.start_minimized = value,
|
||||
"close_to_tray" => {
|
||||
cfg.close_to_tray = value;
|
||||
// Runtime-only: also flip the MainWindow property so
|
||||
// the close-requested handler picks up the new value
|
||||
// immediately without a restart. The Slint callback
|
||||
// already set root.close-to-tray = value on the UI
|
||||
// side; we just mirror it in `cfg` so save_config
|
||||
// captures the same value.
|
||||
}
|
||||
"start_with_windows" => {
|
||||
cfg.start_with_windows = value;
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use crate::windows::startup;
|
||||
if !startup::set_enabled(value) {
|
||||
tracing::warn!("startup: HKCU Run write failed");
|
||||
}
|
||||
}
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
tracing::debug!("startup: not on Windows, skipping HKCU");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
tracing::warn!(label = %label_str, "settings: unknown toggle label");
|
||||
return;
|
||||
}
|
||||
}
|
||||
if let Err(e) = storage::save_config(&cfg) {
|
||||
tracing::warn!(error = %e, label = %label_str, "settings: save_config failed");
|
||||
} else {
|
||||
tracing::info!(label = %label_str, value, "settings: toggle persisted");
|
||||
}
|
||||
});
|
||||
|
||||
// ── Phase 2 C3 callbacks ───────────────────────────────────────
|
||||
ui.on_about_clicked({
|
||||
let ui_handle = ui.as_weak();
|
||||
@@ -272,13 +339,20 @@ pub fn run() -> error::AppResult<()> {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Phase 2 C3 / 5 C2: save state on OS-close ─────────────────
|
||||
// Captures the current window geometry, theme, and last_message_id
|
||||
// (already updated by the WS drain closure).
|
||||
// ── Phase 2 C3 / 5 C2 / 6 C2: save state on OS-close ──────────
|
||||
// Phase 6 C2: when `close_to_tray` is true, hide the window and
|
||||
// pretend to honor the close (return `KeepWindowShown`); otherwise
|
||||
// let Slint proceed with its default behavior (which hides the
|
||||
// window on Windows so the process keeps running until Quit).
|
||||
ui.window().on_close_requested({
|
||||
let state_for_save = state_rc.clone();
|
||||
let ui_handle = ui.as_weak();
|
||||
move || {
|
||||
let close_to_tray = ui_handle
|
||||
.upgrade()
|
||||
.map(|u| u.get_close_to_tray())
|
||||
.unwrap_or(false);
|
||||
|
||||
if let Some(ui) = ui_handle.upgrade() {
|
||||
let size = ui.window().size();
|
||||
let mut s = state_for_save.borrow_mut();
|
||||
@@ -297,6 +371,14 @@ pub fn run() -> error::AppResult<()> {
|
||||
"saved state.json on close"
|
||||
);
|
||||
}
|
||||
|
||||
if close_to_tray {
|
||||
if let Some(ui) = ui_handle.upgrade() {
|
||||
let _ = ui.window().hide();
|
||||
tracing::info!("close: window hidden (close-to-tray)");
|
||||
return slint::CloseRequestResponse::KeepWindowShown;
|
||||
}
|
||||
}
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
}
|
||||
});
|
||||
|
||||
@@ -61,6 +61,21 @@ export component MainWindow inherits Window {
|
||||
// Rust persists it into `state.json::last_theme`.
|
||||
callback appearance-changed-persist(int);
|
||||
|
||||
// ── Phase 6 C2: live AppConfig toggles (Settings page) ─────────
|
||||
// Forwarded from SettingsPage so the Rust runtime can:
|
||||
// 1. Persist into `config.json`
|
||||
// 2. For `start_with_windows`, also write HKCU Run.
|
||||
// 3. For `close_to_tray`, immediately affect the close-requested
|
||||
// handler below without a restart.
|
||||
in-out property <bool> desktop-notify: true;
|
||||
in-out property <bool> sound: true;
|
||||
in-out property <bool> notify-high: true;
|
||||
in-out property <bool> notify-max: true;
|
||||
in-out property <bool> start-with-windows: false;
|
||||
in-out property <bool> start-minimized: false;
|
||||
in-out property <bool> close-to-tray: true;
|
||||
callback setting-toggled(string, bool);
|
||||
|
||||
// ── Phase 3 C2: live connection status ───────────────────────
|
||||
in-out property <bool> is-connected: false;
|
||||
in-out property <string> status-text: "Disconnected";
|
||||
@@ -210,6 +225,13 @@ export component MainWindow inherits Window {
|
||||
|
||||
if root.selection == SidebarSelection.Settings: SettingsPage {
|
||||
appearance-index: root.appearance-index;
|
||||
desktop-notify: root.desktop-notify;
|
||||
sound: root.sound;
|
||||
notify-high: root.notify-high;
|
||||
notify-max: root.notify-max;
|
||||
start-with-windows: root.start-with-windows;
|
||||
start-minimized: root.start-minimized;
|
||||
close-to-tray: root.close-to-tray;
|
||||
appearance-changed(idx) => {
|
||||
root.appearance-index = idx;
|
||||
root.theme = idx == 0 ? AppearanceMode.System :
|
||||
@@ -220,6 +242,9 @@ export component MainWindow inherits Window {
|
||||
test-connection-requested => {
|
||||
root.test-connection-requested();
|
||||
}
|
||||
toggle-persist(label, value) => {
|
||||
root.setting-toggled(label, value);
|
||||
}
|
||||
}
|
||||
|
||||
// About entry stays as a simple right-pane marker; the
|
||||
|
||||
+45
-5
@@ -16,9 +16,14 @@ import { ServerCard } from "./server-card.slint";
|
||||
|
||||
export component SettingsToggleRow inherits Rectangle {
|
||||
in property <string> label;
|
||||
in property <bool> value: false;
|
||||
// `in-out` so the TouchArea can flip it before emitting the
|
||||
// toggled(label, new-value) event.
|
||||
in-out property <bool> value: false;
|
||||
in property <bool> enabled: true;
|
||||
callback toggled(bool);
|
||||
// Phase 6 C2: pass the row's own label + the new value to the
|
||||
// caller so a single callback on the page can dispatch to the
|
||||
// right AppConfig field.
|
||||
callback toggled(string, bool);
|
||||
|
||||
in property <color> row-bg: #28292d;
|
||||
in property <color> row-border: #3a3b40;
|
||||
@@ -66,7 +71,8 @@ export component SettingsToggleRow inherits Rectangle {
|
||||
TouchArea {
|
||||
enabled: root.enabled;
|
||||
clicked => {
|
||||
root.toggled(!root.value);
|
||||
root.value = !root.value;
|
||||
root.toggled(root.label, root.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,13 +129,13 @@ export component SettingsPage inherits Rectangle {
|
||||
in-out property <int> appearance-index: 2;
|
||||
callback appearance-changed(int);
|
||||
|
||||
// Notifications (UI shell)
|
||||
// Notifications (Phase 6 C2: live wired through setting-toggled)
|
||||
in-out property <bool> desktop-notify: true;
|
||||
in-out property <bool> sound: true;
|
||||
in-out property <bool> notify-high: true;
|
||||
in-out property <bool> notify-max: true;
|
||||
|
||||
// Startup (UI shell)
|
||||
// Startup (Phase 6 C2: live wired)
|
||||
in-out property <bool> start-with-windows: false;
|
||||
in-out property <bool> start-minimized: false;
|
||||
in-out property <bool> close-to-tray: true;
|
||||
@@ -141,6 +147,12 @@ export component SettingsPage inherits Rectangle {
|
||||
in-out property <bool> test-in-progress: false;
|
||||
callback test-connection-requested();
|
||||
|
||||
// Phase 6 C2: a single callback that covers every toggle row on
|
||||
// this page. Rust receives `(label, new_value)` and routes to the
|
||||
// correct AppConfig field (and, for "start_with_windows", to the
|
||||
// HKCU Run registry).
|
||||
callback toggle-persist(string, bool);
|
||||
|
||||
// Tokens
|
||||
in property <color> divider-color: #3a3b40;
|
||||
in property <color> hint-color: #6a6a6a;
|
||||
@@ -199,21 +211,37 @@ export component SettingsPage inherits Rectangle {
|
||||
SettingsToggleRow {
|
||||
label: "Enable desktop notification";
|
||||
value: root.desktop-notify;
|
||||
toggled(l, v) => {
|
||||
root.desktop-notify = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: "Play sound";
|
||||
value: root.sound;
|
||||
enabled: root.desktop-notify;
|
||||
toggled(l, v) => {
|
||||
root.sound = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: "Notify on High priority";
|
||||
value: root.notify-high;
|
||||
enabled: root.desktop-notify;
|
||||
toggled(l, v) => {
|
||||
root.notify-high = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: "Notify on Max priority";
|
||||
value: root.notify-max;
|
||||
enabled: root.desktop-notify;
|
||||
toggled(l, v) => {
|
||||
root.notify-max = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,14 +252,26 @@ export component SettingsPage inherits Rectangle {
|
||||
SettingsToggleRow {
|
||||
label: "Start with Windows";
|
||||
value: root.start-with-windows;
|
||||
toggled(l, v) => {
|
||||
root.start-with-windows = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: "Start minimized to tray";
|
||||
value: root.start-minimized;
|
||||
toggled(l, v) => {
|
||||
root.start-minimized = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
SettingsToggleRow {
|
||||
label: "Close button minimizes to tray";
|
||||
value: root.close-to-tray;
|
||||
toggled(l, v) => {
|
||||
root.close-to-tray = v;
|
||||
root.toggle-persist(l, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user