feat(ui): Phase 2 C3 — Settings 页 + Dialogs + 窗口状态保存

- src/storage.rs: AppState (last_window_width/height) + load/save
  atomic write 到 %APPDATA%/WindowsGotify/state.json(§18)
- ui/settings-page.slint: 5 分组(Connection / Notifications / Startup
  / Appearance / Advanced),Appearance 实时切主题(System/Light/Dark
  radio);Advanced 折叠
- ui/dialogs.slint: AboutDialog + ConfirmDialog(§54)
- ui/server-card.slint: 单 server 行(name/url/enabled toggle/edit/remove)
- ui/app.slint: settings 页切换、About 导航、theme 联动
- src/lib.rs: load_state → 启动时应用窗口尺寸;ui.window()
  .on_close_requested 在 OS 关闭前 save_state;wire about/close
  callbacks
- src/error.rs: 加 AppError::Storage(String) 变体
- Cargo.toml: 加 serde + serde_json

避坑:
- Slint 1.x 同文件内 export component 必须在引用前定义(sub-components
  移到 SettingsPage 上面)
- Slint 不支持 padding 两值简写 (padding: 0 16px) → 用 padding-* 四属性
- Slint 不支持 Rectangle 内建 border-color 覆盖 → 命名 card-border
- Slint 不支持 alignment: right → 用 spacer + horizontal-stretch: 1
- 修了一处 .slint 文件被 PowerShell Set-Content GBK 损坏的问题(已
  通过 write 工具 + UTF-8 no BOM 重写并清理 ui/ 所有 .slint)

验证:cargo build / build --release 都 exit 0,0 warning
- debug   27.55 MB, subsystem = WINDOWS_CUI
- release  8.10 MB, subsystem = WINDOWS_GUI
This commit is contained in:
2026-09-19 04:47:54 +08:00
parent 11a122216b
commit 680fbfa0ad
9 changed files with 792 additions and 51 deletions
+127 -4
View File
@@ -1,6 +1,129 @@
// AGENTS.md §21 multi-server card.
//
// Placeholder for Phase 2 C1. The Settings → Connection section uses these
// to list/edit each configured server (name / URL / token / enabled).
// Real data wiring lands in Phase 5 (Storage) when `AppConfig.servers` is
// persisted.
// Phase 2 C3 is a visual + structural shell: one placeholder row so the
// Settings page has the right rhythm. The Settings `Connection` section
// hosts a `Repeater` over these once `AppConfig.servers` lands in Phase 5.
export component ServerCard inherits Rectangle {
in property <string> server-name: "(unnamed server)";
in property <string> server-url: "";
in property <bool> enabled: true;
in property <bool> is-current: false;
callback toggled();
callback edit-clicked();
callback remove-clicked();
in property <color> bg-color: #28292d;
in property <color> card-border: #3a3b40;
in property <color> text-primary-color: #e6e6e6;
in property <color> text-secondary-color: #a0a0a0;
in property <color> accent-color: #4ea1ff;
in property <color> success-color: #4caf50;
in property <color> toggle-on: #4ea1ff;
in property <color> toggle-off: #6a6a6a;
in property <color> button-bg: #2f3035;
width: 100%;
height: 64px;
border-radius: 8px;
background: bg-color;
border-color: card-border;
border-width: 1px;
HorizontalLayout {
padding-top: 12px;
padding-bottom: 12px;
padding-left: 16px;
padding-right: 16px;
spacing: 12px;
alignment: center;
// On/off pill toggle (visual only in C3 — Phase 5 makes it mutate state).
Rectangle {
width: 36px;
height: 20px;
border-radius: 10px;
background: enabled ? toggle-on : toggle-off;
y: parent.height / 2 - self.height / 2;
Rectangle {
width: 16px;
height: 16px;
border-radius: 8px;
background: #ffffff;
x: enabled ? parent.width - self.width - 2px : 2px;
y: (parent.height - self.height) / 2;
}
TouchArea {
clicked => {
root.toggled();
}
}
}
VerticalLayout {
horizontal-stretch: 1;
spacing: 2px;
HorizontalLayout {
spacing: 8px;
Text {
text: server-name;
color: text-primary-color;
font-size: 13px;
font-weight: 600;
}
if is-current: Text {
text: "• current";
color: accent-color;
font-size: 11px;
}
}
Text {
text: server-url;
color: text-secondary-color;
font-size: 11px;
overflow: elide;
}
}
// Action buttons.
Rectangle {
width: 56px;
height: 28px;
background: button-bg;
border-radius: 6px;
Text {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
text: "Edit";
color: text-primary-color;
font-size: 11px;
}
TouchArea {
clicked => {
root.edit-clicked();
}
}
}
Rectangle {
width: 56px;
height: 28px;
background: button-bg;
border-radius: 6px;
Text {
x: (parent.width - self.width) / 2;
y: (parent.height - self.height) / 2;
text: "Remove";
color: text-secondary-color;
font-size: 11px;
}
TouchArea {
clicked => {
root.remove-clicked();
}
}
}
}
}