client/client.go
2024-10-13 23:12:55 +08:00

335 lines
6.7 KiB
Go

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
isAsync bool
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}
startChan := make(chan bool, 1)
go func() {
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()
startChan <- true
w.w.Run()
// fmt.Println("====== w.w.Run() end")
w.doClose()
}()
<-startChan
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,
})
}