在不启动CGO时使用轻量级chromedp
This commit is contained in:
parent
88c35cb97c
commit
eb3b523eaf
336
_client_cgo.go
Normal file
336
_client_cgo.go
Normal file
@ -0,0 +1,336 @@
|
||||
//go:build !cgo
|
||||
|
||||
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)
|
||||
gojs.RunInMain(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,
|
||||
OnKill: CloseAll,
|
||||
})
|
||||
}
|
17
aaa/main.go
Normal file
17
aaa/main.go
Normal file
@ -0,0 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
webview "github.com/webview/webview_go"
|
||||
)
|
||||
|
||||
func main() {
|
||||
runtime.LockOSThread() // 锁定当前线程为主线程
|
||||
w := webview.New(false)
|
||||
defer w.Destroy()
|
||||
w.SetTitle("WebView Demo")
|
||||
w.SetSize(800, 600, webview.HintNone)
|
||||
w.Navigate("https://example.com") // 或本地文件:"file:///path/to/index.html"
|
||||
w.Run()
|
||||
}
|
2
cgo.yml
2
cgo.yml
@ -1,3 +1,3 @@
|
||||
CGO_ENABLED: true
|
||||
# CGO_ENABLED: true
|
||||
CGO_CPPFLAGS:
|
||||
- -I%PROJECT%/include
|
||||
|
186
client.go
186
client.go
@ -2,8 +2,6 @@ package client
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@ -11,9 +9,11 @@ import (
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/goja"
|
||||
"github.com/ssgo/tool/watcher"
|
||||
webview "github.com/webview/webview_go"
|
||||
)
|
||||
|
||||
// TODO 桌面应用使用 https://github.com/neutralinojs/neutralinojs
|
||||
// TODO App使用 https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile
|
||||
|
||||
//go:embed client.ts
|
||||
var clientTS string
|
||||
|
||||
@ -43,19 +43,6 @@ func Unbind(name string) {
|
||||
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()
|
||||
@ -84,107 +71,6 @@ func CloseAll() {
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
@ -203,36 +89,21 @@ func (w *Webview) Eval(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
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 (w *Webview) Close(argsIn goja.FunctionCall, vm *goja.Runtime) goja.Value {
|
||||
w.close()
|
||||
return nil
|
||||
}
|
||||
|
||||
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 (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 init() {
|
||||
@ -275,30 +146,37 @@ func init() {
|
||||
|
||||
w := &Webview{isDebug: isDebug, html: html, url: url}
|
||||
// startChan := make(chan bool, 1)
|
||||
// fmt.Println("====== start")
|
||||
gojs.RunInMain(func() {
|
||||
// fmt.Println("====== gojs.RunInMain")
|
||||
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))
|
||||
// fmt.Println("====== w.New()")
|
||||
w.New(isDebug)
|
||||
// fmt.Println("====== 1")
|
||||
w.setTitle(title)
|
||||
// fmt.Println("====== 2")
|
||||
w.setSize(width, height, sizeMode)
|
||||
// fmt.Println("====== 3")
|
||||
w.binds()
|
||||
|
||||
// fmt.Println("====== w.Run()")
|
||||
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) {
|
||||
w.watcher, _ = watcher.Start([]string{w.webRoot}, []string{"html", "js", "css"}, nil, func(filename string, event string) {
|
||||
if !isWaitingRun {
|
||||
isWaitingRun = true
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 10)
|
||||
isWaitingRun = false
|
||||
w.w.Eval("location.reload()")
|
||||
w.eval("location.reload()")
|
||||
}()
|
||||
}
|
||||
})
|
||||
@ -307,10 +185,12 @@ func init() {
|
||||
|
||||
w.isRunning = true
|
||||
|
||||
// fmt.Println("====== w.reload()")
|
||||
w.reload()
|
||||
// startChan <- true
|
||||
w.w.Run()
|
||||
// fmt.Println("====== w.w.Run() end")
|
||||
// fmt.Println("====== w.Run()")
|
||||
w.Run()
|
||||
// fmt.Println("====== w.Run() end")
|
||||
w.doClose()
|
||||
})
|
||||
|
||||
@ -325,7 +205,7 @@ func init() {
|
||||
|
||||
gojs.Register("apigo.cc/gojs/client", gojs.Module{
|
||||
Object: obj,
|
||||
Desc: "web client framework by github.com/webview/webview",
|
||||
Desc: "web client framework by github.com/chromedp/chromedp or github.com/webview/webview",
|
||||
TsCode: clientTS,
|
||||
Example: clientMD,
|
||||
WaitForStop: Wait,
|
||||
|
352
client_chrome.go
Normal file
352
client_chrome.go
Normal file
@ -0,0 +1,352 @@
|
||||
//go:build !cgo
|
||||
// +build !cgo
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
chromeRuntime "github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/ssgo/tool/watcher"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
type Webview struct {
|
||||
id uint64
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
isRunning bool
|
||||
// isAsync bool
|
||||
isDebug bool
|
||||
webRoot string
|
||||
watcher *watcher.Watcher
|
||||
html string
|
||||
url string
|
||||
closeChan chan bool
|
||||
width int
|
||||
height int
|
||||
title string
|
||||
evals []string
|
||||
bindFuncs map[string]any
|
||||
}
|
||||
|
||||
var chromeVersion = -1
|
||||
|
||||
func (w *Webview) New(isDebug bool) {
|
||||
if chromeVersion == -1 {
|
||||
chromeVersion = getChromeVersion()
|
||||
}
|
||||
w.isDebug = isDebug
|
||||
}
|
||||
|
||||
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.cancel()
|
||||
// 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()")
|
||||
if w.cancel != nil {
|
||||
w.cancel()
|
||||
}
|
||||
// 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.url = "data:text/html;charset=utf-8," + url.PathEscape(html)
|
||||
if w.ctx != nil {
|
||||
chromedp.Run(w.ctx, chromedp.Navigate(w.url))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) loadURL(url string) {
|
||||
if url != "" {
|
||||
if strings.Contains(url, "://") {
|
||||
// chromedp.Run(w.ctx, chromedp.Navigate(url))
|
||||
w.url = url
|
||||
} else {
|
||||
if !filepath.IsAbs(url) {
|
||||
curPath, _ := os.Getwd()
|
||||
url = filepath.Join(curPath, url)
|
||||
}
|
||||
w.webRoot = filepath.Dir(url)
|
||||
w.url = "file://" + url
|
||||
// chromedp.Run(w.ctx, chromedp.Navigate(url))
|
||||
}
|
||||
} else {
|
||||
// chromedp.Run(w.ctx, chromedp.Navigate("about:blank"))
|
||||
w.url = "about:blank"
|
||||
}
|
||||
|
||||
if w.ctx != nil {
|
||||
chromedp.Run(w.ctx, chromedp.Navigate(w.url))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) reload() {
|
||||
if w.html != "" {
|
||||
w.loadHtml(w.html)
|
||||
} else {
|
||||
w.loadURL(w.url)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) setTitle(title string) {
|
||||
w.title = title
|
||||
if w.ctx != nil {
|
||||
chromedp.Run(w.ctx, chromedp.Evaluate(`document.title = "`+w.title+`"`, nil))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) setSize(width int, height int, sizeMode string) {
|
||||
w.width = width
|
||||
w.height = height
|
||||
if w.ctx != nil {
|
||||
// chromedp.Run(w.ctx, chromedp.EmulateViewport(int64(w.width), int64(w.height)))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) eval(code string) any {
|
||||
if w.ctx == nil {
|
||||
w.evals = append(w.evals, code)
|
||||
return nil
|
||||
}
|
||||
|
||||
var result any
|
||||
chromedp.Run(w.ctx, chromedp.Evaluate(code, &result))
|
||||
return result
|
||||
}
|
||||
|
||||
func (w *Webview) Dispatch(f func()) {
|
||||
// w.w.Dispatch(f)
|
||||
f()
|
||||
}
|
||||
|
||||
func (w *Webview) Run() {
|
||||
// chromeVersion = 0
|
||||
if chromeVersion > 0 {
|
||||
// 根据版本调整配置
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", false), // 显示窗口
|
||||
chromedp.Flag("window-size", fmt.Sprintf("%d,%d", w.width, w.height)), // 窗口尺寸
|
||||
chromedp.Flag("app", w.url), // 直接启动到目标页
|
||||
chromedp.Flag("disable-notifications", true), // 禁用通知
|
||||
chromedp.Flag("no-first-run", true), // 跳过首次运行
|
||||
chromedp.Flag("no-default-browser-check", true), // 禁用默认浏览器检查
|
||||
chromedp.Flag("disable-component-update", true), // 禁用组件更新
|
||||
chromedp.Flag("disable-background-networking", true), // 禁用后台网络
|
||||
chromedp.Flag("disable-client-side-phishing-detection", true), // 禁用钓鱼检测
|
||||
)
|
||||
|
||||
if chromeVersion >= 115 {
|
||||
// 新版本
|
||||
opts = append(opts,
|
||||
chromedp.Flag("enable-automation", false),
|
||||
// chromedp.Flag("excludeSwitches", []string{"enable-automation"}),
|
||||
chromedp.Flag("useAutomationExtension", false),
|
||||
)
|
||||
} else if chromeVersion >= 90 {
|
||||
// Chrome 90+ 参数
|
||||
opts = append(opts,
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
)
|
||||
} else {
|
||||
// Chrome 89 及以下参数
|
||||
opts = append(opts,
|
||||
chromedp.Flag("disable-infobars", true),
|
||||
chromedp.Flag("disable-extensions", true),
|
||||
)
|
||||
}
|
||||
|
||||
allocCtx, cancel1 := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
ctx, cancel2 := chromedp.NewContext(allocCtx)
|
||||
w.ctx = ctx
|
||||
w.cancel = func() {
|
||||
cancel2()
|
||||
cancel1()
|
||||
}
|
||||
|
||||
codes := []string{getDetectionScript(chromeVersion)}
|
||||
if w.evals != nil {
|
||||
codes = append(codes, strings.Join(w.evals, "\n"))
|
||||
}
|
||||
|
||||
chromedp.ListenTarget(w.ctx, func(ev any) {
|
||||
if ev, ok := ev.(*chromeRuntime.EventBindingCalled); ok {
|
||||
if fn, ok := w.bindFuncs[ev.Name]; ok {
|
||||
fnV := reflect.ValueOf(fn)
|
||||
if fnV.Kind() == reflect.Func {
|
||||
var args []reflect.Value
|
||||
if ev.Payload != "" {
|
||||
argsV := u.FinalValue(reflect.ValueOf(u.UnJson(ev.Payload, nil)))
|
||||
if argsV.Kind() == reflect.Slice {
|
||||
for i := 0; i < fnV.Type().NumIn(); i++ {
|
||||
argType := fnV.Type().In(i)
|
||||
argValueP := reflect.New(argType)
|
||||
argValue := argValueP.Elem()
|
||||
if i < argsV.Len() {
|
||||
u.Convert(argsV.Index(i), argValueP)
|
||||
}
|
||||
args = append(args, argValue)
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
_ = fnV.Call(args)
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
actions := []chromedp.Action{}
|
||||
|
||||
for k, _ := range w.bindFuncs {
|
||||
actions = append(actions, chromeRuntime.AddBinding(k))
|
||||
codes = append(codes, fmt.Sprintf("window.__%s = %s; window.%s = function () { __%s(JSON.stringify(Array.from(arguments))) }", k, k, k, k))
|
||||
}
|
||||
codes = append(codes, appCode)
|
||||
actions = append(actions, chromedp.Evaluate(strings.Join(codes, "\n"), nil))
|
||||
|
||||
exitChan := make(chan bool, 1)
|
||||
chromedp.Run(w.ctx, actions...)
|
||||
|
||||
go func() {
|
||||
<-w.ctx.Done()
|
||||
exitChan <- true
|
||||
}()
|
||||
|
||||
<-exitChan
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
u.RunCommand("start", w.url)
|
||||
} else if runtime.GOOS == "darwin" {
|
||||
u.RunCommand("open", w.url)
|
||||
} else {
|
||||
u.RunCommand("xdg-open", w.url)
|
||||
}
|
||||
|
||||
fmt.Println("URL: ", w.url)
|
||||
fmt.Println("Press Enter to exit...")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
_, _ = reader.ReadString('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Webview) binds() {
|
||||
w.bindFuncs = map[string]any{}
|
||||
bindsLock.RLock()
|
||||
for k, v := range binds {
|
||||
w.bindFuncs[k] = v
|
||||
}
|
||||
bindsLock.RUnlock()
|
||||
|
||||
w.bindFuncs["__closeWindow"] = w.close
|
||||
w.bindFuncs["__setTitle"] = w.setTitle
|
||||
w.bindFuncs["__setSize"] = w.setSize
|
||||
w.bindFuncs["__eval"] = w.eval
|
||||
}
|
||||
|
||||
// 获取浏览器版本(精确到主版本号)
|
||||
func getChromeVersion() int {
|
||||
var ua string
|
||||
ctx, cancel := chromedp.NewContext(context.Background())
|
||||
defer cancel()
|
||||
if err := chromedp.Run(ctx,
|
||||
chromedp.Evaluate("navigator.userAgent", &ua),
|
||||
); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
// 解析示例:Mozilla/5.0... Chrome/124.0.0.0...
|
||||
start := strings.Index(ua, "Chrome/")
|
||||
if start == -1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
versionPart := ua[start+7:]
|
||||
end := strings.Index(versionPart, ".")
|
||||
if end == -1 {
|
||||
return 0
|
||||
}
|
||||
|
||||
version, _ := strconv.Atoi(versionPart[:end])
|
||||
return version
|
||||
}
|
||||
|
||||
// 生成版本自适应的检测脚本
|
||||
func getDetectionScript(version int) string {
|
||||
baseJS := `
|
||||
(() => {
|
||||
// 核心属性修改
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => false,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
// 通用内存清理
|
||||
const legacyProps = ['$cdc', '__driver_evaluate'];
|
||||
const modernProps = ['_cdc', '__driver', '__automation'];
|
||||
[...legacyProps, ...modernProps].forEach(p => {
|
||||
try { delete window[p] } catch(e){}
|
||||
try { delete document[p] } catch(e){}
|
||||
});`
|
||||
|
||||
if version >= 120 {
|
||||
// 修复点:将 JS 模板字符串改为普通字符串拼接
|
||||
baseJS += `
|
||||
// 伪造 Chrome 运行时
|
||||
window.chrome = {
|
||||
runtime: {
|
||||
id: 'mock_extension',
|
||||
getManifest: () => ({ version: '1.0' }),
|
||||
getURL: function(path) {
|
||||
return 'chrome-extension://mock_extension/' + path;
|
||||
},
|
||||
onMessage: { addListener: function(){} }
|
||||
}
|
||||
};`
|
||||
}
|
||||
|
||||
baseJS += "\n})();"
|
||||
return baseJS + "\n"
|
||||
}
|
153
client_webview.go
Normal file
153
client_webview.go
Normal file
@ -0,0 +1,153 @@
|
||||
//go:build cgo
|
||||
// +build cgo
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ssgo/tool/watcher"
|
||||
webview "github.com/webview/webview_go"
|
||||
)
|
||||
|
||||
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 (w *Webview) New(isDebug bool) {
|
||||
w.w = webview.New(isDebug)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
} else {
|
||||
w.w.Navigate("about:blank")
|
||||
}
|
||||
}
|
||||
|
||||
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) any {
|
||||
w.w.Eval(code)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *Webview) Dispatch(f func()) {
|
||||
w.w.Dispatch(f)
|
||||
}
|
||||
|
||||
func (w *Webview) Run() {
|
||||
w.w.Run()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
13
demo/app/.gitignore
vendored
Normal file
13
demo/app/.gitignore
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
# Developer tools' files
|
||||
.lite_workspace.lua
|
||||
|
||||
# Neutralinojs binaries and builds
|
||||
/bin
|
||||
/dist
|
||||
|
||||
# Neutralinojs client (minified)
|
||||
neutralino.js
|
||||
|
||||
# Neutralinojs related files
|
||||
.storage
|
||||
*.log
|
21
demo/app/LICENSE
Normal file
21
demo/app/LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Neutralinojs and contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
15
demo/app/README.md
Normal file
15
demo/app/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# neutralinojs-minimal
|
||||
|
||||
The default template for a Neutralinojs app. It's possible to use your favorite frontend framework by using [these steps](https://neutralino.js.org/docs/getting-started/using-frontend-libraries).
|
||||
|
||||
## Contributors
|
||||
|
||||
[](https://github.com/neutralinojs/neutralinojs-minimal/graphs/contributors)
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
|
||||
## Icon credits
|
||||
|
||||
- `trayIcon.png` - Made by [Freepik](https://www.freepik.com) and downloaded from [Flaticon](https://www.flaticon.com)
|
83
demo/app/neutralino.config.json
Normal file
83
demo/app/neutralino.config.json
Normal file
@ -0,0 +1,83 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/neutralinojs/neutralinojs/main/schemas/neutralino.config.schema.json",
|
||||
"applicationId": "js.neutralino.sample",
|
||||
"version": "1.0.0",
|
||||
"defaultMode": "window",
|
||||
"port": 0,
|
||||
"documentRoot": "/resources/",
|
||||
"url": "/",
|
||||
"enableServer": true,
|
||||
"enableNativeAPI": true,
|
||||
"tokenSecurity": "one-time",
|
||||
"logging": {
|
||||
"enabled": true,
|
||||
"writeToLogFile": true
|
||||
},
|
||||
"nativeAllowList": [
|
||||
"app.*",
|
||||
"os.*",
|
||||
"debug.log"
|
||||
],
|
||||
"globalVariables": {
|
||||
"TEST1": "Hello",
|
||||
"TEST2": [
|
||||
2,
|
||||
4,
|
||||
5
|
||||
],
|
||||
"TEST3": {
|
||||
"value1": 10,
|
||||
"value2": {}
|
||||
}
|
||||
},
|
||||
"modes": {
|
||||
"window": {
|
||||
"title": "app",
|
||||
"width": 800,
|
||||
"height": 500,
|
||||
"minWidth": 400,
|
||||
"minHeight": 200,
|
||||
"center": true,
|
||||
"fullScreen": false,
|
||||
"alwaysOnTop": false,
|
||||
"icon": "/resources/icons/appIcon.png",
|
||||
"enableInspector": true,
|
||||
"borderless": false,
|
||||
"maximize": false,
|
||||
"hidden": false,
|
||||
"resizable": true,
|
||||
"exitProcessOnClose": false
|
||||
},
|
||||
"browser": {
|
||||
"globalVariables": {
|
||||
"TEST": "Test value browser"
|
||||
},
|
||||
"nativeBlockList": [
|
||||
"filesystem.*"
|
||||
]
|
||||
},
|
||||
"cloud": {
|
||||
"url": "/resources/#cloud",
|
||||
"nativeAllowList": [
|
||||
"app.*"
|
||||
]
|
||||
},
|
||||
"chrome": {
|
||||
"width": 800,
|
||||
"height": 500,
|
||||
"args": "--user-agent=\"Neutralinojs chrome mode\"",
|
||||
"nativeBlockList": [
|
||||
"filesystem.*",
|
||||
"os.*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"binaryName": "app",
|
||||
"resourcesPath": "/resources/",
|
||||
"extensionsPath": "/extensions/",
|
||||
"clientLibrary": "/resources/js/neutralino.js",
|
||||
"binaryVersion": "6.1.0",
|
||||
"clientVersion": "6.1.0"
|
||||
}
|
||||
}
|
BIN
demo/app/resources/icons/appIcon.png
Normal file
BIN
demo/app/resources/icons/appIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
BIN
demo/app/resources/icons/logo.gif
Normal file
BIN
demo/app/resources/icons/logo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 232 KiB |
BIN
demo/app/resources/icons/trayIcon.png
Normal file
BIN
demo/app/resources/icons/trayIcon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 965 B |
27
demo/app/resources/index.html
Normal file
27
demo/app/resources/index.html
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>NeutralinoJs sample app</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="neutralinoapp">
|
||||
<h1>NeutralinoJs</h1>
|
||||
<div id="info"></div>
|
||||
<br/>
|
||||
<img src="/icons/logo.gif" alt="Neutralinojs" />
|
||||
<div>
|
||||
<a href="#" onclick="openDocs();">Docs</a> ·
|
||||
<a href="#" onclick="openTutorial();">Video tutorial</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Neutralino.js client. This file is gitignored,
|
||||
because `neu update` typically downloads it.
|
||||
Avoid copy-pasting it.
|
||||
-->
|
||||
<script src="/js/neutralino.js"></script>
|
||||
<!-- Your app's source files -->
|
||||
<script src="/js/main.js"></script>
|
||||
</body>
|
||||
</html>
|
99
demo/app/resources/js/main.js
Normal file
99
demo/app/resources/js/main.js
Normal file
@ -0,0 +1,99 @@
|
||||
// This is just a sample app. You can structure your Neutralinojs app code as you wish.
|
||||
// This example app is written with vanilla JavaScript and HTML.
|
||||
// Feel free to use any frontend framework you like :)
|
||||
// See more details: https://neutralino.js.org/docs/how-to/use-a-frontend-library
|
||||
|
||||
/*
|
||||
Function to display information about the Neutralino app.
|
||||
This function updates the content of the 'info' element in the HTML
|
||||
with details regarding the running Neutralino application, including
|
||||
its ID, port, operating system, and version information.
|
||||
*/
|
||||
function showInfo() {
|
||||
document.getElementById('info').innerHTML = `
|
||||
${NL_APPID} is running on port ${NL_PORT} inside ${NL_OS}
|
||||
<br/><br/>
|
||||
<span>server: v${NL_VERSION} . client: v${NL_CVERSION}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
/*
|
||||
Function to open the official Neutralino documentation in the default web browser.
|
||||
*/
|
||||
function openDocs() {
|
||||
Neutralino.os.open("https://neutralino.js.org/docs");
|
||||
}
|
||||
|
||||
/*
|
||||
Function to open a tutorial video on Neutralino's official YouTube channel in the default web browser.
|
||||
*/
|
||||
function openTutorial() {
|
||||
Neutralino.os.open("https://www.youtube.com/c/CodeZri");
|
||||
}
|
||||
|
||||
/*
|
||||
Function to set up a system tray menu with options specific to the window mode.
|
||||
This function checks if the application is running in window mode, and if so,
|
||||
it defines the tray menu items and sets up the tray accordingly.
|
||||
*/
|
||||
function setTray() {
|
||||
// Tray menu is only available in window mode
|
||||
if(NL_MODE != "window") {
|
||||
console.log("INFO: Tray menu is only available in the window mode.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Define tray menu items
|
||||
let tray = {
|
||||
icon: "/resources/icons/trayIcon.png",
|
||||
menuItems: [
|
||||
{id: "VERSION", text: "Get version"},
|
||||
{id: "SEP", text: "-"},
|
||||
{id: "QUIT", text: "Quit"}
|
||||
]
|
||||
};
|
||||
|
||||
// Set the tray menu
|
||||
Neutralino.os.setTray(tray);
|
||||
}
|
||||
|
||||
/*
|
||||
Function to handle click events on the tray menu items.
|
||||
This function performs different actions based on the clicked item's ID,
|
||||
such as displaying version information or exiting the application.
|
||||
*/
|
||||
function onTrayMenuItemClicked(event) {
|
||||
switch(event.detail.id) {
|
||||
case "VERSION":
|
||||
// Display version information
|
||||
Neutralino.os.showMessageBox("Version information",
|
||||
`Neutralinojs server: v${NL_VERSION} | Neutralinojs client: v${NL_CVERSION}`);
|
||||
break;
|
||||
case "QUIT":
|
||||
// Exit the application
|
||||
Neutralino.app.exit();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Function to handle the window close event by gracefully exiting the Neutralino application.
|
||||
*/
|
||||
function onWindowClose() {
|
||||
Neutralino.app.exit();
|
||||
}
|
||||
|
||||
// Initialize Neutralino
|
||||
Neutralino.init();
|
||||
|
||||
// Register event listeners
|
||||
Neutralino.events.on("trayMenuItemClicked", onTrayMenuItemClicked);
|
||||
Neutralino.events.on("windowClose", onWindowClose);
|
||||
|
||||
// Conditional initialization: Set up system tray if not running on macOS
|
||||
if(NL_OS != "Darwin") { // TODO: Fix https://github.com/neutralinojs/neutralinojs/issues/615
|
||||
setTray();
|
||||
}
|
||||
|
||||
// Display app information
|
||||
showInfo();
|
897
demo/app/resources/js/neutralino.d.ts
vendored
Normal file
897
demo/app/resources/js/neutralino.d.ts
vendored
Normal file
@ -0,0 +1,897 @@
|
||||
// Type definitions for Neutralino 6.1.0
|
||||
// Project: https://github.com/neutralinojs
|
||||
// Definitions project: https://github.com/neutralinojs/neutralino.js
|
||||
|
||||
declare namespace Neutralino {
|
||||
|
||||
namespace filesystem {
|
||||
interface DirectoryEntry {
|
||||
entry: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
interface FileReaderOptions {
|
||||
pos: number;
|
||||
size: number;
|
||||
}
|
||||
interface DirectoryReaderOptions {
|
||||
recursive: boolean;
|
||||
}
|
||||
interface OpenedFile {
|
||||
id: number;
|
||||
eof: boolean;
|
||||
pos: number;
|
||||
lastRead: number;
|
||||
}
|
||||
interface Stats {
|
||||
size: number;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
createdAt: number;
|
||||
modifiedAt: number;
|
||||
}
|
||||
interface Watcher {
|
||||
id: number;
|
||||
path: string;
|
||||
}
|
||||
interface CopyOptions {
|
||||
recursive: boolean;
|
||||
overwrite: boolean;
|
||||
skip: boolean;
|
||||
}
|
||||
interface PathParts {
|
||||
rootName: string;
|
||||
rootDirectory: string;
|
||||
rootPath: string;
|
||||
relativePath: string;
|
||||
parentPath: string;
|
||||
filename: string;
|
||||
stem: string;
|
||||
extension: string;
|
||||
}
|
||||
interface Permissions {
|
||||
all: boolean;
|
||||
ownerAll: boolean;
|
||||
ownerRead: boolean;
|
||||
ownerWrite: boolean;
|
||||
ownerExec: boolean;
|
||||
groupAll: boolean;
|
||||
groupRead: boolean;
|
||||
groupWrite: boolean;
|
||||
groupExec: boolean;
|
||||
othersAll: boolean;
|
||||
othersRead: boolean;
|
||||
othersWrite: boolean;
|
||||
othersExec: boolean;
|
||||
}
|
||||
type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
|
||||
function createDirectory(path: string): Promise<void>;
|
||||
function remove(path: string): Promise<void>;
|
||||
function writeFile(path: string, data: string): Promise<void>;
|
||||
function appendFile(path: string, data: string): Promise<void>;
|
||||
function writeBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
|
||||
function appendBinaryFile(path: string, data: ArrayBuffer): Promise<void>;
|
||||
function readFile(path: string, options?: FileReaderOptions): Promise<string>;
|
||||
function readBinaryFile(path: string, options?: FileReaderOptions): Promise<ArrayBuffer>;
|
||||
function openFile(path: string): Promise<number>;
|
||||
function createWatcher(path: string): Promise<number>;
|
||||
function removeWatcher(id: number): Promise<number>;
|
||||
function getWatchers(): Promise<Watcher[]>;
|
||||
function updateOpenedFile(id: number, event: string, data?: any): Promise<void>;
|
||||
function getOpenedFileInfo(id: number): Promise<OpenedFile>;
|
||||
function readDirectory(path: string, options?: DirectoryReaderOptions): Promise<DirectoryEntry[]>;
|
||||
function copy(source: string, destination: string, options?: CopyOptions): Promise<void>;
|
||||
function move(source: string, destination: string): Promise<void>;
|
||||
function getStats(path: string): Promise<Stats>;
|
||||
function getAbsolutePath(path: string): Promise<string>;
|
||||
function getRelativePath(path: string, base?: string): Promise<string>;
|
||||
function getPathParts(path: string): Promise<PathParts>;
|
||||
function getPermissions(path: string): Promise<Permissions>;
|
||||
function setPermissions(path: string, permissions: Permissions, mode: PermissionsMode): Promise<void>;
|
||||
}
|
||||
namespace os {
|
||||
// debug
|
||||
enum LoggerType {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO"
|
||||
}
|
||||
// os
|
||||
enum Icon {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO",
|
||||
QUESTION = "QUESTION"
|
||||
}
|
||||
enum MessageBoxChoice {
|
||||
OK = "OK",
|
||||
OK_CANCEL = "OK_CANCEL",
|
||||
YES_NO = "YES_NO",
|
||||
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||
RETRY_CANCEL = "RETRY_CANCEL",
|
||||
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||
}
|
||||
//clipboard
|
||||
enum ClipboardFormat {
|
||||
unknown = "unknown",
|
||||
text = "text",
|
||||
image = "image"
|
||||
}
|
||||
// NL_GLOBALS
|
||||
enum Mode {
|
||||
window = "window",
|
||||
browser = "browser",
|
||||
cloud = "cloud",
|
||||
chrome = "chrome"
|
||||
}
|
||||
enum OperatingSystem {
|
||||
Linux = "Linux",
|
||||
Windows = "Windows",
|
||||
Darwin = "Darwin",
|
||||
FreeBSD = "FreeBSD",
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
enum Architecture {
|
||||
x64 = "x64",
|
||||
arm = "arm",
|
||||
itanium = "itanium",
|
||||
ia32 = "ia32",
|
||||
unknown = "unknown"
|
||||
}
|
||||
interface ExecCommandOptions {
|
||||
stdIn?: string;
|
||||
background?: boolean;
|
||||
cwd?: string;
|
||||
}
|
||||
interface ExecCommandResult {
|
||||
pid: number;
|
||||
stdOut: string;
|
||||
stdErr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
interface SpawnedProcess {
|
||||
id: number;
|
||||
pid: number;
|
||||
}
|
||||
interface SpawnedProcessOptions {
|
||||
cwd?: string;
|
||||
envs?: Record<string, string>;
|
||||
}
|
||||
interface Envs {
|
||||
[key: string]: string;
|
||||
}
|
||||
interface OpenDialogOptions {
|
||||
multiSelections?: boolean;
|
||||
filters?: Filter[];
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface FolderDialogOptions {
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface SaveDialogOptions {
|
||||
forceOverwrite?: boolean;
|
||||
filters?: Filter[];
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface Filter {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
}
|
||||
interface TrayOptions {
|
||||
icon: string;
|
||||
menuItems: TrayMenuItem[];
|
||||
}
|
||||
interface TrayMenuItem {
|
||||
id?: string;
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
|
||||
function execCommand(command: string, options?: ExecCommandOptions): Promise<ExecCommandResult>;
|
||||
function spawnProcess(command: string, options?: SpawnedProcessOptions): Promise<SpawnedProcess>;
|
||||
function updateSpawnedProcess(id: number, event: string, data?: any): Promise<void>;
|
||||
function getSpawnedProcesses(): Promise<SpawnedProcess[]>;
|
||||
function getEnv(key: string): Promise<string>;
|
||||
function getEnvs(): Promise<Envs>;
|
||||
function showOpenDialog(title?: string, options?: OpenDialogOptions): Promise<string[]>;
|
||||
function showFolderDialog(title?: string, options?: FolderDialogOptions): Promise<string>;
|
||||
function showSaveDialog(title?: string, options?: SaveDialogOptions): Promise<string>;
|
||||
function showNotification(title: string, content: string, icon?: Icon): Promise<void>;
|
||||
function showMessageBox(title: string, content: string, choice?: MessageBoxChoice, icon?: Icon): Promise<string>;
|
||||
function setTray(options: TrayOptions): Promise<void>;
|
||||
function open(url: string): Promise<void>;
|
||||
function getPath(name: KnownPath): Promise<string>;
|
||||
}
|
||||
namespace computer {
|
||||
interface MemoryInfo {
|
||||
physical: {
|
||||
total: number;
|
||||
available: number;
|
||||
};
|
||||
virtual: {
|
||||
total: number;
|
||||
available: number;
|
||||
};
|
||||
}
|
||||
interface KernelInfo {
|
||||
variant: string;
|
||||
version: string;
|
||||
}
|
||||
interface OSInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
}
|
||||
interface CPUInfo {
|
||||
vendor: string;
|
||||
model: string;
|
||||
frequency: number;
|
||||
architecture: string;
|
||||
logicalThreads: number;
|
||||
physicalCores: number;
|
||||
physicalUnits: number;
|
||||
}
|
||||
interface Display {
|
||||
id: number;
|
||||
resolution: Resolution;
|
||||
dpi: number;
|
||||
bpp: number;
|
||||
refreshRate: number;
|
||||
}
|
||||
interface Resolution {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
function getMemoryInfo(): Promise<MemoryInfo>;
|
||||
function getArch(): Promise<string>;
|
||||
function getKernelInfo(): Promise<KernelInfo>;
|
||||
function getOSInfo(): Promise<OSInfo>;
|
||||
function getCPUInfo(): Promise<CPUInfo>;
|
||||
function getDisplays(): Promise<Display[]>;
|
||||
function getMousePosition(): Promise<MousePosition>;
|
||||
}
|
||||
namespace storage {
|
||||
function setData(key: string, data: string): Promise<void>;
|
||||
function getData(key: string): Promise<string>;
|
||||
function getKeys(): Promise<string[]>;
|
||||
}
|
||||
namespace debug {
|
||||
// debug
|
||||
enum LoggerType {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO"
|
||||
}
|
||||
// os
|
||||
enum Icon {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO",
|
||||
QUESTION = "QUESTION"
|
||||
}
|
||||
enum MessageBoxChoice {
|
||||
OK = "OK",
|
||||
OK_CANCEL = "OK_CANCEL",
|
||||
YES_NO = "YES_NO",
|
||||
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||
RETRY_CANCEL = "RETRY_CANCEL",
|
||||
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||
}
|
||||
//clipboard
|
||||
enum ClipboardFormat {
|
||||
unknown = "unknown",
|
||||
text = "text",
|
||||
image = "image"
|
||||
}
|
||||
// NL_GLOBALS
|
||||
enum Mode {
|
||||
window = "window",
|
||||
browser = "browser",
|
||||
cloud = "cloud",
|
||||
chrome = "chrome"
|
||||
}
|
||||
enum OperatingSystem {
|
||||
Linux = "Linux",
|
||||
Windows = "Windows",
|
||||
Darwin = "Darwin",
|
||||
FreeBSD = "FreeBSD",
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
enum Architecture {
|
||||
x64 = "x64",
|
||||
arm = "arm",
|
||||
itanium = "itanium",
|
||||
ia32 = "ia32",
|
||||
unknown = "unknown"
|
||||
}
|
||||
function log(message: string, type?: LoggerType): Promise<void>;
|
||||
}
|
||||
namespace app {
|
||||
interface OpenActionOptions {
|
||||
url: string;
|
||||
}
|
||||
interface RestartOptions {
|
||||
args: string;
|
||||
}
|
||||
function exit(code?: number): Promise<void>;
|
||||
function killProcess(): Promise<void>;
|
||||
function restartProcess(options?: RestartOptions): Promise<void>;
|
||||
function getConfig(): Promise<any>;
|
||||
function broadcast(event: string, data?: any): Promise<void>;
|
||||
function readProcessInput(readAll?: boolean): Promise<string>;
|
||||
function writeProcessOutput(data: string): Promise<void>;
|
||||
function writeProcessError(data: string): Promise<void>;
|
||||
}
|
||||
namespace window {
|
||||
interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
fullScreen?: boolean;
|
||||
alwaysOnTop?: boolean;
|
||||
enableInspector?: boolean;
|
||||
borderless?: boolean;
|
||||
maximize?: boolean;
|
||||
hidden?: boolean;
|
||||
maximizable?: boolean;
|
||||
useSavedState?: boolean;
|
||||
exitProcessOnClose?: boolean;
|
||||
extendUserAgentWith?: string;
|
||||
injectGlobals?: boolean;
|
||||
injectClientLibrary?: boolean;
|
||||
injectScript?: string;
|
||||
processArgs?: string;
|
||||
}
|
||||
interface WindowSizeOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
resizable?: boolean;
|
||||
}
|
||||
interface WindowPosOptions {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface WindowMenu extends Array<WindowMenuItem> {
|
||||
}
|
||||
interface WindowMenuItem {
|
||||
id?: string;
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
isChecked?: boolean;
|
||||
menuItems?: WindowMenuItem[];
|
||||
}
|
||||
function setTitle(title: string): Promise<void>;
|
||||
function getTitle(): Promise<string>;
|
||||
function maximize(): Promise<void>;
|
||||
function unmaximize(): Promise<void>;
|
||||
function isMaximized(): Promise<boolean>;
|
||||
function minimize(): Promise<void>;
|
||||
function unminimize(): Promise<void>;
|
||||
function isMinimized(): Promise<boolean>;
|
||||
function setFullScreen(): Promise<void>;
|
||||
function exitFullScreen(): Promise<void>;
|
||||
function isFullScreen(): Promise<boolean>;
|
||||
function show(): Promise<void>;
|
||||
function hide(): Promise<void>;
|
||||
function isVisible(): Promise<boolean>;
|
||||
function focus(): Promise<void>;
|
||||
function setIcon(icon: string): Promise<void>;
|
||||
function move(x: number, y: number): Promise<void>;
|
||||
function center(): Promise<void>;
|
||||
type DraggableRegionOptions = {
|
||||
/**
|
||||
* If set to `true`, the region will always capture the pointer,
|
||||
* ensuring dragging doesn't break on fast pointer movement.
|
||||
* Note that it prevents child elements from receiving any pointer events.
|
||||
* Defaults to `false`.
|
||||
*/
|
||||
alwaysCapture?: boolean;
|
||||
/**
|
||||
* Minimum distance between cursor's starting and current position
|
||||
* after which dragging is started. This helps prevent accidental dragging
|
||||
* while interacting with child elements.
|
||||
* Defaults to `10`. (In pixels.)
|
||||
*/
|
||||
dragMinDistance?: number;
|
||||
};
|
||||
function setDraggableRegion(domElementOrId: string | HTMLElement, options?: DraggableRegionOptions): Promise<{
|
||||
success: true;
|
||||
message: string;
|
||||
}>;
|
||||
function unsetDraggableRegion(domElementOrId: string | HTMLElement): Promise<{
|
||||
success: true;
|
||||
message: string;
|
||||
}>;
|
||||
function setSize(options: WindowSizeOptions): Promise<void>;
|
||||
function getSize(): Promise<WindowSizeOptions>;
|
||||
function getPosition(): Promise<WindowPosOptions>;
|
||||
function setAlwaysOnTop(onTop: boolean): Promise<void>;
|
||||
function create(url: string, options?: WindowOptions): Promise<void>;
|
||||
function snapshot(path: string): Promise<void>;
|
||||
function setMainMenu(options: WindowMenu): Promise<void>;
|
||||
}
|
||||
namespace events {
|
||||
interface Response {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
|
||||
function on(event: string, handler: (ev: CustomEvent) => void): Promise<Response>;
|
||||
function off(event: string, handler: (ev: CustomEvent) => void): Promise<Response>;
|
||||
function dispatch(event: string, data?: any): Promise<Response>;
|
||||
function broadcast(event: string, data?: any): Promise<void>;
|
||||
}
|
||||
namespace extensions {
|
||||
interface ExtensionStats {
|
||||
loaded: string[];
|
||||
connected: string[];
|
||||
}
|
||||
function dispatch(extensionId: string, event: string, data?: any): Promise<void>;
|
||||
function broadcast(event: string, data?: any): Promise<void>;
|
||||
function getStats(): Promise<ExtensionStats>;
|
||||
}
|
||||
namespace updater {
|
||||
interface Manifest {
|
||||
applicationId: string;
|
||||
version: string;
|
||||
resourcesURL: string;
|
||||
}
|
||||
function checkForUpdates(url: string): Promise<Manifest>;
|
||||
function install(): Promise<void>;
|
||||
}
|
||||
namespace clipboard {
|
||||
interface ClipboardImage {
|
||||
width: number;
|
||||
height: number;
|
||||
bpp: number;
|
||||
bpr: number;
|
||||
redMask: number;
|
||||
greenMask: number;
|
||||
blueMask: number;
|
||||
redShift: number;
|
||||
greenShift: number;
|
||||
blueShift: number;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
// debug
|
||||
enum LoggerType {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO"
|
||||
}
|
||||
// os
|
||||
enum Icon {
|
||||
WARNING = "WARNING",
|
||||
ERROR = "ERROR",
|
||||
INFO = "INFO",
|
||||
QUESTION = "QUESTION"
|
||||
}
|
||||
enum MessageBoxChoice {
|
||||
OK = "OK",
|
||||
OK_CANCEL = "OK_CANCEL",
|
||||
YES_NO = "YES_NO",
|
||||
YES_NO_CANCEL = "YES_NO_CANCEL",
|
||||
RETRY_CANCEL = "RETRY_CANCEL",
|
||||
ABORT_RETRY_IGNORE = "ABORT_RETRY_IGNORE"
|
||||
}
|
||||
//clipboard
|
||||
enum ClipboardFormat {
|
||||
unknown = "unknown",
|
||||
text = "text",
|
||||
image = "image"
|
||||
}
|
||||
// NL_GLOBALS
|
||||
enum Mode {
|
||||
window = "window",
|
||||
browser = "browser",
|
||||
cloud = "cloud",
|
||||
chrome = "chrome"
|
||||
}
|
||||
enum OperatingSystem {
|
||||
Linux = "Linux",
|
||||
Windows = "Windows",
|
||||
Darwin = "Darwin",
|
||||
FreeBSD = "FreeBSD",
|
||||
Unknown = "Unknown"
|
||||
}
|
||||
enum Architecture {
|
||||
x64 = "x64",
|
||||
arm = "arm",
|
||||
itanium = "itanium",
|
||||
ia32 = "ia32",
|
||||
unknown = "unknown"
|
||||
}
|
||||
function getFormat(): Promise<ClipboardFormat>;
|
||||
function readText(): Promise<string>;
|
||||
function readImage(format?: string): Promise<ClipboardImage | null>;
|
||||
function writeText(data: string): Promise<void>;
|
||||
function writeImage(image: ClipboardImage): Promise<void>;
|
||||
function readHTML(): Promise<string>;
|
||||
function writeHTML(data: string): Promise<void>;
|
||||
function clear(): Promise<void>;
|
||||
}
|
||||
namespace resources {
|
||||
interface Stats {
|
||||
size: number;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
}
|
||||
function getFiles(): Promise<string[]>;
|
||||
function getStats(path: string): Promise<Stats>;
|
||||
function extractFile(path: string, destination: string): Promise<void>;
|
||||
function extractDirectory(path: string, destination: string): Promise<void>;
|
||||
function readFile(path: string): Promise<string>;
|
||||
function readBinaryFile(path: string): Promise<ArrayBuffer>;
|
||||
}
|
||||
namespace server {
|
||||
function mount(path: string, target: string): Promise<void>;
|
||||
function unmount(path: string): Promise<void>;
|
||||
function getMounts(): Promise<void>;
|
||||
}
|
||||
namespace custom {
|
||||
function getMethods(): Promise<string[]>;
|
||||
}
|
||||
interface InitOptions {
|
||||
exportCustomMethods?: boolean;
|
||||
}
|
||||
function init(options?: InitOptions): void;
|
||||
type ErrorCode = "NE_FS_DIRCRER" | "NE_FS_RMDIRER" | "NE_FS_FILRDER" | "NE_FS_FILWRER" | "NE_FS_FILRMER" | "NE_FS_NOPATHE" | "NE_FS_COPYFER" | "NE_FS_MOVEFER" | "NE_OS_INVMSGA" | "NE_OS_INVKNPT" | "NE_ST_INVSTKY" | "NE_ST_STKEYWE" | "NE_RT_INVTOKN" | "NE_RT_NATPRME" | "NE_RT_APIPRME" | "NE_RT_NATRTER" | "NE_RT_NATNTIM" | "NE_CL_NSEROFF" | "NE_EX_EXTNOTC" | "NE_UP_CUPDMER" | "NE_UP_CUPDERR" | "NE_UP_UPDNOUF" | "NE_UP_UPDINER";
|
||||
interface Error {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
}
|
||||
interface OpenActionOptions {
|
||||
url: string;
|
||||
}
|
||||
interface RestartOptions {
|
||||
args: string;
|
||||
}
|
||||
interface MemoryInfo {
|
||||
physical: {
|
||||
total: number;
|
||||
available: number;
|
||||
};
|
||||
virtual: {
|
||||
total: number;
|
||||
available: number;
|
||||
};
|
||||
}
|
||||
interface KernelInfo {
|
||||
variant: string;
|
||||
version: string;
|
||||
}
|
||||
interface OSInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
version: string;
|
||||
}
|
||||
interface CPUInfo {
|
||||
vendor: string;
|
||||
model: string;
|
||||
frequency: number;
|
||||
architecture: string;
|
||||
logicalThreads: number;
|
||||
physicalCores: number;
|
||||
physicalUnits: number;
|
||||
}
|
||||
interface Display {
|
||||
id: number;
|
||||
resolution: Resolution;
|
||||
dpi: number;
|
||||
bpp: number;
|
||||
refreshRate: number;
|
||||
}
|
||||
interface Resolution {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
interface MousePosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface ClipboardImage {
|
||||
width: number;
|
||||
height: number;
|
||||
bpp: number;
|
||||
bpr: number;
|
||||
redMask: number;
|
||||
greenMask: number;
|
||||
blueMask: number;
|
||||
redShift: number;
|
||||
greenShift: number;
|
||||
blueShift: number;
|
||||
data: ArrayBuffer;
|
||||
}
|
||||
interface ExtensionStats {
|
||||
loaded: string[];
|
||||
connected: string[];
|
||||
}
|
||||
interface DirectoryEntry {
|
||||
entry: string;
|
||||
path: string;
|
||||
type: string;
|
||||
}
|
||||
interface FileReaderOptions {
|
||||
pos: number;
|
||||
size: number;
|
||||
}
|
||||
interface DirectoryReaderOptions {
|
||||
recursive: boolean;
|
||||
}
|
||||
interface OpenedFile {
|
||||
id: number;
|
||||
eof: boolean;
|
||||
pos: number;
|
||||
lastRead: number;
|
||||
}
|
||||
interface Stats {
|
||||
size: number;
|
||||
isFile: boolean;
|
||||
isDirectory: boolean;
|
||||
createdAt: number;
|
||||
modifiedAt: number;
|
||||
}
|
||||
interface Watcher {
|
||||
id: number;
|
||||
path: string;
|
||||
}
|
||||
interface CopyOptions {
|
||||
recursive: boolean;
|
||||
overwrite: boolean;
|
||||
skip: boolean;
|
||||
}
|
||||
interface PathParts {
|
||||
rootName: string;
|
||||
rootDirectory: string;
|
||||
rootPath: string;
|
||||
relativePath: string;
|
||||
parentPath: string;
|
||||
filename: string;
|
||||
stem: string;
|
||||
extension: string;
|
||||
}
|
||||
interface Permissions {
|
||||
all: boolean;
|
||||
ownerAll: boolean;
|
||||
ownerRead: boolean;
|
||||
ownerWrite: boolean;
|
||||
ownerExec: boolean;
|
||||
groupAll: boolean;
|
||||
groupRead: boolean;
|
||||
groupWrite: boolean;
|
||||
groupExec: boolean;
|
||||
othersAll: boolean;
|
||||
othersRead: boolean;
|
||||
othersWrite: boolean;
|
||||
othersExec: boolean;
|
||||
}
|
||||
type PermissionsMode = "ADD" | "REPLACE" | "REMOVE";
|
||||
interface ExecCommandOptions {
|
||||
stdIn?: string;
|
||||
background?: boolean;
|
||||
cwd?: string;
|
||||
}
|
||||
interface ExecCommandResult {
|
||||
pid: number;
|
||||
stdOut: string;
|
||||
stdErr: string;
|
||||
exitCode: number;
|
||||
}
|
||||
interface SpawnedProcess {
|
||||
id: number;
|
||||
pid: number;
|
||||
}
|
||||
interface SpawnedProcessOptions {
|
||||
cwd?: string;
|
||||
envs?: Record<string, string>;
|
||||
}
|
||||
interface Envs {
|
||||
[key: string]: string;
|
||||
}
|
||||
interface OpenDialogOptions {
|
||||
multiSelections?: boolean;
|
||||
filters?: Filter[];
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface FolderDialogOptions {
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface SaveDialogOptions {
|
||||
forceOverwrite?: boolean;
|
||||
filters?: Filter[];
|
||||
defaultPath?: string;
|
||||
}
|
||||
interface Filter {
|
||||
name: string;
|
||||
extensions: string[];
|
||||
}
|
||||
interface TrayOptions {
|
||||
icon: string;
|
||||
menuItems: TrayMenuItem[];
|
||||
}
|
||||
interface TrayMenuItem {
|
||||
id?: string;
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
isChecked?: boolean;
|
||||
}
|
||||
type KnownPath = "config" | "data" | "cache" | "documents" | "pictures" | "music" | "video" | "downloads" | "savedGames1" | "savedGames2" | "temp";
|
||||
interface Manifest {
|
||||
applicationId: string;
|
||||
version: string;
|
||||
resourcesURL: string;
|
||||
}
|
||||
interface WindowOptions extends WindowSizeOptions, WindowPosOptions {
|
||||
title?: string;
|
||||
icon?: string;
|
||||
fullScreen?: boolean;
|
||||
alwaysOnTop?: boolean;
|
||||
enableInspector?: boolean;
|
||||
borderless?: boolean;
|
||||
maximize?: boolean;
|
||||
hidden?: boolean;
|
||||
maximizable?: boolean;
|
||||
useSavedState?: boolean;
|
||||
exitProcessOnClose?: boolean;
|
||||
extendUserAgentWith?: string;
|
||||
injectGlobals?: boolean;
|
||||
injectClientLibrary?: boolean;
|
||||
injectScript?: string;
|
||||
processArgs?: string;
|
||||
}
|
||||
interface WindowSizeOptions {
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
maxWidth?: number;
|
||||
maxHeight?: number;
|
||||
resizable?: boolean;
|
||||
}
|
||||
interface WindowPosOptions {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
interface WindowMenu extends Array<WindowMenuItem> {
|
||||
}
|
||||
interface WindowMenuItem {
|
||||
id?: string;
|
||||
text: string;
|
||||
isDisabled?: boolean;
|
||||
isChecked?: boolean;
|
||||
menuItems?: WindowMenuItem[];
|
||||
}
|
||||
interface Response {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
type Builtin = "ready" | "trayMenuItemClicked" | "windowClose" | "serverOffline" | "clientConnect" | "clientDisconnect" | "appClientConnect" | "appClientDisconnect" | "extClientConnect" | "extClientDisconnect" | "extensionReady" | "neuDev_reloadApp";
|
||||
|
||||
}
|
||||
|
||||
// debug
|
||||
enum LoggerType {
|
||||
WARNING = 'WARNING',
|
||||
ERROR = 'ERROR',
|
||||
INFO = 'INFO'
|
||||
}
|
||||
|
||||
// os
|
||||
enum Icon {
|
||||
WARNING = 'WARNING',
|
||||
ERROR = 'ERROR',
|
||||
INFO = 'INFO',
|
||||
QUESTION = 'QUESTION'
|
||||
}
|
||||
|
||||
enum MessageBoxChoice {
|
||||
OK = 'OK',
|
||||
OK_CANCEL = 'OK_CANCEL',
|
||||
YES_NO = 'YES_NO',
|
||||
YES_NO_CANCEL = 'YES_NO_CANCEL',
|
||||
RETRY_CANCEL = 'RETRY_CANCEL',
|
||||
ABORT_RETRY_IGNORE = 'ABORT_RETRY_IGNORE'
|
||||
}
|
||||
|
||||
//clipboard
|
||||
enum ClipboardFormat {
|
||||
unknown = 'unknown',
|
||||
text = 'text',
|
||||
image = 'image'
|
||||
}
|
||||
|
||||
// NL_GLOBALS
|
||||
enum Mode {
|
||||
window = 'window',
|
||||
browser = 'browser',
|
||||
cloud = 'cloud',
|
||||
chrome = 'chrome'
|
||||
}
|
||||
|
||||
enum OperatingSystem {
|
||||
Linux = 'Linux',
|
||||
Windows = 'Windows',
|
||||
Darwin = 'Darwin',
|
||||
FreeBSD = 'FreeBSD',
|
||||
Unknown = 'Unknown'
|
||||
}
|
||||
|
||||
enum Architecture {
|
||||
x64 = 'x64',
|
||||
arm = 'arm',
|
||||
itanium = 'itanium',
|
||||
ia32 = 'ia32',
|
||||
unknown = 'unknown'
|
||||
}
|
||||
|
||||
|
||||
interface Response {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
type Builtin =
|
||||
'ready' |
|
||||
'trayMenuItemClicked' |
|
||||
'windowClose' |
|
||||
'serverOffline' |
|
||||
'clientConnect' |
|
||||
'clientDisconnect' |
|
||||
'appClientConnect' |
|
||||
'appClientDisconnect' |
|
||||
'extClientConnect' |
|
||||
'extClientDisconnect' |
|
||||
'extensionReady' |
|
||||
'neuDev_reloadApp'
|
||||
|
||||
|
||||
// --- globals ---
|
||||
/** Mode of the application: window, browser, cloud, or chrome */
|
||||
declare const NL_MODE: Mode;
|
||||
/** Application port */
|
||||
declare const NL_PORT: number;
|
||||
/** Command-line arguments */
|
||||
declare const NL_ARGS: string[];
|
||||
/** Basic authentication token */
|
||||
declare const NL_TOKEN: string;
|
||||
/** Neutralinojs client version */
|
||||
declare const NL_CVERSION: string;
|
||||
/** Application identifier */
|
||||
declare const NL_APPID: string;
|
||||
/** Application version */
|
||||
declare const NL_APPVERSION: string;
|
||||
/** Application path */
|
||||
declare const NL_PATH: string;
|
||||
/** Application data path */
|
||||
declare const NL_DATAPATH: string;
|
||||
/** Returns true if extensions are enabled */
|
||||
declare const NL_EXTENABLED: boolean;
|
||||
/** Returns true if the client library is injected */
|
||||
declare const NL_GINJECTED: boolean;
|
||||
/** Returns true if globals are injected */
|
||||
declare const NL_CINJECTED: boolean;
|
||||
/** Operating system name: Linux, Windows, Darwin, FreeBSD, or Uknown */
|
||||
declare const NL_OS: OperatingSystem;
|
||||
/** CPU architecture: x64, arm, itanium, ia32, or unknown */
|
||||
declare const NL_ARCH: Architecture;
|
||||
/** Neutralinojs server version */
|
||||
declare const NL_VERSION: string;
|
||||
/** Current working directory */
|
||||
declare const NL_CWD: string;
|
||||
/** Identifier of the current process */
|
||||
declare const NL_PID: string;
|
||||
/** Source of application resources: bundle or directory */
|
||||
declare const NL_RESMODE: string;
|
||||
/** Release commit of the client library */
|
||||
declare const NL_CCOMMIT: string;
|
||||
/** An array of custom methods */
|
||||
declare const NL_CMETHODS: string[];
|
||||
|
20
demo/app/resources/styles.css
Normal file
20
demo/app/resources/styles.css
Normal file
@ -0,0 +1,20 @@
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#neutralinoapp {
|
||||
text-align: center;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
#neutralinoapp h1{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#neutralinoapp > div {
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
}
|
29
go.mod
29
go.mod
@ -1,23 +1,30 @@
|
||||
module apigo.cc/gojs/client
|
||||
|
||||
go 1.18
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
apigo.cc/gojs v0.0.3
|
||||
github.com/ssgo/tool v0.4.27
|
||||
apigo.cc/gojs v0.0.25
|
||||
github.com/chromedp/cdproto v0.0.0-20250724212937-08a3db8b4327
|
||||
github.com/chromedp/chromedp v0.14.0
|
||||
github.com/ssgo/tool v0.4.29
|
||||
github.com/ssgo/u v1.7.21
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/chromedp/sysutil v1.1.0 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-json-experiment/json v0.0.0-20250725192818-e39067aee2d2 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
||||
github.com/ssgo/config v1.7.7 // indirect
|
||||
github.com/ssgo/log v1.7.7 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.4.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect
|
||||
github.com/ssgo/config v1.7.9 // indirect
|
||||
github.com/ssgo/log v1.7.9 // indirect
|
||||
github.com/ssgo/standard v1.7.7 // indirect
|
||||
github.com/ssgo/u v1.7.9 // indirect
|
||||
golang.org/x/sys v0.26.0 // indirect
|
||||
golang.org/x/text v0.19.0 // indirect
|
||||
golang.org/x/sys v0.34.0 // indirect
|
||||
golang.org/x/text v0.27.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
45
tests/client.go
Normal file
45
tests/client.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/client"
|
||||
"github.com/ssgo/u"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gojs.ExportForDev()
|
||||
testOK := false
|
||||
client.Bind("setTestOK", func(testIsOK bool) {
|
||||
testOK = testIsOK
|
||||
})
|
||||
|
||||
scriptFile := "client.js"
|
||||
if len(os.Args) > 1 {
|
||||
scriptFile = os.Args[1]
|
||||
}
|
||||
|
||||
exePath, _ := os.Executable()
|
||||
appDir := ""
|
||||
if strings.Contains(exePath, "Contents/MacOS") {
|
||||
appDir = filepath.Join(filepath.Dir(filepath.Dir(exePath)), "Resources")
|
||||
os.Chdir(appDir)
|
||||
}
|
||||
u.WriteFile("/tmp/aaa.txt", appDir)
|
||||
|
||||
r, err := gojs.RunFile(scriptFile)
|
||||
if err != nil {
|
||||
fmt.Println(u.Red(err.Error()))
|
||||
}
|
||||
gojs.WaitAll()
|
||||
|
||||
if !testOK {
|
||||
fmt.Println(u.BRed("test failed"))
|
||||
} else {
|
||||
fmt.Println(u.BGreen("test OK"), r)
|
||||
}
|
||||
}
|
@ -13,7 +13,8 @@
|
||||
app.setSize(1200, 400, "fixed")
|
||||
setTimeout(() => {
|
||||
setTestOK(true)
|
||||
app.close()
|
||||
document.querySelector('._dialogButton').click()
|
||||
// app.close()
|
||||
}, 1000)
|
||||
}, 1000)
|
||||
})
|
||||
|
@ -5,4 +5,5 @@ client.open({
|
||||
height: 600,
|
||||
title: 'Hello',
|
||||
url: 'client.html',
|
||||
isDebug: false,
|
||||
})
|
||||
|
@ -1,29 +0,0 @@
|
||||
package client_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"apigo.cc/gojs"
|
||||
"apigo.cc/gojs/client"
|
||||
)
|
||||
|
||||
func TestClient(t *testing.T) {
|
||||
gojs.ExportForDev()
|
||||
testOK := false
|
||||
client.Bind("setTestOK", func(testIsOK bool) {
|
||||
testOK = testIsOK
|
||||
})
|
||||
|
||||
r, err := gojs.RunFile("client.js")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
gojs.WaitAll()
|
||||
|
||||
if !testOK {
|
||||
t.Fatal("test failed")
|
||||
} else {
|
||||
fmt.Println("test OK", r)
|
||||
}
|
||||
}
|
2
tests/test_chromedp.sh
Executable file
2
tests/test_chromedp.sh
Executable file
@ -0,0 +1,2 @@
|
||||
export CGO_ENABLED=0
|
||||
go run .
|
2
tests/test_webview.sh
Executable file
2
tests/test_webview.sh
Executable file
@ -0,0 +1,2 @@
|
||||
export CGO_ENABLED=1
|
||||
go run .
|
Loading…
x
Reference in New Issue
Block a user