- 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
242 lines
8.8 KiB
Plaintext
242 lines
8.8 KiB
Plaintext
// Main window + sidebar (AGENTS.md §6, §7).
|
|
//
|
|
// 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";
|
|
import { MessageItem } from "./message-card.slint";
|
|
import { MessageList } from "./message-list.slint";
|
|
import { EmptyState } from "./empty-state.slint";
|
|
import { SettingsPage } from "./settings-page.slint";
|
|
import { AboutDialog } from "./dialogs.slint";
|
|
|
|
export enum SidebarSelection {
|
|
All,
|
|
Settings,
|
|
About,
|
|
}
|
|
|
|
export component MainWindow inherits Window {
|
|
title: "Gotify";
|
|
preferred-width: 960px;
|
|
preferred-height: 680px;
|
|
min-width: 760px;
|
|
min-height: 520px;
|
|
|
|
in-out property <AppearanceMode> theme: AppearanceMode.Dark;
|
|
out property <bool> is-dark: theme != AppearanceMode.Light;
|
|
|
|
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;
|
|
out property <color> border: is-dark ? #3a3b40 : #e4e6ea;
|
|
out property <color> text-primary: is-dark ? #e6e6e6 : #1a1a1a;
|
|
out property <color> text-second: is-dark ? #a0a0a0 : #5c5c5c;
|
|
out property <color> text-muted: is-dark ? #6a6a6a : #9a9a9a;
|
|
out property <color> accent: is-dark ? #4ea1ff : #0a66c2;
|
|
out property <color> success: #4caf50;
|
|
out property <color> warning: #ff9800;
|
|
out property <color> danger: #f44336;
|
|
out property <color> priority-low: #6a6a6a;
|
|
out property <color> priority-normal: is-dark ? #4ea1ff : #0a66c2;
|
|
out property <color> priority-high: #ff9800;
|
|
out property <color> priority-max: #f44336;
|
|
|
|
background: bg;
|
|
|
|
in-out property <SidebarSelection> selection: SidebarSelection.All;
|
|
|
|
// ── Phase 2 C2: mock messages + selection ────────────────────
|
|
in-out property <[MessageItem]> messages;
|
|
in-out property <int> selected-message-index: -1;
|
|
callback test-notification-clicked();
|
|
callback message-card-clicked(int);
|
|
|
|
// ── 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;
|
|
in-out property <string> status-text: "Disconnected";
|
|
callback test-connection-requested();
|
|
|
|
HorizontalLayout {
|
|
// §7 Sidebar width: 220-240px.
|
|
Rectangle {
|
|
width: 232px;
|
|
background: surface;
|
|
|
|
VerticalLayout {
|
|
padding: 20px;
|
|
spacing: 6px;
|
|
|
|
Text {
|
|
text: "Gotify";
|
|
color: text-primary;
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
}
|
|
Text {
|
|
text: "Windows Client · Phase 3";
|
|
color: text-muted;
|
|
font-size: 10px;
|
|
}
|
|
|
|
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 {
|
|
label: "All";
|
|
selected: root.selection == SidebarSelection.All;
|
|
clicked => { root.selection = SidebarSelection.All; }
|
|
}
|
|
|
|
Rectangle { height: 12px; }
|
|
|
|
SectionLabel { label: "SERVERS"; text-color: text-muted; }
|
|
Text {
|
|
text: "(no servers configured)";
|
|
color: text-muted;
|
|
font-size: 11px;
|
|
}
|
|
|
|
Rectangle { height: 12px; }
|
|
|
|
SectionLabel { label: "FILTERS"; text-color: text-muted; }
|
|
NavItem {
|
|
label: "High";
|
|
selected: false;
|
|
has-accent: false;
|
|
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
|
}
|
|
NavItem {
|
|
label: "Normal";
|
|
selected: false;
|
|
has-accent: false;
|
|
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
|
}
|
|
NavItem {
|
|
label: "Low";
|
|
selected: false;
|
|
has-accent: false;
|
|
clicked => { /* TODO Phase 3 C2: priority filter */ }
|
|
}
|
|
|
|
Rectangle { height: 12px; }
|
|
|
|
SectionLabel { label: "APP"; text-color: text-muted; }
|
|
NavItem {
|
|
label: "Settings";
|
|
selected: root.selection == SidebarSelection.Settings;
|
|
clicked => { root.selection = SidebarSelection.Settings; }
|
|
}
|
|
NavItem {
|
|
label: "About";
|
|
selected: root.selection == SidebarSelection.About;
|
|
clicked => {
|
|
root.selection = SidebarSelection.About;
|
|
root.about-clicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right pane — driven by `selection`.
|
|
Rectangle {
|
|
background: bg;
|
|
|
|
VerticalLayout {
|
|
// Title bar with status text under it.
|
|
padding: 24px;
|
|
spacing: 4px;
|
|
|
|
Text {
|
|
text: messages.length == 0 ? "Messages" : "Messages · " + messages.length;
|
|
color: text-primary;
|
|
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;
|
|
vertical-stretch: 1;
|
|
message-clicked(idx) => {
|
|
root.selected-message-index = idx;
|
|
root.message-card-clicked(idx);
|
|
}
|
|
}
|
|
|
|
if messages.length == 0: EmptyState {
|
|
vertical-stretch: 1;
|
|
test-notification => {
|
|
root.test-notification-clicked();
|
|
}
|
|
}
|
|
}
|
|
|
|
if root.selection == SidebarSelection.Settings: SettingsPage {
|
|
appearance-index: root.appearance-index;
|
|
appearance-changed(idx) => {
|
|
root.appearance-index = idx;
|
|
root.theme = idx == 0 ? AppearanceMode.System :
|
|
idx == 1 ? AppearanceMode.Light :
|
|
AppearanceMode.Dark;
|
|
root.appearance-changed-persist(idx);
|
|
}
|
|
test-connection-requested => {
|
|
root.test-connection-requested();
|
|
}
|
|
}
|
|
|
|
// 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 {
|
|
x: 24px;
|
|
y: 24px;
|
|
text: "About — see the dialog.";
|
|
color: text-muted;
|
|
font-size: 13px;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
AboutDialog {}
|
|
} |