Files
Windows-Gotify-Client-Rust/ui/app.slint
T
KennethCheng d2de452c53 fix(ui): Phase 7 C3 修复 — AboutDialog 改用 Rectangle overlay + backdrop
原 AboutDialog inherits Slint Dialog(PopupWindow 子类)并 inline 实例化,没经过运行时 show(),close() 是空操作 → 用户点 Close 没反应、UI 卡死。\n- dialogs.slint:AboutDialog / ConfirmDialog 改为 inherits Rectangle,手写 backdrop(点击关闭)+ content card(Close 按钮调 close-dialog 回调)\n- app.slint:新增 show-about bool 属性;Sidebar About 点击同时触发 about-clicked + show-about=true;最外层 if root.show-about: AboutDialog 渲染\n- 删掉 selection==About 的右栏 marker(现在 About 是模态,不再需要)
2026-09-19 19:40:16 +08:00

451 lines
19 KiB
Plaintext

// Main window + sidebar (AGENTS.md §6, §7).
//
// Phase 7 C3 additions:
// - Sidebar priority filter (All / Low / Normal / High / Max)
// flips `priority-filter` on MainWindow; MessageList hides
// non-matching cards via `visible` (see message-list.slint).
// - Right-hand detail panel that opens when a message is clicked:
// app + priority dot, title, absolute timestamp, full body,
// and an "Open Link" button (re-uses the URL opener wired up
// in Phase 7 C2).
import { AppearanceMode } from "./theme.slint";
import { NavItem, SectionLabel } from "./components.slint";
import { MessageItem, Priority } 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 7 C3 fix: AboutDialog overlay. The previous version
// inherited Slint's `Dialog` (a PopupWindow-derived type) and
// instantiated it inline; without a runtime `.show()` call,
// close() was a no-op and the Close button became inert. We
// now drive the dialog with a plain boolean (toggled from Rust)
// and render it as a manual Rectangle overlay in `if
// root.show-about: AboutDialog { … }`. See dialogs.slint. ──
in-out property <bool> show-about: false;
callback close-about();
// ── 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 7 C3: priority filter (0=All, 1=Low, 2=Normal,
// 3=High, 4=Max). The integer values intentionally align with
// `PriorityFilter` (0=All is the special "no filter" sentinel;
// 1..4 map to the four priority buckets). See message-list.slint
// for the matching `visible` predicate. ──
in-out property <int> priority-filter: 0;
// ── 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 6 C2: live AppConfig toggles (Settings page) ─────────
// Forwarded from SettingsPage so the Rust runtime can:
// 1. Persist into `config.json`
// 2. For `start_with_windows`, also write HKCU Run.
// 3. For `close_to_tray`, immediately affect the close-requested
// handler below without a restart.
in-out property <bool> desktop-notify: true;
in-out property <bool> sound: true;
in-out property <bool> notify-high: true;
in-out property <bool> notify-max: true;
in-out property <bool> start-with-windows: false;
in-out property <bool> start-minimized: false;
in-out property <bool> close-to-tray: true;
callback setting-toggled(string, bool);
// ── 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 7";
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";
// Phase 7 C3: highlight the All nav item whenever
// the priority filter is off (i.e. the user is
// looking at the unfiltered list).
selected: root.selection == SidebarSelection.All
&& root.priority-filter == 0;
clicked => {
root.selection = SidebarSelection.All;
root.priority-filter = 0;
}
}
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; }
// Phase 7 C3: each row maps to one priority bucket.
// The dot's color is the priority color so the row
// doubles as a legend.
NavItem {
label: "Max";
selected: root.selection == SidebarSelection.All
&& root.priority-filter == 4;
has-accent: true;
accent-color: priority-max;
clicked => {
root.selection = SidebarSelection.All;
root.priority-filter = 4;
}
}
NavItem {
label: "High";
selected: root.selection == SidebarSelection.All
&& root.priority-filter == 3;
has-accent: true;
accent-color: priority-high;
clicked => {
root.selection = SidebarSelection.All;
root.priority-filter = 3;
}
}
NavItem {
label: "Normal";
selected: root.selection == SidebarSelection.All
&& root.priority-filter == 2;
has-accent: true;
accent-color: priority-normal;
clicked => {
root.selection = SidebarSelection.All;
root.priority-filter = 2;
}
}
NavItem {
label: "Low";
selected: root.selection == SidebarSelection.All
&& root.priority-filter == 1;
has-accent: true;
accent-color: priority-low;
clicked => {
root.selection = SidebarSelection.All;
root.priority-filter = 1;
}
}
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();
// Phase 7 C3 fix: surface the (now
// Rectangle-overlay) About dialog. The
// Rust `about-clicked` callback still runs
// for telemetry; the actual show happens
// here so the dialog opens the moment the
// user clicks the nav row.
root.show-about = true;
}
}
}
}
// Right pane — driven by `selection`.
Rectangle {
background: bg;
// Phase 7 C3: split the right pane horizontally so the
// detail panel can sit alongside the message list when
// the user picks a card. The detail panel is hidden when
// no card is selected or when the user is on Settings /
// About (which have their own full-pane content).
HorizontalLayout {
padding: 0;
spacing: 0;
// Messages column (always visible when on "All").
Rectangle {
background: bg;
horizontal-stretch: 1;
visible: root.selection == SidebarSelection.All;
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;
priority-filter: root.priority-filter;
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();
}
}
}
}
// Phase 7 C3: detail panel. Visible when a card is
// selected AND the user is on the All view. Always
// reads from `messages[selected-message-index]` so
// it stays in sync with whatever the card list
// currently shows.
if root.selection == SidebarSelection.All
&& root.selected-message-index >= 0
&& root.selected-message-index < root.messages.length: Rectangle {
width: 360px;
background: surface-elev;
border-color: border;
border-width: 1px;
VerticalLayout {
padding: 20px;
spacing: 8px;
HorizontalLayout {
spacing: 8px;
alignment: center;
Rectangle {
width: 10px;
height: 10px;
border-radius: 5px;
background: root.messages[root.selected-message-index].priority == Priority.Low ? priority-low :
root.messages[root.selected-message-index].priority == Priority.Normal ? priority-normal :
root.messages[root.selected-message-index].priority == Priority.High ? priority-high :
priority-max;
}
Text {
text: root.messages[root.selected-message-index].app_name;
color: text-second;
font-size: 12px;
font-weight: 600;
horizontal-stretch: 1;
}
Text {
text: root.messages[root.selected-message-index].timestamp_unix == 0
? ""
: "id #" + root.messages[root.selected-message-index].id;
color: text-muted;
font-size: 11px;
}
}
Text {
text: root.messages[root.selected-message-index].title != ""
? root.messages[root.selected-message-index].title
: root.messages[root.selected-message-index].body;
color: text-primary;
font-size: 16px;
font-weight: 700;
wrap: word-wrap;
}
Text {
text: root.messages[root.selected-message-index].relative_time;
color: text-muted;
font-size: 11px;
}
Rectangle {
height: 1px;
background: border;
}
Text {
text: root.messages[root.selected-message-index].body;
color: text-primary;
font-size: 13px;
wrap: word-wrap;
vertical-stretch: 1;
}
// Open-link button: only when the message
// carries a click URL. Reuses the same
// Rust callback as the card click.
if root.messages[root.selected-message-index].has_click_url: Rectangle {
height: 32px;
background: accent;
border-radius: 6px;
TouchArea {
clicked => {
root.message-card-clicked(root.selected-message-index);
}
}
Text {
x: 0;
y: 0;
width: 100%;
height: 100%;
text: "Open link ↗";
color: white;
font-size: 12px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
}
if root.selection == SidebarSelection.Settings: SettingsPage {
appearance-index: root.appearance-index;
desktop-notify: root.desktop-notify;
sound: root.sound;
notify-high: root.notify-high;
notify-max: root.notify-max;
start-with-windows: root.start-with-windows;
start-minimized: root.start-minimized;
close-to-tray: root.close-to-tray;
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();
}
toggle-persist(label, value) => {
root.setting-toggled(label, value);
}
}
// Phase 7 C3 fix: the About page is no longer a separate
// right-pane marker. Clicking the sidebar About row opens
// a modal dialog overlay (see `if root.show-about:
// AboutDialog { … }` further down), so there's nothing
// to show in the right pane for `selection == About`.
}
}
// Phase 7 C3 fix: conditional About overlay driven by
// `show-about`. See the comment on the property above for why
// we no longer use Slint's `Dialog` type here.
if root.show-about: AboutDialog {
close-dialog => {
root.show-about = false;
}
}
}