- sidebar FILTERS 加 Max/High/Normal/Low 四档(dot 色与 priority 配色一致),点击切换 priority-filter(0=All, 1=Low, 2=Normal, 3=High, 4=Max) - MessageList 加 priority-filter property,通过 visible 谓词按 Slint Priority enum 匹配;保留原 idx 让 selected-index 不随 filter 错位 - 右栏加 detail panel:app + priority dot、title、id、relative_time、body、has_click_url 时显示 Open link 按钮(复用 C2 的 message-card-clicked 走 url::open) - detail panel 与消息列表水平分割,selected-message-index < 0 时自动隐藏;切换 Settings/About 时不显示
435 lines
18 KiB
Plaintext
435 lines
18 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 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();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
// 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 {}
|
|
} |