@yufengtadian/freedom-cli 1.12.18 → 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.
Files changed (79) hide show
  1. package/lib/build.js +1 -1
  2. package/lib/security.js +1 -1
  3. package/package.json +5 -1
  4. package/shell/darwin-arm64/freedom-shell +0 -0
  5. package/shell/linux-x64/freedom-shell +0 -0
  6. package/shell/win-x64/freedom-shell.exe +0 -0
  7. package/templates/go/go.mod +1 -7
  8. package/templates/go/go.sum +2 -2
  9. package/templates/go/main.go +22 -8
  10. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  11. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  12. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  13. package/templates/go/pkg/freedom/assets/freedom.js +200 -5
  14. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  15. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  16. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  17. package/templates/go/pkg/freedom/backend_proc.go +291 -53
  18. package/templates/go/pkg/freedom/bridge.go +13 -2
  19. package/templates/go/pkg/freedom/capability.go +77 -0
  20. package/templates/go/pkg/freedom/center_other.go +3 -0
  21. package/templates/go/pkg/freedom/center_windows.go +45 -35
  22. package/templates/go/pkg/freedom/dispatch.go +60 -0
  23. package/templates/go/pkg/freedom/events_windows.go +319 -0
  24. package/templates/go/pkg/freedom/freedom.go +193 -99
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  26. package/templates/go/pkg/freedom/osver_other.go +15 -0
  27. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  28. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  29. package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
  30. package/templates/go/pkg/freedom/security.go +19 -12
  31. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  32. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  33. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  34. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  35. package/templates/go/pkg/freedom/store.go +458 -0
  36. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  37. package/templates/go/pkg/freedom/syscap_other.go +15 -3
  38. package/templates/go/pkg/freedom/syscap_windows.go +228 -36
  39. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  40. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  41. package/templates/go/pkg/freedom/tray_common.go +12 -0
  42. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  43. package/templates/go/pkg/freedom/tray_other.go +2 -2
  44. package/templates/go/pkg/freedom/tray_windows.go +69 -39
  45. package/templates/go/pkg/freedom/updater.go +372 -0
  46. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  47. package/templates/go/pkg/freedom/window_other.go +32 -66
  48. package/templates/go/pkg/freedom/window_windows.go +397 -254
  49. package/templates/go/build_all.txt +0 -0
  50. package/templates/go/build_err.txt +0 -0
  51. package/templates/go/gen_config.go +0 -22
  52. package/templates/go/pkg/freedom/center_windows_test.go +0 -32
  53. package/templates/go/pkg/freedom/security_test.go +0 -153
  54. package/templates/go/temp_vet_test/main.go +0 -40
  55. package/templates/go/test_err.txt +0 -0
  56. package/templates/go/vet_err.txt +0 -1
  57. package/templates/go/vet_err2.txt +0 -2
  58. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  59. package/templates/go/webview_go/CHANGELOG.md +0 -15
  60. package/templates/go/webview_go/LICENSE +0 -22
  61. package/templates/go/webview_go/README.md +0 -50
  62. package/templates/go/webview_go/examples/basic/main.go +0 -12
  63. package/templates/go/webview_go/examples/bind/main.go +0 -38
  64. package/templates/go/webview_go/glue.c +0 -36
  65. package/templates/go/webview_go/go.mod +0 -3
  66. package/templates/go/webview_go/go.sum +0 -0
  67. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  68. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  69. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  70. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  71. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  72. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  73. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  74. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  75. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  77. package/templates/go/webview_go/webview.cc +0 -1
  78. package/templates/go/webview_go/webview.go +0 -481
  79. package/templates/go/webview_go/webview_test.go +0 -50
