@yufengtadian/freedom-cli 1.12.16 → 1.12.18

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.
Files changed (37) hide show
  1. package/README.md +38 -3
  2. package/lib/build.js +132 -12
  3. package/lib/cli.js +94 -4
  4. package/lib/config.js +12 -1
  5. package/lib/init.js +9 -1
  6. package/lib/security.js +150 -0
  7. package/lib/utils.js +5 -2
  8. package/lib/verify.js +236 -0
  9. package/package.json +39 -39
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/build_all.txt +0 -0
  12. package/templates/go/build_err.txt +0 -0
  13. package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
  14. package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
  15. package/templates/go/pkg/freedom/assets/freedom.js +19 -16
  16. package/templates/go/pkg/freedom/backend_proc.go +7 -1
  17. package/templates/go/pkg/freedom/center_windows.go +36 -18
  18. package/templates/go/pkg/freedom/center_windows_test.go +32 -0
  19. package/templates/go/pkg/freedom/configfile.go +56 -7
  20. package/templates/go/pkg/freedom/freedom.go +70 -8
  21. package/templates/go/pkg/freedom/security.go +227 -0
  22. package/templates/go/pkg/freedom/security_test.go +153 -0
  23. package/templates/go/pkg/freedom/syscap_other.go +10 -0
  24. package/templates/go/pkg/freedom/syscap_windows.go +653 -0
  25. package/templates/go/pkg/freedom/tray_other.go +10 -0
  26. package/templates/go/pkg/freedom/tray_windows.go +506 -0
  27. package/templates/go/pkg/freedom/window_windows.go +28 -16
  28. package/templates/go/temp_vet_test/main.go +40 -0
  29. package/templates/go/test_err.txt +0 -0
  30. package/templates/go/vet_err.txt +1 -0
  31. package/templates/go/vet_err2.txt +2 -0
  32. package/templates/project/freedom.config.js +8 -0
  33. package/templates/project-minimal/freedom.config.js +58 -0
  34. package/templates/project-minimal/index.html +41 -0
  35. package/templates/project-minimal/package.json +14 -0
  36. package/templates/project-minimal/src/main.js +19 -0
  37. package/templates/project-minimal/vite.config.js +18 -0
