feat(storage): Phase 5 C2 — Theme + last_message_id 持久化

- src/storage/state.rs: AppState 加 last_theme (string "system" /
  "light" / "dark") + last_message_id (i64);#[serde(default)]
  让 legacy state.json(只有 window width/height)也能 forward-
  compat parse;3 个 test(含 legacy round-trip)
- src/gotify/api.rs: messages() 加 since: Option<i64> 参数,
  since>0 时拼 ?since=N&limit=M
- src/models.rs: try_fetch_initial_data 加 since 参数透传
- src/lib.rs: state_rc: Rc<RefCell<AppState>> 替代只读 initial_state
  · 启动读 last_theme 应用到 MainWindow.theme + appearance_index
  · HTTP catch-up 用 since=last_message_id;返回的 msgs 推
    watermark
  · WS drain closure 同步更新 last_message_id(共享 Rc 进 UI
    线程 closure)
  · on_close_requested 把整个 state clone 一份再 save(state_for_
    save Rc 也被 drain 用,不能 hold borrow 同时 serialize)
  · 新 callback appearance-changed-persist:Settings → Appearance
    radio 改 theme 时立刻写 state.last_theme
- ui/app.slint: MainWindow 加 appearance-changed-persist callback;
  SettingsPage 的 appearance-changed 调它

避坑:
- VecModel 不是 Send → last_message_id 维护必须放在 UI 线程的
  timer drain closure 里(不是 WS worker),所以走 Rc<RefCell>
  共享 + 不需要锁
- on_close_requested 之前用 state_for_save.borrow_mut() 修改然后
  borrow() save——drain 也在 borrow_mut() 会死锁(同一 RefCell)。
  改成"修改 borrow_mut + save clone",避免同时持有两个 borrow
- AppearanceMode::Dark 的 parse fallback:state.json 写错时不
  锁死在 Light
- legacy state.json 没 last_theme / last_message_id 也能正常
  parse + 用 default(serde(default) + 1 个测试覆盖)

验证:cargo build / build --release 都 exit 0,0 warning
- cargo test: 15 passed (6 storage unit + 6 api + 3 websocket); 0 failed
- debug   35.56 MB, subsystem = WINDOWS_CUI
- release  9.77 MB, subsystem = WINDOWS_GUI
This commit is contained in:
2026-09-19 06:54:42 +08:00
parent 07317e2890
commit 28524b4128
5 changed files with 178 additions and 48 deletions
+4
View File
@@ -57,6 +57,9 @@ export component MainWindow inherits Window {
// ── Phase 2 C3: Settings / About / window state ──────────────
in-out property <int> appearance-index: 2;
callback about-clicked();
// Phase 5 C2: fires whenever the user picks a new theme in Settings.
// Rust persists it into `state.json::last_theme`.
callback appearance-changed-persist(int);
// ── Phase 3 C2: live connection status ───────────────────────
in-out property <bool> is-connected: false;
@@ -212,6 +215,7 @@ export component MainWindow inherits Window {
root.theme = idx == 0 ? AppearanceMode.System :
idx == 1 ? AppearanceMode.Light :
AppearanceMode.Dark;
root.appearance-changed-persist(idx);
}
test-connection-requested => {
root.test-connection-requested();