Added wasm support

This commit is contained in:
Alexei Anoshenko 2022-10-27 16:14:30 +03:00
parent 3c4315ed78
commit 4523a9a427
10 changed files with 968 additions and 545 deletions

307
appServer.go Normal file
View File

@ -0,0 +1,307 @@
//go:build !wasm
package rui
import (
"context"
_ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os/exec"
"runtime"
"strconv"
"strings"
"time"
)
//go:embed app_socket.js
var socketScripts string
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
}
func (app *application) getStartPage() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
getStartPage(buffer, app.params, socketScripts)
buffer.WriteString("\n</html>")
return buffer.String()
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
delete(app.sessions, id)
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case "GET":
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if brige := CreateSocketBrige(w, req); brige != nil {
go app.socketReader(brige)
}
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func (app *application) socketReader(brige webBrige) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := brige.readMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
command := obj.Tag()
switch command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
session.setBrige(events, brige)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !brige.writeMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, brige)
return
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj, session)
case "imageError":
session.imageManager().imageLoadError(obj, session)
default:
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, brige webBrige) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
brige.close()
default:
session.handleEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject, brige webBrige) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBrige(events, brige)
if !session.setContent(app.createContentFunc(session), session) {
return nil, ""
}
app.sessions[session.ID()] = session
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if params.CertFile != "" && params.KeyFile != "" {
addr += ":443"
} else {
addr += ":80"
}
}
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if params.CertFile != "" && params.KeyFile != "" {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
} else {
serverRun(app.server.ListenAndServe())
}
}
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}

235
appWasm.go Normal file
View File

@ -0,0 +1,235 @@
//go:build wasm
package rui
import (
_ "embed"
"strings"
"syscall/js"
)
//go:embed app_wasm.js
var wasmScripts string
type wasmApp struct {
params AppParams
createContentFunc func(Session) SessionContent
session Session
brige webBrige
}
func (app *wasmApp) Finish() {
app.session.close()
}
/*
func (app *wasmApp) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case "GET":
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if brige := CreateSocketBrige(w, req); brige != nil {
go app.socketReader(brige)
}
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
*/
func (app *wasmApp) startSession(this js.Value, args []js.Value) interface{} {
if app.createContentFunc == nil || len(args) == 1 {
return nil
}
params := ParseDataText(args[0].String())
session := newSession(app, 0, "", params)
session.setBrige(make(chan DataObject), app.brige)
if !session.setContent(app.createContentFunc(session), session) {
return nil
}
app.session = session
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return nil
}
func (app *wasmApp) removeSession(id int) {
}
// StartApp - create the new wasmApp and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(wasmApp)
app.params = params
app.createContentFunc = createContentFunc
if createContentFunc == nil {
return
}
app.brige = createWasmBrige()
js.Global().Set("startSession", js.FuncOf(app.startSession))
script := defaultScripts + wasmScripts
script = strings.ReplaceAll(script, "\\", `\\`)
script = strings.ReplaceAll(script, "\n", `\n`)
script = strings.ReplaceAll(script, "\t", `\t`)
script = strings.ReplaceAll(script, "\"", `\"`)
script = strings.ReplaceAll(script, "'", `\'`)
//window := js.Global().Get("window")
//window.Call("execScript", `document.getElementById('ruiscript').text = "`+script+`"`)
js.Global().Call("execScript", `document.getElementById('ruiscript').text += "`+script+`"`)
document := js.Global().Get("document")
body := document.Call("querySelector", "body")
body.Set("innerHTML", `<div class="ruiRoot" id="ruiRootView"></div>
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div>
<a id="ruiDownloader" download style="display: none;"></a>`)
js.Global().Call("execScript", "initSession()")
//window.Call("execScript", "initSession()")
for true {
if message, ok := app.brige.readMessage(); ok && app.session != nil {
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
switch command := obj.Tag(); command {
/*
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
session.setBrige(events, brige)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !brige.writeMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, brige)
return
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.writeMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
brige.close()
*/
case "answer":
app.session.handleAnswer(obj)
case "imageLoaded":
app.session.imageManager().imageLoaded(obj, app.session)
case "imageError":
app.session.imageManager().imageLoadError(obj, app.session)
default:
app.session.handleEvent(command, obj)
}
}
}
}
}
func FinishApp() {
//app.Finish()
}
func OpenBrowser(url string) bool {
return false
}
/*
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}
*/