@@ -0,0 +1,385 @@
1
+ package freedom
2
+
3
+ import (
4
+ "encoding/json"
5
+ "fmt"
6
+ "runtime"
7
+ "sort"
8
+ "sync"
9
+ "sync/atomic"
10
+
11
+ webview "github.com/webview/webview_go"
12
+ )
13
+
14
+ // M2 多窗口:主窗口跑在 Run() 的 UI 线程(现状不变),次级窗口各自持有一条
15
+ // LockOSThread 的 goroutine 跑独立消息泵(webview.h 每实例自带 GetMessage 循环)。
16
+ // 生命周期:主窗口关闭 = 应用退出(Run 返回前回收全部次级窗口);次级窗口可独立
17
+ // 关闭。限制:托盘/热键/单实例/窗口事件子类化/状态记忆等平台层单例仍只挂主窗口,
18
+ // 次级窗口的页面里 __freedom_sys/__freedom_tray 桥缺席(SDK 给出可读拒绝)。
19
+
20
+ // WindowSpec 描述一个次级窗口的创建参数。零值可用,全部有默认值。
21
+ type WindowSpec struct {
22
+ Title string // 空则用主窗口标题
23
+ Width int // <=0 用主窗口配置值
24
+ Height int // <=0 用主窗口配置值
25
+ Center bool // 屏幕居中(Windows 生效)
26
+ // URL 非空时加载该地址(Navigate);否则依次尝试 HTML(Go 侧函数)、
27
+ // Page(前端 create 传来的内联字符串)、主窗口 Config.HTML。全空且主页面
28
+ // 含开窗脚本会级联——前端 create 务必带 html 或 url。
29
+ URL string
30
+ // Page 是内联 HTML 字符串。前端 window.create 经 JSON 传参,无法携带
31
+ // Go 闭包,故序列化通道用本字段;Go 侧 NewWindow 优先用 HTML 函数。
32
+ Page string `json:"html"`
33
+ HTML func() (string, error)
34
+ Debug bool
35
+ }
36
+
37
+ // Window 是一个窗口句柄:主窗口(id "main")与 NewWindow 创建的次级窗口同型。
38
+ // 方法对并发调用安全。
39
+ type Window struct {
40
+ id string
41
+ app *App
42
+ spec WindowSpec
43
+ doneCh chan struct{} // 窗口线程回收后 close
44
+
45
+ mu sync.Mutex
46
+ view webview.WebView // 未创建/已销毁为 nil
47
+ closing bool // Close 早于 view 就绪时置位,runWindow 见后即撤
48
+ // tearing 在 Destroy 前置位。Dispatch 只是入队:webview2 的销毁路径会在
49
+ // 拆除线程上把排队回调泵出来执行(此刻 w.mu 仍被 destroyView 持有,闭包
50
+ // 只能读原子量),无守卫即对半销毁实例 Eval → 0xc0000005(CI 实测崩溃)。
51
+ tearing atomic.Bool
52
+ }
53
+
54
+ // ID 返回窗口标识("main" 或 "w1"…)。
55
+ func (w *Window) ID() string { return w.id }
56
+
57
+ // WaitClosed 阻塞直到该窗口消息循环退出(被关闭或进程退出)。
58
+ func (w *Window) WaitClosed() { <-w.doneCh }
59
+
60
+ // live 报告窗口 webview 是否仍存活。
61
+ func (w *Window) live() bool {
62
+ w.mu.Lock()
63
+ defer w.mu.Unlock()
64
+ return w.view != nil
65
+ }
66
+
67
+ // Close 请求关闭窗口:投递 Terminate 到该窗口消息泵;对尚在启动中的窗口,
68
+ // 置 closing 标志由创建线程自行退出。幂等。主窗口 Close 等价 App.Quit
69
+ // (主窗口关闭 = 应用退出,与 Run 的回收语义一致)。
70
+ func (w *Window) Close() {
71
+ if w.id == mainWindowID {
72
+ w.app.Quit()
73
+ return
74
+ }
75
+ // Dispatch 在 w.mu 临界区内发起:与销毁侧 destroyView(同锁内 Destroy)互斥,
76
+ // 不会对已销毁实例投递(否则 use-after-free)。
77
+ w.mu.Lock()
78
+ defer w.mu.Unlock()
79
+ w.closing = true
80
+ if view := w.view; view != nil {
81
+ view.Dispatch(func() {
82
+ if !w.tearing.Load() {
83
+ view.Terminate()
84
+ }
85
+ })
86
+ }
87
+ }
88
+
89
+ // SetTitle 修改窗口标题(投递到该窗口消息泵)。
90
+ func (w *Window) SetTitle(title string) {
91
+ if w.id == mainWindowID {
92
+ w.app.withView(func(view webview.WebView) {
93
+ view.Dispatch(func() {
94
+ if !w.app.viewTearing.Load() {
95
+ view.SetTitle(title)
96
+ }
97
+ })
98
+ })
99
+ return
100
+ }
101
+ w.mu.Lock()
102
+ defer w.mu.Unlock()
103
+ if view := w.view; view != nil {
104
+ // 闭包捕获局部引用:延迟执行时不再读共享字段 w.view(可能已摘除)。
105
+ view.Dispatch(func() {
106
+ if !w.tearing.Load() {
107
+ view.SetTitle(title)
108
+ }
109
+ })
110
+ }
111
+ }
112
+
113
+ func (w *Window) handle() uintptr {
114
+ w.mu.Lock()
115
+ defer w.mu.Unlock()
116
+ if w.view == nil {
117
+ return 0
118
+ }
119
+ return uintptr(w.view.Window())
120
+ }
121
+
122
+ // evalJS 在本窗口消息泵上执行脚本;窗口未存活时静默丢弃。
123
+ func (w *Window) evalJS(js string) {
124
+ w.mu.Lock()
125
+ defer w.mu.Unlock()
126
+ if view := w.view; view != nil {
127
+ view.Dispatch(func() {
128
+ if !w.tearing.Load() {
129
+ view.Eval(js)
130
+ }
131
+ })
132
+ }
133
+ }
134
+
135
+ // destroyView 由窗口线程在消息循环退出后调用:摘引用与 Destroy 同处 w.mu
136
+ // 临界区,之后一切读引用+Dispatch 的方法(Close/SetTitle/evalJS/handle)自动失活。
137
+ // tearing 先于 Destroy 置位:Destroy 会把队列里已入队的 dispatch 回调泵出来执行,
138
+ // 闭包凭该原子量自我作废(不能读 w.mu——回调就跑在本临界区内)。
139
+ func (w *Window) destroyView(view webview.WebView) {
140
+ w.mu.Lock()
141
+ defer w.mu.Unlock()
142
+ w.tearing.Store(true)
143
+ if w.view == view {
144
+ w.view = nil
145
+ }
146
+ view.Destroy()
147
+ }
148
+
149
+ // NewWindow 异步创建一个次级窗口并立即返回句柄。必须在 App.Run() 已启动后
150
+ // 调用(任意 goroutine 皆可,含 OnReady 与 Bind handler)。
151
+ func (a *App) NewWindow(spec WindowSpec) (*Window, error) {
152
+ a.winMu.Lock()
153
+ if !a.running {
154
+ a.winMu.Unlock()
155
+ return nil, fmt.Errorf("freedom: NewWindow requires the app to be running (App.Run)")
156
+ }
157
+ a.winSeq++
158
+ w := &Window{id: fmt.Sprintf("w%d", a.winSeq), app: a, spec: spec, doneCh: make(chan struct{})}
159
+ if spec.Title == "" {
160
+ w.spec.Title = a.cfg.Title
161
+ }
162
+ if spec.Width <= 0 {
163
+ w.spec.Width = a.cfg.Width
164
+ }
165
+ if spec.Height <= 0 {
166
+ w.spec.Height = a.cfg.Height
167
+ }
168
+ a.windows[w.id] = w
169
+ a.winMu.Unlock()
170
+ go a.runWindow(w)
171
+ return w, nil
172
+ }
173
+
174
+ // Window 按 id 查找窗口;不存在或已关闭返回 nil。"main" 返回主窗口句柄。
175
+ func (a *App) Window(id string) *Window {
176
+ a.winMu.Lock()
177
+ defer a.winMu.Unlock()
178
+ return a.windows[id]
179
+ }
180
+
181
+ // Windows 返回存活次级窗口 id(升序,不含 "main")。
182
+ func (a *App) Windows() []string {
183
+ a.winMu.Lock()
184
+ defer a.winMu.Unlock()
185
+ ids := make([]string, 0, len(a.windows))
186
+ for id := range a.windows {
187
+ if id != mainWindowID {
188
+ ids = append(ids, id)
189
+ }
190
+ }
191
+ sort.Strings(ids)
192
+ return ids
193
+ }
194
+
195
+ // CloseWindows 关闭全部次级窗口并等待其线程回收;主窗口不受影响。
196
+ func (a *App) CloseWindows() {
197
+ a.winMu.Lock()
198
+ pending := make([]*Window, 0, len(a.windows))
199
+ for id, w := range a.windows {
200
+ if id != mainWindowID {
201
+ pending = append(pending, w)
202
+ }
203
+ }
204
+ a.winMu.Unlock()
205
+ for _, w := range pending {
206
+ w.Close()
207
+ }
208
+ for _, w := range pending {
209
+ w.WaitClosed()
210
+ }
211
+ }
212
+
213
+ // runWindow 在独立 OS 线程上创建并运行一个次级窗口的完整消息循环。
214
+ func (a *App) runWindow(w *Window) {
215
+ runtime.LockOSThread()
216
+ defer func() {
217
+ a.winMu.Lock()
218
+ delete(a.windows, w.id)
219
+ a.winMu.Unlock()
220
+ close(w.doneCh)
221
+ }()
222
+
223
+ if stop := func() bool { w.mu.Lock(); defer w.mu.Unlock(); return w.closing }(); stop {
224
+ return // Close 早于创建完成:直接退场
225
+ }
226
+
227
+ view := webview.New(w.spec.Debug)
228
+ if view == nil {
229
+ fmt.Printf("freedom: window %q: failed to create webview\n", w.id)
230
+ return
231
+ }
232
+ defer w.destroyView(view)
233
+ w.mu.Lock()
234
+ if w.closing {
235
+ w.mu.Unlock()
236
+ return
237
+ }
238
+ w.view = view
239
+ w.mu.Unlock()
240
+
241
+ view.SetTitle(w.spec.Title)
242
+ view.SetSize(w.spec.Width, w.spec.Height, webview.HintNone)
243
+ if w.spec.Center {
244
+ a.centerHWND(uintptr(view.Window()))
245
+ }
246
+
247
+ view.Init(jsSDK)
248
+ // 每窗口独立桥:调用路由同一后端,但结果回写与窗口动作作用于本窗口。
249
+ if err := view.Bind("__freedom_bridge", func(id float64, method, paramsJSON string) {
250
+ a.dispatchBridge(id, method, paramsJSON, w.evalJS)
251
+ }); err != nil {
252
+ fmt.Printf("freedom: window %q: bind bridge: %v\n", w.id, err)
253
+ return
254
+ }
255
+ if err := view.Bind("__freedom_window", func(action, paramsJSON string) (interface{}, error) {
256
+ return a.windowControlFor(w, action, paramsJSON)
257
+ }); err != nil {
258
+ fmt.Printf("freedom: window %q: bind window control: %v\n", w.id, err)
259
+ return
260
+ }
261
+ if w.spec.URL != "" {
262
+ view.Navigate(w.spec.URL)
263
+ } else {
264
+ html, err := a.resolvePage(w.spec)
265
+ if err != nil {
266
+ fmt.Printf("freedom: window %q: resolve html: %v\n", w.id, err)
267
+ return
268
+ }
269
+ view.SetHtml(html)
270
+ }
271
+ view.Run()
272
+ }
273
+
274
+ // resolvePage 按声明优先序解析次级窗口页面源(URL 分支不走本函数):
275
+ // HTML(Go 侧函数)> Page(前端 create 内联 JSON)> 主窗口 Config.HTML。
276
+ func (a *App) resolvePage(spec WindowSpec) (string, error) {
277
+ if spec.HTML != nil {
278
+ return spec.HTML()
279
+ }
280
+ if spec.Page != "" {
281
+ return spec.Page, nil
282
+ }
283
+ return a.resolveHTML()
284
+ }
285
+
286
+ // windowControlFor 处理次级窗口动作:窗口级动作在此拦截,其余下沉平台层
287
+ // (以本窗口 HWND 为目标)。
288
+ func (a *App) windowControlFor(w *Window, action, paramsJSON string) (result interface{}, err error) {
289
+ defer func() {
290
+ if r := recover(); r != nil {
291
+ result = nil
292
+ err = fmt.Errorf("freedom: window action %q panicked: %v", action, r)
293
+ }
294
+ }()
295
+ // M3 能力闸:与主窗口同闸(action 补 window. 前缀判定)。
296
+ if err := a.capCheckWindow(action); err != nil {
297
+ return nil, err
298
+ }
299
+ switch action {
300
+ case "id":
301
+ // 本窗口身份优先于 windowManage 的 main 应答。
302
+ return w.id, nil
303
+ case "close":
304
+ w.Close()
305
+ return nil, nil
306
+ }
307
+ // 管理动作(id/list/create/closeWindow/focusWindow)任意窗口皆可发起。
308
+ if res, ok, merr := a.windowManage(action, paramsJSON); ok {
309
+ return res, merr
310
+ }
311
+ return windowControl(w.handle(), action, a.cfg.TitleBar, paramsJSON)
312
+ }
313
+
314
+ // windowManage 处理窗口管理动作(create/list/closeWindow/focusWindow/id);
315
+ // 主窗口经 a.windowControl 前置调用,次级窗口经 windowControlFor 调用。
316
+ // 返回 ok=false 表示不是管理动作,交回普通窗口控制。
317
+ func (a *App) windowManage(action, paramsJSON string) (result interface{}, ok bool, err error) {
318
+ switch action {
319
+ case "id":
320
+ return mainWindowID, true, nil
321
+ case "create":
322
+ var spec WindowSpec
323
+ if err := json.Unmarshal([]byte(orEmptyJSON(paramsJSON)), &spec); err != nil {
324
+ return nil, true, fmt.Errorf("freedom: window.create params: %w", err)
325
+ }
326
+ w, err := a.NewWindow(spec)
327
+ if err != nil {
328
+ return nil, true, err
329
+ }
330
+ return map[string]string{"id": w.id}, true, nil
331
+ case "list":
332
+ return append(a.Windows(), mainWindowID), true, nil
333
+ case "closeWindow", "focusWindow":
334
+ var p struct {
335
+ ID string `json:"id"`
336
+ }
337
+ _ = json.Unmarshal([]byte(orEmptyJSON(paramsJSON)), &p)
338
+ w := a.Window(p.ID)
339
+ if w == nil {
340
+ return nil, true, fmt.Errorf("freedom: no such window: %q", p.ID)
341
+ }
342
+ if w.id == mainWindowID {
343
+ // 主窗口:closeWindow 仍拒绝(关主窗口=退应用,走 App.Quit/用户关窗);
344
+ // focusWindow 允许——M2 前主窗口是唯一窗口,聚焦即置顶主窗。
345
+ if action == "closeWindow" {
346
+ return nil, true, fmt.Errorf("freedom: cannot closeWindow main window")
347
+ }
348
+ _, err := windowControl(a.WindowHandle(), "focus", a.cfg.TitleBar, "{}")
349
+ return nil, true, err
350
+ }
351
+ if action == "closeWindow" {
352
+ w.Close()
353
+ return nil, true, nil
354
+ }
355
+ _, err := windowControl(w.handle(), "focus", a.cfg.TitleBar, "{}")
356
+ return nil, true, err
357
+ }
358
+ return nil, false, nil
359
+ }
360
+
361
+ func orEmptyJSON(s string) string {
362
+ if s == "" || s == "null" {
363
+ return "{}"
364
+ }
365
+ return s
366
+ }
367
+
368
+ // ---- 主窗口/广播接线 ----
369
+
370
+ const mainWindowID = "main"
371
+
372
+ // emitSecondary 把事件推给全部次级窗口(主窗口走既有 view 路径)。
373
+ func (a *App) emitSecondary(js string) {
374
+ a.winMu.Lock()
375
+ ws := make([]*Window, 0, len(a.windows))
376
+ for id, w := range a.windows {
377
+ if id != mainWindowID {
378
+ ws = append(ws, w)
379
+ }
380
+ }
381
+ a.winMu.Unlock()
382
+ for _, w := range ws {
383
+ w.evalJS(js)
384
+ }
385
+ }
@@ -2,80 +2,46 @@
2
2
 
