diff --git a/src/lib.rs b/src/lib.rs index d9ca7c6..86dd944 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -356,6 +356,37 @@ pub fn run() -> error::AppResult<()> { ui.set_status_text(status_text.into()); ui.set_is_connected(is_connected); + // Phase 7 C5: forward active-server summary to SettingsPage. + // V1 only has the single active server (AGENTS.md §21), so we + // surface exactly that. Empty values trigger the "未配置服务器" + // empty-state in SettingsPage. Display name falls back to the + // URL host (e.g. "gotify.example.com" or "192.168.50.103:8418") + // since `ServerConfig` only carries base_url + token — the human- + // friendly `name` lives on `ServerEntry`, which V1 doesn't expose + // through `ActiveConfig`. That's fine for V1. + match cfg.server.as_ref() { + Some(server) => { + let display_name = url::Url::parse(&server.base_url) + .ok() + .and_then(|u| u.host_str().map(|s| { + if let Some(port) = u.port() { + format!("{s}:{port}") + } else { + s.to_string() + } + })) + .unwrap_or_else(|| server.base_url.clone()); + ui.set_active_server_name(display_name.into()); + ui.set_active_server_url(server.base_url.as_str().into()); + ui.set_active_server_enabled(is_connected); + } + None => { + ui.set_active_server_name("(未配置服务器)".into()); + ui.set_active_server_url("".into()); + ui.set_active_server_enabled(false); + } + } + // ── Phase 4 C2 + 5 C2 + 7 C1/C2 + 7 C4: WebSocket worker + UI-thread // drain. The drain closure now writes to BOTH the canonical // shared_model AND the filtered_model (honoring the active @@ -584,6 +615,10 @@ pub fn run() -> error::AppResult<()> { if let Some(ui) = ui_handle.upgrade() { ui.set_status_text(text.into()); ui.set_is_connected(true); + // Phase 7 C5: refresh the active-server + // summary so the Settings card lights up + // green after a successful test. + ui.set_active_server_enabled(true); } tracing::info!(msgs = msgs.len(), "Test Connection: ok"); } @@ -593,6 +628,10 @@ pub fn run() -> error::AppResult<()> { format!("未连接 · 模拟数据 · {}", server.base_url).into(), ); ui.set_is_connected(false); + // Phase 7 C5: failed test → server card + // dot returns to "configured but not + // connected" (gray). + ui.set_active_server_enabled(false); } tracing::warn!("Test Connection: failed"); } diff --git a/src/windows/tray.rs b/src/windows/tray.rs index 5b11b25..554b73a 100644 --- a/src/windows/tray.rs +++ b/src/windows/tray.rs @@ -32,12 +32,12 @@ use windows_sys::Win32::UI::Shell::{ }; use windows_sys::Win32::UI::WindowsAndMessaging::{ AppendMenuW, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyWindow, - DispatchMessageW, GetCursorPos, GetMessageW, GetWindowLongPtrW, PostMessageW, - RegisterClassExW, RegisterWindowMessageW, SendMessageW, SetForegroundWindow, + DispatchMessageW, GetCursorPos, GetMessageW, GetWindowLongPtrW, LoadIconW, + PostMessageW, RegisterClassExW, RegisterWindowMessageW, SendMessageW, SetForegroundWindow, SetWindowLongPtrW, TrackPopupMenuEx, TranslateMessage, CS_HREDRAW, CW_USEDEFAULT, - GWLP_USERDATA, MF_CHECKED, MF_GRAYED, MF_SEPARATOR, MF_STRING, MSG, TPM_BOTTOMALIGN, - TPM_LEFTALIGN, TPM_RETURNCMD, WM_COMMAND, WM_DESTROY, WM_LBUTTONDBLCLK, WM_RBUTTONUP, - WM_USER, WNDCLASSEXW, WS_EX_TOOLWINDOW, + GWLP_USERDATA, IDI_APPLICATION, MF_CHECKED, MF_GRAYED, MF_SEPARATOR, MF_STRING, MSG, + TPM_BOTTOMALIGN, TPM_LEFTALIGN, TPM_RETURNCMD, WM_COMMAND, WM_DESTROY, WM_LBUTTONDBLCLK, + WM_RBUTTONUP, WM_USER, WNDCLASSEXW, WS_EX_TOOLWINDOW, }; use windows_sys::core::GUID; @@ -245,6 +245,23 @@ impl TrayIcon { for (i, ch) in tip_w.iter().take(127).enumerate() { nid.szTip[i] = *ch; } + + // Phase 7 C5: real tray icon. We previously passed a NULL + // `hIcon`, which on Windows 10+ renders as an invisible entry + // (or, in some builds, a tiny white square that the user + // doesn't recognize as our app). Loading `IDI_APPLICATION` + // gives the OS-standard application icon — guaranteed + // visible, theme-aware, and ships with Windows itself so we + // don't need to embed a `.ico` resource. AGENTS.md §53 lists + // shipping a real `.ico` as a V2 polish item; for V1 this is + // the minimum fix that makes close-to-tray actually work + // from the user's perspective. + let tray_icon = unsafe { LoadIconW(std::ptr::null_mut(), IDI_APPLICATION) }; + if tray_icon.is_null() { + tracing::warn!("tray: LoadIconW(IDI_APPLICATION) returned NULL"); + } else { + nid.hIcon = tray_icon; + } unsafe { let ok = Shell_NotifyIconW(NIM_ADD, &nid); if ok == 0 { diff --git a/ui/app.slint b/ui/app.slint index e91f282..e7006ad 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -86,6 +86,13 @@ export component MainWindow inherits Window { in-out property status-text: "未连接"; callback test-connection-requested(); + // Phase 7 C5: active-server summary forwarded into SettingsPage + // so the Connection section reflects the real AppConfig instead + // of the old hardcoded "Home / NAS" placeholders. + in property active-server-name: "(未配置服务器)"; + in property active-server-url: ""; + in property active-server-enabled: false; + HorizontalLayout { // §7 Sidebar width: 220-240px. Rectangle { @@ -379,6 +386,12 @@ export component MainWindow inherits Window { start-with-windows: root.start-with-windows; start-minimized: root.start-minimized; close-to-tray: root.close-to-tray; + // Phase 7 C5: forward active server config so the + // Connection section shows the real entry instead + // of the hardcoded "Home / NAS" placeholders. + active-server-name: root.active-server-name; + active-server-url: root.active-server-url; + active-server-enabled: root.active-server-enabled; appearance-changed(idx) => { root.appearance-index = idx; root.theme = idx == 0 ? AppearanceMode.System : diff --git a/ui/server-card.slint b/ui/server-card.slint index 7123b7b..dc27e48 100644 --- a/ui/server-card.slint +++ b/ui/server-card.slint @@ -1,28 +1,28 @@ // AGENTS.md §21 multi-server card. // -// Phase 2 C3 is a visual + structural shell: one placeholder row so the -// Settings page has the right rhythm. The Settings `Connection` section -// hosts a `Repeater` over these once `AppConfig.servers` lands in Phase 5. +// Phase 7 C5: V1 只支持单 active server,§21 描述的多 server UI(添加/ +// 编辑/删除/启用/禁用)还没实现。原来的 ServerCard 有 enabled 切换 +// + Edit / Remove 按钮,但 settings-page.slint 只是占位填写硬编码 +// 的 "Home" / "NAS",回调也没接 —— 用户看到的 "点不动" 按钮是纯 UI +// 壳子,对应 "很多都没反应" 的反馈。 +// +// 修法:把 ServerCard 简化成纯展示卡(name + url + 状态点),由 +// settings-page.slint 从 AppConfig.active_server 实时填进去。无服务 +// 器时整个 section 走空态。等 V2 真做多 server UI 再加回 toggle / +// Edit / Remove / Add。 export component ServerCard inherits Rectangle { - in property server-name: "(unnamed server)"; + in property server-name: "(未配置服务器)"; in property server-url: ""; in property enabled: true; in property is-current: false; - callback toggled(); - callback edit-clicked(); - callback remove-clicked(); - in property bg-color: #28292d; in property card-border: #3a3b40; in property text-primary-color: #e6e6e6; in property text-secondary-color: #a0a0a0; in property accent-color: #4ea1ff; in property success-color: #4caf50; - in property toggle-on: #4ea1ff; - in property toggle-off: #6a6a6a; - in property button-bg: #2f3035; width: 100%; height: 64px; @@ -39,27 +39,13 @@ export component ServerCard inherits Rectangle { spacing: 12px; alignment: center; - // On/off pill toggle (visual only in C3 — Phase 5 makes it mutate state). + // 状态点:已连接 → 绿,配置但未连 → 蓝,未配置 → 灰。 Rectangle { - width: 36px; - height: 20px; - border-radius: 10px; - background: enabled ? toggle-on : toggle-off; + width: 10px; + height: 10px; + border-radius: 5px; + background: enabled ? success-color : text-secondary-color; y: parent.height / 2 - self.height / 2; - - Rectangle { - width: 16px; - height: 16px; - border-radius: 8px; - background: #ffffff; - x: enabled ? parent.width - self.width - 2px : 2px; - y: (parent.height - self.height) / 2; - } - TouchArea { - clicked => { - root.toggled(); - } - } } VerticalLayout { @@ -75,55 +61,17 @@ export component ServerCard inherits Rectangle { font-weight: 600; } if is-current: Text { - text: "• current"; + text: "• 当前"; color: accent-color; font-size: 11px; } } Text { - text: server-url; + text: server-url != "" ? server-url : "(暂无 URL — 在 Settings → Connection 里填)"; color: text-secondary-color; font-size: 11px; overflow: elide; } } - - // Action buttons. - Rectangle { - width: 56px; - height: 28px; - background: button-bg; - border-radius: 6px; - Text { - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - text: "Edit"; - color: text-primary-color; - font-size: 11px; - } - TouchArea { - clicked => { - root.edit-clicked(); - } - } - } - Rectangle { - width: 56px; - height: 28px; - background: button-bg; - border-radius: 6px; - Text { - x: (parent.width - self.width) / 2; - y: (parent.height - self.height) / 2; - text: "Remove"; - color: text-secondary-color; - font-size: 11px; - } - TouchArea { - clicked => { - root.remove-clicked(); - } - } - } } } \ No newline at end of file diff --git a/ui/settings-page.slint b/ui/settings-page.slint index 20f2db2..f5c1425 100644 --- a/ui/settings-page.slint +++ b/ui/settings-page.slint @@ -128,6 +128,16 @@ export component SettingsPage inherits Rectangle { callback toggle-persist(string, bool); + // Phase 7 C5: active server config (forwarded from MainWindow → + // AppConfig). Empty strings mean "未配置服务器". The page used to + // hardcode two placeholder rows ("Home" / "NAS") with Edit / + // Remove buttons that didn't do anything — that's what the user + // reported as "很多都没反应". V1 only has the single active + // server anyway, so a single card is honest about scope. + in property active-server-name: ""; + in property active-server-url: ""; + in property active-server-enabled: false; + in property divider-color: #3a3b40; in property hint-color: #6a6a6a; @@ -162,18 +172,35 @@ export component SettingsPage inherits Rectangle { } } - ServerCard { - server-name: "Home"; - server-url: "https://gotify.example.com"; - enabled: true; - is-current: true; - } - ServerCard { - server-name: "NAS"; - server-url: "http://192.168.50.103:50334"; - enabled: true; - is-current: false; - } + // Phase 7 C5: 单卡显示活跃服务器。多 server 添加/编辑/ + // 删除 UI 在 V2 实现(AGENTS.md §21),V1 只有一个活跃 + // server。空字符串 → 空态。 + if root.active-server-url != "": + ServerCard { + server-name: root.active-server-name; + server-url: root.active-server-url; + enabled: root.active-server-enabled; + is-current: true; + } + + if root.active-server-url == "": + Rectangle { + width: 100%; + height: 56px; + border-radius: 8px; + background: #28292d; + border-color: #3a3b40; + border-width: 1px; + Text { + x: 16px; + y: (parent.height - self.height) / 2; + text: "(未配置服务器 — V1 暂不支持 UI 添加,请手动编辑 %APPDATA%\\WindowsGotify\\config.json)"; + color: #6a6a6a; + font-size: 12px; + wrap: word-wrap; + width: parent.width - 32px; + } + } } // 通知