@yufengtadian/freedom-cli 1.12.17 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build.js +74 -9
- package/lib/cli.js +61 -3
- package/lib/init.js +9 -1
- package/lib/security.js +2 -2
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +43 -39
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +219 -21
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +295 -51
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +64 -36
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +242 -92
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +56 -32
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +22 -0
- package/templates/go/pkg/freedom/syscap_windows.go +845 -0
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +536 -0
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +406 -251
- package/templates/project-minimal/freedom.config.js +58 -0
- package/templates/project-minimal/index.html +41 -0
- package/templates/project-minimal/package.json +14 -0
- package/templates/project-minimal/src/main.js +19 -0
- package/templates/project-minimal/vite.config.js +18 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- package/templates/go/webview_go/webview_test.go +0 -50
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
// 系统托盘(Shell_NotifyIcon)与原生菜单栏(HMENU)。
|
|
6
|
+
//
|
|
7
|
+
// 托盘通过一个隐藏消息窗口接收回调消息(WM_APP+1),菜单项点击经 WM_COMMAND
|
|
8
|
+
// 分发,统一通过 App.Emit 推送到前端(freedom.tray.on('click'/'menu', ...))。
|
|
9
|
+
// 原生菜单栏通过 SetMenu 挂到主窗口,同样经 WM_COMMAND 回调。
|
|
10
|
+
//
|
|
11
|
+
// 与主窗口同一 UI 线程创建,消息由同一消息泵派发,无需额外线程。
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"encoding/json"
|
|
15
|
+
"fmt"
|
|
16
|
+
"sync"
|
|
17
|
+
"syscall"
|
|
18
|
+
"unsafe"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// ---- 常量 ----
|
|
22
|
+
|
|
23
|
+
const (
|
|
24
|
+
wmApp = 0x8000 // WM_APP
|
|
25
|
+
wmCommand = 0x0111
|
|
26
|
+
wmLButtonUp = 0x0202
|
|
27
|
+
wmLButtonDbl = 0x0203
|
|
28
|
+
wmRButtonUp = 0x0205
|
|
29
|
+
wmNull = 0x0000
|
|
30
|
+
|
|
31
|
+
nimAdd = 0x00000000
|
|
32
|
+
nimModify = 0x00000001
|
|
33
|
+
nimDelete = 0x00000002
|
|
34
|
+
|
|
35
|
+
nifMessage = 0x00000001
|
|
36
|
+
nifIcon = 0x00000002
|
|
37
|
+
nifTip = 0x00000004
|
|
38
|
+
|
|
39
|
+
tpmRightButton = 0x00000002
|
|
40
|
+
tpmLeftAlign = 0x00000000
|
|
41
|
+
|
|
42
|
+
mfString = 0x00000000
|
|
43
|
+
mfSeparator = 0x00000800
|
|
44
|
+
mfChecked = 0x00000008
|
|
45
|
+
mfGrayed = 0x00000001
|
|
46
|
+
mfEnabled = 0x00000000
|
|
47
|
+
mfPopup = 0x00000010
|
|
48
|
+
mfByPosition = 0x00000400
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
var (
|
|
52
|
+
shell32 = syscall.NewLazyDLL("shell32.dll")
|
|
53
|
+
procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW")
|
|
54
|
+
|
|
55
|
+
procRegisterClassExW = user32win.NewProc("RegisterClassExW")
|
|
56
|
+
procCreateWindowExW = user32win.NewProc("CreateWindowExW")
|
|
57
|
+
procDefWindowProcW = user32win.NewProc("DefWindowProcW")
|
|
58
|
+
procCreatePopupMenu = user32win.NewProc("CreatePopupMenu")
|
|
59
|
+
procCreateMenu = user32win.NewProc("CreateMenu")
|
|
60
|
+
procAppendMenuW = user32win.NewProc("AppendMenuW")
|
|
61
|
+
procInsertMenuW = user32win.NewProc("InsertMenuW")
|
|
62
|
+
procDestroyMenu = user32win.NewProc("DestroyMenu")
|
|
63
|
+
procTrackPopupMenu = user32win.NewProc("TrackPopupMenu")
|
|
64
|
+
procGetCursorPos = user32win.NewProc("GetCursorPos")
|
|
65
|
+
procSetForegroundWindow = user32win.NewProc("SetForegroundWindow")
|
|
66
|
+
procSetMenu = user32win.NewProc("SetMenu")
|
|
67
|
+
procDrawMenuBar = user32win.NewProc("DrawMenuBar")
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// ---- 结构 ----
|
|
71
|
+
|
|
72
|
+
type point struct {
|
|
73
|
+
X, Y int32
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type wndClassExW struct {
|
|
77
|
+
CbSize uint32
|
|
78
|
+
Style uint32
|
|
79
|
+
LpfnWndProc uintptr
|
|
80
|
+
CbClsExtra int32
|
|
81
|
+
CbWndExtra int32
|
|
82
|
+
HInstance uintptr
|
|
83
|
+
HIcon uintptr
|
|
84
|
+
HCursor uintptr
|
|
85
|
+
HbrBackground uintptr
|
|
86
|
+
LpszMenuName *uint16
|
|
87
|
+
LpszClassName *uint16
|
|
88
|
+
HIconSm uintptr
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// notifyIconDataW 对应 NOTIFYICONDATAW(amd64 布局,cbSize=984)。
|
|
92
|
+
type notifyIconDataW struct {
|
|
93
|
+
CbSize uint32
|
|
94
|
+
_ uint32 // 对齐填充
|
|
95
|
+
HWnd uintptr
|
|
96
|
+
UID uint32
|
|
97
|
+
UFlags uint32
|
|
98
|
+
UCallbackMessage uint32
|
|
99
|
+
HIcon uintptr
|
|
100
|
+
SzTip [128]uint16
|
|
101
|
+
DwState uint32
|
|
102
|
+
DwStateMask uint32
|
|
103
|
+
SzInfo [256]uint16
|
|
104
|
+
UVersion uint32
|
|
105
|
+
SzInfoTitle [64]uint16
|
|
106
|
+
DwInfoFlags uint32
|
|
107
|
+
GuidItem [16]byte
|
|
108
|
+
HBalloonIcon uintptr
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// trayState 保存当前托盘实例(单窗口框架内为单例)。
|
|
112
|
+
type trayState struct {
|
|
113
|
+
mu sync.Mutex
|
|
114
|
+
hwnd uintptr // 隐藏消息窗口
|
|
115
|
+
menu uintptr // 右键 popup 菜单
|
|
116
|
+
menuIDs []uint32 // menu 占用的条目 id(重建时清理映射)
|
|
117
|
+
menuBar uintptr // 原生菜单栏 HMENU
|
|
118
|
+
menuBarIDs []uint32 // menuBar 占用的条目 id
|
|
119
|
+
items map[uint32]string // menu id -> 前端 id
|
|
120
|
+
nextID uint32
|
|
121
|
+
emit func(event string, data interface{})
|
|
122
|
+
icon uintptr // 当前托盘图标 HICON
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var (
|
|
126
|
+
trayProcMu sync.Mutex
|
|
127
|
+
trayStateInst *trayState
|
|
128
|
+
trayWndProcCB uintptr
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
// trayProcCallback 是隐藏窗口的 WndProc(Go 回调,经 syscall.NewCallback 注册)。
|
|
132
|
+
// 由 RegisterClassEx 注册到系统,消息泵在 UI 线程派发。
|
|
133
|
+
func trayProcCallback(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
|
134
|
+
ts := trayStateInst
|
|
135
|
+
if ts == nil {
|
|
136
|
+
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
137
|
+
return r
|
|
138
|
+
}
|
|
139
|
+
switch msg {
|
|
140
|
+
case uint32(wmApp) + 1: // uCallbackMessage
|
|
141
|
+
switch lParam & 0xFFFF {
|
|
142
|
+
case wmLButtonUp:
|
|
143
|
+
ts.emit("tray:click", map[string]interface{}{"button": "left"})
|
|
144
|
+
case wmLButtonDbl:
|
|
145
|
+
ts.emit("tray:double-click", map[string]interface{}{"button": "left"})
|
|
146
|
+
case wmRButtonUp:
|
|
147
|
+
trayShowMenu(ts)
|
|
148
|
+
}
|
|
149
|
+
case uint32(wmCommand):
|
|
150
|
+
id := uint32(wParam & 0xFFFF)
|
|
151
|
+
ts.mu.Lock()
|
|
152
|
+
label, ok := ts.items[id]
|
|
153
|
+
ts.mu.Unlock()
|
|
154
|
+
if ok {
|
|
155
|
+
ts.emit("tray:menu", map[string]interface{}{"id": label})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
159
|
+
return r
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ensureTrayWindow 创建隐藏消息窗口(仅一次)。
|
|
163
|
+
func ensureTrayWindow(a *App) (*trayState, error) {
|
|
164
|
+
trayProcMu.Lock()
|
|
165
|
+
defer trayProcMu.Unlock()
|
|
166
|
+
if trayStateInst != nil {
|
|
167
|
+
return trayStateInst, nil
|
|
168
|
+
}
|
|
169
|
+
className, _ := syscall.UTF16PtrFromString("FreedomTrayWnd")
|
|
170
|
+
if trayWndProcCB == 0 {
|
|
171
|
+
trayWndProcCB = syscall.NewCallback(trayProcCallback)
|
|
172
|
+
}
|
|
173
|
+
hInst := uintptr(0)
|
|
174
|
+
if m, _, _ := procGetModuleHandle.Call(0); m != 0 {
|
|
175
|
+
hInst = m
|
|
176
|
+
}
|
|
177
|
+
wc := wndClassExW{
|
|
178
|
+
CbSize: uint32(unsafe.Sizeof(wndClassExW{})),
|
|
179
|
+
LpfnWndProc: trayWndProcCB,
|
|
180
|
+
HInstance: hInst,
|
|
181
|
+
LpszClassName: className,
|
|
182
|
+
}
|
|
183
|
+
if r, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
|
|
184
|
+
// 可能已注册,继续尝试。
|
|
185
|
+
}
|
|
186
|
+
ts := &trayState{
|
|
187
|
+
items: map[uint32]string{},
|
|
188
|
+
nextID: 1,
|
|
189
|
+
emit: a.Emit,
|
|
190
|
+
}
|
|
191
|
+
hwnd, _, _ := procCreateWindowExW.Call(
|
|
192
|
+
0,
|
|
193
|
+
uintptr(unsafe.Pointer(className)),
|
|
194
|
+
uintptr(unsafe.Pointer(className)),
|
|
195
|
+
0,
|
|
196
|
+
0, 0, 0, 0,
|
|
197
|
+
0, 0, hInst, 0,
|
|
198
|
+
)
|
|
199
|
+
if hwnd == 0 {
|
|
200
|
+
return nil, fmt.Errorf("tray: 创建隐藏窗口失败")
|
|
201
|
+
}
|
|
202
|
+
ts.hwnd = hwnd
|
|
203
|
+
trayStateInst = ts
|
|
204
|
+
return ts, nil
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// traySetIcon 创建或更新托盘图标。
|
|
208
|
+
func traySetIcon(ts *trayState, iconData []byte, tooltip string) error {
|
|
209
|
+
ts.mu.Lock()
|
|
210
|
+
defer ts.mu.Unlock()
|
|
211
|
+
// 释放旧图标。
|
|
212
|
+
if ts.icon != 0 {
|
|
213
|
+
procDestroyIcon.Call(ts.icon)
|
|
214
|
+
ts.icon = 0
|
|
215
|
+
}
|
|
216
|
+
nid := ¬ifyIconDataW{
|
|
217
|
+
CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})),
|
|
218
|
+
HWnd: ts.hwnd,
|
|
219
|
+
UID: 1,
|
|
220
|
+
UFlags: nifMessage | nifIcon | nifTip,
|
|
221
|
+
UCallbackMessage: wmApp + 1,
|
|
222
|
+
}
|
|
223
|
+
if len(iconData) > 0 {
|
|
224
|
+
ic := hiconFromPNG(iconData)
|
|
225
|
+
if ic == 0 {
|
|
226
|
+
return fmt.Errorf("tray: 无法从图标数据创建 HICON")
|
|
227
|
+
}
|
|
228
|
+
nid.HIcon = ic
|
|
229
|
+
ts.icon = ic
|
|
230
|
+
} else {
|
|
231
|
+
// 无图标数据:使用 exe 内嵌图标。
|
|
232
|
+
cx, _, _ := procGetSystemMetrics.Call(smCXSmall)
|
|
233
|
+
cy, _, _ := procGetSystemMetrics.Call(smCYSmall)
|
|
234
|
+
if ic := loadExeIcon(cx, cy); ic != 0 {
|
|
235
|
+
nid.HIcon = ic
|
|
236
|
+
ts.icon = ic
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if tooltip != "" {
|
|
240
|
+
tp, _ := syscall.UTF16FromString(tooltip)
|
|
241
|
+
copy(nid.SzTip[:], tp[:min(len(tp), 127)])
|
|
242
|
+
}
|
|
243
|
+
// 首次 NIM_ADD,之后 NIM_MODIFY。
|
|
244
|
+
if ts.hwnd == 0 || !trayIconAdded {
|
|
245
|
+
trayIconAdded = true
|
|
246
|
+
r, _, _ := procShellNotifyIconW.Call(nimAdd, uintptr(unsafe.Pointer(nid)))
|
|
247
|
+
if r == 0 {
|
|
248
|
+
return fmt.Errorf("tray: Shell_NotifyIcon(NIM_ADD) 失败")
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
r, _, _ := procShellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(nid)))
|
|
252
|
+
if r == 0 {
|
|
253
|
+
return fmt.Errorf("tray: Shell_NotifyIcon(NIM_MODIFY) 失败")
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return nil
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var trayIconAdded bool
|
|
260
|
+
|
|
261
|
+
// trayShowMenu 在鼠标位置弹出右键菜单。
|
|
262
|
+
func trayShowMenu(ts *trayState) {
|
|
263
|
+
if ts == nil || ts.menu == 0 {
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
var pt point
|
|
267
|
+
procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
268
|
+
procSetForegroundWindow.Call(ts.hwnd)
|
|
269
|
+
procTrackPopupMenu.Call(
|
|
270
|
+
ts.menu, tpmRightButton|tpmLeftAlign,
|
|
271
|
+
uintptr(pt.X), uintptr(pt.Y), 0, ts.hwnd, 0,
|
|
272
|
+
)
|
|
273
|
+
procPostMessage.Call(ts.hwnd, wmNull, 0, 0)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
|
|
277
|
+
// 返回菜单句柄与本次分配的全部条目 id(供整体替换时清理旧映射)。
|
|
278
|
+
func buildMenu(ts *trayState, items []menuItem) (uintptr, []uint32) {
|
|
279
|
+
hMenu, _, _ := procCreatePopupMenu.Call()
|
|
280
|
+
if hMenu == 0 {
|
|
281
|
+
return 0, nil
|
|
282
|
+
}
|
|
283
|
+
var ids []uint32
|
|
284
|
+
appendMenuItems(ts, hMenu, items, false, &ids)
|
|
285
|
+
return hMenu, ids
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// appendMenuItems 把条目追加到指定菜单;新分配的 id 记入 *ids。
|
|
289
|
+
func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool, ids *[]uint32) {
|
|
290
|
+
for _, it := range items {
|
|
291
|
+
if it.Type == "separator" {
|
|
292
|
+
procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
|
|
296
|
+
label, err := syscall.UTF16PtrFromString(it.Label)
|
|
297
|
+
if err != nil {
|
|
298
|
+
continue // 标签含 NUL:跳过该条目
|
|
299
|
+
}
|
|
300
|
+
sub, subIDs := buildMenu(ts, it.Submenu)
|
|
301
|
+
if sub == 0 {
|
|
302
|
+
continue
|
|
303
|
+
}
|
|
304
|
+
*ids = append(*ids, subIDs...)
|
|
305
|
+
procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
306
|
+
continue
|
|
307
|
+
}
|
|
308
|
+
label, err := syscall.UTF16PtrFromString(it.Label)
|
|
309
|
+
if err != nil {
|
|
310
|
+
continue // 标签含 NUL:跳过该条目(id 不消耗)
|
|
311
|
+
}
|
|
312
|
+
var flags uintptr = mfString | mfEnabled
|
|
313
|
+
if it.Checked != nil && *it.Checked {
|
|
314
|
+
flags |= mfChecked
|
|
315
|
+
}
|
|
316
|
+
if it.Enabled != nil && !*it.Enabled {
|
|
317
|
+
flags |= mfGrayed
|
|
318
|
+
}
|
|
319
|
+
ts.mu.Lock()
|
|
320
|
+
id := ts.nextID
|
|
321
|
+
ts.nextID++
|
|
322
|
+
ts.items[id] = it.ID
|
|
323
|
+
ts.mu.Unlock()
|
|
324
|
+
*ids = append(*ids, id)
|
|
325
|
+
procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// traySetMenu 设置托盘右键菜单(整体替换,旧 id 映射随之清理)。
|
|
330
|
+
func traySetMenu(ts *trayState, items []menuItem) {
|
|
331
|
+
ts.mu.Lock()
|
|
332
|
+
old := ts.menu
|
|
333
|
+
oldIDs := ts.menuIDs
|
|
334
|
+
ts.menu = 0
|
|
335
|
+
ts.menuIDs = nil
|
|
336
|
+
ts.mu.Unlock()
|
|
337
|
+
if old != 0 {
|
|
338
|
+
procDestroyMenu.Call(old)
|
|
339
|
+
}
|
|
340
|
+
releaseItemIDs(ts, oldIDs)
|
|
341
|
+
if len(items) == 0 {
|
|
342
|
+
return
|
|
343
|
+
}
|
|
344
|
+
h, ids := buildMenu(ts, items)
|
|
345
|
+
ts.mu.Lock()
|
|
346
|
+
ts.menu = h
|
|
347
|
+
ts.menuIDs = ids
|
|
348
|
+
ts.mu.Unlock()
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// releaseItemIDs 从 id→前端 id 映射中移除一批条目(防重建后残留无主映射无限增长)。
|
|
352
|
+
func releaseItemIDs(ts *trayState, ids []uint32) {
|
|
353
|
+
if len(ids) == 0 {
|
|
354
|
+
return
|
|
355
|
+
}
|
|
356
|
+
ts.mu.Lock()
|
|
357
|
+
for _, id := range ids {
|
|
358
|
+
delete(ts.items, id)
|
|
359
|
+
}
|
|
360
|
+
ts.mu.Unlock()
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// menuBarSet 设置主窗口原生菜单栏(整体替换,旧 id 映射随之清理)。
|
|
364
|
+
func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
|
|
365
|
+
ts.mu.Lock()
|
|
366
|
+
old := ts.menuBar
|
|
367
|
+
oldIDs := ts.menuBarIDs
|
|
368
|
+
ts.menuBar = 0
|
|
369
|
+
ts.menuBarIDs = nil
|
|
370
|
+
ts.mu.Unlock()
|
|
371
|
+
if old != 0 {
|
|
372
|
+
procDestroyMenu.Call(old)
|
|
373
|
+
}
|
|
374
|
+
releaseItemIDs(ts, oldIDs)
|
|
375
|
+
if len(items) == 0 {
|
|
376
|
+
procSetMenu.Call(hwnd, 0)
|
|
377
|
+
procDrawMenuBar.Call(hwnd)
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
hBar, _, _ := procCreateMenu.Call()
|
|
381
|
+
if hBar == 0 {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
var ids []uint32
|
|
385
|
+
for _, top := range items {
|
|
386
|
+
if top.Type == "separator" {
|
|
387
|
+
continue
|
|
388
|
+
}
|
|
389
|
+
label, err := syscall.UTF16PtrFromString(top.Label)
|
|
390
|
+
if err != nil {
|
|
391
|
+
continue // 标签含 NUL:跳过该条目
|
|
392
|
+
}
|
|
393
|
+
if len(top.Submenu) > 0 {
|
|
394
|
+
sub, subIDs := buildMenu(ts, top.Submenu)
|
|
395
|
+
if sub == 0 {
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
ids = append(ids, subIDs...)
|
|
399
|
+
procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
400
|
+
} else {
|
|
401
|
+
// 顶层无子菜单:视为可点击菜单项。
|
|
402
|
+
var flags uintptr = mfString | mfEnabled
|
|
403
|
+
if top.Checked != nil && *top.Checked {
|
|
404
|
+
flags |= mfChecked
|
|
405
|
+
}
|
|
406
|
+
ts.mu.Lock()
|
|
407
|
+
id := ts.nextID
|
|
408
|
+
ts.nextID++
|
|
409
|
+
ts.items[id] = top.ID
|
|
410
|
+
ts.mu.Unlock()
|
|
411
|
+
ids = append(ids, id)
|
|
412
|
+
procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
ts.mu.Lock()
|
|
416
|
+
ts.menuBar = hBar
|
|
417
|
+
ts.menuBarIDs = ids
|
|
418
|
+
ts.mu.Unlock()
|
|
419
|
+
procSetMenu.Call(hwnd, hBar)
|
|
420
|
+
procDrawMenuBar.Call(hwnd)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// trayDestroy 移除托盘图标并清理隐藏窗口。
|
|
424
|
+
func trayDestroy(ts *trayState) {
|
|
425
|
+
if ts == nil {
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
ts.mu.Lock()
|
|
429
|
+
defer ts.mu.Unlock()
|
|
430
|
+
if trayIconAdded {
|
|
431
|
+
nid := ¬ifyIconDataW{CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})), HWnd: ts.hwnd, UID: 1}
|
|
432
|
+
procShellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(nid)))
|
|
433
|
+
trayIconAdded = false
|
|
434
|
+
}
|
|
435
|
+
if ts.menu != 0 {
|
|
436
|
+
procDestroyMenu.Call(ts.menu)
|
|
437
|
+
ts.menu = 0
|
|
438
|
+
}
|
|
439
|
+
if ts.menuBar != 0 {
|
|
440
|
+
procDestroyMenu.Call(ts.menuBar)
|
|
441
|
+
ts.menuBar = 0
|
|
442
|
+
}
|
|
443
|
+
ts.menuIDs = nil
|
|
444
|
+
ts.menuBarIDs = nil
|
|
445
|
+
ts.items = map[uint32]string{} // 已持锁:直接重建映射,不留无主条目
|
|
446
|
+
if ts.icon != 0 {
|
|
447
|
+
procDestroyIcon.Call(ts.icon)
|
|
448
|
+
ts.icon = 0
|
|
449
|
+
}
|
|
450
|
+
if ts.hwnd != 0 {
|
|
451
|
+
procDestroyWindow.Call(ts.hwnd)
|
|
452
|
+
ts.hwnd = 0
|
|
453
|
+
}
|
|
454
|
+
trayStateInst = nil
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
var procDestroyWindow = user32win.NewProc("DestroyWindow")
|
|
458
|
+
|
|
459
|
+
// trayCall 处理前端 __freedom_tray 请求。
|
|
460
|
+
func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
|
|
461
|
+
defer func() {
|
|
462
|
+
if r := recover(); r != nil {
|
|
463
|
+
result = nil
|
|
464
|
+
err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
|
|
465
|
+
}
|
|
466
|
+
}()
|
|
467
|
+
|
|
468
|
+
var args map[string]json.RawMessage
|
|
469
|
+
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
470
|
+
if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
|
|
471
|
+
return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
argStr := func(k string) string {
|
|
475
|
+
if v, ok := args[k]; ok {
|
|
476
|
+
var s string
|
|
477
|
+
if json.Unmarshal(v, &s) == nil {
|
|
478
|
+
return s
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return ""
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
switch method {
|
|
485
|
+
case "tray.create":
|
|
486
|
+
ts, err := ensureTrayWindow(a)
|
|
487
|
+
if err != nil {
|
|
488
|
+
return nil, err
|
|
489
|
+
}
|
|
490
|
+
return nil, traySetIcon(ts, dataURLToBytes(argStr("icon")), argStr("tooltip"))
|
|
491
|
+
case "tray.destroy":
|
|
492
|
+
trayDestroy(trayStateInst)
|
|
493
|
+
return nil, nil
|
|
494
|
+
case "tray.tooltip":
|
|
495
|
+
ts := trayStateInst
|
|
496
|
+
if ts == nil {
|
|
497
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
498
|
+
}
|
|
499
|
+
return nil, traySetIcon(ts, nil, argStr("tooltip"))
|
|
500
|
+
case "tray.menu":
|
|
501
|
+
ts := trayStateInst
|
|
502
|
+
if ts == nil {
|
|
503
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
504
|
+
}
|
|
505
|
+
var items []menuItem
|
|
506
|
+
if v, ok := args["items"]; ok {
|
|
507
|
+
if err := json.Unmarshal(v, &items); err != nil {
|
|
508
|
+
return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
traySetMenu(ts, items)
|
|
512
|
+
return nil, nil
|
|
513
|
+
case "menu.set":
|
|
514
|
+
ts := trayStateInst
|
|
515
|
+
if ts == nil {
|
|
516
|
+
ts, err = ensureTrayWindow(a)
|
|
517
|
+
if err != nil {
|
|
518
|
+
return nil, err
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
var items []menuItem
|
|
522
|
+
if v, ok := args["items"]; ok {
|
|
523
|
+
if err := json.Unmarshal(v, &items); err != nil {
|
|
524
|
+
return nil, fmt.Errorf("menu: 菜单格式错误: %w", err)
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
hwnd := a.WindowHandle()
|
|
528
|
+
if hwnd == 0 {
|
|
529
|
+
return nil, fmt.Errorf("menu: 窗口未就绪")
|
|
530
|
+
}
|
|
531
|
+
menuBarSet(hwnd, ts, items)
|
|
532
|
+
return nil, nil
|
|
533
|
+
default:
|
|
534
|
+
return nil, fmt.Errorf("freedom: unknown tray method %q", method)
|
|
535
|
+
}
|
|
536
|
+
}
|