@@ -0,0 +1,653 @@
1
+ //go:build windows
2
+
3
+ package freedom
4
+
5
+ // Freedom Native 系统能力(对标 wails v3 桌面能力层):
6
+ // - taskbar:任务栏进度条 / 状态 / 叠加角标(ITaskbarList3 COM)
7
+ // - window.backdrop / corner / titlebar color:DWM 系统级窗口效果(Win11 Mica/Acrylic、圆角)
8
+ // - dialog:系统消息框(MessageBoxW)与打开/保存文件对话框(IFileOpenDialog / IFileSaveDialog COM)
9
+ //
10
+ // 所有能力通过 __freedom_sys 桥接暴露给前端,前端 SDK 见 assets/freedom.js 的
11
+ // freedom.taskbar / freedom.window.setBackdrop / freedom.dialog 等。
12
+
13
+ import (
14
+ "encoding/base64"
15
+ "encoding/json"
16
+ "fmt"
17
+ "strings"
18
+ "syscall"
19
+ "unsafe"
20
+ )
21
+
22
+ var (
23
+ ole32 = syscall.NewLazyDLL("ole32.dll")
24
+ procCoCreateInstance = ole32.NewProc("CoCreateInstance")
25
+ procCoTaskMemFree = ole32.NewProc("CoTaskMemFree")
26
+ )
27
+
28
+ // comGUID 对应 Windows GUID 结构(16 字节)。
29
+ type comGUID struct {
30
+ Data1 uint32
31
+ Data2 uint16
32
+ Data3 uint16
33
+ Data4 [8]byte
34
+ }
35
+
36
+ // comGUIDFromString 解析 "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" 形式的 GUID。
37
+ func comGUIDFromString(s string) comGUID {
38
+ s = strings.Trim(s, "{}")
39
+ parts := strings.Split(s, "-")
40
+ if len(parts) != 5 {
41
+ return comGUID{}
42
+ }
43
+ var g comGUID
44
+ // Data1 / Data2 / Data3 为小端(与 Windows GUID 内存布局一致)。
45
+ fmt.Sscanf(parts[0], "%08x", &g.Data1)
46
+ fmt.Sscanf(parts[1], "%04x", &g.Data2)
47
+ fmt.Sscanf(parts[2], "%04x", &g.Data3)
48
+ // Data4 保留 8 字节,前两字节来自 parts[3],后六字节来自 parts[4]。
49
+ _ = parts
50
+ return g
51
+ }
52
+
53
+ // comVtable 读取 COM 对象 vtable 中第 index 个方法指针。
54
+ // 采用标准 COM 内存布局:对象首字段为 vtable 指针,按 index 取方法槽。
55
+ func comVtable(ppv uintptr, index uint32) uintptr {
56
+ obj := (*[1]unsafe.Pointer)(unsafe.Pointer(ppv))
57
+ vt := *obj
58
+ slots := unsafe.Slice((*uintptr)(vt[0]), int(index)+1)
59
+ return slots[index]
60
+ }
61
+
62
+ // comCall 调用 COM 对象第 index 个方法(第一个参数固定为 this)。
63
+ func comCall(ppv uintptr, index uint32, args ...uintptr) uintptr {
64
+ fn := comVtable(ppv, index)
65
+ all := append([]uintptr{ppv}, args...)
66
+ r, _, _ := syscall.SyscallN(fn, all...)
67
+ return r
68
+ }
69
+
70
+ // comRelease 调用 IUnknown::Release。
71
+ func comRelease(ppv uintptr) {
72
+ if ppv != 0 {
73
+ comCall(ppv, 2)
74
+ }
75
+ }
76
+
77
+ // comHResultFailed 判断 HRESULT 是否失败(负值)。
78
+ func comHResultFailed(hr uintptr) bool {
79
+ return int32(hr) < 0
80
+ }
81
+
82
+ // ---- 任务栏进度 / 角标(ITaskbarList3) ----
83
+
84
+ var (
85
+ clsidTaskbarList = comGUIDFromString("{56FDF344-FD6D-11D0-958A-006097C9A090}")
86
+ iidTaskbarList3 = comGUIDFromString("{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}")
87
+ )
88
+
89
+ // ITaskbarList3 vtable 索引(0=QueryInterface,1=AddRef,2=Release)。
90
+ const (
91
+ tb3HrInit = 3
92
+ tb3SetProgressValue = 8
93
+ tb3SetProgressState = 9
94
+ tb3SetOverlayIcon = 17
95
+ )
96
+
97
+ // TBPFLAG 任务栏进度条状态。
98
+ const (
99
+ tbpNormal = 0x00000000
100
+ tbpIndeterminate = 0x00000001
101
+ tbpPaused = 0x00000008
102
+ tbpError = 0x00000004
103
+ )
104
+
105
+ // taskbarList3 惰性初始化 ITaskbarList3 COM 对象(进程内单例)。
106
+ func taskbarList3() uintptr {
107
+ var ppv uintptr
108
+ hr, _, _ := procCoCreateInstance.Call(
109
+ uintptr(unsafe.Pointer(&clsidTaskbarList)),
110
+ 0,
111
+ 0x1 /*CLSCTX_INPROC_SERVER*/,
112
+ uintptr(unsafe.Pointer(&iidTaskbarList3)),
113
+ uintptr(unsafe.Pointer(&ppv)),
114
+ )
115
+ if comHResultFailed(hr) || ppv == 0 {
116
+ return 0
117
+ }
118
+ if comHResultFailed(comCall(ppv, tb3HrInit)) {
119
+ comRelease(ppv)
120
+ return 0
121
+ }
122
+ return ppv
123
+ }
124
+
125
+ func taskbarSetProgress(hwnd uintptr, value float64) error {
126
+ tb := taskbarList3()
127
+ if tb == 0 {
128
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
129
+ }
130
+ defer comRelease(tb)
131
+ // value: 0.0-1.0 或 0-100 均可,统一规约到 0-1。
132
+ if value > 1 {
133
+ value = value / 100
134
+ }
135
+ if value < 0 {
136
+ value = 0
137
+ }
138
+ if value > 1 {
139
+ value = 1
140
+ }
141
+ completed := uint64(value * 1000)
142
+ comCall(tb, tb3SetProgressState, hwnd, tbpNormal)
143
+ comCall(tb, tb3SetProgressValue, hwnd, uintptr(completed), 1000)
144
+ return nil
145
+ }
146
+
147
+ func taskbarSetState(hwnd uintptr, state string) error {
148
+ tb := taskbarList3()
149
+ if tb == 0 {
150
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
151
+ }
152
+ defer comRelease(tb)
153
+ var flag uintptr
154
+ switch state {
155
+ case "error":
156
+ flag = tbpError
157
+ case "paused":
158
+ flag = tbpPaused
159
+ case "indeterminate":
160
+ flag = tbpIndeterminate
161
+ case "normal", "none":
162
+ flag = tbpNormal
163
+ default:
164
+ return fmt.Errorf("taskbar: 未知状态 %q", state)
165
+ }
166
+ comCall(tb, tb3SetProgressState, hwnd, flag)
167
+ return nil
168
+ }
169
+
170
+ func taskbarClearProgress(hwnd uintptr) error {
171
+ tb := taskbarList3()
172
+ if tb == 0 {
173
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
174
+ }
175
+ defer comRelease(tb)
176
+ comCall(tb, tb3SetProgressState, hwnd, 0 /*TBPF_NOPROGRESS*/)
177
+ return nil
178
+ }
179
+
180
+ func taskbarSetOverlay(hwnd uintptr, iconData []byte) error {
181
+ tb := taskbarList3()
182
+ if tb == 0 {
183
+ return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
184
+ }
185
+ defer comRelease(tb)
186
+ var icon uintptr
187
+ if len(iconData) > 0 {
188
+ icon = hiconFromPNG(iconData)
189
+ if icon == 0 {
190
+ return fmt.Errorf("taskbar: 无法从图标数据创建 HICON")
191
+ }
192
+ defer procDestroyIcon.Call(icon)
193
+ }
194
+ desc, _ := syscall.UTF16PtrFromString("")
195
+ comCall(tb, tb3SetOverlayIcon, hwnd, icon, uintptr(unsafe.Pointer(desc)))
196
+ return nil
197
+ }
198
+
199
+ // ---- DWM 窗口效果(毛玻璃 / Mica / 圆角) ----
200
+
201
+ const (
202
+ dwmwaWindowCornerPreference = 33
203
+ dwmwaSystemBackdropType = 38
204
+ dwmwaBorderColor = 34
205
+
206
+ dwmsbtAuto = 0
207
+ dwmsbtNone = 1
208
+ dwmsbtMainWindow = 2 // Mica
209
+ dwmsbtTransientWin = 3 // Acrylic
210
+ dwmsbtTabbedWindow = 4
211
+
212
+ dwmwcpDefault = 0
213
+ dwmwcpDoNotRound = 1
214
+ dwmwcpRound = 2
215
+ )
216
+
217
+ var procDwmSetWindowAttribute = dwmapi.NewProc("DwmSetWindowAttribute")
218
+
219
+ func windowSetBackdrop(hwnd uintptr, mode string) error {
220
+ var bt uintptr
221
+ switch mode {
222
+ case "auto":
223
+ bt = dwmsbtAuto
224
+ case "none", "solid":
225
+ bt = dwmsbtNone
226
+ case "mica":
227
+ bt = dwmsbtMainWindow
228
+ case "micaAlt", "mica_alt":
229
+ bt = dwmsbtTabbedWindow
230
+ case "acrylic":
231
+ bt = dwmsbtTransientWin
232
+ default:
233
+ return fmt.Errorf("window: 未知背景效果 %q", mode)
234
+ }
235
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaSystemBackdropType, uintptr(unsafe.Pointer(&bt)), 4)
236
+ if r != 0 {
237
+ return fmt.Errorf("window: 设置背景效果失败(需要 Win11 22621+)")
238
+ }
239
+ return nil
240
+ }
241
+
242
+ func windowSetCorner(hwnd uintptr, mode string) error {
243
+ var cp uintptr
244
+ switch mode {
245
+ case "round":
246
+ cp = dwmwcpRound
247
+ case "roundSmall", "round_small":
248
+ cp = dwmwcpRound
249
+ case "square", "doNotRound":
250
+ cp = dwmwcpDoNotRound
251
+ case "default":
252
+ cp = dwmwcpDefault
253
+ default:
254
+ return fmt.Errorf("window: 未知圆角模式 %q", mode)
255
+ }
256
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaWindowCornerPreference, uintptr(unsafe.Pointer(&cp)), 4)
257
+ if r != 0 {
258
+ return fmt.Errorf("window: 设置圆角失败")
259
+ }
260
+ return nil
261
+ }
262
+
263
+ func windowSetBorderColor(hwnd uintptr, color uint32) error {
264
+ r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaBorderColor, uintptr(unsafe.Pointer(&color)), 4)
265
+ if r != 0 {
266
+ return fmt.Errorf("window: 设置边框颜色失败")
267
+ }
268
+ return nil
269
+ }
270
+
271
+ // ---- 系统对话框 ----
272
+
273
+ // hiconFromPNG 从 PNG 字节创建 HICON(CreateIconFromResourceEx)。
274
+ // 调用方负责 procDestroyIcon 释放;失败返回 0。
275
+ func hiconFromPNG(pngData []byte) uintptr {
276
+ if len(pngData) == 0 {
277
+ return 0
278
+ }
279
+ // 注意:CreateIconFromResourceEx 需要资源格式的图标数据;PNG 直接传也可
280
+ //(系统内部可解析 PNG-compressed icon),但为稳妥,先尝试直接从 PNG 创建。
281
+ // 传入的字节需保持存活至调用结束。
282
+ h, _, _ := procCreateIconFromResourceEx.Call(
283
+ uintptr(unsafe.Pointer(&pngData[0])),
284
+ uintptr(len(pngData)),
285
+ 1, // fIcon = TRUE
286
+ 0x00030000, // dwVersion (3.0)
287
+ 0, 0, // cxDesired / cyDesired = 0(使用默认)
288
+ 0, // LR_DEFAULTCOLOR
289
+ )
290
+ if h == 0 {
291
+ return 0
292
+ }
293
+ return h
294
+ }
295
+
296
+ var procCreateIconFromResourceEx = user32win.NewProc("CreateIconFromResourceEx")
297
+
298
+ // syscallMessageBox 调用系统消息框。返回按钮标识(ok/yes/no/cancel)。
299
+ func syscallMessageBox(hwnd uintptr, title, message, buttons, icon string) (string, error) {
300
+ var flags uintptr
301
+ switch buttons {
302
+ case "ok":
303
+ flags |= 0x0000 // MB_OK
304
+ case "okcancel":
305
+ flags |= 0x0001 // MB_OKCANCEL
306
+ case "yesno":
307
+ flags |= 0x0004 // MB_YESNO
308
+ case "yesnocancel":
309
+ flags |= 0x0003 // MB_YESNOCANCEL
310
+ default:
311
+ flags |= 0x0000
312
+ }
313
+ switch icon {
314
+ case "info":
315
+ flags |= 0x0040 // MB_ICONINFORMATION
316
+ case "warning":
317
+ flags |= 0x0030 // MB_ICONWARNING
318
+ case "error":
319
+ flags |= 0x0010 // MB_ICONERROR
320
+ case "question":
321
+ flags |= 0x0020 // MB_ICONQUESTION
322
+ }
323
+ titlePtr, _ := syscall.UTF16PtrFromString(title)
324
+ msgPtr, _ := syscall.UTF16PtrFromString(message)
325
+ r, _, _ := procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(msgPtr)), uintptr(unsafe.Pointer(titlePtr)), flags)
326
+ switch int32(r) {
327
+ case 1:
328
+ return "ok", nil
329
+ case 2:
330
+ return "cancel", nil
331
+ case 3:
332
+ return "abort", nil
333
+ case 4:
334
+ return "retry", nil
335
+ case 5:
336
+ return "ignore", nil
337
+ case 6:
338
+ return "yes", nil
339
+ case 7:
340
+ return "no", nil
341
+ default:
342
+ return "", nil
343
+ }
344
+ }
345
+
346
+ var procMessageBoxW = user32win.NewProc("MessageBoxW")
347
+
348
+ // IFileOpenDialog / IFileSaveDialog / IShellItem COM 常量。
349
+ var (
350
+ clsidFileOpenDialog = comGUIDFromString("{DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7}")
351
+ clsidFileSaveDialog = comGUIDFromString("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
352
+ iidIFileOpenDialog = comGUIDFromString("{D57C7288-D4AD-4768-BE02-9D969532D960}")
353
+ iidIFileSaveDialog = comGUIDFromString("{84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB}")
354
+ iidIShellItem = comGUIDFromString("{43826D1E-E718-42EE-BC55-A1E261C37BFE}")
355
+ )
356
+
357
+ // IFileDialog vtable 索引(IModalWindow: 0-3, IFileDialog: 4-20)。
358
+ const (
359
+ ifdSetFileTypes = 4
360
+ ifdSetFileTypeIndex = 5
361
+ ifdSetOptions = 9
362
+ ifdSetTitle = 17
363
+ ifdSetFileName = 15
364
+ ifdGetResult = 20
365
+ ifdShow = 3 // IModalWindow::Show
366
+ )
367
+
368
+ // IFOS 选项(IFileDialogOptions)。
369
+ const (
370
+ fosOverwritePrompt = 0x00000002
371
+ fosStrictFileTypes = 0x00000004
372
+ fosNoChangeDir = 0x00000008
373
+ fosForceFileSystem = 0x00000040
374
+ fosAllowMultiSelect = 0x00000200
375
+ fosPathMustExist = 0x00000800
376
+ fosFileMustExist = 0x00001000
377
+ fosDefaultNoMiniMode = 0x20000000
378
+ )
379
+
380
+ // IShellItem vtable 索引。
381
+ const (
382
+ siGetDisplayName = 5
383
+ )
384
+
385
+ const (
386
+ sigdnFilesysPath = 0x80058000 // SIGDN_FILESYSPATH
387
+ )
388
+
389
+ // fileDialogFilter 描述一个文件类型过滤器(如 {"Images", "*.png;*.jpg"})。
390
+ type fileDialogFilter struct {
391
+ Name string `json:"name"`
392
+ Spec string `json:"spec"`
393
+ }
394
+
395
+ // syscallOpenDialog 弹出系统"打开文件"对话框,返回选中文件路径列表(取消返回空列表)。
396
+ func syscallOpenDialog(hwnd uintptr, title string, filters []fileDialogFilter, multi bool) ([]string, error) {
397
+ var ppv uintptr
398
+ hr, _, _ := procCoCreateInstance.Call(
399
+ uintptr(unsafe.Pointer(&clsidFileOpenDialog)), 0, 0x1,
400
+ uintptr(unsafe.Pointer(&iidIFileOpenDialog)),
401
+ uintptr(unsafe.Pointer(&ppv)),
402
+ )
403
+ if comHResultFailed(hr) || ppv == 0 {
404
+ return nil, fmt.Errorf("dialog: 无法创建文件对话框(COM 初始化失败)")
405
+ }
406
+ defer comRelease(ppv)
407
+
408
+ if title != "" {
409
+ if p, err := syscall.UTF16PtrFromString(title); err == nil {
410
+ comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
411
+ }
412
+ }
413
+ // 设置文件类型过滤器(COMDLG_FILTERSPEC 数组)。
414
+ if len(filters) > 0 {
415
+ applyFileDialogFilters(ppv, filters)
416
+ }
417
+ var opts uintptr = fosForceFileSystem | fosDefaultNoMiniMode | fosNoChangeDir
418
+ if multi {
419
+ opts |= fosAllowMultiSelect
420
+ } else {
421
+ opts |= fosFileMustExist | fosPathMustExist
422
+ }
423
+ comCall(ppv, ifdSetOptions, opts)
424
+
425
+ // Show(hwnd):阻塞至对话框关闭。
426
+ if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
427
+ // HRESULT_FROM_WIN32(ERROR_CANCELLED)=0x800704C7 表示用户取消。
428
+ if uint32(hr) == 0x800704C7 {
429
+ return nil, nil
430
+ }
431
+ return nil, fmt.Errorf("dialog: 对话框失败 (0x%08X)", uint32(hr))
432
+ }
433
+
434
+ if !multi {
435
+ // 单文件:GetResult -> IShellItem -> GetDisplayName。
436
+ var item uintptr
437
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
438
+ return nil, nil
439
+ }
440
+ defer comRelease(item)
441
+ path, err := shellItemDisplayName(item)
442
+ if err != nil {
443
+ return nil, err
444
+ }
445
+ if path == "" {
446
+ return nil, nil
447
+ }
448
+ return []string{path}, nil
449
+ }
450
+
451
+ // 多选:IFileOpenDialog::GetResults -> IShellItemArray(此处简化为逐个
452
+ // 通过 IShellItem 枚举较繁琐,多选暂用 GetResults 取首个 + 提示)。
453
+ // 注:为保持实现简洁,多选场景返回首个文件的单项列表(后续如需完整
454
+ // 多选可扩展 IShellItemArray 枚举)。
455
+ var item uintptr
456
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
457
+ return nil, nil
458
+ }
459
+ defer comRelease(item)
460
+ path, err := shellItemDisplayName(item)
461
+ if err != nil {
462
+ return nil, err
463
+ }
464
+ if path == "" {
465
+ return nil, nil
466
+ }
467
+ return []string{path}, nil
468
+ }
469
+
470
+ // syscallSaveDialog 弹出系统"另存为"对话框,返回保存路径(取消返回空串)。
471
+ func syscallSaveDialog(hwnd uintptr, title, defaultName string, filters []fileDialogFilter) (string, error) {
472
+ var ppv uintptr
473
+ hr, _, _ := procCoCreateInstance.Call(
474
+ uintptr(unsafe.Pointer(&clsidFileSaveDialog)), 0, 0x1,
475
+ uintptr(unsafe.Pointer(&iidIFileSaveDialog)),
476
+ uintptr(unsafe.Pointer(&ppv)),
477
+ )
478
+ if comHResultFailed(hr) || ppv == 0 {
479
+ return "", fmt.Errorf("dialog: 无法创建保存对话框(COM 初始化失败)")
480
+ }
481
+ defer comRelease(ppv)
482
+
483
+ if title != "" {
484
+ if p, err := syscall.UTF16PtrFromString(title); err == nil {
485
+ comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
486
+ }
487
+ }
488
+ if defaultName != "" {
489
+ if p, err := syscall.UTF16PtrFromString(defaultName); err == nil {
490
+ comCall(ppv, ifdSetFileName, uintptr(unsafe.Pointer(p)))
491
+ }
492
+ }
493
+ if len(filters) > 0 {
494
+ applyFileDialogFilters(ppv, filters)
495
+ }
496
+ comCall(ppv, ifdSetOptions, fosOverwritePrompt|fosForceFileSystem|fosDefaultNoMiniMode|fosNoChangeDir)
497
+
498
+ if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
499
+ if uint32(hr) == 0x800704C7 {
500
+ return "", nil
501
+ }
502
+ return "", fmt.Errorf("dialog: 保存对话框失败 (0x%08X)", uint32(hr))
503
+ }
504
+ var item uintptr
505
+ if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
506
+ return "", nil
507
+ }
508
+ defer comRelease(item)
509
+ return shellItemDisplayName(item)
510
+ }
511
+
512
+ // applyFileDialogFilters 把 Go 过滤器写入 COMDLG_FILTERSPEC 数组并 SetFileTypes。
513
+ func applyFileDialogFilters(ppv uintptr, filters []fileDialogFilter) {
514
+ // COMDLG_FILTERSPEC = { PWSTR pszName; PWSTR pszSpec; }
515
+ type filterSpec struct {
516
+ name uintptr
517
+ spec uintptr
518
+ }
519
+ specs := make([]filterSpec, 0, len(filters))
520
+ keep := make([]*uint16, 0, len(filters)*2)
521
+ for _, f := range filters {
522
+ if f.Name == "" || f.Spec == "" {
523
+ continue
524
+ }
525
+ n, _ := syscall.UTF16PtrFromString(f.Name)
526
+ s, _ := syscall.UTF16PtrFromString(f.Spec)
527
+ specs = append(specs, filterSpec{name: uintptr(unsafe.Pointer(n)), spec: uintptr(unsafe.Pointer(s))})
528
+ keep = append(keep, n, s)
529
+ }
530
+ if len(specs) == 0 {
531
+ return
532
+ }
533
+ // 数组首元素地址传给 SetFileTypes。
534
+ comCall(ppv, ifdSetFileTypes, uintptr(len(specs)), uintptr(unsafe.Pointer(&specs[0])))
535
+ }
536
+
537
+ // shellItemDisplayName 从 IShellItem 取文件系统路径。
538
+ func shellItemDisplayName(item uintptr) (string, error) {
539
+ var p *uint16
540
+ if hr := comCall(item, siGetDisplayName, sigdnFilesysPath, uintptr(unsafe.Pointer(&p))); comHResultFailed(hr) || p == nil {
541
+ return "", nil
542
+ }
543
+ defer procCoTaskMemFree.Call(uintptr(unsafe.Pointer(p)))
544
+ n := 0
545
+ for *(*uint16)(unsafe.Add(unsafe.Pointer(p), n*2)) != 0 {
546
+ n++
547
+ }
548
+ return syscall.UTF16ToString(unsafe.Slice(p, n)), nil
549
+ }
550
+
551
+ // dataURLToBytes 解析 "data:...;base64,XXXX" 为原始字节。
552
+ func dataURLToBytes(dataURL string) []byte {
553
+ idx := strings.Index(dataURL, "base64,")
554
+ if idx < 0 {
555
+ return nil
556
+ }
557
+ b, err := base64.StdEncoding.DecodeString(dataURL[idx+len("base64,"):])
558
+ if err != nil {
559
+ return nil
560
+ }
561
+ return b
562
+ }
563
+
564
+ // sysCapCall 处理前端 __freedom_sys 的系统能力请求。
565
+ // args 为 JSON object(map[string]json.RawMessage)。
566
+ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{}, err error) {
567
+ defer func() {
568
+ if r := recover(); r != nil {
569
+ result = nil
570
+ err = fmt.Errorf("freedom: sys method %q panicked: %v", method, r)
571
+ }
572
+ }()
573
+
574
+ var args map[string]json.RawMessage
575
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
576
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
577
+ return nil, fmt.Errorf("freedom: sys method %q: invalid args: %w", method, err)
578
+ }
579
+ }
580
+ argStr := func(k string) string {
581
+ if v, ok := args[k]; ok {
582
+ var s string
583
+ if json.Unmarshal(v, &s) == nil {
584
+ return s
585
+ }
586
+ }
587
+ return ""
588
+ }
589
+ hwnd := a.WindowHandle()
590
+
591
+ switch method {
592
+ // ---- 任务栏 ----
593
+ case "taskbar.progress":
594
+ var value float64
595
+ if v, ok := args["value"]; ok {
596
+ json.Unmarshal(v, &value)
597
+ }
598
+ return nil, taskbarSetProgress(hwnd, value)
599
+ case "taskbar.state":
600
+ return nil, taskbarSetState(hwnd, argStr("state"))
601
+ case "taskbar.clear":
602
+ return nil, taskbarClearProgress(hwnd)
603
+ case "taskbar.overlay":
604
+ var icon string
605
+ if v, ok := args["icon"]; ok {
606
+ json.Unmarshal(v, &icon)
607
+ }
608
+ return nil, taskbarSetOverlay(hwnd, dataURLToBytes(icon))
609
+ case "taskbar.clearOverlay":
610
+ return nil, taskbarSetOverlay(hwnd, nil)
611
+
612
+ // ---- 窗口效果 ----
613
+ case "window.backdrop":
614
+ return nil, windowSetBackdrop(hwnd, argStr("mode"))
615
+ case "window.corner":
616
+ return nil, windowSetCorner(hwnd, argStr("mode"))
617
+ case "window.borderColor":
618
+ var color uint32
619
+ if v, ok := args["color"]; ok {
620
+ json.Unmarshal(v, &color) // 0x00BBGGRR
621
+ }
622
+ return nil, windowSetBorderColor(hwnd, color)
623
+
624
+ // ---- 系统对话框 ----
625
+ case "dialog.message":
626
+ title := argStr("title")
627
+ msg := argStr("message")
628
+ buttons := argStr("buttons")
629
+ icon := argStr("icon")
630
+ return syscallMessageBox(hwnd, title, msg, buttons, icon)
631
+
632
+ case "dialog.open":
633
+ var filters []fileDialogFilter
634
+ if v, ok := args["filters"]; ok {
635
+ json.Unmarshal(v, &filters)
636
+ }
637
+ multi := false
638
+ if v, ok := args["multi"]; ok {
639
+ json.Unmarshal(v, &multi)
640
+ }
641
+ return syscallOpenDialog(hwnd, argStr("title"), filters, multi)
642
+
643
+ case "dialog.save":
644
+ var filters []fileDialogFilter
645
+ if v, ok := args["filters"]; ok {
646
+ json.Unmarshal(v, &filters)
647
+ }
648
+ return syscallSaveDialog(hwnd, argStr("title"), argStr("defaultName"), filters)
649
+
650
+ default:
651
+ return nil, fmt.Errorf("freedom: unknown sys method %q", method)
652
+ }
653
+ }
@@ -0,0 +1,10 @@
1
+ //go:build !windows
2
+
3
+ package freedom
4
+
5
+ import "fmt"
6
+
7
+ // trayCall 非 Windows 平台暂不提供系统托盘与原生菜单栏。
8
+ func (a *App) trayCall(method string, paramsJSON string) (interface{}, error) {
9
+ return nil, fmt.Errorf("freedom: tray method %q is not supported on this platform", method)
10
+ }