3
3
  package freedom
4
4
 
5
- import (
6
- "fmt"
5
+ import "fmt"
7
6
 
8
- webview "github.com/webview/webview_go"
9
- )
7
+ // applyTitleBar 在 macOS / Linux 上为空实现:
8
+ // 原生标题栏由窗口管理器接管,hidden / frameless 模式暂回退为原生标题栏,
9
+ // 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
10
+ func (a *App) applyTitleBar() {}
10
11
 
11
- // applyTitleBar 在 macOS / Linux 上通过 webview 层的 set_decorated 控制原生标题栏:
12
- // - frameless:GTK 走 gtk_window_set_decorated(FALSE),Cocoa 走隐藏标题栏 + 透明标题栏
13
- // + 移除原生最小化/关闭按钮,彻底去除"UI 上方残留原生标题栏"问题;
14
- // - native:恢复系统原生标题栏。
15
- //
16
- // 窗口在 webview.New() 时已创建,故此处可安全调用(须在 UI 线程,即 Run 流程内)。
17
- func (a *App) applyTitleBar() {
18
- if a.view == nil {
19
- return
20
- }
21
- a.view.SetDecorated(a.cfg.TitleBar != TitleBarFrameless)
22
- }
23
-
24
- // setWindowIcon 在 macOS / Linux 上为空实现(图标由应用包 / 窗口管理器决定)。
25
- func (a *App) setWindowIcon() {}
26
-
27
- // webview2Available 非 Windows 平台恒返回 true(macOS WKWebView / Linux WebKitGTK,
28
- // 不依赖 WebView2 Runtime)。
29
- func webview2Available() bool { return true }
12
+ // installWindowEvents / uninstallWindowEvents 在非 Windows 平台为空实现
13
+ // (窗口事件与关闭拦截依赖 WndProc 子类化,仅 Windows 提供)。
14
+ func (a *App) installWindowEvents() {}
15
+ func (a *App) uninstallWindowEvents() {}
30
16
 
