2024-10-13 23:12:55 +08:00
|
|
|
package client
|
|
|
|
|
|
|
|
import (
|
|
|
|
_ "embed"
|
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"strings"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"apigo.cc/gojs"
|
|
|
|
"apigo.cc/gojs/goja"
|
|
|
|
"github.com/ssgo/tool/watcher"
|
|
|
|
webview "github.com/webview/webview_go"
|
|
|
|
)
|
|
|
|
|
|
|
|
//go:embed client.ts
|
|
|
|
var clientTS string
|
|
|
|
|
|
|
|
//go:embed README.md
|
|
|
|
var clientMD string
|
|
|
|
|
|
|
|
//go:embed app.js
|
|
|
|
var appCode string
|
|
|
|
|
|
|
|
var binds = map[string]any{}
|
|
|
|
var bindsLock = sync.RWMutex{}
|
|
|
|
|
|
|
|
var windowsId uint64
|
|
|
|
var windows = map[uint64]*Webview{}
|
|
|
|
var windowsLock = sync.RWMutex{}
|
|
|
|
var waitChan chan bool
|
|
|
|
|
|
|
|
func Bind(name string, fn any) {
|
|
|
|
bindsLock.Lock()
|
|
|
|
binds[name] = fn
|
|
|
|
bindsLock.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
func Unbind(name string) {
|
|
|
|
bindsLock.Lock()
|
|
|
|
delete(binds, name)
|
|
|
|
bindsLock.Unlock()
|
|
|
|
}
|
|
|
|
|
|
|
|
type Webview struct {
|
|
|
|
id uint64
|
|
|
|
w webview.WebView
|
|
|
|
isRunning bool
|
2024-10-24 13:29:33 +08:00
|
|
|
// isAsync bool
|
2024-10-13 23:12:55 +08:00
|
|
|
isDebug bool
|
|
|
|
webRoot string
|
|
|
|
watcher *watcher.Watcher
|
|
|
|
html string
|
|
|
|
url string
|
|
|
|
closeChan chan bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func Wait() {
|
|
|
|
// 如果有窗口在运行,等待窗口关闭
|
|
|
|
windowsLock.RLock()
|
|
|
|
if len(windows) > 0 {
|
|
|
|
waitChan = make(chan bool, 1)
|
|
|
|
}
|
|
|
|
windowsLock.RUnlock()
|
|
|
|
|
|
|
|
// fmt.Println("====== wait start")
|
|
|
|
if waitChan != nil {
|
|
|
|
<-waitChan
|
|
|
|
waitChan = nil
|
|
|
|
}
|
|
|
|
// fmt.Println("====== wait end")
|
|
|
|
}
|
|
|
|
|
|
|
|
func CloseAll() {
|
|
|
|
windows1 := map[uint64]*Webview{}
|
|
|
|
windowsLock.Lock()
|
|
|
|
for id, w := range windows {
|
|
|
|
windows1[id] = w
|
|
|
|
}
|
|
|
|
windowsLock.Unlock()
|
|
|
|
for _, w := range windows1 {
|
|
|
|
w.close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) close() {
|
|
|
|
// fmt.Println("====== w.Close()")
|
|
|
|
if w.isRunning {
|
|
|
|
w.isRunning = false
|
|
|
|
// w.closeChan = make(chan bool, 1)
|
|
|
|
// fmt.Println("====== w.w.Terminate()")
|
|
|
|
w.w.Terminate()
|
|
|
|
// fmt.Println("====== w.w.Terminate() ok")
|
|
|
|
// <-w.closeChan
|
|
|
|
// w.closeChan = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) doClose() {
|
|
|
|
// fmt.Println("====== w.doClose()")
|
|
|
|
if w.watcher != nil {
|
|
|
|
w.watcher.Stop()
|
|
|
|
w.watcher = nil
|
|
|
|
}
|
|
|
|
windowsLock.Lock()
|
|
|
|
delete(windows, w.id)
|
|
|
|
n := len(windows)
|
|
|
|
windowsLock.Unlock()
|
|
|
|
|
|
|
|
// fmt.Println("====== w.w.Destroy()")
|
|
|
|
w.w.Destroy()
|
|
|
|
// fmt.Println("====== w.w.Destroy() ok")
|
|
|
|
|
|
|
|
// 解除 w.Close 的等待
|
|
|
|
if w.closeChan != nil {
|
|
|
|
w.closeChan <- true
|
|
|
|
}
|
|
|
|
|
|
|
|
// 解除 gojs.WaitAll 的等待
|
|
|
|
if waitChan != nil && n == 0 {
|
|
|
|
waitChan <- true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) loadHtml(html string) {
|
|
|
|
w.w.SetHtml(html)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) loadURL(url string) {
|
|
|
|
if url != "" {
|
|
|
|
if strings.Contains(url, "://") {
|
|
|
|
w.w.Navigate(url)
|
|
|
|
} else {
|
|
|
|
if !filepath.IsAbs(url) {
|
|
|
|
curPath, _ := os.Getwd()
|
|
|
|
url = filepath.Join(curPath, url)
|
|
|
|
}
|
|
|
|
w.webRoot = filepath.Dir(url)
|
|
|
|
url = "file://" + url
|
|
|
|
w.w.Navigate(url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
w.w.Navigate(url)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) reload() {
|
|
|
|
if w.html != "" {
|
|
|
|
w.loadHtml(w.html)
|
|
|
|
} else {
|
|
|
|
w.loadURL(w.url)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) setTitle(title string) {
|
|
|
|
w.w.SetTitle(title)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) setSize(width int, height int, sizeMode string) {
|
|
|
|
w.w.SetSize(width, height, getSizeMode(sizeMode))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) eval(code string) {
|
|
|
|
w.w.Eval(code)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
w.close()
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) LoadHtml(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
|
|
|
w.loadHtml(args.Str(0))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) LoadURL(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
|
|
|
w.loadURL(args.Str(0))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) Dispatch(f func()) {
|
|
|
|
w.w.Dispatch(f)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) SetTitle(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
|
|
|
w.setTitle(args.Str(0))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) SetSize(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm).Check(2)
|
|
|
|
w.setSize(args.Int(0), args.Int(1), args.Str(2))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) Eval(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm).Check(1)
|
|
|
|
w.eval(args.Str(0))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (w *Webview) binds() {
|
|
|
|
tmpBinds := map[string]any{}
|
|
|
|
bindsLock.RLock()
|
|
|
|
for k, v := range binds {
|
|
|
|
tmpBinds[k] = v
|
|
|
|
}
|
|
|
|
bindsLock.RUnlock()
|
|
|
|
for k, v := range tmpBinds {
|
|
|
|
_ = w.w.Bind(k, v)
|
|
|
|
}
|
|
|
|
_ = w.w.Bind("__closeWindow", w.close)
|
|
|
|
_ = w.w.Bind("__setTitle", w.setTitle)
|
|
|
|
_ = w.w.Bind("__setSize", w.setSize)
|
|
|
|
_ = w.w.Bind("__eval", w.eval)
|
|
|
|
w.w.Init(appCode)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getSizeMode(sizeMode string) webview.Hint {
|
|
|
|
switch sizeMode {
|
|
|
|
case "none":
|
|
|
|
return webview.HintNone
|
|
|
|
case "fixed":
|
|
|
|
return webview.HintFixed
|
|
|
|
case "min":
|
|
|
|
return webview.HintMin
|
|
|
|
case "max":
|
|
|
|
return webview.HintMax
|
|
|
|
default:
|
|
|
|
return webview.HintNone
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func init() {
|
|
|
|
obj := map[string]any{
|
|
|
|
"open": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
args := gojs.MakeArgs(&argsIn, vm)
|
|
|
|
title := ""
|
|
|
|
width := 800
|
|
|
|
height := 600
|
|
|
|
sizeMode := "none"
|
|
|
|
isDebug := false
|
|
|
|
html := ""
|
|
|
|
url := ""
|
|
|
|
if opt := args.Obj(0); opt != nil {
|
|
|
|
if titleV := opt.Str("title"); titleV != "" {
|
|
|
|
title = titleV
|
|
|
|
}
|
|
|
|
if widthV := opt.Int("width"); widthV != 0 {
|
|
|
|
width = widthV
|
|
|
|
}
|
|
|
|
if heightV := opt.Int("height"); heightV != 0 {
|
|
|
|
height = heightV
|
|
|
|
}
|
|
|
|
if sizeModeV := opt.Str("sizeMode"); sizeModeV != "" {
|
|
|
|
sizeMode = sizeModeV
|
|
|
|
}
|
|
|
|
if isDebugV := opt.Bool("isDebug"); isDebugV {
|
|
|
|
isDebug = isDebugV
|
|
|
|
}
|
|
|
|
if htmlV := opt.Str("html"); htmlV != "" {
|
|
|
|
html = htmlV
|
|
|
|
}
|
|
|
|
if urlV := opt.Str("url"); urlV != "" {
|
|
|
|
url = urlV
|
|
|
|
if strings.HasPrefix(url, "file://") {
|
|
|
|
url = url[7:]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w := &Webview{isDebug: isDebug, html: html, url: url}
|
2024-10-24 13:29:33 +08:00
|
|
|
// startChan := make(chan bool, 1)
|
|
|
|
gojs.RunInMain(func() {
|
2024-10-13 23:12:55 +08:00
|
|
|
windowsLock.Lock()
|
|
|
|
w.id = windowsId
|
|
|
|
windowsId++
|
|
|
|
windows[w.id] = w
|
|
|
|
windowsLock.Unlock()
|
|
|
|
w.w = webview.New(isDebug)
|
|
|
|
w.w.SetTitle(title)
|
|
|
|
w.w.SetSize(width, height, getSizeMode(sizeMode))
|
|
|
|
w.binds()
|
|
|
|
|
|
|
|
if w.isDebug {
|
|
|
|
var isWaitingRun = false
|
|
|
|
if w.webRoot == "" && w.html != "" {
|
|
|
|
w.webRoot = "."
|
|
|
|
}
|
|
|
|
if w.webRoot != "" {
|
|
|
|
w.watcher, _ = watcher.Start([]string{w.webRoot}, []string{"html", "js", "css"}, func(filename string, event string) {
|
|
|
|
if !isWaitingRun {
|
|
|
|
isWaitingRun = true
|
|
|
|
go func() {
|
|
|
|
time.Sleep(time.Millisecond * 10)
|
|
|
|
isWaitingRun = false
|
|
|
|
w.w.Eval("location.reload()")
|
|
|
|
}()
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
w.isRunning = true
|
|
|
|
|
|
|
|
w.reload()
|
2024-10-24 13:29:33 +08:00
|
|
|
// startChan <- true
|
2024-10-13 23:12:55 +08:00
|
|
|
w.w.Run()
|
|
|
|
// fmt.Println("====== w.w.Run() end")
|
|
|
|
w.doClose()
|
2024-10-24 13:29:33 +08:00
|
|
|
})
|
2024-10-13 23:12:55 +08:00
|
|
|
|
2024-10-24 13:29:33 +08:00
|
|
|
// <-startChan
|
2024-10-13 23:12:55 +08:00
|
|
|
return vm.ToValue(gojs.MakeMap(w))
|
|
|
|
},
|
|
|
|
"closeAll": func(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
|
|
|
CloseAll()
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
gojs.Register("apigo.cc/gojs/client", gojs.Module{
|
|
|
|
Object: obj,
|
|
|
|
Desc: "web client framework by github.com/webview/webview",
|
|
|
|
TsCode: clientTS,
|
|
|
|
Example: clientMD,
|
|
|
|
WaitForStop: Wait,
|
2024-10-24 13:29:33 +08:00
|
|
|
OnKill: CloseAll,
|
2024-10-13 23:12:55 +08:00
|
|
|
})
|
|
|
|
}
|