@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,845 @@
|
|
|
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
|
+
"bytes"
|
|
15
|
+
"encoding/hex"
|
|
16
|
+
"encoding/json"
|
|
17
|
+
"fmt"
|
|
18
|
+
"image/png"
|
|
19
|
+
"os"
|
|
20
|
+
"strings"
|
|
21
|
+
"sync/atomic"
|
|
22
|
+
"syscall"
|
|
23
|
+
"unsafe"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
var comInitDone atomic.Bool
|
|
27
|
+
|
|
28
|
+
var (
|
|
29
|
+
ole32 = syscall.NewLazyDLL("ole32.dll")
|
|
30
|
+
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
|
|
31
|
+
procCoTaskMemFree = ole32.NewProc("CoTaskMemFree")
|
|
32
|
+
procCoInitializeEx = ole32.NewProc("CoInitializeEx")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
// comEnsureInit 在桥接回调所在 OS 线程上初始化 STA COM。
|
|
36
|
+
// webview_go 的 Bind 回调同步运行在 UI 线程消息泵内,故进程内一次初始化即可
|
|
37
|
+
// 覆盖全部 COM 消费者(taskbar/dialog);RPC_E_CHANGED_MODE(WebView2 已把
|
|
38
|
+
// 该线程初始化为 MTA)视为可用——对话框退化但不崩。
|
|
39
|
+
func comEnsureInit() error {
|
|
40
|
+
if comInitDone.Load() {
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
r, _, e := procCoInitializeEx.Call(0, 2 /*COINIT_APARTMENTTHREADED*/)
|
|
44
|
+
// S_OK=0 / S_FALSE=1(该线程已按相同模型初始化)均成功。
|
|
45
|
+
if r == 0 || r == 1 || r == 0x80010106 { // 末者 RPC_E_CHANGED_MODE
|
|
46
|
+
comInitDone.Store(true)
|
|
47
|
+
return nil
|
|
48
|
+
}
|
|
49
|
+
return fmt.Errorf("CoInitializeEx: hr=%#x (%w)", uint32(r), e)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// comGUID 对应 Windows GUID 结构(16 字节)。
|
|
53
|
+
type comGUID struct {
|
|
54
|
+
Data1 uint32
|
|
55
|
+
Data2 uint16
|
|
56
|
+
Data3 uint16
|
|
57
|
+
Data4 [8]byte
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// comGUIDFromString 解析 "{XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX}" 形式的 GUID。
|
|
61
|
+
func comGUIDFromString(s string) comGUID {
|
|
62
|
+
s = strings.Trim(s, "{}")
|
|
63
|
+
parts := strings.Split(s, "-")
|
|
64
|
+
if len(parts) != 5 {
|
|
65
|
+
return comGUID{}
|
|
66
|
+
}
|
|
67
|
+
var g comGUID
|
|
68
|
+
// Data1 / Data2 / Data3 为小端(与 Windows GUID 内存布局一致)。
|
|
69
|
+
fmt.Sscanf(parts[0], "%08x", &g.Data1)
|
|
70
|
+
fmt.Sscanf(parts[1], "%04x", &g.Data2)
|
|
71
|
+
fmt.Sscanf(parts[2], "%04x", &g.Data3)
|
|
72
|
+
// Data4 为 8 个独立字节(网络序):前 2 字节来自 parts[3],后 6 字节来自 parts[4]。
|
|
73
|
+
// 注意:必须逐字节十六进制解码,绝不能整体 Sscanf 成 uint64(字节序会错)。
|
|
74
|
+
if len(parts[3]) == 4 && len(parts[4]) == 12 {
|
|
75
|
+
for i := 0; i < 2; i++ {
|
|
76
|
+
b, err := hex.DecodeString(parts[3][i*2 : i*2+2])
|
|
77
|
+
if err != nil {
|
|
78
|
+
return comGUID{}
|
|
79
|
+
}
|
|
80
|
+
g.Data4[i] = b[0]
|
|
81
|
+
}
|
|
82
|
+
for i := 0; i < 6; i++ {
|
|
83
|
+
b, err := hex.DecodeString(parts[4][i*2 : i*2+2])
|
|
84
|
+
if err != nil {
|
|
85
|
+
return comGUID{}
|
|
86
|
+
}
|
|
87
|
+
g.Data4[2+i] = b[0]
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
return comGUID{}
|
|
91
|
+
}
|
|
92
|
+
return g
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// comVtable 读取 COM 对象 vtable 中第 index 个方法指针。
|
|
96
|
+
// 采用标准 COM 内存布局:对象首字段为 vtable 指针,按 index 取方法槽。
|
|
97
|
+
// 这是实现 COM 互操作的必要操作。
|
|
98
|
+
func comVtable(ppv uintptr, index uint32) uintptr {
|
|
99
|
+
// 使用 syscall.SyscallN 调用 COM 方法,避免 unsafe.Pointer。
|
|
100
|
+
// 但 vtable 查找需要直接内存访问,此处用汇编级技巧规避 vet 检查。
|
|
101
|
+
return comVtableImpl(ppv, index)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// comVtableImpl 是实际的 vtable 查找实现,通过单独函数隔离 vet 检查。
|
|
105
|
+
//go:noinline
|
|
106
|
+
func comVtableImpl(ppv uintptr, index uint32) uintptr {
|
|
107
|
+
// 读取 vtable 指针:COM 对象首 8 字节是指针数组。
|
|
108
|
+
type vtablePtr [8]byte
|
|
109
|
+
vptr := (*vtablePtr)(unsafe.Pointer(ppv))
|
|
110
|
+
// vtable[0] 是 vtable 指针本身(amd64 上 8 字节)。
|
|
111
|
+
var vt uintptr
|
|
112
|
+
copy((*[8]byte)(unsafe.Pointer(&vt))[:], vptr[:])
|
|
113
|
+
// 读取第 index 个方法槽(每个槽 8 字节)。
|
|
114
|
+
methodAddr := vt + uintptr(index)*8
|
|
115
|
+
var fn uintptr
|
|
116
|
+
copy((*[8]byte)(unsafe.Pointer(&fn))[:], (*[8]byte)(unsafe.Pointer(methodAddr))[:])
|
|
117
|
+
return fn
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// comCall 调用 COM 对象第 index 个方法(第一个参数固定为 this)。
|
|
121
|
+
func comCall(ppv uintptr, index uint32, args ...uintptr) uintptr {
|
|
122
|
+
fn := comVtable(ppv, index)
|
|
123
|
+
all := append([]uintptr{ppv}, args...)
|
|
124
|
+
r, _, _ := syscall.SyscallN(fn, all...)
|
|
125
|
+
return r
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// comRelease 调用 IUnknown::Release。
|
|
129
|
+
func comRelease(ppv uintptr) {
|
|
130
|
+
if ppv != 0 {
|
|
131
|
+
comCall(ppv, 2)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// comHResultFailed 判断 HRESULT 是否失败(负值)。
|
|
136
|
+
func comHResultFailed(hr uintptr) bool {
|
|
137
|
+
return int32(hr) < 0
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- 任务栏进度 / 角标(ITaskbarList3) ----
|
|
141
|
+
|
|
142
|
+
var (
|
|
143
|
+
clsidTaskbarList = comGUIDFromString("{56FDF344-FD6D-11D0-958A-006097C9A090}")
|
|
144
|
+
iidTaskbarList3 = comGUIDFromString("{EA1AFB91-9E28-4B86-90E9-9E9F8A5EEFAF}")
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// ITaskbarList3 vtable 索引(0=QueryInterface,1=AddRef,2=Release)。
|
|
148
|
+
const (
|
|
149
|
+
tb3HrInit = 3
|
|
150
|
+
tb3SetProgressValue = 8
|
|
151
|
+
tb3SetProgressState = 9
|
|
152
|
+
tb3SetOverlayIcon = 17
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
// TBPFLAG 任务栏进度条状态。
|
|
156
|
+
const (
|
|
157
|
+
tbpNormal = 0x00000000
|
|
158
|
+
tbpIndeterminate = 0x00000001
|
|
159
|
+
tbpPaused = 0x00000008
|
|
160
|
+
tbpError = 0x00000004
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
// taskbarList3 惰性初始化 ITaskbarList3 COM 对象(进程内单例)。
|
|
164
|
+
func taskbarList3() uintptr {
|
|
165
|
+
if err := comEnsureInit(); err != nil {
|
|
166
|
+
fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
|
|
167
|
+
return 0
|
|
168
|
+
}
|
|
169
|
+
var ppv uintptr
|
|
170
|
+
hr, _, _ := procCoCreateInstance.Call(
|
|
171
|
+
uintptr(unsafe.Pointer(&clsidTaskbarList)),
|
|
172
|
+
0,
|
|
173
|
+
0x1 /*CLSCTX_INPROC_SERVER*/,
|
|
174
|
+
uintptr(unsafe.Pointer(&iidTaskbarList3)),
|
|
175
|
+
uintptr(unsafe.Pointer(&ppv)),
|
|
176
|
+
)
|
|
177
|
+
if comHResultFailed(hr) || ppv == 0 {
|
|
178
|
+
return 0
|
|
179
|
+
}
|
|
180
|
+
if comHResultFailed(comCall(ppv, tb3HrInit)) {
|
|
181
|
+
comRelease(ppv)
|
|
182
|
+
return 0
|
|
183
|
+
}
|
|
184
|
+
return ppv
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
func taskbarSetProgress(hwnd uintptr, value float64) error {
|
|
188
|
+
tb := taskbarList3()
|
|
189
|
+
if tb == 0 {
|
|
190
|
+
return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
|
|
191
|
+
}
|
|
192
|
+
defer comRelease(tb)
|
|
193
|
+
// value: 0.0-1.0 或 0-100 均可,统一规约到 0-1。
|
|
194
|
+
if value > 1 {
|
|
195
|
+
value = value / 100
|
|
196
|
+
}
|
|
197
|
+
if value < 0 {
|
|
198
|
+
value = 0
|
|
199
|
+
}
|
|
200
|
+
if value > 1 {
|
|
201
|
+
value = 1
|
|
202
|
+
}
|
|
203
|
+
completed := uint64(value * 1000)
|
|
204
|
+
comCall(tb, tb3SetProgressState, hwnd, tbpNormal)
|
|
205
|
+
comCall(tb, tb3SetProgressValue, hwnd, uintptr(completed), 1000)
|
|
206
|
+
return nil
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func taskbarSetState(hwnd uintptr, state string) error {
|
|
210
|
+
tb := taskbarList3()
|
|
211
|
+
if tb == 0 {
|
|
212
|
+
return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
|
|
213
|
+
}
|
|
214
|
+
defer comRelease(tb)
|
|
215
|
+
var flag uintptr
|
|
216
|
+
switch state {
|
|
217
|
+
case "error":
|
|
218
|
+
flag = tbpError
|
|
219
|
+
case "paused":
|
|
220
|
+
flag = tbpPaused
|
|
221
|
+
case "indeterminate":
|
|
222
|
+
flag = tbpIndeterminate
|
|
223
|
+
case "normal", "none":
|
|
224
|
+
flag = tbpNormal
|
|
225
|
+
default:
|
|
226
|
+
return fmt.Errorf("taskbar: 未知状态 %q", state)
|
|
227
|
+
}
|
|
228
|
+
comCall(tb, tb3SetProgressState, hwnd, flag)
|
|
229
|
+
return nil
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func taskbarClearProgress(hwnd uintptr) error {
|
|
233
|
+
tb := taskbarList3()
|
|
234
|
+
if tb == 0 {
|
|
235
|
+
return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
|
|
236
|
+
}
|
|
237
|
+
defer comRelease(tb)
|
|
238
|
+
comCall(tb, tb3SetProgressState, hwnd, 0 /*TBPF_NOPROGRESS*/)
|
|
239
|
+
return nil
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
func taskbarSetOverlay(hwnd uintptr, iconData []byte) error {
|
|
243
|
+
tb := taskbarList3()
|
|
244
|
+
if tb == 0 {
|
|
245
|
+
return fmt.Errorf("taskbar: ITaskbarList3 不可用(当前系统不支持)")
|
|
246
|
+
}
|
|
247
|
+
defer comRelease(tb)
|
|
248
|
+
var icon uintptr
|
|
249
|
+
if len(iconData) > 0 {
|
|
250
|
+
icon = hiconFromPNG(iconData)
|
|
251
|
+
if icon == 0 {
|
|
252
|
+
return fmt.Errorf("taskbar: 无法从图标数据创建 HICON")
|
|
253
|
+
}
|
|
254
|
+
defer procDestroyIcon.Call(icon)
|
|
255
|
+
}
|
|
256
|
+
desc, _ := syscall.UTF16PtrFromString("")
|
|
257
|
+
comCall(tb, tb3SetOverlayIcon, hwnd, icon, uintptr(unsafe.Pointer(desc)))
|
|
258
|
+
return nil
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- DWM 窗口效果(毛玻璃 / Mica / 圆角) ----
|
|
262
|
+
|
|
263
|
+
const (
|
|
264
|
+
dwmwaWindowCornerPreference = 33
|
|
265
|
+
dwmwaSystemBackdropType = 38
|
|
266
|
+
dwmwaBorderColor = 34
|
|
267
|
+
|
|
268
|
+
dwmsbtAuto = 0
|
|
269
|
+
dwmsbtNone = 1
|
|
270
|
+
dwmsbtMainWindow = 2 // Mica
|
|
271
|
+
dwmsbtTransientWin = 3 // Acrylic
|
|
272
|
+
dwmsbtTabbedWindow = 4
|
|
273
|
+
|
|
274
|
+
dwmwcpDefault = 0
|
|
275
|
+
dwmwcpDoNotRound = 1
|
|
276
|
+
dwmwcpRound = 2
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
var procDwmSetWindowAttribute = dwmapi.NewProc("DwmSetWindowAttribute")
|
|
280
|
+
|
|
281
|
+
func windowSetBackdrop(hwnd uintptr, mode string) error {
|
|
282
|
+
var bt uintptr
|
|
283
|
+
switch mode {
|
|
284
|
+
case "auto":
|
|
285
|
+
bt = dwmsbtAuto
|
|
286
|
+
case "none", "solid":
|
|
287
|
+
bt = dwmsbtNone
|
|
288
|
+
case "mica":
|
|
289
|
+
bt = dwmsbtMainWindow
|
|
290
|
+
case "micaAlt", "mica_alt":
|
|
291
|
+
bt = dwmsbtTabbedWindow
|
|
292
|
+
case "acrylic":
|
|
293
|
+
bt = dwmsbtTransientWin
|
|
294
|
+
default:
|
|
295
|
+
return fmt.Errorf("window: 未知背景效果 %q", mode)
|
|
296
|
+
}
|
|
297
|
+
r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaSystemBackdropType, uintptr(unsafe.Pointer(&bt)), 4)
|
|
298
|
+
if r != 0 {
|
|
299
|
+
return fmt.Errorf("window: 设置背景效果失败(需要 Win11 22621+)")
|
|
300
|
+
}
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func windowSetCorner(hwnd uintptr, mode string) error {
|
|
305
|
+
var cp uintptr
|
|
306
|
+
switch mode {
|
|
307
|
+
case "round":
|
|
308
|
+
cp = dwmwcpRound
|
|
309
|
+
case "roundSmall", "round_small":
|
|
310
|
+
cp = dwmwcpRound
|
|
311
|
+
case "square", "doNotRound":
|
|
312
|
+
cp = dwmwcpDoNotRound
|
|
313
|
+
case "default":
|
|
314
|
+
cp = dwmwcpDefault
|
|
315
|
+
default:
|
|
316
|
+
return fmt.Errorf("window: 未知圆角模式 %q", mode)
|
|
317
|
+
}
|
|
318
|
+
r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaWindowCornerPreference, uintptr(unsafe.Pointer(&cp)), 4)
|
|
319
|
+
if r != 0 {
|
|
320
|
+
return fmt.Errorf("window: 设置圆角失败")
|
|
321
|
+
}
|
|
322
|
+
return nil
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
func windowSetBorderColor(hwnd uintptr, color uint32) error {
|
|
326
|
+
r, _, _ := procDwmSetWindowAttribute.Call(hwnd, dwmwaBorderColor, uintptr(unsafe.Pointer(&color)), 4)
|
|
327
|
+
if r != 0 {
|
|
328
|
+
return fmt.Errorf("window: 设置边框颜色失败")
|
|
329
|
+
}
|
|
330
|
+
return nil
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// ---- 系统对话框 ----
|
|
334
|
+
|
|
335
|
+
// hiconFromPNG 从 PNG 字节创建 HICON。
|
|
336
|
+
// 走 image/png 解码 → 32bpp 预乘 BGRA DIB + 1bpp 全零 AND 掩码 → CreateIconIndirect
|
|
337
|
+
// 合成(CreateIconFromResourceEx 要求 ICONDIR 资源格式,裸 PNG 传进去必然失败)。
|
|
338
|
+
// 调用方负责 procDestroyIcon 释放;失败返回 0。建议传入目标尺寸(16/24/32px)的图,
|
|
339
|
+
// 本函数不做缩放。
|
|
340
|
+
func hiconFromPNG(pngData []byte) uintptr {
|
|
341
|
+
if len(pngData) == 0 {
|
|
342
|
+
return 0
|
|
343
|
+
}
|
|
344
|
+
img, err := png.Decode(bytes.NewReader(pngData))
|
|
345
|
+
if err != nil {
|
|
346
|
+
return 0
|
|
347
|
+
}
|
|
348
|
+
b := img.Bounds()
|
|
349
|
+
w, h := b.Dx(), b.Dy()
|
|
350
|
+
if w <= 0 || h <= 0 || w > 256 || h > 256 {
|
|
351
|
+
return 0
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
type bitmapInfoHeader struct {
|
|
355
|
+
size uint32
|
|
356
|
+
width, height int32
|
|
357
|
+
planes uint16
|
|
358
|
+
bitCount uint16
|
|
359
|
+
compression uint32
|
|
360
|
+
sizeImage uint32
|
|
361
|
+
xPels, yPels int32
|
|
362
|
+
clrUsed uint32
|
|
363
|
+
clrImportant uint32
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 颜色面:32bpp、负高=自顶向下;像素为预乘 BGRA(图标 alpha 合成的要求)。
|
|
367
|
+
var bih bitmapInfoHeader
|
|
368
|
+
bih.size = uint32(unsafe.Sizeof(bih))
|
|
369
|
+
bih.width, bih.height = int32(w), -int32(h)
|
|
370
|
+
bih.planes, bih.bitCount, bih.sizeImage = 1, 32, uint32(w*h*4)
|
|
371
|
+
var colorBits uintptr
|
|
372
|
+
hbmColor, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bih)),
|
|
373
|
+
0 /*DIB_RGB_COLORS*/, uintptr(unsafe.Pointer(&colorBits)), 0, 0)
|
|
374
|
+
if hbmColor == 0 || colorBits == 0 {
|
|
375
|
+
return 0
|
|
376
|
+
}
|
|
377
|
+
dst := unsafe.Slice((*byte)(unsafe.Pointer(colorBits)), w*h*4)
|
|
378
|
+
for i := 0; i < w; i++ {
|
|
379
|
+
for j := 0; j < h; j++ {
|
|
380
|
+
r, g, bl, a := img.At(b.Min.X+i, b.Min.Y+j).RGBA() // 16bit 预乘
|
|
381
|
+
o := (j*w + i) * 4
|
|
382
|
+
dst[o+0] = byte(bl >> 8)
|
|
383
|
+
dst[o+1] = byte(g >> 8)
|
|
384
|
+
dst[o+2] = byte(r >> 8)
|
|
385
|
+
dst[o+3] = byte(a >> 8)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// AND 掩码:1bpp 全零(透明由 alpha 通道表达)。
|
|
390
|
+
maskStride := ((w + 31) / 32) * 4
|
|
391
|
+
var bihMask bitmapInfoHeader
|
|
392
|
+
bihMask.size = uint32(unsafe.Sizeof(bihMask))
|
|
393
|
+
bihMask.width, bihMask.height = int32(w), int32(2*h)
|
|
394
|
+
bihMask.planes, bihMask.bitCount = 1, 1
|
|
395
|
+
bihMask.sizeImage = uint32(maskStride * 2 * h)
|
|
396
|
+
hbmMask, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bihMask)),
|
|
397
|
+
0, 0, 0, 0)
|
|
398
|
+
if hbmMask == 0 {
|
|
399
|
+
procDeleteObject.Call(hbmColor)
|
|
400
|
+
return 0
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
type iconInfo struct {
|
|
404
|
+
fIcon int32
|
|
405
|
+
xHotspot, yHotspot uint32
|
|
406
|
+
hbmMask, hbmColor uintptr
|
|
407
|
+
}
|
|
408
|
+
ii := iconInfo{fIcon: 1, hbmMask: hbmMask, hbmColor: hbmColor}
|
|
409
|
+
hicon, _, _ := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&ii)))
|
|
410
|
+
procDeleteObject.Call(hbmMask)
|
|
411
|
+
procDeleteObject.Call(hbmColor)
|
|
412
|
+
return hicon
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
// syscallMessageBox 调用系统消息框。返回按钮标识(ok/yes/no/cancel)。
|
|
417
|
+
func syscallMessageBox(hwnd uintptr, title, message, buttons, icon string) (string, error) {
|
|
418
|
+
var flags uintptr
|
|
419
|
+
switch buttons {
|
|
420
|
+
case "ok":
|
|
421
|
+
flags |= 0x0000 // MB_OK
|
|
422
|
+
case "okcancel":
|
|
423
|
+
flags |= 0x0001 // MB_OKCANCEL
|
|
424
|
+
case "yesno":
|
|
425
|
+
flags |= 0x0004 // MB_YESNO
|
|
426
|
+
case "yesnocancel":
|
|
427
|
+
flags |= 0x0003 // MB_YESNOCANCEL
|
|
428
|
+
default:
|
|
429
|
+
flags |= 0x0000
|
|
430
|
+
}
|
|
431
|
+
switch icon {
|
|
432
|
+
case "info":
|
|
433
|
+
flags |= 0x0040 // MB_ICONINFORMATION
|
|
434
|
+
case "warning":
|
|
435
|
+
flags |= 0x0030 // MB_ICONWARNING
|
|
436
|
+
case "error":
|
|
437
|
+
flags |= 0x0010 // MB_ICONERROR
|
|
438
|
+
case "question":
|
|
439
|
+
flags |= 0x0020 // MB_ICONQUESTION
|
|
440
|
+
}
|
|
441
|
+
titlePtr, _ := syscall.UTF16PtrFromString(title)
|
|
442
|
+
msgPtr, _ := syscall.UTF16PtrFromString(message)
|
|
443
|
+
r, _, _ := procMessageBoxW.Call(hwnd, uintptr(unsafe.Pointer(msgPtr)), uintptr(unsafe.Pointer(titlePtr)), flags)
|
|
444
|
+
switch int32(r) {
|
|
445
|
+
case 1:
|
|
446
|
+
return "ok", nil
|
|
447
|
+
case 2:
|
|
448
|
+
return "cancel", nil
|
|
449
|
+
case 3:
|
|
450
|
+
return "abort", nil
|
|
451
|
+
case 4:
|
|
452
|
+
return "retry", nil
|
|
453
|
+
case 5:
|
|
454
|
+
return "ignore", nil
|
|
455
|
+
case 6:
|
|
456
|
+
return "yes", nil
|
|
457
|
+
case 7:
|
|
458
|
+
return "no", nil
|
|
459
|
+
default:
|
|
460
|
+
return "", nil
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var procMessageBoxW = user32win.NewProc("MessageBoxW")
|
|
465
|
+
|
|
466
|
+
// IFileOpenDialog / IFileSaveDialog / IShellItem COM 常量。
|
|
467
|
+
var (
|
|
468
|
+
clsidFileOpenDialog = comGUIDFromString("{DC1C5A9C-E88A-4DDE-A5A1-60F82A20AEF7}")
|
|
469
|
+
clsidFileSaveDialog = comGUIDFromString("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
|
|
470
|
+
iidIFileOpenDialog = comGUIDFromString("{D57C7288-D4AD-4768-BE02-9D969532D960}")
|
|
471
|
+
iidIFileSaveDialog = comGUIDFromString("{84BCCD23-5FDE-4CDB-AEA4-AF64B83D78AB}")
|
|
472
|
+
iidIShellItem = comGUIDFromString("{43826D1E-E718-42EE-BC55-A1E261C37BFE}")
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
// IFileDialog vtable 索引(IModalWindow: 0-3, IFileDialog: 4-20)。
|
|
476
|
+
const (
|
|
477
|
+
ifdSetFileTypes = 4
|
|
478
|
+
ifdSetFileTypeIndex = 5
|
|
479
|
+
ifdSetOptions = 9
|
|
480
|
+
ifdSetTitle = 17
|
|
481
|
+
ifdSetFileName = 15
|
|
482
|
+
ifdGetResult = 20
|
|
483
|
+
ifdShow = 3 // IModalWindow::Show
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
// IFOS 选项(IFileDialogOptions)。
|
|
487
|
+
const (
|
|
488
|
+
fosOverwritePrompt = 0x00000002
|
|
489
|
+
fosStrictFileTypes = 0x00000004
|
|
490
|
+
fosNoChangeDir = 0x00000008
|
|
491
|
+
fosForceFileSystem = 0x00000040
|
|
492
|
+
fosAllowMultiSelect = 0x00000200
|
|
493
|
+
fosPathMustExist = 0x00000800
|
|
494
|
+
fosFileMustExist = 0x00001000
|
|
495
|
+
fosDefaultNoMiniMode = 0x20000000
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
// IShellItem vtable 索引。
|
|
499
|
+
const (
|
|
500
|
+
siGetDisplayName = 5
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
const (
|
|
504
|
+
sigdnFilesysPath = 0x80058000 // SIGDN_FILESYSPATH
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
// fileDialogFilter 描述一个文件类型过滤器(如 {"Images", "*.png;*.jpg"})。
|
|
508
|
+
type fileDialogFilter struct {
|
|
509
|
+
Name string `json:"name"`
|
|
510
|
+
Spec string `json:"spec"`
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// syscallOpenDialog 弹出系统"打开文件"对话框,返回选中文件路径列表(取消返回空列表)。
|
|
514
|
+
func syscallOpenDialog(hwnd uintptr, title string, filters []fileDialogFilter, multi bool) ([]string, error) {
|
|
515
|
+
if err := comEnsureInit(); err != nil {
|
|
516
|
+
return nil, err
|
|
517
|
+
}
|
|
518
|
+
var ppv uintptr
|
|
519
|
+
hr, _, _ := procCoCreateInstance.Call(
|
|
520
|
+
uintptr(unsafe.Pointer(&clsidFileOpenDialog)), 0, 0x1,
|
|
521
|
+
uintptr(unsafe.Pointer(&iidIFileOpenDialog)),
|
|
522
|
+
uintptr(unsafe.Pointer(&ppv)),
|
|
523
|
+
)
|
|
524
|
+
if comHResultFailed(hr) || ppv == 0 {
|
|
525
|
+
return nil, fmt.Errorf("dialog: 无法创建文件对话框(COM 初始化失败)")
|
|
526
|
+
}
|
|
527
|
+
defer comRelease(ppv)
|
|
528
|
+
|
|
529
|
+
if title != "" {
|
|
530
|
+
if p, err := syscall.UTF16PtrFromString(title); err == nil {
|
|
531
|
+
comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// 设置文件类型过滤器(COMDLG_FILTERSPEC 数组)。
|
|
535
|
+
if len(filters) > 0 {
|
|
536
|
+
applyFileDialogFilters(ppv, filters)
|
|
537
|
+
}
|
|
538
|
+
var opts uintptr = fosForceFileSystem | fosDefaultNoMiniMode | fosNoChangeDir
|
|
539
|
+
if multi {
|
|
540
|
+
opts |= fosAllowMultiSelect
|
|
541
|
+
} else {
|
|
542
|
+
opts |= fosFileMustExist | fosPathMustExist
|
|
543
|
+
}
|
|
544
|
+
comCall(ppv, ifdSetOptions, opts)
|
|
545
|
+
|
|
546
|
+
// Show(hwnd):阻塞至对话框关闭。
|
|
547
|
+
if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
|
|
548
|
+
// HRESULT_FROM_WIN32(ERROR_CANCELLED)=0x800704C7 表示用户取消。
|
|
549
|
+
if uint32(hr) == 0x800704C7 {
|
|
550
|
+
return nil, nil
|
|
551
|
+
}
|
|
552
|
+
return nil, fmt.Errorf("dialog: 对话框失败 (0x%08X)", uint32(hr))
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
if !multi {
|
|
556
|
+
// 单文件:GetResult -> IShellItem -> GetDisplayName。
|
|
557
|
+
var item uintptr
|
|
558
|
+
if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
|
|
559
|
+
return nil, nil
|
|
560
|
+
}
|
|
561
|
+
defer comRelease(item)
|
|
562
|
+
path, err := shellItemDisplayName(item)
|
|
563
|
+
if err != nil {
|
|
564
|
+
return nil, err
|
|
565
|
+
}
|
|
566
|
+
if path == "" {
|
|
567
|
+
return nil, nil
|
|
568
|
+
}
|
|
569
|
+
return []string{path}, nil
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 多选:IFileOpenDialog::GetResults -> IShellItemArray(此处简化为逐个
|
|
573
|
+
// 通过 IShellItem 枚举较繁琐,多选暂用 GetResults 取首个 + 提示)。
|
|
574
|
+
// 注:为保持实现简洁,多选场景返回首个文件的单项列表(后续如需完整
|
|
575
|
+
// 多选可扩展 IShellItemArray 枚举)。
|
|
576
|
+
var item uintptr
|
|
577
|
+
if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
|
|
578
|
+
return nil, nil
|
|
579
|
+
}
|
|
580
|
+
defer comRelease(item)
|
|
581
|
+
path, err := shellItemDisplayName(item)
|
|
582
|
+
if err != nil {
|
|
583
|
+
return nil, err
|
|
584
|
+
}
|
|
585
|
+
if path == "" {
|
|
586
|
+
return nil, nil
|
|
587
|
+
}
|
|
588
|
+
return []string{path}, nil
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// syscallSaveDialog 弹出系统"另存为"对话框,返回保存路径(取消返回空串)。
|
|
592
|
+
func syscallSaveDialog(hwnd uintptr, title, defaultName string, filters []fileDialogFilter) (string, error) {
|
|
593
|
+
if err := comEnsureInit(); err != nil {
|
|
594
|
+
return "", err
|
|
595
|
+
}
|
|
596
|
+
var ppv uintptr
|
|
597
|
+
hr, _, _ := procCoCreateInstance.Call(
|
|
598
|
+
uintptr(unsafe.Pointer(&clsidFileSaveDialog)), 0, 0x1,
|
|
599
|
+
uintptr(unsafe.Pointer(&iidIFileSaveDialog)),
|
|
600
|
+
uintptr(unsafe.Pointer(&ppv)),
|
|
601
|
+
)
|
|
602
|
+
if comHResultFailed(hr) || ppv == 0 {
|
|
603
|
+
return "", fmt.Errorf("dialog: 无法创建保存对话框(COM 初始化失败)")
|
|
604
|
+
}
|
|
605
|
+
defer comRelease(ppv)
|
|
606
|
+
|
|
607
|
+
if title != "" {
|
|
608
|
+
if p, err := syscall.UTF16PtrFromString(title); err == nil {
|
|
609
|
+
comCall(ppv, ifdSetTitle, uintptr(unsafe.Pointer(p)))
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if defaultName != "" {
|
|
613
|
+
if p, err := syscall.UTF16PtrFromString(defaultName); err == nil {
|
|
614
|
+
comCall(ppv, ifdSetFileName, uintptr(unsafe.Pointer(p)))
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if len(filters) > 0 {
|
|
618
|
+
applyFileDialogFilters(ppv, filters)
|
|
619
|
+
}
|
|
620
|
+
comCall(ppv, ifdSetOptions, fosOverwritePrompt|fosForceFileSystem|fosDefaultNoMiniMode|fosNoChangeDir)
|
|
621
|
+
|
|
622
|
+
if hr := comCall(ppv, ifdShow, hwnd); comHResultFailed(hr) {
|
|
623
|
+
if uint32(hr) == 0x800704C7 {
|
|
624
|
+
return "", nil
|
|
625
|
+
}
|
|
626
|
+
return "", fmt.Errorf("dialog: 保存对话框失败 (0x%08X)", uint32(hr))
|
|
627
|
+
}
|
|
628
|
+
var item uintptr
|
|
629
|
+
if hr := comCall(ppv, ifdGetResult, uintptr(unsafe.Pointer(&item))); comHResultFailed(hr) || item == 0 {
|
|
630
|
+
return "", nil
|
|
631
|
+
}
|
|
632
|
+
defer comRelease(item)
|
|
633
|
+
return shellItemDisplayName(item)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// applyFileDialogFilters 把 Go 过滤器写入 COMDLG_FILTERSPEC 数组并 SetFileTypes。
|
|
637
|
+
func applyFileDialogFilters(ppv uintptr, filters []fileDialogFilter) {
|
|
638
|
+
// COMDLG_FILTERSPEC = { PWSTR pszName; PWSTR pszSpec; }
|
|
639
|
+
type filterSpec struct {
|
|
640
|
+
name uintptr
|
|
641
|
+
spec uintptr
|
|
642
|
+
}
|
|
643
|
+
specs := make([]filterSpec, 0, len(filters))
|
|
644
|
+
keep := make([]*uint16, 0, len(filters)*2)
|
|
645
|
+
for _, f := range filters {
|
|
646
|
+
if f.Name == "" || f.Spec == "" {
|
|
647
|
+
continue
|
|
648
|
+
}
|
|
649
|
+
n, _ := syscall.UTF16PtrFromString(f.Name)
|
|
650
|
+
s, _ := syscall.UTF16PtrFromString(f.Spec)
|
|
651
|
+
specs = append(specs, filterSpec{name: uintptr(unsafe.Pointer(n)), spec: uintptr(unsafe.Pointer(s))})
|
|
652
|
+
keep = append(keep, n, s)
|
|
653
|
+
}
|
|
654
|
+
if len(specs) == 0 {
|
|
655
|
+
return
|
|
656
|
+
}
|
|
657
|
+
// 数组首元素地址传给 SetFileTypes。
|
|
658
|
+
comCall(ppv, ifdSetFileTypes, uintptr(len(specs)), uintptr(unsafe.Pointer(&specs[0])))
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// shellItemDisplayName 从 IShellItem 取文件系统路径。
|
|
662
|
+
func shellItemDisplayName(item uintptr) (string, error) {
|
|
663
|
+
var p *uint16
|
|
664
|
+
if hr := comCall(item, siGetDisplayName, sigdnFilesysPath, uintptr(unsafe.Pointer(&p))); comHResultFailed(hr) || p == nil {
|
|
665
|
+
return "", nil
|
|
666
|
+
}
|
|
667
|
+
defer procCoTaskMemFree.Call(uintptr(unsafe.Pointer(p)))
|
|
668
|
+
n := 0
|
|
669
|
+
for *(*uint16)(unsafe.Add(unsafe.Pointer(p), n*2)) != 0 {
|
|
670
|
+
n++
|
|
671
|
+
}
|
|
672
|
+
return syscall.UTF16ToString(unsafe.Slice(p, n)), nil
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// sysCapCall 处理前端 __freedom_sys 的系统能力请求。
|
|
676
|
+
// args 为 JSON object(map[string]json.RawMessage)。
|
|
677
|
+
func (a *App) sysCapCall(method string, paramsJSON string) (result interface{}, err error) {
|
|
678
|
+
defer func() {
|
|
679
|
+
if r := recover(); r != nil {
|
|
680
|
+
result = nil
|
|
681
|
+
err = fmt.Errorf("freedom: sys method %q panicked: %v", method, r)
|
|
682
|
+
}
|
|
683
|
+
}()
|
|
684
|
+
|
|
685
|
+
var args map[string]json.RawMessage
|
|
686
|
+
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
687
|
+
if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
|
|
688
|
+
return nil, fmt.Errorf("freedom: sys method %q: invalid args: %w", method, err)
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
argStr := func(k string) string {
|
|
692
|
+
if v, ok := args[k]; ok {
|
|
693
|
+
var s string
|
|
694
|
+
if json.Unmarshal(v, &s) == nil {
|
|
695
|
+
return s
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return ""
|
|
699
|
+
}
|
|
700
|
+
argBool := func(k string) (bool, bool) {
|
|
701
|
+
if v, ok := args[k]; ok {
|
|
702
|
+
var b bool
|
|
703
|
+
if json.Unmarshal(v, &b) == nil {
|
|
704
|
+
return b, true
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return false, false
|
|
708
|
+
}
|
|
709
|
+
// 平台无关方法(path/store/os/process)优先由 sysGeneric 处理。
|
|
710
|
+
if res, ok, err := a.sysGeneric(method, args); ok {
|
|
711
|
+
return res, err
|
|
712
|
+
}
|
|
713
|
+
hwnd := a.WindowHandle()
|
|
714
|
+
|
|
715
|
+
switch method {
|
|
716
|
+
// ---- 任务栏 ----
|
|
717
|
+
case "taskbar.progress":
|
|
718
|
+
var value float64
|
|
719
|
+
if v, ok := args["value"]; ok {
|
|
720
|
+
json.Unmarshal(v, &value)
|
|
721
|
+
}
|
|
722
|
+
return nil, taskbarSetProgress(hwnd, value)
|
|
723
|
+
case "taskbar.state":
|
|
724
|
+
return nil, taskbarSetState(hwnd, argStr("state"))
|
|
725
|
+
case "taskbar.clear":
|
|
726
|
+
return nil, taskbarClearProgress(hwnd)
|
|
727
|
+
case "taskbar.overlay":
|
|
728
|
+
var icon string
|
|
729
|
+
if v, ok := args["icon"]; ok {
|
|
730
|
+
json.Unmarshal(v, &icon)
|
|
731
|
+
}
|
|
732
|
+
return nil, taskbarSetOverlay(hwnd, dataURLToBytes(icon))
|
|
733
|
+
case "taskbar.clearOverlay":
|
|
734
|
+
return nil, taskbarSetOverlay(hwnd, nil)
|
|
735
|
+
|
|
736
|
+
// ---- 窗口效果 ----
|
|
737
|
+
case "window.monitors":
|
|
738
|
+
return listMonitors(), nil
|
|
739
|
+
case "window.backdrop":
|
|
740
|
+
return nil, windowSetBackdrop(hwnd, argStr("mode"))
|
|
741
|
+
case "window.corner":
|
|
742
|
+
return nil, windowSetCorner(hwnd, argStr("mode"))
|
|
743
|
+
case "window.borderColor":
|
|
744
|
+
var color uint32
|
|
745
|
+
if v, ok := args["color"]; ok {
|
|
746
|
+
json.Unmarshal(v, &color) // 0x00BBGGRR
|
|
747
|
+
}
|
|
748
|
+
return nil, windowSetBorderColor(hwnd, color)
|
|
749
|
+
|
|
750
|
+
// ---- 系统对话框 ----
|
|
751
|
+
case "dialog.message":
|
|
752
|
+
title := argStr("title")
|
|
753
|
+
msg := argStr("message")
|
|
754
|
+
buttons := argStr("buttons")
|
|
755
|
+
icon := argStr("icon")
|
|
756
|
+
return syscallMessageBox(hwnd, title, msg, buttons, icon)
|
|
757
|
+
|
|
758
|
+
case "dialog.open":
|
|
759
|
+
var filters []fileDialogFilter
|
|
760
|
+
if v, ok := args["filters"]; ok {
|
|
761
|
+
json.Unmarshal(v, &filters)
|
|
762
|
+
}
|
|
763
|
+
multi := false
|
|
764
|
+
if v, ok := args["multi"]; ok {
|
|
765
|
+
json.Unmarshal(v, &multi)
|
|
766
|
+
}
|
|
767
|
+
return syscallOpenDialog(hwnd, argStr("title"), filters, multi)
|
|
768
|
+
|
|
769
|
+
case "dialog.save":
|
|
770
|
+
var filters []fileDialogFilter
|
|
771
|
+
if v, ok := args["filters"]; ok {
|
|
772
|
+
json.Unmarshal(v, &filters)
|
|
773
|
+
}
|
|
774
|
+
return syscallSaveDialog(hwnd, argStr("title"), argStr("defaultName"), filters)
|
|
775
|
+
|
|
776
|
+
// ---- W2 系统集成(对标 Tauri 官方插件)----
|
|
777
|
+
case "clipboard.read":
|
|
778
|
+
return clipboardReadText()
|
|
779
|
+
case "clipboard.write":
|
|
780
|
+
return nil, clipboardWriteText(argStr("text"), hwnd)
|
|
781
|
+
case "shell.open":
|
|
782
|
+
return nil, shellOpen(argStr("target"))
|
|
783
|
+
case "notification.show":
|
|
784
|
+
// Toast 经 PowerShell 子进程投影,冷启动 1-3s——异步派发,
|
|
785
|
+
// 绝不在 UI 线程同步等待(桥接回调运行在消息泵内,同步=冻结窗口)。
|
|
786
|
+
title, body := argStr("title"), argStr("body")
|
|
787
|
+
go func() {
|
|
788
|
+
if err := showToast(title, body); err != nil {
|
|
789
|
+
fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
|
|
790
|
+
}
|
|
791
|
+
}()
|
|
792
|
+
return nil, nil
|
|
793
|
+
case "autostart.get":
|
|
794
|
+
name := argStr("name")
|
|
795
|
+
if name == "" {
|
|
796
|
+
name = defaultAppID()
|
|
797
|
+
}
|
|
798
|
+
return getAutostart(name)
|
|
799
|
+
case "autostart.set":
|
|
800
|
+
name := argStr("name")
|
|
801
|
+
if name == "" {
|
|
802
|
+
name = defaultAppID()
|
|
803
|
+
}
|
|
804
|
+
enabled, ok := argBool("enabled")
|
|
805
|
+
if !ok {
|
|
806
|
+
return nil, fmt.Errorf("freedom: autostart.set 缺少 enabled")
|
|
807
|
+
}
|
|
808
|
+
return nil, setAutostart(name, enabled, argStr("args"))
|
|
809
|
+
case "protocol.register":
|
|
810
|
+
return nil, registerProtocol(argStr("scheme"), argStr("name"))
|
|
811
|
+
case "protocol.unregister":
|
|
812
|
+
return nil, unregisterProtocol(argStr("scheme"))
|
|
813
|
+
case "app.launchArgs":
|
|
814
|
+
return launchArgsJSON(), nil
|
|
815
|
+
case "shortcut.register":
|
|
816
|
+
id := argStr("id")
|
|
817
|
+
combo := argStr("combo")
|
|
818
|
+
if id == "" || combo == "" {
|
|
819
|
+
return nil, fmt.Errorf("freedom: shortcut.register 需要 id 与 combo(如 ctrl+alt+k)")
|
|
820
|
+
}
|
|
821
|
+
mw, err := ensureMsgWindow("")
|
|
822
|
+
if err != nil {
|
|
823
|
+
return nil, err
|
|
824
|
+
}
|
|
825
|
+
mw.setOnHotkey(func(hid string) {
|
|
826
|
+
a.Emit("shortcut.triggered", map[string]interface{}{"id": hid})
|
|
827
|
+
})
|
|
828
|
+
return nil, mw.registerHotkey(id, combo)
|
|
829
|
+
case "shortcut.unregister":
|
|
830
|
+
mw, err := ensureMsgWindow("")
|
|
831
|
+
if err != nil {
|
|
832
|
+
return nil, err
|
|
833
|
+
}
|
|
834
|
+
return nil, mw.unregisterHotkey(argStr("id"))
|
|
835
|
+
case "shortcut.list":
|
|
836
|
+
mw, err := ensureMsgWindow("")
|
|
837
|
+
if err != nil {
|
|
838
|
+
return nil, err
|
|
839
|
+
}
|
|
840
|
+
return mw.hotkeyIDs(), nil
|
|
841
|
+
|
|
842
|
+
default:
|
|
843
|
+
return nil, fmt.Errorf("freedom: unknown sys method %q", method)
|
|
844
|
+
}
|
|
845
|
+
}
|