// 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 theme: AppearanceMode.Dark; out property is-dark: theme != AppearanceMode.Light; out property bg: is-dark ? #1f2024 : #f5f6f8; out property surface: is-dark ? #28292d : #ffffff; out property surface-elev: is-dark ? #2f3035 : #fafbfc; out property border: is-dark ? #3a3b40 : #e4e6ea; out property text-primary: is-dark ? #e6e6e6 : #1a1a1a; out property text-second: is-dark ? #a0a0a0 : #5c5c5c; out property text-muted: is-dark ? #6a6a6a : #9a9a9a; out property accent: is-dark ? #4ea1ff : #0a66c2; out property success: #4caf50; out property warning: #ff9800; out property danger: #f44336; out property priority-low: #6a6a6a; out property priority-normal: is-dark ? #4ea1ff : #0a66c2; out property priority-high: #ff9800; out property priority-max: #f44336; background: bg; in-out property 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 show-about: false; callback close-about(); // ── Phase 2 C2: mock messages + selection ──────────────────── in-out property <[MessageItem]> messages; in-out property 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 priority-filter: 0; // ── Phase 2 C3: Settings / About / window state ────────────── in-out property 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 desktop-notify: true; in-out property sound: true; in-out property notify-high: true; in-out property notify-max: true; in-out property start-with-windows: false; in-out property start-minimized: false; in-out property close-to-tray: true; callback setting-toggled(string, bool); // ── Phase 3 C2: live connection status ─────────────────────── in-out property is-connected: false; in-out property 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; } } }