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)
This commit is contained in:
2026-09-19 05:18:26 +08:00
parent c6ad3883f8
commit 0cf7a4eaaa
8 changed files with 260 additions and 41 deletions
+52 -27
View File
@@ -1,9 +1,8 @@
// Main window + sidebar (AGENTS.md §6, §7).
//
// Phase 2 C3 wires the Settings page, About dialog, and window-state
// persistence. The Appearance section in `settings-page.slint` is the only
// settings UI that drives real state today — every other control is a
// shell that Phase 5 hooks to `AppConfig`.
// Phase 3 C2 wires the live connection status (dot + status text under the
// title bar) and accepts a `test-connection-requested` callback fired by
// the Settings → Connection section.
import { AppearanceMode } from "./theme.slint";
import { NavItem, SectionLabel } from "./components.slint";
@@ -26,11 +25,9 @@ export component MainWindow inherits Window {
min-width: 760px;
min-height: 520px;
// Theme switch — Rust-controllable via `ui.set_theme(...)`.
in-out property <AppearanceMode> theme: AppearanceMode.Dark;
out property <bool> is-dark: theme != AppearanceMode.Light;
// Palette — all derived from `is-dark` (AGENTS.md §52).
out property <color> bg: is-dark ? #1f2024 : #f5f6f8;
out property <color> surface: is-dark ? #28292d : #ffffff;
out property <color> surface-elev: is-dark ? #2f3035 : #fafbfc;
@@ -58,13 +55,13 @@ export component MainWindow inherits Window {
callback message-card-clicked(int);
// ── Phase 2 C3: Settings / About / window state ──────────────
// Initial appearance radio (0=System/1=Light/2=Dark). Maps 1:1 to
// AppearanceMode: System=0, Light=1, Dark=2.
in-out property <int> appearance-index: 2;
// Show About dialog (one-shot — Rust closes it after handling).
callback about-clicked();
callback close-requested-save-state();
// ── Phase 3 C2: live connection status ───────────────────────
in-out property <bool> is-connected: false;
in-out property <string> status-text: "Disconnected";
callback test-connection-requested();
HorizontalLayout {
// §7 Sidebar width: 220-240px.
@@ -83,12 +80,32 @@ export component MainWindow inherits Window {
font-weight: 700;
}
Text {
text: "Windows Client · Phase 2";
text: "Windows Client · Phase 3";
color: text-muted;
font-size: 10px;
}
Rectangle { height: 16px; }
Rectangle { height: 12px; }
// Live status row (Phase 3 C2): dot + text.
HorizontalLayout {
spacing: 6px;
alignment: center;
Rectangle {
width: 8px;
height: 8px;
border-radius: 4px;
background: is-connected ? success : text-muted;
}
Text {
text: is-connected ? "Connected" : "Disconnected";
color: is-connected ? success : text-muted;
font-size: 11px;
font-weight: 600;
}
}
Rectangle { height: 12px; }
SectionLabel { label: "MESSAGES"; text-color: text-muted; }
NavItem {
@@ -113,19 +130,19 @@ export component MainWindow inherits Window {
label: "High";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
clicked => { /* TODO Phase 3 C2: priority filter */ }
}
NavItem {
label: "Normal";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
clicked => { /* TODO Phase 3 C2: priority filter */ }
}
NavItem {
label: "Low";
selected: false;
has-accent: false;
clicked => { /* TODO Phase 2 C2 */ }
clicked => { /* TODO Phase 3 C2: priority filter */ }
}
Rectangle { height: 12px; }
@@ -151,9 +168,10 @@ export component MainWindow inherits Window {
Rectangle {
background: bg;
if root.selection == SidebarSelection.All: VerticalLayout {
VerticalLayout {
// Title bar with status text under it.
padding: 24px;
spacing: 12px;
spacing: 4px;
Text {
text: messages.length == 0 ? "Messages" : "Messages · " + messages.length;
@@ -161,11 +179,18 @@ export component MainWindow inherits Window {
font-size: 18px;
font-weight: 700;
}
Text {
text: status-text;
color: text-muted;
font-size: 11px;
}
Rectangle { height: 12px; }
if messages.length > 0: MessageList {
messages: messages;
selected-index: root.selected-message-index;
height: parent.height - 48px;
vertical-stretch: 1;
message-clicked(idx) => {
root.selected-message-index = idx;
root.message-card-clicked(idx);
@@ -173,6 +198,7 @@ export component MainWindow inherits Window {
}
if messages.length == 0: EmptyState {
vertical-stretch: 1;
test-notification => {
root.test-notification-clicked();
}
@@ -187,11 +213,14 @@ export component MainWindow inherits Window {
idx == 1 ? AppearanceMode.Light :
AppearanceMode.Dark;
}
test-connection-requested => {
root.test-connection-requested();
}
}
// About is rendered via a Dialog overlay — not in the right pane.
// A separate About row keeps the layout stable; the dialog is
// shown via `about-clicked` callback to Rust.
// About entry stays as a simple right-pane marker; the
// modal AboutDialog overlay is wired through AboutDialog in
// this component (visibility toggled via `about-clicked`).
if root.selection == SidebarSelection.About: Rectangle {
background: bg;
Text {
@@ -205,9 +234,5 @@ export component MainWindow inherits Window {
}
}
// Modal About dialog — opened when the user clicks About in the sidebar.
AboutDialog {
// visible-by-default is false. Toggled via Rust by listening to
// `about-clicked`. C3 wires the bare open/close plumbing.
}
AboutDialog {}
}
+27
View File
@@ -137,6 +137,10 @@ export component SettingsPage inherits Rectangle {
// 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();
// Tokens
in property <color> divider-color: #3a3b40;
in property <color> hint-color: #6a6a6a;
@@ -151,6 +155,29 @@ export component SettingsPage inherits Rectangle {
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";