31
- // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 实现)。
32
- //
33
- // 窗口控制类动作(最小化/最大化/还原/关闭/查询)直接转发到 webview 层的原生
34
- // 窗口控制(GTK GtkWindow / Cocoa NSWindow),前端自绘标题栏三按钮因此可用:
35
- // - GTK: gtk_window_iconify / maximize / unmaximize / close / is_maximized
36
- // - Cocoa: performMiniaturize: / zoom: / performClose: / isZoomed
37
- //
38
- // 注意:webview 层窗口控制动作在 UI 线程执行(前端桥接本身运行于 UI 线程回调中,
39
- // webview_go 的 binding 回调在主线程派发),无需额外 Dispatch。
40
- func windowControl(a *App, action string) (interface{}, error) {
41
- if a.view == nil {
42
- return nil, fmt.Errorf("window not ready")
43
- }
44
- var act webview.WindowAction
17
+ // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 占位实现)。
18
+ // 动作集与 window_windows.go 保持一致:查询类返回中性值,动作类静默 no-op,
19
+ // 保证前端在任一平台调用同一方法不致因"未知动作"报错。
20
+ func windowControl(hwnd uintptr, action string, mode TitleBarMode, paramsJSON string) (interface{}, error) {
45
21
  switch action {
46
- case "minimize":
47
- act = webview.WindowMinimize
48
- case "maximize":
49
- act = webview.WindowMaximize
50
- case "unmaximize", "restore":
51
- act = webview.WindowRestore
52
- case "toggleMaximize":
53
- act = webview.WindowToggleMaximize
54
- case "close":
55
- act = webview.WindowClose
56
- case "isMaximized":
57
- act = webview.WindowIsMaximized
58
22
  case "isFrameless":
59
- return a.cfg.TitleBar == TitleBarFrameless, nil
60
- case "appIcon":
61
- // macOS / Linux 暂不提供 exe 内嵌图标提取,前端隐藏标题栏图标。
62
- return "", nil
63
- case "startDrag":
64
- // 无边框窗口拖动:GTK 走 gtk_window_begin_move_drag(root_x/y=-1 用当前指针),
65
- // Cocoa 走 performWindowDragWithEvent:(用当前鼠标位置合成事件)。
66
- // 与 Windows 的 WM_NCLBUTTONDOWN+HTCAPTION 语义等价,保留页面双击/右键事件。
67
- if a.view.BeginMoveDrag() != 0 {
68
- return nil, fmt.Errorf("window drag is not supported on this platform")
23
+ // 仅 frameless 返回 true(与 window_windows.go 语义一致);
24
+ // hidden 模式回退原生标题栏,前端不应自绘按钮。
25
+ return mode == TitleBarFrameless, nil
26
+ case "isMaximized", "isMinimized", "isVisible", "isFocused", "isFullscreen":
27
+ if action == "isVisible" {
28
+ return true, nil
69
29
  }
30
+ return false, nil
31
+ case "getInfo":
32
+ return map[string]interface{}{
33
+ "outerWidth": 0, "outerHeight": 0,
34
+ "innerWidth": 0, "innerHeight": 0,
35
+ "scaleFactor": 1.0,
36
+ "visible": true,
37
+ }, nil
38
+ case "minimize", "maximize", "unmaximize", "restore", "toggleMaximize", "close",
39
+ "setPosition", "setSize", "getPosition", "getSize", "innerSize", "center",
40
+ "setTitle", "show", "hide", "focus", "setAlwaysOnTop", "setSkipTaskbar",
41
+ "setResizable", "setMaximizable", "setMinimizable", "setFullscreen", "interceptClose":
42
+ // 非 Windows 平台暂不提供底层窗口控制,前端自绘按钮可对事件静默处理
70
43
  return nil, nil
71
44
  default:
72
45
  return nil, fmt.Errorf("unknown window action %q", action)
73
46
  }
74
- if act == webview.WindowIsMaximized {
75
- return a.view.WindowControl(act) == 1, nil
76
- }
77
- if r := a.view.WindowControl(act); r != 0 {
78
- return nil, fmt.Errorf("window action %q is not supported on this platform", action)
79
- }
80
- return nil, nil
81
47
  }