@yufengtadian/freedom-cli 1.12.0 → 1.12.12
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 +25 -29
- package/lib/build.js +1 -1
- package/lib/cli.js +10 -10
- package/lib/config.js +115 -31
- package/lib/tui.js +1 -1
- package/lib/utils.js +7 -3
- package/package.json +1 -1
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/build_out.txt +0 -0
- package/templates/go/err.txt +2 -0
- package/templates/go/err10.txt +0 -0
- package/templates/go/err11.txt +0 -0
- package/templates/go/err2.txt +0 -0
- package/templates/go/err3.txt +0 -0
- package/templates/go/err4.txt +1 -0
- package/templates/go/err5.txt +0 -0
- package/templates/go/err6.txt +0 -0
- package/templates/go/err7.txt +0 -0
- package/templates/go/err8.txt +0 -0
- package/templates/go/err9.txt +0 -0
- package/templates/go/go.mod +6 -0
- package/templates/go/go.mod.bak_gbk +11 -0
- package/templates/go/go.sum +2 -2
- package/templates/go/pkg/freedom/assets/freedom.js +6 -0
- package/templates/go/pkg/freedom/assets/index.html +1 -1
- package/templates/go/pkg/freedom/configfile.go +0 -2
- package/templates/go/pkg/freedom/freedom.go +16 -10
- package/templates/go/pkg/freedom/window_other.go +60 -12
- package/templates/go/pkg/freedom/window_windows.go +244 -29
- package/templates/go/tidy.txt +0 -0
- package/templates/go/vet_out.txt +0 -0
- package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
- package/templates/go/webview_go/CHANGELOG.md +15 -0
- package/templates/go/webview_go/LICENSE +22 -0
- package/templates/go/webview_go/README.md +50 -0
- package/templates/go/webview_go/examples/basic/main.go +12 -0
- package/templates/go/webview_go/examples/bind/main.go +38 -0
- package/templates/go/webview_go/glue.c +36 -0
- package/templates/go/webview_go/go.mod +3 -0
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/include/webview.h +3840 -0
- package/templates/go/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/webview_go/webview.cc +1 -0
- package/templates/go/webview_go/webview.go +371 -0
- package/templates/go/webview_go/webview_test.go +50 -0
- package/templates/project/freedom.config.js +5 -5
- package/templates/project/index.html +45 -5
- package/templates/project/src/main.js +140 -6
- package/tutorial/tutorial.html +4 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fb6b17d826041411e6346cd9a785a5ceba7987c4
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#include "webview.h"
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
package webview
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#cgo CFLAGS: -I${SRCDIR}/libs/webview/include
|
|
5
|
+
#cgo CXXFLAGS: -I${SRCDIR}/libs/webview/include -DWEBVIEW_STATIC
|
|
6
|
+
|
|
7
|
+
#cgo linux openbsd freebsd netbsd CXXFLAGS: -DWEBVIEW_GTK -std=c++11
|
|
8
|
+
#cgo linux openbsd freebsd netbsd LDFLAGS: -ldl
|
|
9
|
+
#cgo linux openbsd freebsd netbsd pkg-config: gtk+-3.0 webkit2gtk-4.0
|
|
10
|
+
|
|
11
|
+
#cgo darwin CXXFLAGS: -DWEBVIEW_COCOA -std=c++11
|
|
12
|
+
#cgo darwin LDFLAGS: -framework WebKit -ldl
|
|
13
|
+
|
|
14
|
+
#cgo windows CXXFLAGS: -DWEBVIEW_EDGE -std=c++14 -I${SRCDIR}/libs/mswebview2/include
|
|
15
|
+
#cgo windows LDFLAGS: -static -ladvapi32 -lole32 -lshell32 -lshlwapi -luser32 -lversion
|
|
16
|
+
|
|
17
|
+
#include "webview.h"
|
|
18
|
+
|
|
19
|
+
#include <stdlib.h>
|
|
20
|
+
#include <stdint.h>
|
|
21
|
+
|
|
22
|
+
void CgoWebViewDispatch(webview_t w, uintptr_t arg);
|
|
23
|
+
void CgoWebViewBind(webview_t w, const char *name, uintptr_t index);
|
|
24
|
+
void CgoWebViewUnbind(webview_t w, const char *name);
|
|
25
|
+
*/
|
|
26
|
+
import "C"
|
|
27
|
+
import (
|
|
28
|
+
_ "github.com/webview/webview_go/libs/mswebview2"
|
|
29
|
+
_ "github.com/webview/webview_go/libs/mswebview2/include"
|
|
30
|
+
_ "github.com/webview/webview_go/libs/webview"
|
|
31
|
+
_ "github.com/webview/webview_go/libs/webview/include"
|
|
32
|
+
"encoding/json"
|
|
33
|
+
"errors"
|
|
34
|
+
"reflect"
|
|
35
|
+
"runtime"
|
|
36
|
+
"sync"
|
|
37
|
+
"unsafe"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
func init() {
|
|
41
|
+
// Ensure that main.main is called from the main thread
|
|
42
|
+
runtime.LockOSThread()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Hints are used to configure window sizing and resizing
|
|
46
|
+
type Hint int
|
|
47
|
+
|
|
48
|
+
// WindowAction 是原生窗口控制动作(前端自绘标题栏三按钮使用)。
|
|
49
|
+
type WindowAction int
|
|
50
|
+
|
|
51
|
+
const (
|
|
52
|
+
// WindowMinimize 最小化窗口
|
|
53
|
+
WindowMinimize WindowAction = iota
|
|
54
|
+
// WindowMaximize 最大化窗口
|
|
55
|
+
WindowMaximize
|
|
56
|
+
// WindowUnmaximize 取消最大化
|
|
57
|
+
WindowUnmaximize
|
|
58
|
+
// WindowRestore 还原窗口(取消最大化并恢复到前台)
|
|
59
|
+
WindowRestore
|
|
60
|
+
// WindowClose 关闭窗口
|
|
61
|
+
WindowClose
|
|
62
|
+
// WindowToggleMaximize 在最大化/还原间切换
|
|
63
|
+
WindowToggleMaximize
|
|
64
|
+
// WindowIsMaximized 查询窗口是否最大化
|
|
65
|
+
WindowIsMaximized
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const (
|
|
69
|
+
// Width and height are default size
|
|
70
|
+
HintNone = C.WEBVIEW_HINT_NONE
|
|
71
|
+
|
|
72
|
+
// Window size can not be changed by a user
|
|
73
|
+
HintFixed = C.WEBVIEW_HINT_FIXED
|
|
74
|
+
|
|
75
|
+
// Width and height are minimum bounds
|
|
76
|
+
HintMin = C.WEBVIEW_HINT_MIN
|
|
77
|
+
|
|
78
|
+
// Width and height are maximum bounds
|
|
79
|
+
HintMax = C.WEBVIEW_HINT_MAX
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
type WebView interface {
|
|
83
|
+
|
|
84
|
+
// Run runs the main loop until it's terminated. After this function exits -
|
|
85
|
+
// you must destroy the webview.
|
|
86
|
+
Run()
|
|
87
|
+
|
|
88
|
+
// Terminate stops the main loop. It is safe to call this function from
|
|
89
|
+
// a background thread.
|
|
90
|
+
Terminate()
|
|
91
|
+
|
|
92
|
+
// Dispatch posts a function to be executed on the main thread. You normally
|
|
93
|
+
// do not need to call this function, unless you want to tweak the native
|
|
94
|
+
// window.
|
|
95
|
+
Dispatch(f func())
|
|
96
|
+
|
|
97
|
+
// Destroy destroys a webview and closes the native window.
|
|
98
|
+
Destroy()
|
|
99
|
+
|
|
100
|
+
// Window returns a native window handle pointer. When using GTK backend the
|
|
101
|
+
// pointer is GtkWindow pointer, when using Cocoa backend the pointer is
|
|
102
|
+
// NSWindow pointer, when using Win32 backend the pointer is HWND pointer.
|
|
103
|
+
Window() unsafe.Pointer
|
|
104
|
+
|
|
105
|
+
// SetTitle updates the title of the native window. Must be called from the UI
|
|
106
|
+
// thread.
|
|
107
|
+
SetTitle(title string)
|
|
108
|
+
|
|
109
|
+
// SetDecorated controls whether the native window shows its system title
|
|
110
|
+
// bar decoration. frameless mode should call SetDecorated(false) so the
|
|
111
|
+
// front-end can draw its own min/max/close buttons. Must be called from
|
|
112
|
+
// the UI thread. On Windows this is a no-op (handled by the freedom shell
|
|
113
|
+
// layer via WS_CAPTION / WM_NCHITTEST).
|
|
114
|
+
SetDecorated(decorated bool)
|
|
115
|
+
|
|
116
|
+
// WindowControl 执行原生窗口控制动作(最小化/最大化/还原/关闭/查询)。
|
|
117
|
+
// 返回 0 表示成功,-1 表示平台不支持;WindowIsMaximized 返回 1/0。
|
|
118
|
+
// 供前端自绘标题栏三按钮调用。Windows 上由 freedom 壳层处理,此处返回 -1。
|
|
119
|
+
WindowControl(action WindowAction) int
|
|
120
|
+
|
|
121
|
+
// SetSize updates native window size. See Hint constants.
|
|
122
|
+
SetSize(w int, h int, hint Hint)
|
|
123
|
+
|
|
124
|
+
// Navigate navigates webview to the given URL. URL may be a properly encoded data.
|
|
125
|
+
// URI. Examples:
|
|
126
|
+
// w.Navigate("https://github.com/webview/webview")
|
|
127
|
+
// w.Navigate("data:text/html,%3Ch1%3EHello%3C%2Fh1%3E")
|
|
128
|
+
// w.Navigate("data:text/html;base64,PGgxPkhlbGxvPC9oMT4=")
|
|
129
|
+
Navigate(url string)
|
|
130
|
+
|
|
131
|
+
// SetHtml sets the webview HTML directly.
|
|
132
|
+
// Example: w.SetHtml(w, "<h1>Hello</h1>");
|
|
133
|
+
SetHtml(html string)
|
|
134
|
+
|
|
135
|
+
// Init injects JavaScript code at the initialization of the new page. Every
|
|
136
|
+
// time the webview will open a the new page - this initialization code will
|
|
137
|
+
// be executed. It is guaranteed that code is executed before window.onload.
|
|
138
|
+
Init(js string)
|
|
139
|
+
|
|
140
|
+
// Eval evaluates arbitrary JavaScript code. Evaluation happens asynchronously,
|
|
141
|
+
// also the result of the expression is ignored. Use RPC bindings if you want
|
|
142
|
+
// to receive notifications about the results of the evaluation.
|
|
143
|
+
Eval(js string)
|
|
144
|
+
|
|
145
|
+
// Bind binds a callback function so that it will appear under the given name
|
|
146
|
+
// as a global JavaScript function. Internally it uses webview_init().
|
|
147
|
+
// Callback receives a request string and a user-provided argument pointer.
|
|
148
|
+
// Request string is a JSON array of all the arguments passed to the
|
|
149
|
+
// JavaScript function.
|
|
150
|
+
//
|
|
151
|
+
// f must be a function
|
|
152
|
+
// f must return either value and error or just error
|
|
153
|
+
Bind(name string, f interface{}) error
|
|
154
|
+
|
|
155
|
+
// Removes a callback that was previously set by Bind.
|
|
156
|
+
Unbind(name string) error
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
type webview struct {
|
|
160
|
+
w C.webview_t
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
var (
|
|
164
|
+
m sync.Mutex
|
|
165
|
+
index uintptr
|
|
166
|
+
dispatch = map[uintptr]func(){}
|
|
167
|
+
bindings = map[uintptr]func(id, req string) (interface{}, error){}
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
func boolToInt(b bool) C.int {
|
|
171
|
+
if b {
|
|
172
|
+
return 1
|
|
173
|
+
}
|
|
174
|
+
return 0
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// New calls NewWindow to create a new window and a new webview instance. If debug
|
|
178
|
+
// is non-zero - developer tools will be enabled (if the platform supports them).
|
|
179
|
+
func New(debug bool) WebView { return NewWindow(debug, nil) }
|
|
180
|
+
|
|
181
|
+
// NewWindow creates a new webview instance. If debug is non-zero - developer
|
|
182
|
+
// tools will be enabled (if the platform supports them). Window parameter can be
|
|
183
|
+
// a pointer to the native window handle. If it's non-null - then child WebView is
|
|
184
|
+
// embedded into the given parent window. Otherwise a new window is created.
|
|
185
|
+
// Depending on the platform, a GtkWindow, NSWindow or HWND pointer can be passed
|
|
186
|
+
// here.
|
|
187
|
+
func NewWindow(debug bool, window unsafe.Pointer) WebView {
|
|
188
|
+
w := &webview{}
|
|
189
|
+
w.w = C.webview_create(boolToInt(debug), window)
|
|
190
|
+
return w
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
func (w *webview) Destroy() {
|
|
194
|
+
C.webview_destroy(w.w)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
func (w *webview) Run() {
|
|
198
|
+
C.webview_run(w.w)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
func (w *webview) Terminate() {
|
|
202
|
+
C.webview_terminate(w.w)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
func (w *webview) Window() unsafe.Pointer {
|
|
206
|
+
return C.webview_get_window(w.w)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
func (w *webview) Navigate(url string) {
|
|
210
|
+
s := C.CString(url)
|
|
211
|
+
defer C.free(unsafe.Pointer(s))
|
|
212
|
+
C.webview_navigate(w.w, s)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
func (w *webview) SetHtml(html string) {
|
|
216
|
+
s := C.CString(html)
|
|
217
|
+
defer C.free(unsafe.Pointer(s))
|
|
218
|
+
C.webview_set_html(w.w, s)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func (w *webview) SetTitle(title string) {
|
|
222
|
+
s := C.CString(title)
|
|
223
|
+
defer C.free(unsafe.Pointer(s))
|
|
224
|
+
C.webview_set_title(w.w, s)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
func (w *webview) SetDecorated(decorated bool) {
|
|
228
|
+
C.webview_set_decorated(w.w, boolToInt(decorated))
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
func (w *webview) WindowControl(action WindowAction) int {
|
|
232
|
+
return int(C.webview_window_control(w.w, C.webview_window_action_t(action)))
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func (w *webview) SetSize(width int, height int, hint Hint) {
|
|
236
|
+
C.webview_set_size(w.w, C.int(width), C.int(height), C.webview_hint_t(hint))
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func (w *webview) Init(js string) {
|
|
240
|
+
s := C.CString(js)
|
|
241
|
+
defer C.free(unsafe.Pointer(s))
|
|
242
|
+
C.webview_init(w.w, s)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func (w *webview) Eval(js string) {
|
|
246
|
+
s := C.CString(js)
|
|
247
|
+
defer C.free(unsafe.Pointer(s))
|
|
248
|
+
C.webview_eval(w.w, s)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
func (w *webview) Dispatch(f func()) {
|
|
252
|
+
m.Lock()
|
|
253
|
+
for ; dispatch[index] != nil; index++ {
|
|
254
|
+
}
|
|
255
|
+
dispatch[index] = f
|
|
256
|
+
m.Unlock()
|
|
257
|
+
C.CgoWebViewDispatch(w.w, C.uintptr_t(index))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
//export _webviewDispatchGoCallback
|
|
261
|
+
func _webviewDispatchGoCallback(index unsafe.Pointer) {
|
|
262
|
+
m.Lock()
|
|
263
|
+
f := dispatch[uintptr(index)]
|
|
264
|
+
delete(dispatch, uintptr(index))
|
|
265
|
+
m.Unlock()
|
|
266
|
+
f()
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
//export _webviewBindingGoCallback
|
|
270
|
+
func _webviewBindingGoCallback(w C.webview_t, id *C.char, req *C.char, index uintptr) {
|
|
271
|
+
m.Lock()
|
|
272
|
+
f := bindings[uintptr(index)]
|
|
273
|
+
m.Unlock()
|
|
274
|
+
jsString := func(v interface{}) string { b, _ := json.Marshal(v); return string(b) }
|
|
275
|
+
status, result := 0, ""
|
|
276
|
+
if res, err := f(C.GoString(id), C.GoString(req)); err != nil {
|
|
277
|
+
status = -1
|
|
278
|
+
result = jsString(err.Error())
|
|
279
|
+
} else if b, err := json.Marshal(res); err != nil {
|
|
280
|
+
status = -1
|
|
281
|
+
result = jsString(err.Error())
|
|
282
|
+
} else {
|
|
283
|
+
status = 0
|
|
284
|
+
result = string(b)
|
|
285
|
+
}
|
|
286
|
+
s := C.CString(result)
|
|
287
|
+
defer C.free(unsafe.Pointer(s))
|
|
288
|
+
C.webview_return(w, id, C.int(status), s)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
func (w *webview) Bind(name string, f interface{}) error {
|
|
292
|
+
v := reflect.ValueOf(f)
|
|
293
|
+
// f must be a function
|
|
294
|
+
if v.Kind() != reflect.Func {
|
|
295
|
+
return errors.New("only functions can be bound")
|
|
296
|
+
}
|
|
297
|
+
// f must return either value and error or just error
|
|
298
|
+
if n := v.Type().NumOut(); n > 2 {
|
|
299
|
+
return errors.New("function may only return a value or a value+error")
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
binding := func(id, req string) (interface{}, error) {
|
|
303
|
+
raw := []json.RawMessage{}
|
|
304
|
+
if err := json.Unmarshal([]byte(req), &raw); err != nil {
|
|
305
|
+
return nil, err
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
isVariadic := v.Type().IsVariadic()
|
|
309
|
+
numIn := v.Type().NumIn()
|
|
310
|
+
if (isVariadic && len(raw) < numIn-1) || (!isVariadic && len(raw) != numIn) {
|
|
311
|
+
return nil, errors.New("function arguments mismatch")
|
|
312
|
+
}
|
|
313
|
+
args := []reflect.Value{}
|
|
314
|
+
for i := range raw {
|
|
315
|
+
var arg reflect.Value
|
|
316
|
+
if isVariadic && i >= numIn-1 {
|
|
317
|
+
arg = reflect.New(v.Type().In(numIn - 1).Elem())
|
|
318
|
+
} else {
|
|
319
|
+
arg = reflect.New(v.Type().In(i))
|
|
320
|
+
}
|
|
321
|
+
if err := json.Unmarshal(raw[i], arg.Interface()); err != nil {
|
|
322
|
+
return nil, err
|
|
323
|
+
}
|
|
324
|
+
args = append(args, arg.Elem())
|
|
325
|
+
}
|
|
326
|
+
errorType := reflect.TypeOf((*error)(nil)).Elem()
|
|
327
|
+
res := v.Call(args)
|
|
328
|
+
switch len(res) {
|
|
329
|
+
case 0:
|
|
330
|
+
// No results from the function, just return nil
|
|
331
|
+
return nil, nil
|
|
332
|
+
case 1:
|
|
333
|
+
// One result may be a value, or an error
|
|
334
|
+
if res[0].Type().Implements(errorType) {
|
|
335
|
+
if res[0].Interface() != nil {
|
|
336
|
+
return nil, res[0].Interface().(error)
|
|
337
|
+
}
|
|
338
|
+
return nil, nil
|
|
339
|
+
}
|
|
340
|
+
return res[0].Interface(), nil
|
|
341
|
+
case 2:
|
|
342
|
+
// Two results: first one is value, second is error
|
|
343
|
+
if !res[1].Type().Implements(errorType) {
|
|
344
|
+
return nil, errors.New("second return value must be an error")
|
|
345
|
+
}
|
|
346
|
+
if res[1].Interface() == nil {
|
|
347
|
+
return res[0].Interface(), nil
|
|
348
|
+
}
|
|
349
|
+
return res[0].Interface(), res[1].Interface().(error)
|
|
350
|
+
default:
|
|
351
|
+
return nil, errors.New("unexpected number of return values")
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
m.Lock()
|
|
356
|
+
for ; bindings[index] != nil; index++ {
|
|
357
|
+
}
|
|
358
|
+
bindings[index] = binding
|
|
359
|
+
m.Unlock()
|
|
360
|
+
cname := C.CString(name)
|
|
361
|
+
defer C.free(unsafe.Pointer(cname))
|
|
362
|
+
C.CgoWebViewBind(w.w, cname, C.uintptr_t(index))
|
|
363
|
+
return nil
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
func (w *webview) Unbind(name string) error {
|
|
367
|
+
cname := C.CString(name)
|
|
368
|
+
defer C.free(unsafe.Pointer(cname))
|
|
369
|
+
C.CgoWebViewUnbind(w.w, cname)
|
|
370
|
+
return nil
|
|
371
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
package webview
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"flag"
|
|
5
|
+
"log"
|
|
6
|
+
"os"
|
|
7
|
+
"testing"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
func Example() {
|
|
11
|
+
w := New(true)
|
|
12
|
+
defer w.Destroy()
|
|
13
|
+
w.SetTitle("Hello")
|
|
14
|
+
w.Bind("noop", func() string {
|
|
15
|
+
log.Println("hello")
|
|
16
|
+
return "hello"
|
|
17
|
+
})
|
|
18
|
+
w.Bind("add", func(a, b int) int {
|
|
19
|
+
return a + b
|
|
20
|
+
})
|
|
21
|
+
w.Bind("quit", func() {
|
|
22
|
+
w.Terminate()
|
|
23
|
+
})
|
|
24
|
+
w.SetHtml(`<!doctype html>
|
|
25
|
+
<html>
|
|
26
|
+
<body>hello</body>
|
|
27
|
+
<script>
|
|
28
|
+
window.onload = function() {
|
|
29
|
+
document.body.innerText = ` + "`hello, ${navigator.userAgent}`" + `;
|
|
30
|
+
noop().then(function(res) {
|
|
31
|
+
console.log('noop res', res);
|
|
32
|
+
add(1, 2).then(function(res) {
|
|
33
|
+
console.log('add res', res);
|
|
34
|
+
quit();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
</script>
|
|
39
|
+
</html>
|
|
40
|
+
)`)
|
|
41
|
+
w.Run()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestMain(m *testing.M) {
|
|
45
|
+
flag.Parse()
|
|
46
|
+
if testing.Verbose() {
|
|
47
|
+
Example()
|
|
48
|
+
}
|
|
49
|
+
os.Exit(m.Run())
|
|
50
|
+
}
|
|
@@ -18,11 +18,11 @@ export default {
|
|
|
18
18
|
// 是否开启 WebView 开发者工具。
|
|
19
19
|
debug: false,
|
|
20
20
|
|
|
21
|
-
//
|
|
22
|
-
// 'native' -
|
|
23
|
-
// '
|
|
24
|
-
//
|
|
25
|
-
// 终端命令:freedom titlebar <native|
|
|
21
|
+
// 标题栏策略,在终端随时可改,二选一:
|
|
22
|
+
// 'native' - 保留系统原生标题栏,标题栏图标与 exe 图标一致
|
|
23
|
+
// 'frameless' - 完全无边框,标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,
|
|
24
|
+
// 关闭 / 最大化 / 最小化按钮由前端自绘(模板已内置示例,默认)
|
|
25
|
+
// 终端命令:freedom titlebar <native|frameless>
|
|
26
26
|
titlebar: 'frameless',
|
|
27
27
|
|
|
28
28
|
// 应用图标(可执行文件 / .app 的图标):
|
|
@@ -24,12 +24,21 @@
|
|
|
24
24
|
flex: 0 0 38px;
|
|
25
25
|
align-items: center;
|
|
26
26
|
justify-content: space-between;
|
|
27
|
-
padding-left:
|
|
27
|
+
padding-left: 12px;
|
|
28
28
|
background: rgba(17, 24, 39, .92);
|
|
29
29
|
border-bottom: 1px solid rgba(156, 163, 175, .15);
|
|
30
|
-
|
|
30
|
+
/* 拖动改为 JS 实现(startDrag),以便保留双击最大化 / 右键菜单事件 */
|
|
31
|
+
-webkit-user-select: none;
|
|
32
|
+
}
|
|
33
|
+
.titlebar-left { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
34
|
+
.tb-icon {
|
|
35
|
+
width: 16px; height: 16px; flex: 0 0 16px;
|
|
36
|
+
object-fit: contain; display: none;
|
|
37
|
+
}
|
|
38
|
+
.titlebar .tb-title {
|
|
39
|
+
font-size: 13px; color: #9ca3af;
|
|
40
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
31
41
|
}
|
|
32
|
-
.titlebar .tb-title { font-size: 13px; color: #9ca3af; }
|
|
33
42
|
.tb-btns { display: flex; height: 100%; -webkit-app-region: no-drag; }
|
|
34
43
|
.tb-btn {
|
|
35
44
|
width: 46px; height: 100%;
|
|
@@ -42,6 +51,25 @@
|
|
|
42
51
|
.tb-btn.close:hover { background: #e81123; color: #fff; }
|
|
43
52
|
body.freedom-frameless .titlebar { display: flex; }
|
|
44
53
|
|
|
54
|
+
/* 标题栏右键菜单 */
|
|
55
|
+
.tb-menu {
|
|
56
|
+
position: fixed; z-index: 9999;
|
|
57
|
+
min-width: 160px; padding: 4px;
|
|
58
|
+
background: rgba(30, 41, 59, .98);
|
|
59
|
+
border: 1px solid rgba(156, 163, 175, .25);
|
|
60
|
+
border-radius: 8px;
|
|
61
|
+
box-shadow: 0 8px 24px rgba(0, 0, 0, .4);
|
|
62
|
+
display: none;
|
|
63
|
+
}
|
|
64
|
+
.tb-menu-item {
|
|
65
|
+
display: flex; align-items: center;
|
|
66
|
+
padding: 6px 12px; font-size: 13px; color: #e5e7eb;
|
|
67
|
+
border-radius: 5px; cursor: default;
|
|
68
|
+
}
|
|
69
|
+
.tb-menu-item:hover { background: rgba(99, 102, 241, .35); }
|
|
70
|
+
.tb-menu-item.danger:hover { background: #e81123; }
|
|
71
|
+
.tb-menu-divider { height: 1px; margin: 4px 6px; background: rgba(156, 163, 175, .2); }
|
|
72
|
+
|
|
45
73
|
main {
|
|
46
74
|
flex: 1;
|
|
47
75
|
display: flex; flex-direction: column;
|
|
@@ -61,8 +89,11 @@
|
|
|
61
89
|
</style>
|
|
62
90
|
</head>
|
|
63
91
|
<body>
|
|
64
|
-
<div class="titlebar">
|
|
65
|
-
<div class="
|
|
92
|
+
<div class="titlebar" id="titlebar">
|
|
93
|
+
<div class="titlebar-left">
|
|
94
|
+
<img class="tb-icon" id="tbIcon" alt="" draggable="false">
|
|
95
|
+
<div class="tb-title" id="tbTitle">Freedom App</div>
|
|
96
|
+
</div>
|
|
66
97
|
<div class="tb-btns">
|
|
67
98
|
<button class="tb-btn" id="tbMin"></button>
|
|
68
99
|
<button class="tb-btn" id="tbMax"></button>
|
|
@@ -70,6 +101,15 @@
|
|
|
70
101
|
</div>
|
|
71
102
|
</div>
|
|
72
103
|
|
|
104
|
+
<!-- 标题栏右键菜单 -->
|
|
105
|
+
<div class="tb-menu" id="tbMenu">
|
|
106
|
+
<div class="tb-menu-item" data-action="restore">还原</div>
|
|
107
|
+
<div class="tb-menu-item" data-action="minimize">最小化</div>
|
|
108
|
+
<div class="tb-menu-item" data-action="maximize">最大化</div>
|
|
109
|
+
<div class="tb-menu-divider"></div>
|
|
110
|
+
<div class="tb-menu-item danger" data-action="close">关闭</div>
|
|
111
|
+
</div>
|
|
112
|
+
|
|
73
113
|
<main>
|
|
74
114
|
<h1>Freedom App</h1>
|
|
75
115
|
<div class="desc">你的桌面应用已就绪。</div>
|
|
@@ -5,15 +5,149 @@
|
|
|
5
5
|
// window.minimize() / maximize() / close() 等窗口控制(无边框模式自绘按钮用)
|
|
6
6
|
|
|
7
7
|
// 无边框模式:显示自绘标题栏并绑定按钮。
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
// 壳桥接(__freedom_window)注入时机与页面脚本加载先后不定,早期调用可能被 reject 吞掉导致标题栏不显示,
|
|
9
|
+
// 因此先轮询等待桥接就绪(最多 100 次 × 100ms)再初始化。
|
|
10
|
+
function waitForBridge(tries = 100, interval = 100) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
const check = () => {
|
|
13
|
+
if (window.freedom && window.freedom.window && typeof window.__freedom_window === 'function') {
|
|
14
|
+
resolve(true);
|
|
15
|
+
} else if (tries-- > 0) {
|
|
16
|
+
setTimeout(check, interval);
|
|
17
|
+
} else {
|
|
18
|
+
resolve(false);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
check();
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 统一执行窗口控制动作:成功返回结果,失败打印告警并返回 undefined。
|
|
26
|
+
// 避免 mac/linux 等平台窗口控制不可用时的"静默无反应"假死体验。
|
|
27
|
+
function runWindow(w, action, fallback) {
|
|
28
|
+
return w[action]().catch((e) => {
|
|
29
|
+
console.warn('[freedom] window action "' + action + '" failed:', e && e.message ? e.message : e);
|
|
30
|
+
if (typeof fallback === 'function') return fallback();
|
|
31
|
+
return undefined;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 同步最大化状态到自绘按钮图标(最大化 ⇄ 还原)与 body 状态类。
|
|
36
|
+
async function syncMaxState(w) {
|
|
37
|
+
const maxBtn = document.getElementById('tbMax');
|
|
38
|
+
if (!maxBtn) return;
|
|
39
|
+
try {
|
|
40
|
+
const isMax = await w.isMaximized();
|
|
41
|
+
// Segoe MDL2:E922=最大化,E923=还原
|
|
42
|
+
maxBtn.innerHTML = isMax ? '\uE923' : '\uE922';
|
|
43
|
+
document.body.classList.toggle('freedom-maximized', !!isMax);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
// 平台不支持状态查询(mac/linux)时保持默认最大化图标
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function initFrameless() {
|
|
50
|
+
if (!(await waitForBridge())) return;
|
|
51
|
+
try {
|
|
52
|
+
const frameless = await window.freedom.window.isFrameless();
|
|
53
|
+
if (!frameless) return;
|
|
54
|
+
document.body.classList.add('freedom-frameless');
|
|
55
|
+
const w = window.freedom.window;
|
|
56
|
+
w.bindButtons({ min: '#tbMin', max: '#tbMax', close: '#tbClose' });
|
|
57
|
+
|
|
58
|
+
// 应用图标:从 exe 内嵌图标提取,不依赖 resources 资源文件夹
|
|
59
|
+
const iconUrl = await w.appIcon();
|
|
60
|
+
const iconEl = document.getElementById('tbIcon');
|
|
61
|
+
if (iconUrl && iconEl) {
|
|
62
|
+
iconEl.src = iconUrl;
|
|
63
|
+
iconEl.style.display = 'block';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// JS 拖动标题栏(保留双击 / 右键事件;非 Windows 平台桥接失败则退化为原生 drag 体验)
|
|
67
|
+
const bar = document.getElementById('titlebar');
|
|
68
|
+
if (bar) {
|
|
69
|
+
bar.addEventListener('mousedown', (e) => {
|
|
70
|
+
if (e.button === 0 && e.target.closest('.tb-btn') === null) {
|
|
71
|
+
runWindow(w, 'startDrag');
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
// 双击最大化 / 还原
|
|
75
|
+
bar.addEventListener('dblclick', async (e) => {
|
|
76
|
+
if (e.target.closest('.tb-btn') !== null) return;
|
|
77
|
+
const isMax = await runWindow(w, 'isMaximized', async () => false);
|
|
78
|
+
if (isMax) { await runWindow(w, 'restore', () => runWindow(w, 'toggleMaximize')); }
|
|
79
|
+
else { await runWindow(w, 'maximize', () => runWindow(w, 'toggleMaximize')); }
|
|
80
|
+
syncMaxState(w);
|
|
81
|
+
});
|
|
82
|
+
// 右键菜单
|
|
83
|
+
bar.addEventListener('contextmenu', async (e) => {
|
|
84
|
+
if (e.target.closest('.tb-btn') !== null) return;
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
openTitlebarMenu(e.clientX, e.clientY);
|
|
87
|
+
});
|
|
13
88
|
}
|
|
14
|
-
|
|
89
|
+
|
|
90
|
+
// 初始化同步最大化按钮状态;窗口尺寸变化时(含拖拽到屏幕边缘触发的系统最大化)重新同步
|
|
91
|
+
syncMaxState(w);
|
|
92
|
+
window.addEventListener('resize', () => syncMaxState(w));
|
|
93
|
+
} catch (e) {
|
|
94
|
+
/* 桥接异常时保持默认样式 */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 标题栏右键菜单
|
|
99
|
+
const tbMenu = () => document.getElementById('tbMenu');
|
|
100
|
+
|
|
101
|
+
function openTitlebarMenu(x, y) {
|
|
102
|
+
const menu = tbMenu();
|
|
103
|
+
const win = window.freedom.window;
|
|
104
|
+
menu.style.left = x + 'px';
|
|
105
|
+
menu.style.top = y + 'px';
|
|
106
|
+
menu.style.display = 'block';
|
|
107
|
+
|
|
108
|
+
// 根据当前窗口状态动态置灰「最大化 / 还原」
|
|
109
|
+
const updateItems = async () => {
|
|
110
|
+
const isMax = await runWindow(win, 'isMaximized', () => false);
|
|
111
|
+
menu.querySelectorAll('.tb-menu-item').forEach((item) => {
|
|
112
|
+
const action = item.dataset.action;
|
|
113
|
+
const disabled = (isMax && action === 'maximize') || (!isMax && action === 'restore');
|
|
114
|
+
item.style.opacity = disabled ? '0.4' : '1';
|
|
115
|
+
item.style.pointerEvents = disabled ? 'none' : 'auto';
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
updateItems();
|
|
119
|
+
|
|
120
|
+
const close = () => {
|
|
121
|
+
menu.style.display = 'none';
|
|
122
|
+
document.removeEventListener('mousedown', onDocDown);
|
|
123
|
+
window.removeEventListener('blur', close);
|
|
124
|
+
};
|
|
125
|
+
const onDocDown = (e) => {
|
|
126
|
+
if (!menu.contains(e.target)) close();
|
|
127
|
+
};
|
|
128
|
+
document.addEventListener('mousedown', onDocDown);
|
|
129
|
+
window.addEventListener('blur', close);
|
|
15
130
|
}
|
|
16
131
|
|
|
132
|
+
function initTitlebarMenu() {
|
|
133
|
+
const menu = tbMenu();
|
|
134
|
+
if (!menu) return;
|
|
135
|
+
menu.addEventListener('click', (e) => {
|
|
136
|
+
const item = e.target.closest('.tb-menu-item');
|
|
137
|
+
if (!item) return;
|
|
138
|
+
const action = item.dataset.action;
|
|
139
|
+
const w = window.freedom.window;
|
|
140
|
+
menu.style.display = 'none';
|
|
141
|
+
if (action === 'close') runWindow(w, 'close');
|
|
142
|
+
else if (action === 'minimize') runWindow(w, 'minimize');
|
|
143
|
+
else if (action === 'maximize') { runWindow(w, 'maximize', () => runWindow(w, 'toggleMaximize')); syncMaxState(w); }
|
|
144
|
+
else if (action === 'restore') { runWindow(w, 'restore', () => runWindow(w, 'toggleMaximize')); syncMaxState(w); }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
initTitlebarMenu();
|
|
149
|
+
initFrameless();
|
|
150
|
+
|
|
17
151
|
// 桥接自检。
|
|
18
152
|
document.getElementById('pingBtn').addEventListener('click', async () => {
|
|
19
153
|
const el = document.getElementById('result');
|