28524b4128712b3ddc4d33416179f686bdd9ce5a
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
28524b4128 |
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
|
||
|
|
0cf7a4eaaa |
feat(gotify): Phase 3 C2 — UI 接线 + Connection status + 真实数据
- src/config.rs: AppConfig::placeholder() 硬编码 NAS 地址 + 假 token (§20 token 永不日志;Phase 5 接持久化前占位) - src/models.rs: gotify_messages_to_items 把 GotifyMessage 列表转 Slint MessageItem[];try_fetch_real_messages 一次完成 health → applications → messages(50);失败 None 让调用方 fallback mock - src/gotify/mod.rs: re-export ServerConfig - src/lib.rs: run() 启动时调 try_fetch_real_messages,成功用真实数 据 + Connected + 服务端 URL,失败 fallback mock + Disconnected; on_test_connection_requested 重新跑同一路径 - Cargo.toml: 加 chrono 0.4 (RFC3339 解析) - ui/app.slint: is-connected (绿/灰 dot) + status-text (顶部副标) - ui/settings-page.slint: Connection section 加 Test Connection 按钮 (in-progress 时禁用显示 "Testing...") 避坑: - Gotify RFC3339 date 解析失败 silent fallback 到 unix=0(显示 "just now" 而不是崩) - Gotify priority 0..10 → Slint Priority 4 桶分桶:<=0 Low / <=3 Normal / <=7 High / else Max - ServerConfig 在 crate::gotify::model 子模块,mod.rs 必须 re-export 验证:cargo build / build --release 都 exit 0,0 warning - cargo test --test gotify_api: 6 passed - debug 33.36 MB, subsystem = WINDOWS_CUI - release 9.43 MB, subsystem = WINDOWS_GUI(chrono +1.3 MB) |
||
|
|
680fbfa0ad |
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 |
||
|
|
11a122216b |
feat(ui): Phase 2 C2 — Mock 消息模型 + Message list + Empty state
- ui/message-card.slint: export enum Priority + struct MessageItem + MessageCard 组件(§7 优先级圆点 + 标题 + 应用·预览 + 相对时间 + URL 指示) - ui/message-list.slint: VerticalLayout + for 渲染 MessageCard (避开 std-widgets 的 include_paths 配置需求) - ui/empty-state.slint: §9 ✦ glyph(Path 自绘)+ "You're all caught up" + Test Notification 按钮 - ui/app.slint: messages.length 切换 list / empty-state;点击回 调到 MainWindow 顶层 callback - src/models.rs: mock_messages() 生成 12 条覆盖 4 个 priority 的 假消息;format_relative() 输出 "2 min ago" / Yesterday 等 - src/lib.rs: ui.set_messages(mock_messages_model()) 装载;wire on_test_notification_clicked / on_message_card_clicked 避坑: - Slint 1.18 不允许覆盖 Rectangle 内建 border-color,命名用 card-border - ModelRc::from 需要 Rc<VecModel<T>> 不是裸 VecModel 验证:cargo build / build --release 都 exit 0,0 warning - debug 26.55 MB, subsystem = WINDOWS_CUI - release 7.94 MB, subsystem = WINDOWS_GUI |
||
|
|
49cbd9e371 |
feat(ui): Phase 2 C1 — MainWindow + Sidebar + 颜色/主题系统
完成 AGENTS.md §57 Phase 2 的前 3 项:MainWindow 布局、Sidebar、Dark 主题
基础。剩余的 Message list / Empty state / Settings / Light-Switch /
Window state 留到 C2 / C3。
技术栈变化
----------
本次最大的工程变化是把 Slint UI 从 `slint!{}` macro 内嵌迁到独立
`.slint` 文件 + build.rs 编译:
ui/app.slint MainWindow + Sidebar 骨架(Phase 2 C1 完整实现)
ui/theme.slint export enum AppearanceMode { System, Light, Dark }
(不再有 export global Theme / Palette,见下文)
ui/components.slint NavItem / SectionLabel / Divider(Dark 默认颜色)
ui/message-card.slint 占位 stub(C2 实现)
ui/message-list.slint 占位 stub(C2 实现)
ui/empty-state.slint 占位 stub(C2 实现)
ui/settings-page.slint 占位 stub(C3 实现)
ui/server-card.slint 占位 stub(C3 实现)
ui/dialogs.slint 占位 stub(C3 实现)
build.rs slint_build::compile("ui/app.slint"),
把生成的 .rs 路径写入 SLINT_INCLUDE_GENERATED
src/lib.rs `slint::include_modules!()` 引入编译产物;
`MainWindow::new()` 拿 UI 实例;
`ui.set_theme(AppearanceMode::Dark)` 初始化
(C3 改成从 state.json 读 + 监听用户切换)
为什么不用 `include_slint!` —— 那是 `slint_interpreter` crate 提供的
API,不在 `slint` 1.18 主 crate 里。官方推荐的"build-time precompile"
路线是 `slint_build::compile()` + `slint::include_modules!()`,文档明确
写了这一步。
为什么不用 export global Palette / Theme
---------------------------------------
最初一版我把 Theme + Palette 都做成 `export global`,期望从 Rust 端
用 `ui.global::<Theme>().set_appearance(...)` 切换 Light/Dark。失败
了:slint-build 1.18 在生成 Rust 代码时把这两个 global 都 dead-code
消除了,generated 的 pub use 列表里只剩 `MainWindow` 和
`SidebarSelection`。原因——global 的所有 binding 在编译期都被简化为
常量(Theme.appearance 默认 Dark,Palette.background 绑定直接 inline 成
`#1f2024`),没有任何 dynamic 引用保留 global 的符号表。
解决方案:把 Theme + Palette 全部搬到 MainWindow 上作为 out properties,
由 `MainWindow.theme` 触发 binding 重新计算。Rust 端用
`ui.set_theme(AppearanceMode::Light)` 切。代价是 sidebar 的 NavItem
暂时只有 Dark 默认色(fixed `#2f3035` 等),Light theme 在 C3 通过
property forwarding 扩展 NavItem 时补齐——这是 C1 阶段明确接受的折衷。
这一节值得写进 commit 而不是悄悄改掉——后面任何看到代码的人都会好奇
"为什么 palette 不直接 export global?",是这个原因。下次有 Phase 3+ 的人
碰这块也省得踩同样的坑。
Slint 版本对齐
--------------
之前 commit 时钉过 `slint = "1"`(让 cargo 选最新 1.x 稳定版),现在
slint-build 1.18.0 引入 build-deps:`slint-build`, `i-slint-compiler` 等。
两者锁到同一个 1.18.0 版本是 Slint 自身的 forward-compat 约束
(slint-build 的 `compat-1-18` feature 在 build.rs 启动时强校验)。
MainWindow / Sidebar 布局细节
-----------------------------
按 AGENTS.md §6 / §7:
- 960×680 preferred,760×520 min —— Slint 1.18 不允许同时设
`width` + `min-width`,用 `preferred-width`/`preferred-height` 替代
(之前第一次 build 报 `Cannot specify both 'width' and 'min-width'`,
这里记录一下)。
- 232px sidebar 宽,含 AGENTS.md §7 列出的 6 个 section
(logo + All + Servers 占位 + Filters 三个 + Settings + About)
- SidebarSelection enum 驱动右侧 view-switcher,三个 view 都有占位
文字(Messages / Settings / About),C2 / C3 替换实际内容
- AGENTS.md §52 颜色 token 全部按 dark / light 两套列出,由
`MainWindow.is-dark` 派生;优先级颜色(§7 message card)也按
§7 Low/Normal/High/Max 映射
未做(按计划归到后续 commit)
------------------------------
C2:
- src/models.rs(MessageItem + Priority enum,AGENTS.md §34)
- src/mock.rs(生成 10 条 mock 消息)
- ui/message-card.slint 真实内容
- ui/message-list.slint 真实内容(ListView 接 Rust Model adapter)
- ui/empty-state.slint 真实内容(§9 "You're all caught up" + ✦ + Test button)
- main 区 if-switch 真正根据 list.is_empty 选 list / empty-state
C3:
- src/storage/* —— 持久化 state.json
- ui/settings-page.slint 真实内容(§10 五个分组 + Advanced 折叠)
- ui/server-card.slint 真实内容
- ui/dialogs.slint 真实内容(About + confirm)
- sidebar NavItem 接 palette forwarding,Light theme 真的换 sidebar 颜色
- 主题切到 Light/System 实时生效(System 跟随 Windows 主题)
验证
----
cargo build exit 0, 5.10s(增量)
cargo build --release exit 0(slint + skia + accesskit 全重编,约 8 分钟)
warnings 0
debug .exe 26.01 MB, subsystem = 3 (WINDOWS_CUI / console)
release .exe 7.87 MB, subsystem = 2 (WINDOWS_GUI)
Phase 2 C1 验收:能在 Windows 上打开 MainWindow(960×680、Dark、含完整
Sidebar),通过 release profile 的 WINDOWS_GUI subsystem 验证无 console。
Sidebar 切换 Settings/About 时右侧文案占位更新——C2/C3 替换为实际 view。
|