Files
Windows-Gotify-Client-Rust/ui/settings-page.slint
T
KennethCheng e1622b5959 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
2026-09-19 07:14:35 +08:00

367 lines
12 KiB
Plaintext

// AGENTS.md §10 / §11 settings page.
//
// C3 lays out the five sections (Connection / Notifications / Startup /
// Appearance / Advanced) with controls. The Appearance section is the only
// one wired back to the model: the radio selection drives
// `MainWindow.theme` via the `appearance-changed` callback. Everything else
// is a UI shell — Phase 5 wires it to `AppConfig` + Settings → state.json.
//
// Sub-components MUST be defined before the components that reference them
// in Slint 1.x, so `SettingsToggleRow` / `AppearanceRadio` come first.
import { SectionLabel } from "./components.slint";
import { ServerCard } from "./server-card.slint";
// ─── Sub-components ────────────────────────────────────────────────
export component SettingsToggleRow inherits Rectangle {
in property <string> label;
// `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;
// 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;
in property <color> label-color: #e6e6e6;
in property <color> toggle-on: #4ea1ff;
in property <color> toggle-off: #6a6a6a;
width: 100%;
height: 44px;
border-radius: 8px;
background: row-bg;
border-color: row-border;
border-width: 1px;
opacity: enabled ? 1.0 : 0.5;
HorizontalLayout {
padding-top: 0;
padding-bottom: 0;
padding-left: 16px;
padding-right: 16px;
alignment: center;
Text {
text: label;
color: label-color;
font-size: 13px;
horizontal-stretch: 1;
}
Rectangle {
width: 36px;
height: 20px;
border-radius: 10px;
background: value ? toggle-on : toggle-off;
y: parent.height / 2 - self.height / 2;
Rectangle {
width: 16px;
height: 16px;
border-radius: 8px;
background: #ffffff;
x: value ? parent.width - self.width - 2px : 2px;
y: (parent.height - self.height) / 2;
}
TouchArea {
enabled: root.enabled;
clicked => {
root.value = !root.value;
root.toggled(root.label, root.value);
}
}
}
}
}
export component AppearanceRadio inherits Rectangle {
in property <string> label;
in property <string> description;
in property <bool> selected: false;
callback chosen();
in property <color> row-bg: #28292d;
in property <color> row-border: #3a3b40;
in property <color> row-border-selected: #4ea1ff;
in property <color> label-color: #e6e6e6;
in property <color> hint-color: #6a6a6a;
width: 160px;
height: 64px;
border-radius: 8px;
background: row-bg;
border-color: selected ? row-border-selected : row-border;
border-width: selected ? 2px : 1px;
VerticalLayout {
padding: 12px;
spacing: 2px;
Text {
text: label;
color: label-color;
font-size: 13px;
font-weight: 600;
}
Text {
text: description;
color: hint-color;
font-size: 11px;
}
}
TouchArea {
clicked => {
root.chosen();
}
}
}
// ─── Main page ─────────────────────────────────────────────────────
export component SettingsPage inherits Rectangle {
// Appearance (live wired)
in-out property <int> appearance-index: 2;
callback appearance-changed(int);
// 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 (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;
// Advanced (collapsed by default per §10)
in-out property <bool> advanced-expanded: false;
// Connection (Phase 3 C2)
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;
background: transparent;
VerticalLayout {
padding: 24px;
spacing: 24px;
// CONNECTION
VerticalLayout {
spacing: 8px;
SectionLabel { label: "CONNECTION"; text-color: hint-color; }
// "Test Connection" button — Phase 3 C2 wires this to the
// real `GotifyApi::health` round-trip.
Rectangle {
width: 160px;
height: 32px;
background: #2f3035;
border-radius: 6px;
Text {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
text: root.test-in-progress ? "Testing..." : "Test Connection";
color: #e6e6e6;
font-size: 12px;
}
TouchArea {
enabled: !root.test-in-progress;
clicked => {
root.test-connection-requested();
}
}
}
ServerCard {
server-name: "Home";
server-url: "https://gotify.example.com";
enabled: true;
is-current: true;
}
ServerCard {
server-name: "NAS";
server-url: "http://192.168.50.103:50334";
enabled: true;
is-current: false;
}
}
// NOTIFICATIONS
VerticalLayout {
spacing: 10px;
SectionLabel { label: "NOTIFICATIONS"; text-color: hint-color; }
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);
}
}
}
// STARTUP
VerticalLayout {
spacing: 10px;
SectionLabel { label: "STARTUP"; text-color: hint-color; }
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);
}
}
}
// APPEARANCE (live wired)
VerticalLayout {
spacing: 10px;
SectionLabel { label: "APPEARANCE"; text-color: hint-color; }
HorizontalLayout {
spacing: 12px;
AppearanceRadio {
label: "System";
description: "Follow Windows theme";
selected: root.appearance-index == 0;
chosen => {
root.appearance-index = 0;
root.appearance-changed(0);
}
}
AppearanceRadio {
label: "Light";
description: "Bright neutrals";
selected: root.appearance-index == 1;
chosen => {
root.appearance-index = 1;
root.appearance-changed(1);
}
}
AppearanceRadio {
label: "Dark";
description: "Graphite";
selected: root.appearance-index == 2;
chosen => {
root.appearance-index = 2;
root.appearance-changed(2);
}
}
}
SettingsToggleRow {
label: "Compact message density";
value: false;
enabled: false;
}
}
// ADVANCED (collapsed by default per §10)
VerticalLayout {
spacing: 10px;
HorizontalLayout {
spacing: 8px;
alignment: center;
Text {
text: advanced-expanded ? "v" : ">";
color: hint-color;
font-size: 11px;
}
Text {
text: "ADVANCED";
color: hint-color;
font-size: 11px;
font-weight: 700;
}
TouchArea {
clicked => {
root.advanced-expanded = !root.advanced-expanded;
}
}
}
if advanced-expanded: VerticalLayout {
spacing: 10px;
SettingsToggleRow {
label: "Log level: Info (release default)";
value: false;
enabled: false;
}
SettingsToggleRow {
label: "Clear local state (messages + window size)";
value: false;
enabled: false;
}
SettingsToggleRow {
label: "Reset settings to defaults";
value: false;
enabled: false;
}
}
}
}
}