@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,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
|
-
|
|
9
|
-
|
|
7
|
+
// applyTitleBar 在 macOS / Linux 上为空实现:
|
|
8
|
+
// 原生标题栏由窗口管理器接管,hidden / frameless 模式暂回退为原生标题栏,
|
|
9
|
+
// 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
|
|
10
|
+
func (a *App) applyTitleBar() {}
|
|
10
11
|
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
//
|
|
34
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
}
|