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:
+62
-12
@@ -3,19 +3,22 @@
|
||||
//! Phase 2 surface:
|
||||
//! - [`error`]: AppError / AppResult
|
||||
//! - [`logging`]: tracing-subscriber init
|
||||
//! - [`models`]: MessageItem / Priority (re-exported from slint-generated
|
||||
//! types) + the mock data feed used until Phase 3 lands.
|
||||
//! - [`models`]: Mock message feed (Phase 3 replaces with live Gotify data)
|
||||
//! - [`storage`]: `%APPDATA%/WindowsGotify/state.json` (Phase 2 C3)
|
||||
//! - [`run`]: builds the MainWindow and runs its event loop
|
||||
//!
|
||||
//! Larger subsystems (gotify / storage / windows integration) land in later
|
||||
//! phases per AGENTS.md §57.
|
||||
|
||||
pub mod error;
|
||||
pub mod logging;
|
||||
pub mod models;
|
||||
pub mod storage;
|
||||
|
||||
slint::include_modules!();
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use storage::AppState;
|
||||
|
||||
/// Build the MainWindow and run its event loop until the window is closed.
|
||||
///
|
||||
/// In release builds the Windows subsystem is set to `windows` via the
|
||||
@@ -28,12 +31,20 @@ pub fn run() -> error::AppResult<()> {
|
||||
|
||||
let ui = MainWindow::new().map_err(|e| error::AppError::Ui(e.to_string()))?;
|
||||
|
||||
// Phase 2 C1: dark by default. C3 wires this to the Appearance section
|
||||
// of the Settings page and to `state.json` so the choice persists.
|
||||
ui.set_theme(AppearanceMode::Dark);
|
||||
// Phase 2 C3: load persisted state and apply it before `run()`.
|
||||
let initial_state = storage::load_state();
|
||||
tracing::info!(
|
||||
width = initial_state.last_window_width,
|
||||
height = initial_state.last_window_height,
|
||||
"loaded state.json"
|
||||
);
|
||||
ui.set_theme(AppearanceMode::Dark); // C4 reads theme from state.json.
|
||||
ui.window().set_size(slint::WindowSize::Logical(slint::LogicalSize::new(
|
||||
initial_state.last_window_width.max(0) as f32,
|
||||
initial_state.last_window_height.max(0) as f32,
|
||||
)));
|
||||
|
||||
// Phase 2 C2: hook up the two top-level callbacks. C6/Phase 6 swap these
|
||||
// for real Windows toast firing.
|
||||
// Phase 2 C2 callbacks.
|
||||
ui.on_test_notification_clicked(|| {
|
||||
tracing::info!("empty-state: Test Notification clicked (stub)");
|
||||
});
|
||||
@@ -41,10 +52,49 @@ pub fn run() -> error::AppResult<()> {
|
||||
tracing::debug!(idx, "message card clicked");
|
||||
});
|
||||
|
||||
// Phase 2 C2: feed mock data into the message list. Phase 3 swaps this
|
||||
// for an HTTP catch-up + WebSocket live stream.
|
||||
// Phase 2 C3 callbacks.
|
||||
let state_for_save: Rc<RefCell<AppState>> = Rc::new(RefCell::new(initial_state));
|
||||
ui.on_about_clicked({
|
||||
let ui_handle = ui.as_weak();
|
||||
move || {
|
||||
tracing::info!("about-clicked");
|
||||
if let Some(ui) = ui_handle.upgrade() {
|
||||
ui.set_selection(SidebarSelection::About);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Phase 2 C2 message feed (mock).
|
||||
ui.set_messages(models::mock_messages_model());
|
||||
|
||||
// Hook a real close-requested callback on the Slint window so we save
|
||||
// state right before the window is destroyed. Slint 1.18's `Window` has
|
||||
// a `close-requested` callback that fires on OS-close.
|
||||
ui.window().on_close_requested({
|
||||
let state_for_save = state_for_save.clone();
|
||||
let ui_handle = ui.as_weak();
|
||||
move || {
|
||||
// Capture final size from the live window before saving.
|
||||
if let Some(ui) = ui_handle.upgrade() {
|
||||
let size = ui.window().size();
|
||||
let mut state = state_for_save.borrow_mut();
|
||||
state.last_window_width = size.width as i32;
|
||||
state.last_window_height = size.height as i32;
|
||||
}
|
||||
if let Err(e) = storage::save_state(&state_for_save.borrow()) {
|
||||
tracing::warn!(error = %e, "on close: failed to save state.json");
|
||||
} else {
|
||||
tracing::info!(
|
||||
width = state_for_save.borrow().last_window_width,
|
||||
height = state_for_save.borrow().last_window_height,
|
||||
"saved state.json on close"
|
||||
);
|
||||
}
|
||||
// Allow the window to hide (default CloseRequestResponse behaviour).
|
||||
slint::CloseRequestResponse::HideWindow
|
||||
}
|
||||
});
|
||||
|
||||
ui.run()
|
||||
.map_err(|e| error::AppError::Ui(e.to_string()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user