View File

@ -1,103 +1,6 @@
var sessionID = "0"
var socket
var socketUrl
var images = new Map();
var windowFocus = true
function sendMessage(message) {
if (socket) {
socket.send(message)
}
}
window.onload = function() {
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
var port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = socketOpen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
};
function socketOpen() {
const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0";
var message = "startSession{touch=" + touch_screen
const style = window.getComputedStyle(document.body);
if (style) {
var direction = style.getPropertyValue('direction');
if (direction) {
message += ",direction=" + direction
}
}
const lang = window.navigator.language;
if (lang) {
message += ",language=\"" + lang + "\"";
}
const langs = window.navigator.languages;
if (langs) {
message += ",languages=\"" + langs + "\"";
}
const userAgent = window.navigator.userAgent
if (userAgent) {
message += ",user-agent=\"" + userAgent + "\"";
}
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
if (darkThemeMq.matches) {
message += ",dark=1";
}
const pixelRatio = window.devicePixelRatio;
if (pixelRatio) {
message += ",pixel-ratio=" + pixelRatio;
}
sendMessage( message + "}" );
}
function socketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
socket = new WebSocket(socketUrl);
socket.onopen = socketReopen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
}
}
function socketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
window.setTimeout(socketReconnect, 10000);
}
}
function socketError(error) {
console.log(error);
}
window.onresize = function() {
scanElementsSize();
}
@ -111,15 +14,6 @@ window.onblur = function(event) {
sendMessage( "session-pause{session=" + sessionID +"}" );
}
window.onfocus = function(event) {
windowFocus = true
if (!socket) {
socketReconnect()
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}
function getIntAttribute(element, tag) {
let value = element.getAttribute(tag);
if (value) {
@ -1188,6 +1082,8 @@ function stackTransitionEndEvent(stackId, propertyName, event) {
event.stopPropagation();
}
var images = new Map();
function loadImage(url) {
var img = images.get(url);
if (img != undefined) {

104
app_socket.js Normal file
View File

@ -0,0 +1,104 @@
var socket
var socketUrl
function sendMessage(message) {
if (socket) {
socket.send(message)
}
}
window.onload = function() {
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
socketUrl += document.location.hostname
var port = document.location.port
if (port) {
socketUrl += ":" + port
}
socketUrl += window.location.pathname + "ws"
socket = new WebSocket(socketUrl);
socket.onopen = socketOpen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
};
function socketOpen() {
const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0";
var message = "startSession{touch=" + touch_screen
const style = window.getComputedStyle(document.body);
if (style) {
var direction = style.getPropertyValue('direction');
if (direction) {
message += ",direction=" + direction
}
}
const lang = window.navigator.language;
if (lang) {
message += ",language=\"" + lang + "\"";
}
const langs = window.navigator.languages;
if (langs) {
message += ",languages=\"" + langs + "\"";
}
const userAgent = window.navigator.userAgent
if (userAgent) {
message += ",user-agent=\"" + userAgent + "\"";
}
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
if (darkThemeMq.matches) {
message += ",dark=1";
}
const pixelRatio = window.devicePixelRatio;
if (pixelRatio) {
message += ",pixel-ratio=" + pixelRatio;
}
sendMessage( message + "}" );
}
function socketReopen() {
sendMessage( "reconnect{session=" + sessionID + "}" );
}
function socketReconnect() {
if (!socket) {
socket = new WebSocket(socketUrl);
socket.onopen = socketReopen;
socket.onclose = socketClose;
socket.onerror = socketError;
socket.onmessage = function(event) {
window.execScript ? window.execScript(event.data) : window.eval(event.data);
};
}
}
function socketClose(event) {
console.log("socket closed")
socket = null;
if (!event.wasClean && windowFocus) {
window.setTimeout(socketReconnect, 10000);
}
}
function socketError(error) {
console.log(error);
}
window.onfocus = function(event) {
windowFocus = true
if (!socket) {
socketReconnect()
} else {
sendMessage( "session-resume{session=" + sessionID +"}" );
}
}

48
app_wasm.js Normal file
View File

@ -0,0 +1,48 @@
function initSession() {
const touch_screen = (('ontouchstart' in document.documentElement) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) ? "1" : "0";
var message = "sessionInfo{touch=" + touch_screen
const style = window.getComputedStyle(document.body);
if (style) {
var direction = style.getPropertyValue('direction');
if (direction) {
message += ",direction=" + direction
}
}
const lang = window.navigator.language;
if (lang) {
message += ",language=\"" + lang + "\"";
}
const langs = window.navigator.languages;
if (langs) {
message += ",languages=\"" + langs + "\"";
}
const userAgent = window.navigator.userAgent
if (userAgent) {
message += ",user-agent=\"" + userAgent + "\"";
}
const darkThemeMq = window.matchMedia("(prefers-color-scheme: dark)");
if (darkThemeMq.matches) {
message += ",dark=1";
}
const pixelRatio = window.devicePixelRatio;
if (pixelRatio) {
message += ",pixel-ratio=" + pixelRatio;
}
startSession( message + "}" );
}
window.onfocus = function(event) {
windowFocus = true
sendMessage( "session-resume{session=" + sessionID +"}" );
}

View File

@ -1,21 +1,8 @@
package rui
import (
"bytes"
"context"
_ "embed"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
//go:embed app_scripts.js
@ -30,17 +17,9 @@ var defaultThemeText string
// Application - app interface
type Application interface {
Finish()
nextSessionID() int
removeSession(id int)
}
type application struct {
server *http.Server
params AppParams
createContentFunc func(Session) SessionContent
sessions map[int]Session
}
// AppParams defines parameters of the app
type AppParams struct {
// Title - title of the app window/tab
@ -61,28 +40,23 @@ type AppParams struct {
Redirect80 bool
}
func (app *application) getStartPage() string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
buffer.WriteString(`<!DOCTYPE html>
<html>
<head>
func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) {
buffer.WriteString(`<head>
<meta charset="utf-8">
<title>`)
buffer.WriteString(app.params.Title)
buffer.WriteString(params.Title)
buffer.WriteString("</title>")
if app.params.Icon != "" {
if params.Icon != "" {
buffer.WriteString(`
<link rel="icon" href="`)
buffer.WriteString(app.params.Icon)
buffer.WriteString(params.Icon)
buffer.WriteString(`">`)
}
if app.params.TitleColor != 0 {
if params.TitleColor != 0 {
buffer.WriteString(`
<meta name="theme-color" content="`)
buffer.WriteString(app.params.TitleColor.cssString())
buffer.WriteString(params.TitleColor.cssString())
buffer.WriteString(`">`)
}
@ -92,356 +66,15 @@ func (app *application) getStartPage() string {
<style>`)
buffer.WriteString(appStyles)
buffer.WriteString(`</style>
<script>`)
<script>
`)
buffer.WriteString(defaultScripts)
buffer.WriteString(addScripts)
buffer.WriteString(`</script>
</head>
<body>
<div class="ruiRoot" id="ruiRootView"></div>
<div class="ruiPopupLayer" id="ruiPopupLayer" style="visibility: hidden;" onclick="clickOutsidePopup(event)"></div>
<a id="ruiDownloader" download style="display: none;"></a>
</body>
</html>`)
return buffer.String()
}
func (app *application) Finish() {
for _, session := range app.sessions {
session.close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := app.server.Shutdown(ctx); err != nil {
log.Println(err.Error())
}
}
func (app *application) nextSessionID() int {
n := rand.Intn(0x7FFFFFFE) + 1
_, ok := app.sessions[n]
for ok {
n = rand.Intn(0x7FFFFFFE) + 1
_, ok = app.sessions[n]
}
return n
}
func (app *application) removeSession(id int) {
delete(app.sessions, id)
}
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if ProtocolInDebugLog {
DebugLogF("%s %s", req.Method, req.URL.Path)
}
switch req.Method {
case "GET":
switch req.URL.Path {
case "/":
w.WriteHeader(http.StatusOK)
io.WriteString(w, app.getStartPage())
case "/ws":
if brige := CreateSocketBrige(w, req); brige != nil {
go app.socketReader(brige)
}
default:
filename := req.URL.Path[1:]
if size := len(filename); size > 0 && filename[size-1] == '/' {
filename = filename[:size-1]
}
if !serveResourceFile(filename, w, req) &&
!serveDownloadFile(filename, w, req) {
w.WriteHeader(http.StatusNotFound)
}
}
}
}
func (app *application) socketReader(brige WebBrige) {
var session Session
events := make(chan DataObject, 1024)
for {
message, ok := brige.ReadMessage()
if !ok {
events <- NewDataObject("disconnect")
return
}
if ProtocolInDebugLog {
DebugLog(message)
}
if obj := ParseDataText(message); obj != nil {
command := obj.Tag()
switch command {
case "startSession":
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "reconnect":
if sessionText, ok := obj.PropertyValue("session"); ok {
if sessionID, err := strconv.Atoi(sessionText); err == nil {
if session = app.sessions[sessionID]; session != nil {
session.setBrige(events, brige)
answer := allocStringBuilder()
defer freeStringBuilder(answer)
session.writeInitScript(answer)
if !brige.WriteMessage(answer.String()) {
return
}
session.onReconnect()
go sessionEventHandler(session, events, brige)
return
}
DebugLogF("Session #%d not exists", sessionID)
} else {
ErrorLog(`strconv.Atoi(sessionText) error: ` + err.Error())
}
} else {
ErrorLog(`"session" key not found`)
}
answer := ""
if session, answer = app.startSession(obj, events, brige); session != nil {
if !brige.WriteMessage(answer) {
return
}
session.onStart()
go sessionEventHandler(session, events, brige)
}
case "answer":
session.handleAnswer(obj)
case "imageLoaded":
session.imageManager().imageLoaded(obj, session)
case "imageError":
session.imageManager().imageLoadError(obj, session)
default:
events <- obj
}
}
}
}
func sessionEventHandler(session Session, events chan DataObject, brige WebBrige) {
for {
data := <-events
switch command := data.Tag(); command {
case "disconnect":
session.onDisconnect()
return
case "session-close":
session.onFinish()
session.App().removeSession(session.ID())
brige.Close()
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
default:
session.handleViewEvent(command, data)
}
}
}
func (app *application) startSession(params DataObject, events chan DataObject, brige WebBrige) (Session, string) {
if app.createContentFunc == nil {
return nil, ""
}
session := newSession(app, app.nextSessionID(), "", params)
session.setBrige(events, brige)
if !session.setContent(app.createContentFunc(session), session) {
return nil, ""
}
app.sessions[session.ID()] = session
answer := allocStringBuilder()
defer freeStringBuilder(answer)
answer.WriteString("sessionID = '")
answer.WriteString(strconv.Itoa(session.ID()))
answer.WriteString("';\n")
session.writeInitScript(answer)
answerText := answer.String()
if ProtocolInDebugLog {
DebugLog("Start session:")
DebugLog(answerText)
}
return session, answerText
}
var apps = []*application{}
// StartApp - create the new application and start it
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
app := new(application)
app.params = params
app.sessions = map[int]Session{}
app.createContentFunc = createContentFunc
apps = append(apps, app)
redirectAddr := ""
if index := strings.IndexRune(addr, ':'); index >= 0 {
redirectAddr = addr[:index] + ":80"
} else {
redirectAddr = addr + ":80"
if params.CertFile != "" && params.KeyFile != "" {
addr += ":443"
} else {
addr += ":80"
}
}
app.server = &http.Server{Addr: addr}
http.Handle("/", app)
serverRun := func(err error) {
if err != nil {
if err == http.ErrServerClosed {
log.Println(err)
} else {
log.Fatal(err)
}
}
}
if params.CertFile != "" && params.KeyFile != "" {
if params.Redirect80 {
redirectTLS := func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+addr+r.RequestURI, http.StatusMovedPermanently)
}
go func() {
serverRun(http.ListenAndServe(redirectAddr, http.HandlerFunc(redirectTLS)))
}()
}
serverRun(app.server.ListenAndServeTLS(params.CertFile, params.KeyFile))
} else {
serverRun(app.server.ListenAndServe())
}
}
func FinishApp() {
for _, app := range apps {
app.Finish()
}
apps = []*application{}
}
func OpenBrowser(url string) bool {
var err error
switch runtime.GOOS {
case "linux":
for _, provider := range []string{"xdg-open", "x-www-browser", "www-browser"} {
if _, err = exec.LookPath(provider); err == nil {
if exec.Command(provider, url).Start(); err == nil {
return true
}
}
}
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err != nil
}
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename))
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
</body>`)
}

71
downloadFile.go Normal file
View File

@ -0,0 +1,71 @@
package rui
import (
"bytes"
"fmt"
"math/rand"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
type downloadFile struct {
filename string
path string
data []byte
}
var currentDownloadId = int(rand.Int31())
var downloadFiles = map[string]downloadFile{}
func (session *sessionData) startDownload(file downloadFile) {
currentDownloadId++
id := strconv.Itoa(currentDownloadId)
downloadFiles[id] = file
session.runScript(fmt.Sprintf(`startDowndload("%s", "%s")`, id, file.filename))
}
func serveDownloadFile(id string, w http.ResponseWriter, r *http.Request) bool {
if file, ok := downloadFiles[id]; ok {
delete(downloadFiles, id)
if file.data != nil {
http.ServeContent(w, r, file.filename, time.Now(), bytes.NewReader(file.data))
return true
} else if _, err := os.Stat(file.path); err == nil {
http.ServeFile(w, r, file.path)
return true
}
}
return false
}
// DownloadFile starts downloading the file on the client side.
func (session *sessionData) DownloadFile(path string) {
if _, err := os.Stat(path); err != nil {
ErrorLog(err.Error())
return
}
_, filename := filepath.Split(path)
session.startDownload(downloadFile{
filename: filename,
path: path,
data: nil,
})
}
// DownloadFileData starts downloading the file on the client side. Arguments specify the name of the downloaded file and its contents
func (session *sessionData) DownloadFileData(filename string, data []byte) {
if data == nil {
ErrorLog("Invalid download data. Must be not nil.")
return
}
session.startDownload(downloadFile{
filename: filename,
path: "",
data: data,
})
}

View File

@ -7,6 +7,15 @@ import (
"strings"
)
type webBrige interface {
readMessage() (string, bool)
writeMessage(text string) bool
runGetterScript(script string) DataObject
answerReceived(answer DataObject)
close()
remoteAddr() string
}
// SessionContent is the interface of a session content
type SessionContent interface {
CreateRootView(session Session) View
@ -85,14 +94,14 @@ type Session interface {
nextViewID() string
styleProperty(styleTag, property string) any
setBrige(events chan DataObject, brige WebBrige)
setBrige(events chan DataObject, brige webBrige)
writeInitScript(writer *strings.Builder)
runScript(script string)
runGetterScript(script string) DataObject //, answer chan DataObject)
handleAnswer(data DataObject)
handleRootSize(data DataObject)
handleResize(data DataObject)
handleViewEvent(command string, data DataObject)
handleEvent(command string, data DataObject)
close()
onStart()
@ -137,7 +146,7 @@ type sessionData struct {
ignoreUpdates bool
popups *popupManager
images *imageManager
brige WebBrige
brige webBrige
events chan DataObject
animationCounter int
animationCSS string
@ -166,38 +175,8 @@ func newSession(app Application, id int, customTheme string, params DataObject)
}
}
if value, ok := params.PropertyValue("touch"); ok {
session.touchScreen = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("user-agent"); ok {
session.userAgent = value
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
}
}
if value, ok := params.PropertyValue("language"); ok {
session.language = value
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
if params != nil {
session.handleSessionInfo(params)
}
return session
@ -211,7 +190,7 @@ func (session *sessionData) ID() int {
return session.sessionID
}
func (session *sessionData) setBrige(events chan DataObject, brige WebBrige) {
func (session *sessionData) setBrige(events chan DataObject, brige webBrige) {
session.events = events
session.brige = brige
}
@ -344,7 +323,7 @@ func (session *sessionData) imageManager() *imageManager {
func (session *sessionData) runScript(script string) {
if session.brige != nil {
session.brige.WriteMessage(script)
session.brige.writeMessage(script)
} else {
ErrorLog("No connection")
}
@ -352,7 +331,7 @@ func (session *sessionData) runScript(script string) {
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
if session.brige != nil {
return session.brige.RunGetterScript(script)
return session.brige.runGetterScript(script)
}
ErrorLog("No connection")
@ -362,7 +341,7 @@ func (session *sessionData) runGetterScript(script string) DataObject { //}, ans
}
func (session *sessionData) handleAnswer(data DataObject) {
session.brige.AnswerReceived(data)
session.brige.answerReceived(data)
}
func (session *sessionData) handleRootSize(data DataObject) {
@ -429,13 +408,67 @@ func (session *sessionData) handleResize(data DataObject) {
}
}
func (session *sessionData) handleViewEvent(command string, data DataObject) {
if viewID, ok := data.PropertyValue("id"); ok {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data)
func (session *sessionData) handleSessionInfo(params DataObject) {
if value, ok := params.PropertyValue("touch"); ok {
session.touchScreen = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("user-agent"); ok {
session.userAgent = value
}
if value, ok := params.PropertyValue("direction"); ok {
if value == "rtl" {
session.textDirection = RightToLeftDirection
}
}
if value, ok := params.PropertyValue("language"); ok {
session.language = value
}
if value, ok := params.PropertyValue("languages"); ok {
session.languages = strings.Split(value, ",")
}
if value, ok := params.PropertyValue("dark"); ok {
session.darkTheme = (value == "1" || value == "true")
}
if value, ok := params.PropertyValue("pixel-ratio"); ok {
if f, err := strconv.ParseFloat(value, 64); err != nil {
ErrorLog(err.Error())
} else {
session.pixelRatio = f
}
}
}
func (session *sessionData) handleEvent(command string, data DataObject) {
switch command {
case "session-pause":
session.onPause()
case "session-resume":
session.onResume()
case "root-size":
session.handleRootSize(data)
case "resize":
session.handleResize(data)
case "sessionInfo":
session.handleSessionInfo(data)
default:
if viewID, ok := data.PropertyValue("id"); ok {
if view := session.viewByHTMLID(viewID); view != nil {
view.handleCommand(view, command, data)
}
} else if command != "clickOutsidePopup" {
ErrorLog(`"id" property not found. Event: ` + command)
}
} else if command != "clickOutsidePopup" {
ErrorLog(`"id" property not found. Event: ` + command)
}
}

104
wasmBrige.go Normal file
View File

@ -0,0 +1,104 @@
//go:build wasm
package rui
import (
"strconv"
"sync"
"syscall/js"
"github.com/gorilla/websocket"
)
type wasmBrige struct {
queue chan string
answer map[int]chan DataObject
answerID int
answerMutex sync.Mutex
closed bool
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 8096,
}
func createWasmBrige() webBrige {
brige := new(wasmBrige)
brige.queue = make(chan string, 1000)
brige.answerID = 1
brige.answer = make(map[int]chan DataObject)
brige.closed = false
js.Global().Set("sendMessage", js.FuncOf(brige.sendMessage))
return brige
}
func (brige *wasmBrige) sendMessage(this js.Value, args []js.Value) interface{} {
if len(args) > 0 {
brige.queue <- args[0].String()
}
return nil
}
func (brige *wasmBrige) close() {
}
func (brige *wasmBrige) readMessage() (string, bool) {
return <-brige.queue, true
}
func (brige *wasmBrige) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
}
window := js.Global().Get("window")
window.Call("execScript", script)
return true
}
func (brige *wasmBrige) runGetterScript(script string) DataObject {
brige.answerMutex.Lock()
answerID := brige.answerID
brige.answerID++
brige.answerMutex.Unlock()
answer := make(chan DataObject)
brige.answer[answerID] = answer
errorText := ""
js.Global().Set("answerID", strconv.Itoa(answerID))
window := js.Global().Get("window")
window.Call("execScript", script)
result := NewDataObject("error")
result.SetPropertyValue("text", errorText)
delete(brige.answer, answerID)
return result
}
func (brige *wasmBrige) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil {
if chanel, ok := brige.answer[id]; ok {
chanel <- answer
delete(brige.answer, id)
} else {
ErrorLog("Bad answerID = " + text + " (chan not found)")
}
} else {
ErrorLog("Invalid answerID = " + text)
}
} else {
ErrorLog("answerID not found")
}
}
func (brige *wasmBrige) remoteAddr() string {
return "localhost"
}

View File

@ -1,3 +1,5 @@
//go:build !wasm
package rui
import (
@ -8,15 +10,6 @@ import (
"github.com/gorilla/websocket"
)
type WebBrige interface {
ReadMessage() (string, bool)
WriteMessage(text string) bool
RunGetterScript(script string) DataObject
AnswerReceived(answer DataObject)
Close()
remoteAddr() string
}
type wsBrige struct {
conn *websocket.Conn
answer map[int]chan DataObject
@ -30,7 +23,7 @@ var upgrader = websocket.Upgrader{
WriteBufferSize: 8096,
}
func CreateSocketBrige(w http.ResponseWriter, req *http.Request) WebBrige {
func CreateSocketBrige(w http.ResponseWriter, req *http.Request) webBrige {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
ErrorLog(err.Error())
@ -45,13 +38,12 @@ func CreateSocketBrige(w http.ResponseWriter, req *http.Request) WebBrige {
return brige
}
func (brige *wsBrige) Close() {
func (brige *wsBrige) close() {
brige.closed = true
brige.conn.Close()
}
func (brige *wsBrige) ReadMessage() (string, bool) {
//messageType, p, err := brige.conn.ReadMessage()
func (brige *wsBrige) readMessage() (string, bool) {
_, p, err := brige.conn.ReadMessage()
if err != nil {
if !brige.closed {
@ -63,7 +55,7 @@ func (brige *wsBrige) ReadMessage() (string, bool) {
return string(p), true
}
func (brige *wsBrige) WriteMessage(script string) bool {
func (brige *wsBrige) writeMessage(script string) bool {
if ProtocolInDebugLog {
DebugLog("Run script:")
DebugLog(script)
@ -75,7 +67,7 @@ func (brige *wsBrige) WriteMessage(script string) bool {
return true
}
func (brige *wsBrige) RunGetterScript(script string) DataObject {
func (brige *wsBrige) runGetterScript(script string) DataObject {
brige.answerMutex.Lock()
answerID := brige.answerID
brige.answerID++
@ -107,7 +99,7 @@ func (brige *wsBrige) RunGetterScript(script string) DataObject {
return result
}
func (brige *wsBrige) AnswerReceived(answer DataObject) {
func (brige *wsBrige) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil {
if chanel, ok := brige.answer[id]; ok {