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:
+39
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+35
-8
@@ -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,17 +172,34 @@ export component SettingsPage inherits Rectangle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase 7 C5: 单卡显示活跃服务器。多 server 添加/编辑/
|
||||||
|
// 删除 UI 在 V2 实现(AGENTS.md §21),V1 只有一个活跃
|
||||||
|
// server。空字符串 → 空态。
|
||||||
|
if root.active-server-url != "":
|
||||||
ServerCard {
|
ServerCard {
|
||||||
server-name: "Home";
|
server-name: root.active-server-name;
|
||||||
server-url: "https://gotify.example.com";
|
server-url: root.active-server-url;
|
||||||
enabled: true;
|
enabled: root.active-server-enabled;
|
||||||
is-current: true;
|
is-current: true;
|
||||||
}
|
}
|
||||||
ServerCard {
|
|
||||||
server-name: "NAS";
|
if root.active-server-url == "":
|
||||||
server-url: "http://192.168.50.103:50334";
|
Rectangle {
|
||||||
enabled: true;
|
width: 100%;
|
||||||
is-current: false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user