mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-02 09:00:38 +08:00

* get dimensions working for linux * Cleaning up some GTK code I was getting the following errors due to some bad casts. Gdk-CRITICAL **: 18:58:51.943: gdk_monitor_get_geometry: assertion 'GDK_IS_MONITOR (monitor)' failed Gdk-CRITICAL **: 18:58:51.943: gdk_display_get_monitor_at_window: assertion 'GDK_IS_DISPLAY (display)' failed This commit fixes these errors * Adding Screen namespace along with linux implementation * moving ScreenGetAll into a more appropriate place * Fixing typescript definition mistake, documentation, ordering of functions, and formatting * add ScreenGetAll to more templates * moving screen into its own javascript file * fixing bug where screen objects are not returned from the runtime function * rebuilding frontend wrapper package * adding windows implementation of ScreenGetAll * adding screen get all implementation for darwin * reverting a change that is unrelated to the work on expose-dimensions * removing duplicate comparison * changing GetNthScreen in screen API on macos To use frame instead of visibleframe to keep into account the space the the dock takes up We want to include that space in the calculation in order to keep the sizes of screens consistent across platforms * Correcting screen jsdoc It used to say it returned a single screen object. Now it says that it returns an array of screen objects * Fixing typo in function name * reverting pointless spacing change * reverting pointless spacing change Co-authored-by: Lea Anthony <lea.anthony@gmail.com> Co-authored-by: shmuel.kamensky <shmuel.kamensky@shutterfly.com>
445 lines
11 KiB
Go
445 lines
11 KiB
Go
//go:build dev
|
|
// +build dev
|
|
|
|
// Package devserver provides a web-based frontend so that
|
|
// it is possible to run a Wails app in a browsers.
|
|
package devserver
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
"github.com/wailsapp/wails/v2/internal/binding"
|
|
"github.com/wailsapp/wails/v2/internal/frontend"
|
|
"github.com/wailsapp/wails/v2/internal/frontend/assetserver"
|
|
"github.com/wailsapp/wails/v2/internal/logger"
|
|
"github.com/wailsapp/wails/v2/internal/menumanager"
|
|
"github.com/wailsapp/wails/v2/pkg/menu"
|
|
"github.com/wailsapp/wails/v2/pkg/options"
|
|
"golang.org/x/net/websocket"
|
|
)
|
|
|
|
type Screen = frontend.Screen
|
|
|
|
type DevWebServer struct {
|
|
server *echo.Echo
|
|
ctx context.Context
|
|
appoptions *options.App
|
|
logger *logger.Logger
|
|
appBindings *binding.Bindings
|
|
dispatcher frontend.Dispatcher
|
|
socketMutex sync.Mutex
|
|
websocketClients map[*websocket.Conn]*sync.Mutex
|
|
menuManager *menumanager.Manager
|
|
starttime string
|
|
|
|
// Desktop frontend
|
|
desktopFrontend frontend.Frontend
|
|
|
|
devServerAddr string
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetSystemDefaultTheme() {
|
|
d.desktopFrontend.WindowSetSystemDefaultTheme()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetLightTheme() {
|
|
d.desktopFrontend.WindowSetLightTheme()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetDarkTheme() {
|
|
d.desktopFrontend.WindowSetDarkTheme()
|
|
}
|
|
|
|
func (d *DevWebServer) Run(ctx context.Context) error {
|
|
d.ctx = ctx
|
|
|
|
d.server.GET("/wails/reload", d.handleReload)
|
|
d.server.GET("/wails/ipc", d.handleIPCWebSocket)
|
|
|
|
var assetHandler http.Handler
|
|
_fronendDevServerURL, _ := ctx.Value("frontenddevserverurl").(string)
|
|
if _fronendDevServerURL == "" {
|
|
assetdir, _ := ctx.Value("assetdir").(string)
|
|
d.server.GET("/wails/assetdir", func(c echo.Context) error {
|
|
return c.String(http.StatusOK, assetdir)
|
|
})
|
|
|
|
var err error
|
|
assetHandler, err = assetserver.NewAssetHandler(ctx, d.appoptions)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
} else {
|
|
externalURL, err := url.Parse(_fronendDevServerURL)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if externalURL.Host == "" {
|
|
return fmt.Errorf("Invalid frontend:dev:serverUrl missing protocol scheme?")
|
|
}
|
|
|
|
waitCb := func() { d.LogDebug("Waiting for frontend DevServer '%s' to be ready", externalURL) }
|
|
if !checkPortIsOpen(externalURL.Host, time.Minute, waitCb) {
|
|
d.logger.Error("Timeout waiting for frontend DevServer")
|
|
}
|
|
|
|
assetHandler = newExternalDevServerAssetHandler(d.logger, externalURL, d.appoptions.AssetsHandler)
|
|
}
|
|
|
|
// Setup internal dev server
|
|
bindingsJSON, err := d.appBindings.ToJSON()
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
assetServer, err := assetserver.NewBrowserAssetServer(ctx, assetHandler, bindingsJSON)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
d.server.Any("/*", func(c echo.Context) error {
|
|
assetServer.ServeHTTP(c.Response(), c.Request())
|
|
return nil
|
|
})
|
|
|
|
if devServerAddr := d.devServerAddr; devServerAddr != "" {
|
|
// Start server
|
|
go func(server *echo.Echo, log *logger.Logger) {
|
|
err := server.Start(devServerAddr)
|
|
if err != nil {
|
|
log.Error(err.Error())
|
|
}
|
|
d.LogDebug("Shutdown completed")
|
|
}(d.server, d.logger)
|
|
|
|
d.LogDebug("Serving DevServer at http://%s", devServerAddr)
|
|
|
|
defer func() {
|
|
err := d.server.Shutdown(context.Background())
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Launch desktop app
|
|
err = d.desktopFrontend.Run(ctx)
|
|
d.LogDebug("Starting shutdown")
|
|
|
|
return err
|
|
}
|
|
|
|
func (d *DevWebServer) Quit() {
|
|
d.desktopFrontend.Quit()
|
|
}
|
|
|
|
func (d *DevWebServer) OpenFileDialog(dialogOptions frontend.OpenDialogOptions) (string, error) {
|
|
return d.desktopFrontend.OpenFileDialog(dialogOptions)
|
|
}
|
|
|
|
func (d *DevWebServer) OpenMultipleFilesDialog(dialogOptions frontend.OpenDialogOptions) ([]string, error) {
|
|
return d.OpenMultipleFilesDialog(dialogOptions)
|
|
}
|
|
|
|
func (d *DevWebServer) OpenDirectoryDialog(dialogOptions frontend.OpenDialogOptions) (string, error) {
|
|
return d.OpenDirectoryDialog(dialogOptions)
|
|
}
|
|
|
|
func (d *DevWebServer) SaveFileDialog(dialogOptions frontend.SaveDialogOptions) (string, error) {
|
|
return d.desktopFrontend.SaveFileDialog(dialogOptions)
|
|
}
|
|
|
|
func (d *DevWebServer) MessageDialog(dialogOptions frontend.MessageDialogOptions) (string, error) {
|
|
return d.desktopFrontend.MessageDialog(dialogOptions)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowReload() {
|
|
d.broadcast("reload")
|
|
d.desktopFrontend.WindowReload()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowReloadApp() {
|
|
d.broadcast("reloadapp")
|
|
d.desktopFrontend.WindowReloadApp()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetTitle(title string) {
|
|
d.desktopFrontend.WindowSetTitle(title)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowShow() {
|
|
d.desktopFrontend.WindowShow()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowHide() {
|
|
d.desktopFrontend.WindowHide()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowCenter() {
|
|
d.desktopFrontend.WindowCenter()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowMaximise() {
|
|
d.desktopFrontend.WindowMaximise()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowToggleMaximise() {
|
|
d.desktopFrontend.WindowToggleMaximise()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowUnmaximise() {
|
|
d.desktopFrontend.WindowUnmaximise()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowMinimise() {
|
|
d.desktopFrontend.WindowMinimise()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowUnminimise() {
|
|
d.desktopFrontend.WindowUnminimise()
|
|
}
|
|
func (d *DevWebServer) WindowSetAlwaysOnTop(b bool) {
|
|
d.desktopFrontend.WindowSetAlwaysOnTop(b)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetPosition(x int, y int) {
|
|
d.desktopFrontend.WindowSetPosition(x, y)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowGetPosition() (int, int) {
|
|
return d.desktopFrontend.WindowGetPosition()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetSize(width int, height int) {
|
|
d.desktopFrontend.WindowSetSize(width, height)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowGetSize() (int, int) {
|
|
return d.desktopFrontend.WindowGetSize()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetMinSize(width int, height int) {
|
|
d.desktopFrontend.WindowSetMinSize(width, height)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetMaxSize(width int, height int) {
|
|
d.desktopFrontend.WindowSetMaxSize(width, height)
|
|
}
|
|
|
|
func (d *DevWebServer) WindowFullscreen() {
|
|
d.desktopFrontend.WindowFullscreen()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowUnfullscreen() {
|
|
d.desktopFrontend.WindowUnfullscreen()
|
|
}
|
|
|
|
func (d *DevWebServer) WindowSetBackgroundColour(col *options.RGBA) {
|
|
d.desktopFrontend.WindowSetBackgroundColour(col)
|
|
}
|
|
|
|
func (d *DevWebServer) ScreenGetAll() ([]Screen, error) {
|
|
return d.desktopFrontend.ScreenGetAll()
|
|
}
|
|
|
|
func (d *DevWebServer) MenuSetApplicationMenu(menu *menu.Menu) {
|
|
d.desktopFrontend.MenuSetApplicationMenu(menu)
|
|
}
|
|
|
|
func (d *DevWebServer) MenuUpdateApplicationMenu() {
|
|
d.desktopFrontend.MenuUpdateApplicationMenu()
|
|
}
|
|
|
|
// BrowserOpenURL uses the system default browser to open the url
|
|
func (d *DevWebServer) BrowserOpenURL(url string) {
|
|
d.desktopFrontend.BrowserOpenURL(url)
|
|
}
|
|
|
|
func (d *DevWebServer) Notify(name string, data ...interface{}) {
|
|
d.notify(name, data...)
|
|
}
|
|
|
|
func (d *DevWebServer) handleReload(c echo.Context) error {
|
|
d.WindowReload()
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
func (d *DevWebServer) handleReloadApp(c echo.Context) error {
|
|
d.WindowReloadApp()
|
|
return c.NoContent(http.StatusNoContent)
|
|
}
|
|
|
|
func (d *DevWebServer) handleIPCWebSocket(c echo.Context) error {
|
|
websocket.Handler(func(c *websocket.Conn) {
|
|
d.LogDebug(fmt.Sprintf("Websocket client %p connected", c))
|
|
d.socketMutex.Lock()
|
|
d.websocketClients[c] = &sync.Mutex{}
|
|
locker := d.websocketClients[c]
|
|
d.socketMutex.Unlock()
|
|
|
|
defer func() {
|
|
d.socketMutex.Lock()
|
|
delete(d.websocketClients, c)
|
|
d.socketMutex.Unlock()
|
|
d.LogDebug(fmt.Sprintf("Websocket client %p disconnected", c))
|
|
}()
|
|
|
|
var msg string
|
|
defer c.Close()
|
|
for {
|
|
if err := websocket.Message.Receive(c, &msg); err != nil {
|
|
break
|
|
}
|
|
// We do not support drag in browsers
|
|
if msg == "drag" {
|
|
continue
|
|
}
|
|
|
|
// Notify the other browsers of "EventEmit"
|
|
if len(msg) > 2 && strings.HasPrefix(string(msg), "EE") {
|
|
d.notifyExcludingSender([]byte(msg), c)
|
|
}
|
|
|
|
// Send the message to dispatch to the frontend
|
|
result, err := d.dispatcher.ProcessMessage(string(msg), d)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
}
|
|
if result != "" {
|
|
locker.Lock()
|
|
if err = websocket.Message.Send(c, result); err != nil {
|
|
locker.Unlock()
|
|
break
|
|
}
|
|
locker.Unlock()
|
|
}
|
|
}
|
|
}).ServeHTTP(c.Response(), c.Request())
|
|
return nil
|
|
}
|
|
|
|
func (d *DevWebServer) LogDebug(message string, args ...interface{}) {
|
|
d.logger.Debug("[DevWebServer] "+message, args...)
|
|
}
|
|
|
|
type EventNotify struct {
|
|
Name string `json:"name"`
|
|
Data []interface{} `json:"data"`
|
|
}
|
|
|
|
func (d *DevWebServer) broadcast(message string) {
|
|
d.socketMutex.Lock()
|
|
defer d.socketMutex.Unlock()
|
|
for client, locker := range d.websocketClients {
|
|
go func(client *websocket.Conn, locker *sync.Mutex) {
|
|
if client == nil {
|
|
d.logger.Error("Lost connection to websocket server")
|
|
return
|
|
}
|
|
locker.Lock()
|
|
err := websocket.Message.Send(client, message)
|
|
if err != nil {
|
|
locker.Unlock()
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
locker.Unlock()
|
|
}(client, locker)
|
|
}
|
|
}
|
|
|
|
func (d *DevWebServer) notify(name string, data ...interface{}) {
|
|
// Notify
|
|
notification := EventNotify{
|
|
Name: name,
|
|
Data: data,
|
|
}
|
|
payload, err := json.Marshal(notification)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
d.broadcast("n" + string(payload))
|
|
}
|
|
|
|
func (d *DevWebServer) broadcastExcludingSender(message string, sender *websocket.Conn) {
|
|
d.socketMutex.Lock()
|
|
defer d.socketMutex.Unlock()
|
|
for client, locker := range d.websocketClients {
|
|
go func(client *websocket.Conn, locker *sync.Mutex) {
|
|
if client == sender {
|
|
return
|
|
}
|
|
locker.Lock()
|
|
err := websocket.Message.Send(client, message)
|
|
if err != nil {
|
|
locker.Unlock()
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
locker.Unlock()
|
|
}(client, locker)
|
|
}
|
|
}
|
|
|
|
func (d *DevWebServer) notifyExcludingSender(eventMessage []byte, sender *websocket.Conn) {
|
|
message := "n" + string(eventMessage[2:])
|
|
d.broadcastExcludingSender(message, sender)
|
|
|
|
var notifyMessage EventNotify
|
|
err := json.Unmarshal(eventMessage[2:], ¬ifyMessage)
|
|
if err != nil {
|
|
d.logger.Error(err.Error())
|
|
return
|
|
}
|
|
d.desktopFrontend.Notify(notifyMessage.Name, notifyMessage.Data...)
|
|
}
|
|
|
|
func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.Logger, appBindings *binding.Bindings, dispatcher frontend.Dispatcher, menuManager *menumanager.Manager, desktopFrontend frontend.Frontend) *DevWebServer {
|
|
result := &DevWebServer{
|
|
ctx: ctx,
|
|
desktopFrontend: desktopFrontend,
|
|
appoptions: appoptions,
|
|
logger: myLogger,
|
|
appBindings: appBindings,
|
|
dispatcher: dispatcher,
|
|
server: echo.New(),
|
|
menuManager: menuManager,
|
|
websocketClients: make(map[*websocket.Conn]*sync.Mutex),
|
|
}
|
|
|
|
result.devServerAddr, _ = ctx.Value("devserver").(string)
|
|
result.server.HideBanner = true
|
|
result.server.HidePort = true
|
|
return result
|
|
}
|
|
|
|
func checkPortIsOpen(host string, timeout time.Duration, waitCB func()) (ret bool) {
|
|
if timeout == 0 {
|
|
timeout = time.Minute
|
|
}
|
|
|
|
deadline := time.Now().Add(timeout)
|
|
for time.Now().Before(deadline) {
|
|
conn, _ := net.DialTimeout("tcp", host, 2*time.Second)
|
|
if conn != nil {
|
|
conn.Close()
|
|
return true
|
|
}
|
|
|
|
waitCB()
|
|
time.Sleep(1 * time.Second)
|
|
}
|
|
return false
|
|
}
|