@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.
- package/README.md +38 -3
- package/lib/build.js +132 -12
- package/lib/cli.js +94 -4
- package/lib/config.js +12 -1
- package/lib/init.js +9 -1
- package/lib/security.js +150 -0
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +39 -39
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
- package/templates/go/pkg/freedom/assets/freedom.js +19 -16
- package/templates/go/pkg/freedom/backend_proc.go +7 -1
- package/templates/go/pkg/freedom/center_windows.go +36 -18
- package/templates/go/pkg/freedom/center_windows_test.go +32 -0
- package/templates/go/pkg/freedom/configfile.go +56 -7
- package/templates/go/pkg/freedom/freedom.go +70 -8
- package/templates/go/pkg/freedom/security.go +227 -0
- package/templates/go/pkg/freedom/security_test.go +153 -0
- package/templates/go/pkg/freedom/syscap_other.go +10 -0
- package/templates/go/pkg/freedom/syscap_windows.go +653 -0
- package/templates/go/pkg/freedom/tray_other.go +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +506 -0
- package/templates/go/pkg/freedom/window_windows.go +28 -16
- package/templates/go/temp_vet_test/main.go +40 -0
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +1 -0
- package/templates/go/vet_err2.txt +2 -0
- package/templates/project/freedom.config.js +8 -0
- 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
|
@@ -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
|
+
}
|