client/plugin.go

261 lines
8.1 KiB
Go

package client
import (
"apigo.cc/apigo/plugin"
"fmt"
"github.com/fsnotify/fsnotify"
"github.com/ssgo/u"
webview "github.com/webview/webview_go"
"os"
"path/filepath"
"sync"
)
var binds = map[string]interface{}{}
var bindsLock = sync.RWMutex{}
func Bind(name string, fn interface{}) {
bindsLock.Lock()
binds[name] = fn
bindsLock.Unlock()
}
func Unbind(name string) {
bindsLock.Lock()
delete(binds, name)
bindsLock.Unlock()
}
type Webview struct {
w webview.WebView
isRunning bool
isAsync bool
isDebug bool
webRoot string
}
func addAllDirForWatcher(filename string, watcher *fsnotify.Watcher) {
watcher.Add(filename)
for _, f := range u.ReadDirN(filename) {
if f.IsDir {
addAllDirForWatcher(f.FullName, watcher)
}
}
}
func (w *Webview) Run() {
w.isRunning = true
if w.isDebug && w.webRoot != "" {
if watcher, err := fsnotify.NewWatcher(); err == nil {
fmt.Println("watching...", w.webRoot)
go func() {
for w.isRunning {
select {
case event, ok := <-watcher.Events:
if !ok {
break
}
if event.Has(fsnotify.Write) {
fmt.Println("file changed:", event.Name)
w.Eval("location.reload()")
}
case _, ok := <-watcher.Errors:
if !ok {
break
}
}
}
}()
addAllDirForWatcher(w.webRoot, watcher)
}
}
w.w.Run()
if w.isRunning {
w.isRunning = false
w.w.Destroy()
}
}
func (w *Webview) Close() {
if w.isRunning {
w.w.Terminate()
}
}
func (w *Webview) SetHtml(html string) {
w.w.SetHtml(html)
}
func (w *Webview) LoadURL(url string) {
w.w.Navigate(url)
}
func (w *Webview) LoadFile(file string) {
if !filepath.IsAbs(file) {
curPath, _ := os.Getwd()
file = filepath.Join(curPath, file)
}
w.webRoot = filepath.Dir(file)
w.w.Navigate("file://" + file)
}
func (w *Webview) Dispatch(f func()) {
w.w.Dispatch(f)
}
func (w *Webview) SetTitle(title string) {
w.w.SetTitle(title)
}
func (w *Webview) SetSize(width int, height int, sizeMode *string) {
hint := "none"
if sizeMode != nil {
hint = *sizeMode
}
switch hint {
case "none":
w.w.SetSize(width, height, webview.HintNone)
case "fixed":
w.w.SetSize(width, height, webview.HintFixed)
case "min":
w.w.SetSize(width, height, webview.HintMin)
case "max":
w.w.SetSize(width, height, webview.HintMax)
default:
w.w.SetSize(width, height, webview.HintNone)
}
}
func (w *Webview) Eval(code string) {
w.w.Eval(code)
}
//func (w *Webview) Bind(name string, f interface{}) error {
// fmt.Println(">>>>>>>>.bind", name, f)
// return w.w.Bind(name, f)
//}
//
//func (w *Webview) Unbind(name string) error {
// return w.w.Unbind(name)
//}
func (w *Webview) binds() {
tmpBinds := map[string]interface{}{}
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.w.SetTitle)
_ = w.w.Bind("__setSize", w.SetSize)
_ = w.w.Bind("__eval", w.w.Eval)
w.w.Init(dialogCode)
w.w.Init(appCode)
}
func newWindow(title string, width int, height int, sizeMode *string, isDebug *bool) *Webview {
isDebugV := false
if isDebug != nil {
isDebugV = *isDebug
}
w := &Webview{w: webview.New(isDebugV), isDebug: isDebugV}
w.SetTitle(title)
w.SetSize(width, height, sizeMode)
w.binds()
return w
}
const appCode = `
let app = {
close: __closeWindow,
setTitle: __setTitle,
setSize: __setSize,
eval: __eval,
}
`
const dialogCode = `
window._dialogTexts = {'zh':{'Close':'关闭','Cancel':'取消','Confirm':'确定'}}
function _getDialogDefaultText(text) {return (window._dialogTexts[navigator.language] && window._dialogTexts[navigator.language][text]) || (window._dialogTexts[navigator.language.split('-')[0]] && window._dialogTexts[navigator.language.split('-')[0]][text]) || text}
window.alert = function (msg, buttonText) {return showDialog(msg, [buttonText || _getDialogDefaultText('Close')])}
window.confirm = function (msg, confirmButtonText, cancelButtonText) {return showDialog(msg, [cancelButtonText || _getDialogDefaultText('Cancel'), cancelButtonText || _getDialogDefaultText('Confirm')])}
window.prompt = function (msg, defaultValue, confirmButtonText, cancelButtonText) {return showDialog(msg, [cancelButtonText || _getDialogDefaultText('Cancel'), cancelButtonText || _getDialogDefaultText('Confirm')], [{value:defaultValue||''}])}
window.showDialog = function (msg, buttons, inputs) {
if(!document.querySelector('#_dialogStyle')){
let sDom = document.createElement('style')
sDom.id = '_dialogStyle'
sDom.textContent = '._dialogMask {position:fixed;left:0;right:0;top:0;bottom:0;background:rgba(128,128,128,0.7);display:flex;justify-content:center;align-items:center}\n' +
'._dialogPanel {background:#eee;padding:16px;min-width:300px;max-width:80%;max-height:80%;margin:auto;border-radius:10px;display:flex;flex-flow:column;overflow:auto}\n' +
'._dialogMessage {font-size:0.875rem;flex:1;overflow:auto;white-space:pre-wrap}\n' +
'._dialogInputs {display:flex;flex-flow:column;margin-top:8px}\n' +
'._dialogInputs input {flex:1;border:1px solid #aaa;border-radius:4px;min-height:28px;padding:0 8px}\n' +
'._dialogLine {text-align:end;padding:4px 0}\n' +
'._dialogButtons {text-align:end}\n' +
'._dialogButtons button {background:none;border:none;color:#999;min-height:28px;cursor:pointer;font-size:1rem;margin-left:10px}\n' +
'._dialogButtons button:hover {color:#999}\n' +
'._dialogButtons button:last-child {color:#06f}'
document.head.append(sDom)
}
return new Promise(resolve => {
let a = ['<div class="_dialogPanel">']
if (msg) a.push('<div class="_dialogMessage">__MSG__</div>'.replace(/__MSG__/, msg))
if (inputs && inputs.length) {
let a1 = ['<div class="_dialogInputs">']
for (let i = 0; i < inputs.length; i++){
a1.push('<input id="_dialogInput__ID_____INPUT_INDEX__" placeholder="__INPUT_HINT__" value="__INPUT_VALUE__"/>'.replace(/__INPUT_INDEX__/, i + '').replace(/__INPUT_HINT__/, inputs[i].hint||'').replace(/__INPUT_VALUE__/, inputs[i].value||''))
}
a1.push('</div>')
a.push(a1.join(''))
}
a.push('<div class="_dialogLine"><hr/></div>')
let a2 = ['<div class="_dialogButtons">']
for (let i = 0; i < buttons.length; i++) a2.push('<button class="_dialogButton" onclick="_dialogAction__ID__(__BUTTON_INDEX__)">__BUTTON_LABEL__</button>'.replace(/__BUTTON_INDEX__/, i + '').replace(/__BUTTON_LABEL__/, buttons[i]))
a2.push('</div>')
a.push(a2.join(''))
let dialogId = Math.ceil(Math.random() * 10000000000).toString(36)
let w = document.createElement('div')
w.id = '_dialogWindow' + dialogId
w.className = '_dialogMask'
window['_dialogAction' + dialogId] = function (index) {
let isOK = index === buttons.length - 1
if (inputs && inputs.length) {
if (isOK) {
let inputValues = []
for (let i = 0; i < inputs.length; i++) inputValues.push(document.querySelector('#_dialogInput' + dialogId + '_' + i).value)
resolve(inputs.length === 1 ? inputValues[0] : inputValues)
} else {
resolve(false)
}
} else {
resolve(isOK ? true : index)
}
document.body.removeChild(document.querySelector('#_dialogWindow' + dialogId))
}
w.innerHTML = a.join('').replace(/__ID__/g, dialogId)
document.body.append(w)
})
}
`
func init() {
plugin.Register(plugin.Plugin{
Id: "apigo.cc/apigo/client",
Name: "web client framework by github.com/webview/webview",
Objects: map[string]interface{}{
"newApp": func(title string, width int, height int, sizeMode *string, isDebug *bool) *Webview {
w := newWindow(title, width, height, sizeMode, isDebug)
// svc 框架
//w.w.Init(``)
return w
},
"new": func(title string, width int, height int, sizeMode *string, isDebug *bool) *Webview {
return newWindow(title, width, height, sizeMode, isDebug)
},
},
})
}