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, }) }