forked from mbk-lab/rui_orig
Added NoSocket parameter of the app
This commit is contained in:
parent
30c915d73b
commit
ebcba7f9c2
164
appServer.go
164
appServer.go
|
@ -20,6 +20,9 @@ import (
|
|||
//go:embed app_socket.js
|
||||
var socketScripts string
|
||||
|
||||
//go:embed app_post.js
|
||||
var httpPostScripts string
|
||||
|
||||
func debugLog(text string) {
|
||||
log.Println("\033[34m" + text)
|
||||
}
|
||||
|
@ -28,11 +31,16 @@ func errorLog(text string) {
|
|||
log.Println("\033[31m" + text)
|
||||
}
|
||||
|
||||
type sessionInfo struct {
|
||||
session Session
|
||||
response chan string
|
||||
}
|
||||
|
||||
type application struct {
|
||||
server *http.Server
|
||||
params AppParams
|
||||
createContentFunc func(Session) SessionContent
|
||||
sessions map[int]Session
|
||||
sessions map[int]sessionInfo
|
||||
}
|
||||
|
||||
func (app *application) getStartPage() string {
|
||||
|
@ -40,14 +48,18 @@ func (app *application) getStartPage() string {
|
|||
defer freeStringBuilder(buffer)
|
||||
|
||||
buffer.WriteString("<!DOCTYPE html>\n<html>\n")
|
||||
getStartPage(buffer, app.params, socketScripts)
|
||||
getStartPage(buffer, app.params)
|
||||
buffer.WriteString("\n</html>")
|
||||
return buffer.String()
|
||||
}
|
||||
|
||||
func (app *application) Finish() {
|
||||
for _, session := range app.sessions {
|
||||
session.close()
|
||||
session.session.close()
|
||||
if session.response != nil {
|
||||
close(session.response)
|
||||
session.response = nil
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
@ -69,7 +81,12 @@ func (app *application) nextSessionID() int {
|
|||
}
|
||||
|
||||
func (app *application) removeSession(id int) {
|
||||
delete(app.sessions, id)
|
||||
if info, ok := app.sessions[id]; ok {
|
||||
if info.response != nil {
|
||||
close(info.response)
|
||||
}
|
||||
delete(app.sessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
|
@ -79,6 +96,11 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
switch req.Method {
|
||||
case "POST":
|
||||
if req.URL.Path == "/" {
|
||||
app.postHandler(w, req)
|
||||
}
|
||||
|
||||
case "GET":
|
||||
switch req.URL.Path {
|
||||
case "/":
|
||||
|
@ -86,10 +108,20 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
io.WriteString(w, app.getStartPage())
|
||||
|
||||
case "/ws":
|
||||
if bridge := CreateSocketBridge(w, req); bridge != nil {
|
||||
if bridge := createSocketBridge(w, req); bridge != nil {
|
||||
go app.socketReader(bridge)
|
||||
}
|
||||
|
||||
case "/script.js":
|
||||
w.WriteHeader(http.StatusOK)
|
||||
if app.params.NoSocket {
|
||||
io.WriteString(w, httpPostScripts)
|
||||
} else {
|
||||
io.WriteString(w, socketScripts)
|
||||
}
|
||||
io.WriteString(w, "\n")
|
||||
io.WriteString(w, defaultScripts)
|
||||
|
||||
default:
|
||||
filename := req.URL.Path[1:]
|
||||
if size := len(filename); size > 0 && filename[size-1] == '/' {
|
||||
|
@ -104,7 +136,92 @@ func (app *application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) socketReader(bridge webBridge) {
|
||||
func setSessionIDCookie(w http.ResponseWriter, sessionID int) {
|
||||
cookie := http.Cookie{
|
||||
Name: "session",
|
||||
Value: strconv.Itoa(sessionID),
|
||||
HttpOnly: true,
|
||||
}
|
||||
http.SetCookie(w, &cookie)
|
||||
}
|
||||
|
||||
func (app *application) postHandler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
if reqBody, err := io.ReadAll(req.Body); err == nil {
|
||||
message := string(reqBody)
|
||||
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(message)
|
||||
}
|
||||
|
||||
if obj := ParseDataText(message); obj != nil {
|
||||
var session Session = nil
|
||||
var response chan string = nil
|
||||
|
||||
if cookie, err := req.Cookie("session"); err == nil {
|
||||
sessionID, err := strconv.Atoi(cookie.Value)
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
} else if info, ok := app.sessions[sessionID]; ok && info.response != nil {
|
||||
response = info.response
|
||||
session = info.session
|
||||
}
|
||||
}
|
||||
|
||||
command := obj.Tag()
|
||||
|
||||
if session == nil {
|
||||
switch command {
|
||||
case "startSession":
|
||||
events := make(chan DataObject, 1024)
|
||||
bridge := createHttpBridge(req)
|
||||
response = bridge.response
|
||||
answer := ""
|
||||
session, answer = app.startSession(obj, events, bridge, response)
|
||||
|
||||
bridge.writeMessage(answer)
|
||||
session.onStart()
|
||||
bridge.sendResponse()
|
||||
|
||||
setSessionIDCookie(w, session.ID())
|
||||
|
||||
go sessionEventHandler(session, events, bridge)
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
switch command {
|
||||
case "startSession":
|
||||
|
||||
case "nop":
|
||||
session.sendResponse()
|
||||
/*
|
||||
case "disconnect":
|
||||
session.onDisconnect()
|
||||
return
|
||||
*/
|
||||
case "session-close":
|
||||
session.onFinish()
|
||||
session.App().removeSession(session.ID())
|
||||
return
|
||||
|
||||
default:
|
||||
if !session.handleAnswer(command, obj) {
|
||||
session.addToEventsQueue(obj)
|
||||
}
|
||||
}
|
||||
|
||||
io.WriteString(w, <-response)
|
||||
for len(response) > 0 {
|
||||
io.WriteString(w, <-response)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *application) socketReader(bridge *wsBridge) {
|
||||
var session Session
|
||||
events := make(chan DataObject, 1024)
|
||||
|
||||
|
@ -116,7 +233,7 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
}
|
||||
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(message)
|
||||
DebugLog("🖥️ -> " + message)
|
||||
}
|
||||
|
||||
if obj := ParseDataText(message); obj != nil {
|
||||
|
@ -124,7 +241,7 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
switch command {
|
||||
case "startSession":
|
||||
answer := ""
|
||||
if session, answer = app.startSession(obj, events, bridge); session != nil {
|
||||
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
|
||||
if !bridge.writeMessage(answer) {
|
||||
return
|
||||
}
|
||||
|
@ -135,7 +252,8 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
case "reconnect":
|
||||
if sessionText, ok := obj.PropertyValue("session"); ok {
|
||||
if sessionID, err := strconv.Atoi(sessionText); err == nil {
|
||||
if session = app.sessions[sessionID]; session != nil {
|
||||
if info, ok := app.sessions[sessionID]; ok {
|
||||
session := info.session
|
||||
session.setBridge(events, bridge)
|
||||
answer := allocStringBuilder()
|
||||
defer freeStringBuilder(answer)
|
||||
|
@ -157,7 +275,7 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
}
|
||||
|
||||
answer := ""
|
||||
if session, answer = app.startSession(obj, events, bridge); session != nil {
|
||||
if session, answer = app.startSession(obj, events, bridge, nil); session != nil {
|
||||
if !bridge.writeMessage(answer) {
|
||||
return
|
||||
}
|
||||
|
@ -165,23 +283,16 @@ func (app *application) socketReader(bridge webBridge) {
|
|||
go sessionEventHandler(session, events, bridge)
|
||||
}
|
||||
|
||||
case "answer":
|
||||
session.handleAnswer(obj)
|
||||
|
||||
case "imageLoaded":
|
||||
session.imageManager().imageLoaded(obj)
|
||||
|
||||
case "imageError":
|
||||
session.imageManager().imageLoadError(obj)
|
||||
|
||||
default:
|
||||
events <- obj
|
||||
if !session.handleAnswer(command, obj) {
|
||||
events <- obj
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sessionEventHandler(session Session, events chan DataObject, bridge webBridge) {
|
||||
func sessionEventHandler(session Session, events chan DataObject, bridge bridge) {
|
||||
for {
|
||||
data := <-events
|
||||
|
||||
|
@ -201,7 +312,9 @@ func sessionEventHandler(session Session, events chan DataObject, bridge webBrid
|
|||
}
|
||||
}
|
||||
|
||||
func (app *application) startSession(params DataObject, events chan DataObject, bridge webBridge) (Session, string) {
|
||||
func (app *application) startSession(params DataObject, events chan DataObject,
|
||||
bridge bridge, response chan string) (Session, string) {
|
||||
|
||||
if app.createContentFunc == nil {
|
||||
return nil, ""
|
||||
}
|
||||
|
@ -212,7 +325,10 @@ func (app *application) startSession(params DataObject, events chan DataObject,
|
|||
return nil, ""
|
||||
}
|
||||
|
||||
app.sessions[session.ID()] = session
|
||||
app.sessions[session.ID()] = sessionInfo{
|
||||
session: session,
|
||||
response: response,
|
||||
}
|
||||
|
||||
answer := allocStringBuilder()
|
||||
defer freeStringBuilder(answer)
|
||||
|
@ -236,7 +352,7 @@ var apps = []*application{}
|
|||
func StartApp(addr string, createContentFunc func(Session) SessionContent, params AppParams) {
|
||||
app := new(application)
|
||||
app.params = params
|
||||
app.sessions = map[int]Session{}
|
||||
app.sessions = map[int]sessionInfo{}
|
||||
app.createContentFunc = createContentFunc
|
||||
apps = append(apps, app)
|
||||
|
||||
|
|
15
appWasm.go
15
appWasm.go
|
@ -17,7 +17,7 @@ type wasmApp struct {
|
|||
params AppParams
|
||||
createContentFunc func(Session) SessionContent
|
||||
session Session
|
||||
bridge webBridge
|
||||
bridge bridge
|
||||
close chan DataObject
|
||||
}
|
||||
|
||||
|
@ -44,17 +44,10 @@ func (app *wasmApp) handleMessage(this js.Value, args []js.Value) any {
|
|||
case "session-close":
|
||||
app.close <- obj
|
||||
|
||||
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)
|
||||
if !app.session.handleAnswer(command, obj) {
|
||||
app.session.handleEvent(command, obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
function sendMessage(message) {
|
||||
let xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', '/', true);
|
||||
xhr.onreadystatechange = function() {
|
||||
const script = this.responseText
|
||||
if (script != "") {
|
||||
window.eval(script)
|
||||
//sendMessage("nop{session=" + sessionID +"}")
|
||||
}
|
||||
|
||||
}
|
||||
xhr.send(message);
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
sendMessage( sessionInfo() );
|
||||
}
|
||||
|
||||
/*
|
||||
window.onload = function() {
|
||||
socketUrl = document.location.protocol == "https:" ? "wss://" : "ws://"
|
||||
socketUrl += document.location.hostname
|
||||
const 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() {
|
||||
sendMessage( sessionInfo() );
|
||||
}
|
||||
|
||||
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
|
||||
sendMessage( "session-resume{session=" + sessionID +"}" );
|
||||
}
|
|
@ -1902,10 +1902,28 @@ function getPropertyValue(answerID, elementId, name) {
|
|||
sendMessage('answer{answerID=' + answerID + ', value=""}')
|
||||
}
|
||||
|
||||
function setStyles(styles) {
|
||||
document.querySelector('style').textContent = styles
|
||||
}
|
||||
|
||||
function appendStyles(styles) {
|
||||
document.querySelector('style').textContent += styles
|
||||
}
|
||||
|
||||
function appendAnimationCSS(css) {
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent += css;
|
||||
}
|
||||
}
|
||||
|
||||
function setAnimationCSS(css) {
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent = css;
|
||||
}
|
||||
}
|
||||
|
||||
function getCanvasContext(elementId) {
|
||||
const canvas = document.getElementById(elementId)
|
||||
if (canvas) {
|
||||
|
|
|
@ -38,9 +38,12 @@ type AppParams struct {
|
|||
KeyFile string
|
||||
// Redirect80 - if true then the function of redirect from port 80 to 443 is created
|
||||
Redirect80 bool
|
||||
// NoSocket - if true then WebSockets will not be used and information exchange
|
||||
// between the client and the server will be carried out only via http.
|
||||
NoSocket bool
|
||||
}
|
||||
|
||||
func getStartPage(buffer *strings.Builder, params AppParams, addScripts string) {
|
||||
func getStartPage(buffer *strings.Builder, params AppParams) {
|
||||
buffer.WriteString(`<head>
|
||||
<meta charset="utf-8">
|
||||
<title>`)
|
||||
|
@ -67,11 +70,7 @@ func getStartPage(buffer *strings.Builder, params AppParams, addScripts string)
|
|||
buffer.WriteString(appStyles)
|
||||
buffer.WriteString(`</style>
|
||||
<style id="ruiAnimations"></style>
|
||||
<script>
|
||||
`)
|
||||
buffer.WriteString(defaultScripts)
|
||||
buffer.WriteString(addScripts)
|
||||
buffer.WriteString(`</script>
|
||||
<script src="/script.js"></script>
|
||||
</head>
|
||||
<body id="body" onkeydown="keyDownEvent(this, event)">
|
||||
<div class="ruiRoot" id="ruiRootView"></div>
|
||||
|
|
1
image.go
1
image.go
|
@ -80,6 +80,7 @@ func (manager *imageManager) loadImage(url string, onLoaded func(Image), session
|
|||
manager.images[url] = image
|
||||
|
||||
session.callFunc("loadImage", url)
|
||||
session.sendResponse()
|
||||
return image
|
||||
}
|
||||
|
||||
|
|
70
session.go
70
session.go
|
@ -7,7 +7,7 @@ import (
|
|||
"strings"
|
||||
)
|
||||
|
||||
type webBridge interface {
|
||||
type bridge interface {
|
||||
startUpdateScript(htmlID string) bool
|
||||
finishUpdateScript(htmlID string)
|
||||
callFunc(funcName string, args ...any) bool
|
||||
|
@ -16,10 +16,9 @@ type webBridge interface {
|
|||
updateCSSProperty(htmlID, property, value string)
|
||||
updateProperty(htmlID, property string, value any)
|
||||
removeProperty(htmlID, property string)
|
||||
readMessage() (string, bool)
|
||||
writeMessage(text string) bool
|
||||
addAnimationCSS(css string)
|
||||
clearAnimation()
|
||||
sendResponse()
|
||||
setAnimationCSS(css string)
|
||||
appendAnimationCSS(css string)
|
||||
canvasStart(htmlID string)
|
||||
callCanvasFunc(funcName string, args ...any)
|
||||
callCanvasVarFunc(v any, funcName string, args ...any)
|
||||
|
@ -124,7 +123,7 @@ type Session interface {
|
|||
nextViewID() string
|
||||
styleProperty(styleTag, property string) any
|
||||
|
||||
setBridge(events chan DataObject, bridge webBridge)
|
||||
setBridge(events chan DataObject, bridge bridge)
|
||||
writeInitScript(writer *strings.Builder)
|
||||
callFunc(funcName string, args ...any)
|
||||
updateInnerHTML(htmlID, html string)
|
||||
|
@ -134,6 +133,7 @@ type Session interface {
|
|||
removeProperty(htmlID, property string)
|
||||
startUpdateScript(htmlID string) bool
|
||||
finishUpdateScript(htmlID string)
|
||||
sendResponse()
|
||||
addAnimationCSS(css string)
|
||||
clearAnimation()
|
||||
canvasStart(htmlID string)
|
||||
|
@ -145,7 +145,8 @@ type Session interface {
|
|||
canvasFinish()
|
||||
canvasTextMetrics(htmlID, font, text string) TextMetrics
|
||||
htmlPropertyValue(htmlID, name string) string
|
||||
handleAnswer(data DataObject)
|
||||
addToEventsQueue(data DataObject)
|
||||
handleAnswer(command string, data DataObject) bool
|
||||
handleRootSize(data DataObject)
|
||||
handleResize(data DataObject)
|
||||
handleEvent(command string, data DataObject)
|
||||
|
@ -189,7 +190,7 @@ type sessionData struct {
|
|||
ignoreUpdates bool
|
||||
popups *popupManager
|
||||
images *imageManager
|
||||
bridge webBridge
|
||||
bridge bridge
|
||||
events chan DataObject
|
||||
animationCounter int
|
||||
animationCSS string
|
||||
|
@ -237,7 +238,7 @@ func (session *sessionData) ID() int {
|
|||
return session.sessionID
|
||||
}
|
||||
|
||||
func (session *sessionData) setBridge(events chan DataObject, bridge webBridge) {
|
||||
func (session *sessionData) setBridge(events chan DataObject, bridge bridge) {
|
||||
session.events = events
|
||||
session.bridge = bridge
|
||||
}
|
||||
|
@ -330,23 +331,19 @@ func (session *sessionData) updateTooltipConstants() {
|
|||
}
|
||||
|
||||
func (session *sessionData) reload() {
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
css := appStyles + session.getCurrentTheme().cssText(session) + session.animationCSS
|
||||
css = strings.ReplaceAll(css, "\n", `\n`)
|
||||
css = strings.ReplaceAll(css, "\t", `\t`)
|
||||
buffer.WriteString(`document.querySelector('style').textContent = "`)
|
||||
buffer.WriteString(css)
|
||||
buffer.WriteString("\";\n")
|
||||
session.bridge.callFunc("setStyles", css)
|
||||
|
||||
if session.rootView != nil {
|
||||
buffer.WriteString(`document.getElementById('ruiRootView').innerHTML = '`)
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
viewHTML(session.rootView, buffer)
|
||||
buffer.WriteString("';\nscanElementsSize();")
|
||||
session.bridge.updateInnerHTML("ruiRootView", buffer.String())
|
||||
session.bridge.callFunc("scanElementsSize")
|
||||
}
|
||||
|
||||
session.bridge.writeMessage(buffer.String())
|
||||
session.updateTooltipConstants()
|
||||
}
|
||||
|
||||
|
@ -447,15 +444,21 @@ func (session *sessionData) finishUpdateScript(htmlID string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) sendResponse() {
|
||||
if session.bridge != nil {
|
||||
session.bridge.sendResponse()
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) addAnimationCSS(css string) {
|
||||
if session.bridge != nil {
|
||||
session.bridge.addAnimationCSS(css)
|
||||
session.bridge.appendAnimationCSS(css)
|
||||
}
|
||||
}
|
||||
|
||||
func (session *sessionData) clearAnimation() {
|
||||
if session.bridge != nil {
|
||||
session.bridge.clearAnimation()
|
||||
session.bridge.setAnimationCSS("")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -520,8 +523,23 @@ func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (session *sessionData) handleAnswer(data DataObject) {
|
||||
session.bridge.answerReceived(data)
|
||||
func (session *sessionData) handleAnswer(command string, data DataObject) bool {
|
||||
switch command {
|
||||
case "answer":
|
||||
session.bridge.answerReceived(data)
|
||||
|
||||
case "imageLoaded":
|
||||
session.imageManager().imageLoaded(data)
|
||||
|
||||
case "imageError":
|
||||
session.imageManager().imageLoadError(data)
|
||||
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
session.bridge.sendResponse()
|
||||
return true
|
||||
}
|
||||
|
||||
func (session *sessionData) handleRootSize(data DataObject) {
|
||||
|
@ -672,6 +690,8 @@ func (session *sessionData) handleEvent(command string, data DataObject) {
|
|||
ErrorLog(`"id" property not found. Event: ` + command)
|
||||
}
|
||||
}
|
||||
|
||||
session.bridge.sendResponse()
|
||||
}
|
||||
|
||||
func (session *sessionData) hotKey(event KeyEvent) {
|
||||
|
@ -769,3 +789,7 @@ func (session *sessionData) RemoveAllClientItems() {
|
|||
session.clientStorage = map[string]string{}
|
||||
session.bridge.callFunc("localStorageClear")
|
||||
}
|
||||
|
||||
func (session *sessionData) addToEventsQueue(data DataObject) {
|
||||
session.events <- data
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ type wasmBridge struct {
|
|||
closeEvent chan DataObject
|
||||
}
|
||||
|
||||
func createWasmBridge(close chan DataObject) webBridge {
|
||||
func createWasmBridge(close chan DataObject) bridge {
|
||||
bridge := new(wasmBridge)
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
|
@ -102,10 +102,6 @@ func (bridge *wasmBridge) close() {
|
|||
bridge.closeEvent <- NewDataObject("close")
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) readMessage() (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) writeMessage(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
|
@ -118,21 +114,24 @@ func (bridge *wasmBridge) writeMessage(script string) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) addAnimationCSS(css string) {
|
||||
func (bridge *wasmBridge) prepareCSS(css string) string {
|
||||
css = strings.ReplaceAll(css, `\t`, "\t")
|
||||
css = strings.ReplaceAll(css, `\n`, "\n")
|
||||
css = strings.ReplaceAll(css, `\'`, "'")
|
||||
css = strings.ReplaceAll(css, `\"`, "\"")
|
||||
css = strings.ReplaceAll(css, `\\`, "\\")
|
||||
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
content := styles.Get("textContent").String()
|
||||
styles.Set("textContent", content+"\n"+css)
|
||||
return css
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) clearAnimation() {
|
||||
func (bridge *wasmBridge) appendAnimationCSS(css string) {
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
styles.Set("textContent", "")
|
||||
content := styles.Get("textContent").String()
|
||||
styles.Set("textContent", content+"\n"+bridge.prepareCSS(css))
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) setAnimationCSS(css string) {
|
||||
styles := js.Global().Get("document").Call("getElementById", "ruiAnimations")
|
||||
styles.Set("textContent", bridge.prepareCSS(css))
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) canvasStart(htmlID string) {
|
||||
|
@ -276,3 +275,6 @@ func (bridge *wasmBridge) answerReceived(answer DataObject) {
|
|||
func (bridge *wasmBridge) remoteAddr() string {
|
||||
return "localhost"
|
||||
}
|
||||
|
||||
func (bridge *wasmBridge) sendResponse() {
|
||||
}
|
||||
|
|
327
webBridge.go
327
webBridge.go
|
@ -12,17 +12,30 @@ import (
|
|||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
type webBridge struct {
|
||||
answer map[int]chan DataObject
|
||||
answerID int
|
||||
answerMutex sync.Mutex
|
||||
writeMutex sync.Mutex
|
||||
closed bool
|
||||
canvasBuffer strings.Builder
|
||||
canvasVarNumber int
|
||||
updateScripts map[string]*strings.Builder
|
||||
writeMessage func(string) bool
|
||||
callFuncImmediately func(funcName string, args ...any) bool
|
||||
}
|
||||
|
||||
type wsBridge struct {
|
||||
conn *websocket.Conn
|
||||
answer map[int]chan DataObject
|
||||
answerID int
|
||||
answerMutex sync.Mutex
|
||||
writeMutex sync.Mutex
|
||||
closed bool
|
||||
buffer strings.Builder
|
||||
canvasBuffer strings.Builder
|
||||
canvasVarNumber int
|
||||
updateScripts map[string]*strings.Builder
|
||||
webBridge
|
||||
conn *websocket.Conn
|
||||
}
|
||||
|
||||
type httpBridge struct {
|
||||
webBridge
|
||||
responseBuffer strings.Builder
|
||||
response chan string
|
||||
remoteAddress string
|
||||
//conn *websocket.Conn
|
||||
}
|
||||
|
||||
type canvasVar struct {
|
||||
|
@ -34,7 +47,7 @@ var upgrader = websocket.Upgrader{
|
|||
WriteBufferSize: 8096,
|
||||
}
|
||||
|
||||
func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
|
||||
func createSocketBridge(w http.ResponseWriter, req *http.Request) *wsBridge {
|
||||
conn, err := upgrader.Upgrade(w, req, nil)
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
|
@ -42,42 +55,84 @@ func CreateSocketBridge(w http.ResponseWriter, req *http.Request) webBridge {
|
|||
}
|
||||
|
||||
bridge := new(wsBridge)
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
bridge.initBridge()
|
||||
bridge.conn = conn
|
||||
bridge.closed = false
|
||||
bridge.updateScripts = map[string]*strings.Builder{}
|
||||
bridge.writeMessage = func(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("🖥️ <- " + script)
|
||||
}
|
||||
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
return false
|
||||
}
|
||||
|
||||
bridge.writeMutex.Lock()
|
||||
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
|
||||
bridge.writeMutex.Unlock()
|
||||
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
bridge.callFuncImmediately = bridge.callFunc
|
||||
return bridge
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) close() {
|
||||
bridge.closed = true
|
||||
defer bridge.conn.Close()
|
||||
bridge.conn = nil
|
||||
func createHttpBridge(req *http.Request) *httpBridge {
|
||||
bridge := new(httpBridge)
|
||||
bridge.initBridge()
|
||||
bridge.response = make(chan string, 10)
|
||||
bridge.writeMessage = func(script string) bool {
|
||||
if script != "" {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog(script)
|
||||
}
|
||||
|
||||
if bridge.responseBuffer.Len() > 0 {
|
||||
bridge.responseBuffer.WriteRune('\n')
|
||||
}
|
||||
bridge.responseBuffer.WriteString(script)
|
||||
}
|
||||
return true
|
||||
}
|
||||
bridge.callFuncImmediately = bridge.callImmediately
|
||||
bridge.remoteAddress = req.RemoteAddr
|
||||
return bridge
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) startUpdateScript(htmlID string) bool {
|
||||
func (bridge *webBridge) initBridge() {
|
||||
bridge.answerID = 1
|
||||
bridge.answer = make(map[int]chan DataObject)
|
||||
bridge.closed = false
|
||||
bridge.updateScripts = map[string]*strings.Builder{}
|
||||
}
|
||||
|
||||
func (bridge *webBridge) startUpdateScript(htmlID string) bool {
|
||||
if _, ok := bridge.updateScripts[htmlID]; ok {
|
||||
return false
|
||||
}
|
||||
buffer := allocStringBuilder()
|
||||
bridge.updateScripts[htmlID] = buffer
|
||||
buffer.WriteString("let element = document.getElementById('")
|
||||
buffer.WriteString("{\nlet element = document.getElementById('")
|
||||
buffer.WriteString(htmlID)
|
||||
buffer.WriteString("');\nif (element) {\n")
|
||||
return true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) finishUpdateScript(htmlID string) {
|
||||
func (bridge *webBridge) finishUpdateScript(htmlID string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString("scanElementsSize();\n}\n")
|
||||
buffer.WriteString("scanElementsSize();\n}\n}\n")
|
||||
bridge.writeMessage(buffer.String())
|
||||
|
||||
freeStringBuilder(buffer)
|
||||
delete(bridge.updateScripts, htmlID)
|
||||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) argToString(arg any) (string, bool) {
|
||||
func (bridge *webBridge) argToString(arg any) (string, bool) {
|
||||
switch arg := arg.(type) {
|
||||
case string:
|
||||
arg = strings.ReplaceAll(arg, "\\", `\\`)
|
||||
|
@ -152,47 +207,44 @@ func (bridge *wsBridge) argToString(arg any) (string, bool) {
|
|||
return "", false
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callFunc(funcName string, args ...any) bool {
|
||||
bridge.buffer.Reset()
|
||||
bridge.buffer.WriteString(funcName)
|
||||
bridge.buffer.WriteRune('(')
|
||||
func (bridge *webBridge) callFuncScript(funcName string, args ...any) (string, bool) {
|
||||
buffer := allocStringBuilder()
|
||||
defer freeStringBuilder(buffer)
|
||||
|
||||
buffer.WriteString(funcName)
|
||||
buffer.WriteRune('(')
|
||||
for i, arg := range args {
|
||||
argText, ok := bridge.argToString(arg)
|
||||
if !ok {
|
||||
return false
|
||||
return "", false
|
||||
}
|
||||
|
||||
if i > 0 {
|
||||
bridge.buffer.WriteString(", ")
|
||||
buffer.WriteString(", ")
|
||||
}
|
||||
bridge.buffer.WriteString(argText)
|
||||
buffer.WriteString(argText)
|
||||
}
|
||||
bridge.buffer.WriteString(");")
|
||||
buffer.WriteString(");")
|
||||
|
||||
funcText := bridge.buffer.String()
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run func: " + funcText)
|
||||
}
|
||||
|
||||
bridge.writeMutex.Lock()
|
||||
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(funcText))
|
||||
bridge.writeMutex.Unlock()
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
return buffer.String(), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateInnerHTML(htmlID, html string) {
|
||||
func (bridge *webBridge) callFunc(funcName string, args ...any) bool {
|
||||
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
|
||||
return bridge.writeMessage(funcText)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bridge *webBridge) updateInnerHTML(htmlID, html string) {
|
||||
bridge.callFunc("updateInnerHTML", htmlID, html)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) appendToInnerHTML(htmlID, html string) {
|
||||
func (bridge *webBridge) appendToInnerHTML(htmlID, html string) {
|
||||
bridge.callFunc("appendToInnerHTML", htmlID, html)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
|
||||
func (bridge *webBridge) updateCSSProperty(htmlID, property, value string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString(`element.style['`)
|
||||
buffer.WriteString(property)
|
||||
|
@ -204,7 +256,7 @@ func (bridge *wsBridge) updateCSSProperty(htmlID, property, value string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
|
||||
func (bridge *webBridge) updateProperty(htmlID, property string, value any) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
if val, ok := bridge.argToString(value); ok {
|
||||
buffer.WriteString(`element.setAttribute('`)
|
||||
|
@ -218,7 +270,7 @@ func (bridge *wsBridge) updateProperty(htmlID, property string, value any) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) removeProperty(htmlID, property string) {
|
||||
func (bridge *webBridge) removeProperty(htmlID, property string) {
|
||||
if buffer, ok := bridge.updateScripts[htmlID]; ok {
|
||||
buffer.WriteString(`if (element.hasAttribute('`)
|
||||
buffer.WriteString(property)
|
||||
|
@ -230,28 +282,34 @@ func (bridge *wsBridge) removeProperty(htmlID, property string) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) addAnimationCSS(css string) {
|
||||
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent += '` + css + `';
|
||||
func (bridge *webBridge) appendAnimationCSS(css string) {
|
||||
//bridge.callFunc("appendAnimationCSS", css)
|
||||
bridge.writeMessage(`{
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent += '` + css + `';
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) clearAnimation() {
|
||||
bridge.writeMessage(`var styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent = '';
|
||||
func (bridge *webBridge) setAnimationCSS(css string) {
|
||||
//bridge.callFunc("setAnimationCSS", css)
|
||||
bridge.writeMessage(`{
|
||||
let styles = document.getElementById('ruiAnimations');
|
||||
if (styles) {
|
||||
styles.textContent = '` + css + `';
|
||||
}
|
||||
}`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasStart(htmlID string) {
|
||||
func (bridge *webBridge) canvasStart(htmlID string) {
|
||||
bridge.canvasBuffer.Reset()
|
||||
bridge.canvasBuffer.WriteString(`const ctx = getCanvasContext('`)
|
||||
bridge.canvasBuffer.WriteString("{\nconst ctx = getCanvasContext('")
|
||||
bridge.canvasBuffer.WriteString(htmlID)
|
||||
bridge.canvasBuffer.WriteString(`');`)
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasFunc(funcName string, args ...any) {
|
||||
bridge.canvasBuffer.WriteString("\nctx.")
|
||||
bridge.canvasBuffer.WriteString(funcName)
|
||||
bridge.canvasBuffer.WriteRune('(')
|
||||
|
@ -265,7 +323,7 @@ func (bridge *wsBridge) callCanvasFunc(funcName string, args ...any) {
|
|||
bridge.canvasBuffer.WriteString(");")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
|
||||
func (bridge *webBridge) updateCanvasProperty(property string, value any) {
|
||||
bridge.canvasBuffer.WriteString("\nctx.")
|
||||
bridge.canvasBuffer.WriteString(property)
|
||||
bridge.canvasBuffer.WriteString(" = ")
|
||||
|
@ -274,7 +332,7 @@ func (bridge *wsBridge) updateCanvasProperty(property string, value any) {
|
|||
bridge.canvasBuffer.WriteString(";")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
|
||||
func (bridge *webBridge) createCanvasVar(funcName string, args ...any) any {
|
||||
bridge.canvasVarNumber++
|
||||
result := canvasVar{name: fmt.Sprintf("v%d", bridge.canvasVarNumber)}
|
||||
bridge.canvasBuffer.WriteString("\nlet ")
|
||||
|
@ -293,7 +351,7 @@ func (bridge *wsBridge) createCanvasVar(funcName string, args ...any) any {
|
|||
return result
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
||||
varName, ok := v.(canvasVar)
|
||||
if !ok {
|
||||
return
|
||||
|
@ -313,7 +371,7 @@ func (bridge *wsBridge) callCanvasVarFunc(v any, funcName string, args ...any) {
|
|||
bridge.canvasBuffer.WriteString(");")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
|
||||
func (bridge *webBridge) callCanvasImageFunc(url string, property string, funcName string, args ...any) {
|
||||
|
||||
bridge.canvasBuffer.WriteString("\nimg = images.get('")
|
||||
bridge.canvasBuffer.WriteString(url)
|
||||
|
@ -334,56 +392,12 @@ func (bridge *wsBridge) callCanvasImageFunc(url string, property string, funcNam
|
|||
bridge.canvasBuffer.WriteString(");\n}")
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasFinish() {
|
||||
bridge.canvasBuffer.WriteString("\n")
|
||||
script := bridge.canvasBuffer.String()
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
DebugLog(script)
|
||||
}
|
||||
bridge.writeMutex.Lock()
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
} else if err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
bridge.writeMutex.Unlock()
|
||||
func (bridge *webBridge) canvasFinish() {
|
||||
bridge.canvasBuffer.WriteString("\n}\n")
|
||||
bridge.writeMessage(bridge.canvasBuffer.String())
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) readMessage() (string, bool) {
|
||||
_, p, err := bridge.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if !bridge.closed {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(p), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) writeMessage(script string) bool {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run script:")
|
||||
DebugLog(script)
|
||||
}
|
||||
if bridge.conn == nil {
|
||||
ErrorLog("No connection")
|
||||
return false
|
||||
}
|
||||
bridge.writeMutex.Lock()
|
||||
err := bridge.conn.WriteMessage(websocket.TextMessage, []byte(script))
|
||||
bridge.writeMutex.Unlock()
|
||||
if err != nil {
|
||||
ErrorLog(err.Error())
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
|
||||
result := TextMetrics{}
|
||||
|
||||
func (bridge *webBridge) removeValue(funcName, htmlID string, args ...string) (DataObject, bool) {
|
||||
bridge.answerMutex.Lock()
|
||||
answerID := bridge.answerID
|
||||
bridge.answerID++
|
||||
|
@ -392,36 +406,40 @@ func (bridge *wsBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics
|
|||
answer := make(chan DataObject)
|
||||
bridge.answer[answerID] = answer
|
||||
|
||||
if bridge.callFunc("canvasTextMetrics", answerID, htmlID, font, text) {
|
||||
data := <-answer
|
||||
result.Width = dataFloatProperty(data, "width")
|
||||
funcArgs := []any{answerID, htmlID}
|
||||
for _, arg := range args {
|
||||
funcArgs = append(funcArgs, arg)
|
||||
}
|
||||
|
||||
var result DataObject = nil
|
||||
ok := bridge.callFuncImmediately(funcName, funcArgs...)
|
||||
if ok {
|
||||
result = <-answer
|
||||
}
|
||||
|
||||
close(answer)
|
||||
delete(bridge.answer, answerID)
|
||||
return result, true
|
||||
}
|
||||
|
||||
func (bridge *webBridge) canvasTextMetrics(htmlID, font, text string) TextMetrics {
|
||||
result := TextMetrics{}
|
||||
if data, ok := bridge.removeValue("canvasTextMetrics", htmlID, font, text); ok {
|
||||
result.Width = dataFloatProperty(data, "width")
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) htmlPropertyValue(htmlID, name string) string {
|
||||
bridge.answerMutex.Lock()
|
||||
answerID := bridge.answerID
|
||||
bridge.answerID++
|
||||
bridge.answerMutex.Unlock()
|
||||
|
||||
answer := make(chan DataObject)
|
||||
bridge.answer[answerID] = answer
|
||||
|
||||
if bridge.callFunc("getPropertyValue", answerID, htmlID, name) {
|
||||
data := <-answer
|
||||
func (bridge *webBridge) htmlPropertyValue(htmlID, name string) string {
|
||||
if data, ok := bridge.removeValue("getPropertyValue", htmlID, name); ok {
|
||||
if value, ok := data.PropertyValue("value"); ok {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
delete(bridge.answer, answerID)
|
||||
return ""
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) answerReceived(answer DataObject) {
|
||||
func (bridge *webBridge) answerReceived(answer DataObject) {
|
||||
if text, ok := answer.PropertyValue("answerID"); ok {
|
||||
if id, err := strconv.Atoi(text); err == nil {
|
||||
if chanel, ok := bridge.answer[id]; ok {
|
||||
|
@ -438,6 +456,55 @@ func (bridge *wsBridge) answerReceived(answer DataObject) {
|
|||
}
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) close() {
|
||||
bridge.closed = true
|
||||
defer bridge.conn.Close()
|
||||
bridge.conn = nil
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) readMessage() (string, bool) {
|
||||
_, p, err := bridge.conn.ReadMessage()
|
||||
if err != nil {
|
||||
if !bridge.closed {
|
||||
ErrorLog(err.Error())
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
return string(p), true
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) sendResponse() {
|
||||
}
|
||||
|
||||
func (bridge *wsBridge) remoteAddr() string {
|
||||
return bridge.conn.RemoteAddr().String()
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) close() {
|
||||
bridge.closed = true
|
||||
// TODO
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) callImmediately(funcName string, args ...any) bool {
|
||||
if funcText, ok := bridge.callFuncScript(funcName, args...); ok {
|
||||
if ProtocolInDebugLog {
|
||||
DebugLog("Run func: " + funcText)
|
||||
}
|
||||
bridge.response <- funcText
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) sendResponse() {
|
||||
bridge.writeMutex.Lock()
|
||||
text := bridge.responseBuffer.String()
|
||||
bridge.responseBuffer.Reset()
|
||||
bridge.writeMutex.Unlock()
|
||||
bridge.response <- text
|
||||
}
|
||||
|
||||
func (bridge *httpBridge) remoteAddr() string {
|
||||
return bridge.remoteAddress
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue