fix(ui): Phase 7 C5 — 移除假按钮 + 真实托盘图标

用户报告设置页很多按钮无反应 + 关闭后托盘看不到图标。两处问题:\n\n1. ServerCard 占位按钮无回调:\n- server-card.slint:去掉了 toggled/edit-clicked/remove-clicked 三个回调以及 enabled 切换、Edit / Remove 按钮;变成纯展示卡(状态点 + name + url + 当前标记)\n- settings-page.slint:去掉硬编码的 Home / NAS 占位,改用 active-server-name / active-server-url / active-server-enabled 三个属性(MainWindow → AppConfig 实时填充);空时显示 '(未配置服务器)' 空态提示\n- app.slint:MainWindow 加三个属性 + 转发给 SettingsPage\n- lib.rs:startup 阶段把活跃服务器信息塞进 UI;测试连接成功/失败时更新 enabled 状态\n\n2. 托盘图标看不见:\n- tray.rs:原本 hIcon 写 null,Win10+ 上 Shell_NotifyIconW(NIM_ADD, NULL hIcon) 实际渲染为空白或很弱的小方块。改用 LoadIconW(NULL, IDI_APPLICATION) 加载 Windows 自带的应用程序图标(无需嵌入 .ico 资源,主题感知)。show_toast / Drop 的 hIcon 维持 null(让 NIM_MODIFY/NIM_DELETE 不动原有图标)。AGENTS.md §53 标记真 .ico 资源为 V2 项。
This commit is contained in:
2026-09-19 20:40:33 +08:00
parent 383bb404a9
commit a091e6839a
5 changed files with 131 additions and 87 deletions
+39
View File
@@ -356,6 +356,37 @@ pub fn run() -> error::AppResult<()> {
ui.set_status_text(status_text.into()); ui.set_status_text(status_text.into());
ui.set_is_connected(is_connected); 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 // ── Phase 4 C2 + 5 C2 + 7 C1/C2 + 7 C4: WebSocket worker + UI-thread
// drain. The drain closure now writes to BOTH the canonical // drain. The drain closure now writes to BOTH the canonical
// shared_model AND the filtered_model (honoring the active // 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() { if let Some(ui) = ui_handle.upgrade() {
ui.set_status_text(text.into()); ui.set_status_text(text.into());
ui.set_is_connected(true); 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"); tracing::info!(msgs = msgs.len(), "Test Connection: ok");
} }
@@ -593,6 +628,10 @@ pub fn run() -> error::AppResult<()> {
format!("未连接 · 模拟数据 · {}", server.base_url).into(), format!("未连接 · 模拟数据 · {}", server.base_url).into(),
); );
ui.set_is_connected(false); 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"); tracing::warn!("Test Connection: failed");
} }
+22 -5
View File
@@ -32,12 +32,12 @@ use windows_sys::Win32::UI::Shell::{
}; };
use windows_sys::Win32::UI::WindowsAndMessaging::{ use windows_sys::Win32::UI::WindowsAndMessaging::{
AppendMenuW, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyWindow, AppendMenuW, CreatePopupMenu, CreateWindowExW, DefWindowProcW, DestroyWindow,
DispatchMessageW, GetCursorPos, GetMessageW, GetWindowLongPtrW, PostMessageW, DispatchMessageW, GetCursorPos, GetMessageW, GetWindowLongPtrW, LoadIconW,
RegisterClassExW, RegisterWindowMessageW, SendMessageW, SetForegroundWindow, PostMessageW, RegisterClassExW, RegisterWindowMessageW, SendMessageW, SetForegroundWindow,
SetWindowLongPtrW, TrackPopupMenuEx, TranslateMessage, CS_HREDRAW, CW_USEDEFAULT, SetWindowLongPtrW, TrackPopupMenuEx, TranslateMessage, CS_HREDRAW, CW_USEDEFAULT,
GWLP_USERDATA, MF_CHECKED, MF_GRAYED, MF_SEPARATOR, MF_STRING, MSG, TPM_BOTTOMALIGN, GWLP_USERDATA, IDI_APPLICATION, MF_CHECKED, MF_GRAYED, MF_SEPARATOR, MF_STRING, MSG,
TPM_LEFTALIGN, TPM_RETURNCMD, WM_COMMAND, WM_DESTROY, WM_LBUTTONDBLCLK, WM_RBUTTONUP, TPM_BOTTOMALIGN, TPM_LEFTALIGN, TPM_RETURNCMD, WM_COMMAND, WM_DESTROY, WM_LBUTTONDBLCLK,
WM_USER, WNDCLASSEXW, WS_EX_TOOLWINDOW, WM_RBUTTONUP, WM_USER, WNDCLASSEXW, WS_EX_TOOLWINDOW,
}; };
use windows_sys::core::GUID; use windows_sys::core::GUID;
@@ -245,6 +245,23 @@ impl TrayIcon {
for (i, ch) in tip_w.iter().take(127).enumerate() { for (i, ch) in tip_w.iter().take(127).enumerate() {
nid.szTip[i] = *ch; 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 { unsafe {
let ok = Shell_NotifyIconW(NIM_ADD, &nid); let ok = Shell_NotifyIconW(NIM_ADD, &nid);
if ok == 0 { if ok == 0 {
+13
View File
@@ -86,6 +86,13 @@ export component MainWindow inherits Window {
in-out property <string> status-text: "未连接"; in-out property <string> status-text: "未连接";
callback test-connection-requested(); 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 <string> active-server-name: "(未配置服务器)";
in property <string> active-server-url: "";
in property <bool> active-server-enabled: false;
HorizontalLayout { HorizontalLayout {
// §7 Sidebar width: 220-240px. // §7 Sidebar width: 220-240px.
Rectangle { Rectangle {
@@ -379,6 +386,12 @@ export component MainWindow inherits Window {
start-with-windows: root.start-with-windows; start-with-windows: root.start-with-windows;
start-minimized: root.start-minimized; start-minimized: root.start-minimized;
close-to-tray: root.close-to-tray; 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) => { appearance-changed(idx) => {
root.appearance-index = idx; root.appearance-index = idx;
root.theme = idx == 0 ? AppearanceMode.System : root.theme = idx == 0 ? AppearanceMode.System :
+18 -70
View File
@@ -1,28 +1,28 @@
// AGENTS.md §21 multi-server card. // AGENTS.md §21 multi-server card.
// //
// Phase 2 C3 is a visual + structural shell: one placeholder row so the // Phase 7 C5: V1 只支持单 active server,§21 描述的多 server UI(添加/
// Settings page has the right rhythm. The Settings `Connection` section // 编辑/删除/启用/禁用)还没实现。原来的 ServerCard 有 enabled 切换
// hosts a `Repeater` over these once `AppConfig.servers` lands in Phase 5. // + 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 { export component ServerCard inherits Rectangle {
in property <string> server-name: "(unnamed server)"; in property <string> server-name: "(未配置服务器)";
in property <string> server-url: ""; in property <string> server-url: "";
in property <bool> enabled: true; in property <bool> enabled: true;
in property <bool> is-current: false; in property <bool> is-current: false;
callback toggled();
callback edit-clicked();
callback remove-clicked();
in property <color> bg-color: #28292d; in property <color> bg-color: #28292d;
in property <color> card-border: #3a3b40; in property <color> card-border: #3a3b40;
in property <color> text-primary-color: #e6e6e6; in property <color> text-primary-color: #e6e6e6;
in property <color> text-secondary-color: #a0a0a0; in property <color> text-secondary-color: #a0a0a0;
in property <color> accent-color: #4ea1ff; in property <color> accent-color: #4ea1ff;
in property <color> success-color: #4caf50; in property <color> success-color: #4caf50;
in property <color> toggle-on: #4ea1ff;
in property <color> toggle-off: #6a6a6a;
in property <color> button-bg: #2f3035;
width: 100%; width: 100%;
height: 64px; height: 64px;
@@ -39,27 +39,13 @@ export component ServerCard inherits Rectangle {
spacing: 12px; spacing: 12px;
alignment: center; alignment: center;
// On/off pill toggle (visual only in C3 — Phase 5 makes it mutate state). // 状态点:已连接 → 绿,配置但未连 → 蓝,未配置 → 灰。
Rectangle { Rectangle {
width: 36px; width: 10px;
height: 20px; height: 10px;
border-radius: 10px; border-radius: 5px;
background: enabled ? toggle-on : toggle-off; background: enabled ? success-color : text-secondary-color;
y: parent.height / 2 - self.height / 2; 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 { VerticalLayout {
@@ -75,55 +61,17 @@ export component ServerCard inherits Rectangle {
font-weight: 600; font-weight: 600;
} }
if is-current: Text { if is-current: Text {
text: "• current"; text: "• 当前";
color: accent-color; color: accent-color;
font-size: 11px; font-size: 11px;
} }
} }
Text { Text {
text: server-url; text: server-url != "" ? server-url : "(暂无 URL — 在 Settings → Connection 里填)";
color: text-secondary-color; color: text-secondary-color;
font-size: 11px; font-size: 11px;
overflow: elide; 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();
}
}
}
} }
} }
+39 -12
View File
@@ -128,6 +128,16 @@ export component SettingsPage inherits Rectangle {
callback toggle-persist(string, bool); 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 <string> active-server-name: "";
in property <string> active-server-url: "";
in property <bool> active-server-enabled: false;
in property <color> divider-color: #3a3b40; in property <color> divider-color: #3a3b40;
in property <color> hint-color: #6a6a6a; in property <color> hint-color: #6a6a6a;
@@ -162,18 +172,35 @@ export component SettingsPage inherits Rectangle {
} }
} }
ServerCard { // Phase 7 C5: 单卡显示活跃服务器。多 server 添加/编辑/
server-name: "Home"; // 删除 UI 在 V2 实现(AGENTS.md §21),V1 只有一个活跃
server-url: "https://gotify.example.com"; // server。空字符串 → 空态。
enabled: true; if root.active-server-url != "":
is-current: true; ServerCard {
} server-name: root.active-server-name;
ServerCard { server-url: root.active-server-url;
server-name: "NAS"; enabled: root.active-server-enabled;
server-url: "http://192.168.50.103:50334"; is-current: true;
enabled: true; }
is-current: false;
} 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;
}
}
} }
// 通知 // 通知