mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
[v3 plugin/server] initial implementation
This commit is contained in:
parent
60c44c44ff
commit
2449b473c0
17 changed files with 2395 additions and 0 deletions
34
v3/plugins/server/README.md
Normal file
34
v3/plugins/server/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# Server Plugin
|
||||
|
||||
This plugin provides a simple server for your Wails applications to make them accessible over the local network.
|
||||
Bidirectional communication occurs over a websocket connection.
|
||||
|
||||
## Installation
|
||||
|
||||
Add the plugin to the `Plugins` option in the Applications options:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/plugins/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := application.New(application.Options{
|
||||
// ...
|
||||
Plugins: map[string]application.Plugin{
|
||||
"server": server.NewPlugin(&server.Config{
|
||||
Host: "0.0.0.0",
|
||||
Port: 31115,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
If you find a bug in this plugin, please raise a ticket on the Wails [Issue Tracker](https://github.com/wailsapp/wails/issues).
|
||||
5
v3/plugins/server/client.go
Normal file
5
v3/plugins/server/client.go
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
package server
|
||||
|
||||
type Client struct {
|
||||
Address string
|
||||
}
|
||||
8
v3/plugins/server/client.js
Normal file
8
v3/plugins/server/client.js
Normal file
File diff suppressed because one or more lines are too long
16
v3/plugins/server/emptywriter.go
Normal file
16
v3/plugins/server/emptywriter.go
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package server
|
||||
|
||||
import "net/http"
|
||||
|
||||
type Consumer struct{}
|
||||
|
||||
func (c Consumer) Header() http.Header {
|
||||
return http.Header{}
|
||||
}
|
||||
|
||||
func (c Consumer) Write(data []byte) (int, error) {
|
||||
return len(data), nil
|
||||
}
|
||||
|
||||
func (c Consumer) WriteHeader(statusCode int) {
|
||||
}
|
||||
54
v3/plugins/server/ipc/Overlay.svelte
Normal file
54
v3/plugins/server/ipc/Overlay.svelte
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
|
||||
import {overlayVisible} from './store'
|
||||
import {fade,} from 'svelte/transition';
|
||||
</script>
|
||||
|
||||
{#if $overlayVisible }
|
||||
<div class="wails-reconnect-overlay" transition:fade="{{ duration: 300 }}">
|
||||
<div class="wails-reconnect-overlay-content">
|
||||
<div class="wails-reconnect-overlay-loadingspinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.wails-reconnect-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backdrop-filter: blur(2px) saturate(0%) contrast(50%) brightness(25%);
|
||||
z-index: 999999
|
||||
}
|
||||
|
||||
.wails-reconnect-overlay-content {
|
||||
position: relative;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
margin: 0;
|
||||
background-image: url(data:image/png;base64,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);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center
|
||||
}
|
||||
|
||||
.wails-reconnect-overlay-loadingspinner {
|
||||
pointer-events: none;
|
||||
width: 2.5em;
|
||||
height: 2.5em;
|
||||
border: .4em solid transparent;
|
||||
border-color: #f00 #eee0 #f00 #eee0;
|
||||
border-radius: 50%;
|
||||
animation: loadingspin 1s linear infinite;
|
||||
margin: auto;
|
||||
padding: 2.5em
|
||||
}
|
||||
|
||||
@keyframes loadingspin {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
15
v3/plugins/server/ipc/build.js
Normal file
15
v3/plugins/server/ipc/build.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
/* jshint esversion: 8 */
|
||||
const esbuild = require("esbuild");
|
||||
const sveltePlugin = require("esbuild-svelte");
|
||||
|
||||
esbuild
|
||||
.build({
|
||||
entryPoints: ["main.js"],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
outfile: "../client.js",
|
||||
plugins: [sveltePlugin({compileOptions: {css: true}})],
|
||||
logLevel: "info",
|
||||
sourcemap: "inline",
|
||||
})
|
||||
.catch(() => process.exit(1));
|
||||
8
v3/plugins/server/ipc/log.js
Normal file
8
v3/plugins/server/ipc/log.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export function log(message) {
|
||||
// eslint-disable-next-line
|
||||
console.log(
|
||||
'%c wails dev %c ' + message + ' ',
|
||||
'background: #aa0000; color: #fff; border-radius: 3px 0px 0px 3px; padding: 1px; font-size: 0.7rem',
|
||||
'background: #009900; color: #fff; border-radius: 0px 3px 3px 0px; padding: 1px; font-size: 0.7rem'
|
||||
);
|
||||
}
|
||||
97
v3/plugins/server/ipc/main.js
Normal file
97
v3/plugins/server/ipc/main.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
/* jshint esversion: 6 */
|
||||
|
||||
import {log} from "./log";
|
||||
import Overlay from "./Overlay.svelte";
|
||||
import {hideOverlay, showOverlay} from "./store";
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
|
||||
let components = {};
|
||||
let source = null;
|
||||
|
||||
function handleCallback(e) {
|
||||
const payload = JSON.parse(e.data);
|
||||
_wails.callCallback(payload.id,
|
||||
payload.result,
|
||||
true);
|
||||
}
|
||||
|
||||
function handleCallbackError(e) {
|
||||
const payload = JSON.parse(e.data);
|
||||
_wails.callErrorCallback(payload.id, payload.result);
|
||||
}
|
||||
|
||||
function handleDialog(e) {
|
||||
const payload = JSON.parse(e.data);
|
||||
_wails.dialogCallback(payload.id,
|
||||
payload.result,
|
||||
true);
|
||||
}
|
||||
|
||||
function handleDialogError(e) {
|
||||
const payload = JSON.parse(e.data);
|
||||
_wails.dialogErrorCallback(payload.id, payload.result);
|
||||
}
|
||||
|
||||
function handleWailsEvent(e) {
|
||||
console.log("WailsEvent: " + e.data)
|
||||
}
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
components.overlay = new Overlay({
|
||||
target: document.body,
|
||||
anchor: document.querySelector('#wails-spinner'),
|
||||
});
|
||||
connect();
|
||||
});
|
||||
|
||||
window.onbeforeunload = function () {
|
||||
if (source) {
|
||||
source.onclose = function () { };
|
||||
source.close();
|
||||
source = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Handles sse connections
|
||||
function handleConnect(e) {
|
||||
hideOverlay();
|
||||
source.onclose = handleDisconnect;
|
||||
|
||||
}
|
||||
|
||||
// Handles SSE disconnects
|
||||
// EventSource will attempt to reconnect on it's own
|
||||
function handleDisconnect(e) {
|
||||
if (this.readyState == EventSource.CONNECTING) {
|
||||
showOverlay();
|
||||
} else {
|
||||
console.log(e);
|
||||
}
|
||||
}
|
||||
|
||||
function _connect() {
|
||||
if (source == null) {
|
||||
source = new EventSource("/server/events?clientId="+wails.clientId);
|
||||
source.onopen = handleConnect;
|
||||
source.onerror = handleDisconnect;
|
||||
source.addEventListener('cb', handleCallback);
|
||||
source.addEventListener('cberror', handleCallbackError);
|
||||
source.addEventListener('dlgcb', handleDialog);
|
||||
source.addEventListener('dlgcberror', handleDialogError);
|
||||
source.addEventListener('wailsevent', handleWailsEvent);
|
||||
}
|
||||
}
|
||||
|
||||
// Try to connect to the backend every .5s
|
||||
function connect() {
|
||||
_connect();
|
||||
}
|
||||
1561
v3/plugins/server/ipc/package-lock.json
generated
Normal file
1561
v3/plugins/server/ipc/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
19
v3/plugins/server/ipc/package.json
Normal file
19
v3/plugins/server/ipc/package.json
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "sse",
|
||||
"version": "3.0.0",
|
||||
"description": "Wails Server Plugin SSE",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"build": "run-p build:*",
|
||||
"build:dev": "node build.js"
|
||||
},
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.12.17",
|
||||
"esbuild-svelte": "^0.5.6",
|
||||
"nanoid": "^4.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"svelte": "^3.55.1"
|
||||
}
|
||||
}
|
||||
12
v3/plugins/server/ipc/store.js
Normal file
12
v3/plugins/server/ipc/store.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import {writable} from 'svelte/store';
|
||||
|
||||
/** Overlay */
|
||||
export const overlayVisible = writable(false);
|
||||
|
||||
export function showOverlay() {
|
||||
overlayVisible.set(true);
|
||||
}
|
||||
|
||||
export function hideOverlay() {
|
||||
overlayVisible.set(false);
|
||||
}
|
||||
8
v3/plugins/server/ipc_websocket.js
Normal file
8
v3/plugins/server/ipc_websocket.js
Normal file
File diff suppressed because one or more lines are too long
63
v3/plugins/server/plugin.go
Normal file
63
v3/plugins/server/plugin.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
//go:embed plugin.js
|
||||
var pluginJS string
|
||||
|
||||
//go:embed client.js
|
||||
var clientJS string
|
||||
|
||||
type Config struct {
|
||||
Host string
|
||||
Port int
|
||||
Enabled bool
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
func (c Config) ListenAddress() string {
|
||||
return fmt.Sprintf("%s:%d", c.Host, c.Port)
|
||||
}
|
||||
|
||||
type Plugin struct {
|
||||
config *Config
|
||||
server *Server
|
||||
}
|
||||
|
||||
func NewPlugin(config *Config) *Plugin {
|
||||
return &Plugin{
|
||||
config: config,
|
||||
server: NewServer(config),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Plugin) CallableByJS() []string {
|
||||
return []string{} // maybe # clients?
|
||||
}
|
||||
|
||||
func (p *Plugin) InjectJS() string {
|
||||
return pluginJS
|
||||
}
|
||||
|
||||
// Init is called when the plugin is loaded. It is passed the application.App
|
||||
// instance. This is where you should do any setup.
|
||||
func (p *Plugin) Init() error {
|
||||
p.server.app = application.Get()
|
||||
p.server.run()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown will stop the server
|
||||
func (s *Plugin) Shutdown() {
|
||||
s.server.Shutdown()
|
||||
}
|
||||
|
||||
// Name returns the name of the plugin.
|
||||
func (s *Plugin) Name() string {
|
||||
return "github.com/wailsapp/wails/v3/plugins/server"
|
||||
}
|
||||
1
v3/plugins/server/plugin.js
Normal file
1
v3/plugins/server/plugin.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
9
v3/plugins/server/plugin.toml
Normal file
9
v3/plugins/server/plugin.toml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# This is the plugin definition file for the "server" plugin.
|
||||
|
||||
Name = "server"
|
||||
Description = "A Simple Server for Wails Applications"
|
||||
Author = "Travis McLane <tmclane@gmail.com>"
|
||||
Version = "v1.0.0"
|
||||
Website = "https://wails.io"
|
||||
Repository = "https://github.com/wailsapp/wails/v3/plugins/server"
|
||||
License = "MIT"
|
||||
217
v3/plugins/server/server.go
Normal file
217
v3/plugins/server/server.go
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type message struct {
|
||||
Type string
|
||||
Data string
|
||||
}
|
||||
|
||||
type callback struct {
|
||||
ID string `json:"id"`
|
||||
Result string `json:"result"`
|
||||
}
|
||||
|
||||
type client struct {
|
||||
address string
|
||||
events chan message
|
||||
}
|
||||
|
||||
func (c client) close() {
|
||||
if _, ok := (<-c.events); ok {
|
||||
close(c.events)
|
||||
}
|
||||
}
|
||||
|
||||
func (c client) Identifier() string {
|
||||
return c.address
|
||||
}
|
||||
|
||||
func (c *client) Send(msg message) error {
|
||||
var err error
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
err = fmt.Errorf("connection lost")
|
||||
}
|
||||
}()
|
||||
c.events <- msg
|
||||
return err
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
id uint // to allow for registration as a window
|
||||
app *application.App
|
||||
config *Config
|
||||
srv *http.Server
|
||||
window Window
|
||||
clients map[string]client
|
||||
clientLock sync.Mutex
|
||||
}
|
||||
|
||||
func NewServer(config *Config) *Server {
|
||||
s := &Server{
|
||||
config: config,
|
||||
clients: map[string]client{},
|
||||
}
|
||||
s.window.server = s
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) Info(msg string) {
|
||||
// s.app.Log(&logger.Message{
|
||||
// Level: "INFO",
|
||||
// Message: fmt.Sprintf("[plugin/server]: %v", msg),
|
||||
// })
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() {
|
||||
if err := s.srv.Shutdown(context.TODO()); err != nil {
|
||||
panic(err) // failure/timeout shutting down the server gracefully
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleClient(rw http.ResponseWriter, req *http.Request) {
|
||||
client := client{
|
||||
events: make(chan message, 5),
|
||||
address: req.RemoteAddr,
|
||||
}
|
||||
s.Info(fmt.Sprintf("client %v connected", client.Identifier()))
|
||||
clientID := req.URL.Query().Get("clientId")
|
||||
if clientID != "" {
|
||||
// we only save if we have an identifier
|
||||
s.clientLock.Lock()
|
||||
s.clients[clientID] = client
|
||||
s.clientLock.Unlock()
|
||||
}
|
||||
|
||||
rw.Header().Set("Content-Type", "text/event-stream")
|
||||
rw.Header().Set("Cache-Control", "no-cache")
|
||||
rw.Header().Set("Connection", "keep-alive")
|
||||
rw.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
rw.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
|
||||
for header, value := range s.config.Headers {
|
||||
rw.Header().Set(header, value)
|
||||
}
|
||||
flusher, ok := rw.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(rw, "Connection does not support streaming", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
for {
|
||||
timeout := time.After(500 * time.Millisecond)
|
||||
select {
|
||||
case <-req.Context().Done():
|
||||
client.close()
|
||||
s.removeClient(client.Identifier())
|
||||
return
|
||||
case msg := <-client.events:
|
||||
fmt.Fprintf(rw, "event: %s\n", msg.Type)
|
||||
fmt.Fprintf(rw, "data: %v\n\n", msg.Data)
|
||||
case <-timeout:
|
||||
continue
|
||||
}
|
||||
flusher.Flush()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Server) serveIPC(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Header().Set("Content-Type", "application/javascript")
|
||||
rw.Header().Set("Content-Length", fmt.Sprintf("%d", len(clientJS)))
|
||||
io.WriteString(rw, clientJS)
|
||||
}
|
||||
|
||||
func (s *Server) removeClient(clientID string) {
|
||||
s.clientLock.Lock()
|
||||
defer s.clientLock.Unlock()
|
||||
delete(s.clients, clientID)
|
||||
s.Info(fmt.Sprintf("client %v disconnected", clientID))
|
||||
}
|
||||
|
||||
func (s *Server) sendToClient(requestID string, message message) {
|
||||
client, ok := s.clients[requestID]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := client.Send(message); err != nil {
|
||||
s.removeClient(client.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) sendToAllClients(msg message) {
|
||||
if len(s.clients) == 0 {
|
||||
return
|
||||
}
|
||||
s.clientLock.Lock()
|
||||
defer s.clientLock.Unlock()
|
||||
dead := []client{}
|
||||
for _, client := range s.clients {
|
||||
if err := client.Send(msg); err != nil {
|
||||
dead = append(dead, client)
|
||||
}
|
||||
}
|
||||
for _, d := range dead {
|
||||
s.removeClient(d.Identifier())
|
||||
}
|
||||
}
|
||||
|
||||
func updateCallID(windowID uint, req *http.Request) *http.Request {
|
||||
argMap := map[string]any{}
|
||||
values := req.URL.Query()
|
||||
args := values.Get("args")
|
||||
if args != "" {
|
||||
json.Unmarshal([]byte(args), &argMap)
|
||||
}
|
||||
callID := argMap["call-id"]
|
||||
clientID := req.Header.Get("x-wails-client-id")
|
||||
if clientID != "" {
|
||||
argMap["call-id"] = fmt.Sprintf("%s|%s", clientID, callID)
|
||||
}
|
||||
newArgs, _ := json.Marshal(argMap)
|
||||
values.Set("args", string(newArgs))
|
||||
req.Header.Add("x-wails-window-id", fmt.Sprintf("%d", windowID))
|
||||
req.URL.RawQuery = values.Encode()
|
||||
return req
|
||||
}
|
||||
|
||||
func (s *Server) handleHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
req = updateCallID(s.window.id, req)
|
||||
s.app.AssetServerHandler()(
|
||||
rw,
|
||||
req,
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Server) run() {
|
||||
if s.srv != nil || s.config.Enabled == false {
|
||||
return
|
||||
}
|
||||
address := s.config.ListenAddress()
|
||||
s.srv = &http.Server{Addr: address}
|
||||
http.HandleFunc("/wails/ipc.js", s.serveIPC)
|
||||
http.HandleFunc("/server/events", s.handleClient)
|
||||
http.HandleFunc("/", s.handleHTTP)
|
||||
|
||||
s.window.id = s.app.RegisterWindow(s.window)
|
||||
go s.serve()
|
||||
}
|
||||
|
||||
// ---------------- Plugin Methods ----------------
|
||||
func (s *Server) serve() {
|
||||
s.Info(fmt.Sprintf("listening %s", s.config.ListenAddress()))
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%d", s.config.Host, s.config.Port), nil))
|
||||
}
|
||||
268
v3/plugins/server/window.go
Normal file
268
v3/plugins/server/window.go
Normal file
|
|
@ -0,0 +1,268 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
)
|
||||
|
||||
type Window struct {
|
||||
id uint
|
||||
server *Server
|
||||
}
|
||||
|
||||
// formatJS ensures the 'data' provided marshals to valid json or panics
|
||||
func (w Window) formatJS(f string, callID string, data string) string {
|
||||
j, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return fmt.Sprintf(f, callID, j)
|
||||
}
|
||||
|
||||
func (w Window) AbsolutePosition() (x, y int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (w Window) CallError(callID string, result string) {
|
||||
w.ExecJS(callID, w.formatJS("_wails.callErrorCallback('%s', %s);", callID, result))
|
||||
}
|
||||
|
||||
func (w Window) CallResponse(callID string, result string) {
|
||||
ids := strings.Split(callID, "|")
|
||||
j, err := json.Marshal(callback{
|
||||
ID: ids[1],
|
||||
Result: result,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("Failed to build CallResponse data", result)
|
||||
}
|
||||
|
||||
w.server.sendToClient(ids[0], message{Type: "cb", Data: string(j)})
|
||||
}
|
||||
|
||||
func (w Window) DialogError(dialogID string, result string) {
|
||||
w.ExecJS(dialogID, w.formatJS("_wails.dialogErrorCallback('%s', %s);", dialogID, result))
|
||||
}
|
||||
|
||||
func (w Window) DialogResponse(dialogID string, result string) {
|
||||
w.ExecJS(dialogID, w.formatJS("_wails.dialogCallback('%s', %s, true);", dialogID, result))
|
||||
}
|
||||
|
||||
func (w Window) ID() uint {
|
||||
return w.id
|
||||
}
|
||||
|
||||
func (w Window) Center() {
|
||||
|
||||
}
|
||||
|
||||
func (w Window) Close() {}
|
||||
|
||||
func (w Window) Destroy() {}
|
||||
|
||||
func (w Window) ExecJS(callID, js string) {
|
||||
w.server.sendToClient(callID, message{
|
||||
Type: "javascript",
|
||||
Data: js,
|
||||
})
|
||||
}
|
||||
|
||||
func (w Window) Focus() {}
|
||||
|
||||
func (w Window) ForceReload() {}
|
||||
|
||||
func (w Window) Fullscreen() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) GetScreen() (*application.Screen, error) {
|
||||
return nil, fmt.Errorf("can't return screen for external window")
|
||||
}
|
||||
|
||||
func (w Window) GetZoom() float64 {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
func (w Window) Height() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (w Window) Hide() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) IsFullscreen() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w Window) IsMaximised() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w Window) IsMinimised() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (w Window) Maximise() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) Minimise() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) Minimize() {}
|
||||
|
||||
func (w Window) Name() string {
|
||||
return "external window"
|
||||
}
|
||||
|
||||
func (w Window) On(eventType events.WindowEventType, callback func(ctx *application.WindowEvent)) func() {
|
||||
return func() {
|
||||
fmt.Printf("server.Window.On(%v)\n", eventType)
|
||||
}
|
||||
}
|
||||
|
||||
func (w Window) Position() (int, int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (w Window) RegisterContextMenu(name string, menu *application.Menu) {}
|
||||
|
||||
func (w Window) RelativePosition() (x, y int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (w Window) Reload() {}
|
||||
|
||||
func (w Window) Resizable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (w Window) Restore() {}
|
||||
|
||||
func (w Window) SetAbsolutePosition(x, y int) {}
|
||||
|
||||
func (w Window) SetAlwaysOnTop(b bool) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetBackgroundColour(colour application.RGBA) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetFrameless(frameless bool) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetFullscreenButtonEnabled(enabled bool) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetHTML(html string) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetMaxSize(maxWidth, maxHeight int) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetMinSize(minWidth, minHeight int) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetRelativePosition(x, y int) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetResizable(b bool) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetSize(width, height int) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetTitle(title string) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetURL(s string) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) SetZoom(magnification float64) application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) Show() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) Size() (width int, height int) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (w Window) ToggleDevTools() {
|
||||
}
|
||||
|
||||
func (w Window) ToggleFullscreen() {}
|
||||
|
||||
func (w Window) UnFullscreen() {}
|
||||
|
||||
func (w Window) UnMaximise() {}
|
||||
|
||||
func (w Window) UnMinimise() {}
|
||||
|
||||
func (w Window) Width() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (w Window) Zoom() {}
|
||||
|
||||
func (w Window) ZoomIn() {}
|
||||
|
||||
func (w Window) ZoomOut() {}
|
||||
|
||||
func (w Window) ZoomReset() application.Window {
|
||||
return w
|
||||
}
|
||||
|
||||
func (w Window) DisableSizeConstraints() {}
|
||||
|
||||
func (w Window) DispatchWailsEvent(event *application.WailsEvent) {
|
||||
w.server.sendToAllClients(
|
||||
message{
|
||||
Type: "wailsevent",
|
||||
Data: event.ToJSON(),
|
||||
})
|
||||
}
|
||||
|
||||
func (w Window) EnableSizeConstraints() {}
|
||||
|
||||
func (w Window) Error(message string, args ...any) {}
|
||||
|
||||
func (w Window) HandleDragAndDropMessage(filenames []string) {
|
||||
|
||||
}
|
||||
|
||||
func (w Window) HandleKeyEvent(acceleratorString string) {
|
||||
|
||||
}
|
||||
|
||||
func (w Window) HandleMessage(message string) {
|
||||
log.Println("HandleMessage", message)
|
||||
}
|
||||
|
||||
func (w Window) HandleWindowEvent(id uint) {}
|
||||
|
||||
func (w Window) Info(message string, args ...any) {}
|
||||
|
||||
func (w Window) OpenContextMenu(data *application.ContextMenuData) {}
|
||||
|
||||
func (w Window) Run() {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue