diff --git a/v2/pkg/assetserver/assetserver.go b/v2/pkg/assetserver/assetserver.go index e2e0372eb..78647b0c9 100644 --- a/v2/pkg/assetserver/assetserver.go +++ b/v2/pkg/assetserver/assetserver.go @@ -2,8 +2,11 @@ package assetserver import ( "bytes" + "fmt" + "math/rand" "net/http" "net/http/httptest" + "strings" "golang.org/x/net/html" @@ -42,6 +45,9 @@ type AssetServer struct { // Use http based runtime runtimeHandler RuntimeHandler + // plugin scripts + pluginScripts map[string]string + assetServerWebView } @@ -89,6 +95,16 @@ func (d *AssetServer) UseRuntimeHandler(handler RuntimeHandler) { d.runtimeHandler = handler } +func (d *AssetServer) AddPluginScript(pluginName string, script string) { + if d.pluginScripts == nil { + d.pluginScripts = make(map[string]string) + } + pluginName = strings.ReplaceAll(pluginName, "/", "_") + pluginName = html.EscapeString(pluginName) + pluginScriptName := fmt.Sprintf("/plugin_%s_%d.js", pluginName, rand.Intn(100000)) + d.pluginScripts[pluginScriptName] = script +} + func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { if isWebSocket(req) { // Forward WebSockets to the distinct websocket handler if it exists @@ -149,6 +165,11 @@ func (d *AssetServer) ServeHTTP(rw http.ResponseWriter, req *http.Request) { d.writeBlob(rw, path, content) default: + // Check if this is a plugin script + if script, ok := d.pluginScripts[path]; ok { + d.writeBlob(rw, path, []byte(script)) + return + } d.handler.ServeHTTP(rw, req) } } @@ -174,6 +195,13 @@ func (d *AssetServer) processIndexHTML(indexHTML []byte) ([]byte, error) { return nil, err } + // Inject plugins + for scriptName := range d.pluginScripts { + if err := insertScriptInHead(htmlNode, scriptName); err != nil { + return nil, err + } + } + var buffer bytes.Buffer err = html.Render(&buffer, htmlNode) if err != nil { diff --git a/v3/V3 Changes.md b/v3/V3 Changes.md index 367a2cf5b..e2f8da7e9 100644 --- a/v3/V3 Changes.md +++ b/v3/V3 Changes.md @@ -108,3 +108,75 @@ This attribute specifies which javascript event should trigger the action. The d ``` +## Plugins + +Plugins are a way to extend the functionality of your Wails application. + +### Creating a plugin + +Plugins are standard Go structure that adhere to the following interface: + +```go +type Plugin interface { + Name() string + Init(*application.App) error + Shutdown() + CallableByJS() []string + InjectJS() string +} +``` + +The `Name()` method returns the name of the plugin. This is used for logging purposes. + +The `Init(*application.App) error` method is called when the plugin is loaded. The `*application.App` +parameter is the application that the plugin is being loaded into. Any errors will prevent +the application from starting. + +The `Shutdown()` method is called when the application is shutting down. + +The `CallableByJS()` method returns a list of exported functions that can be called from +the frontend. These method names must exactly match the names of the methods exported +by the plugin. + +The `InjectJS()` method returns JavaScript that should be injected into all windows as they are created. This is useful for adding custom JavaScript functions that complement the plugin. + +### Tips + +#### Enums + +In Go, enums are often defined as a type and a set of constants. For example: + +```go +type MyEnum int + +const ( + MyEnumOne MyEnum = iota + MyEnumTwo + MyEnumThree +) +``` + +Due to incompatibility between Go and JavaScript, custom types cannot be used in this way. The best strategy is to use a type alias for float64: + +```go +type MyEnum = float64 + +const ( + MyEnumOne MyEnum = iota + MyEnumTwo + MyEnumThree +) +``` + +In Javascript, you can then use the following: + +```js +const MyEnum = { + MyEnumOne: 0, + MyEnumTwo: 1, + MyEnumThree: 2 +} +``` + +- Why use `float64`? Can't we use `int`? + - Because JavaScript doesn't have a concept of `int`. Everything is a `number`, which translates to `float64` in Go. There are also restrictions on casting types in Go's reflection package, which means using `int` doesn't work. diff --git a/v3/examples/plugins/plugins/hashes/plugin.go b/v3/examples/plugins/plugins/hashes/plugin.go index b2bbaef4d..afdcec84a 100644 --- a/v3/examples/plugins/plugins/hashes/plugin.go +++ b/v3/examples/plugins/plugins/hashes/plugin.go @@ -5,6 +5,7 @@ import ( "crypto/sha1" "crypto/sha256" "encoding/hex" + "github.com/wailsapp/wails/v3/pkg/application" ) @@ -32,6 +33,10 @@ func (r *Plugin) CallableByJS() []string { } } +func (r *Plugin) InjectJS() string { + return "" +} + // ---------------- Plugin Methods ---------------- type Hashes struct { diff --git a/v3/pkg/application/application.go b/v3/pkg/application/application.go index 6fe6a9090..e16a3e324 100644 --- a/v3/pkg/application/application.go +++ b/v3/pkg/application/application.go @@ -38,6 +38,7 @@ func New(appOptions Options) *App { log: logger.New(appOptions.Logger.CustomLoggers...), contextMenus: make(map[string]*Menu), } + globalApplication = result if !appOptions.Logger.Silent { result.log.AddOutput(&logger.Console{}) @@ -60,7 +61,24 @@ func New(appOptions Options) *App { srv.UseRuntimeHandler(NewMessageProcessor()) result.assets = srv - globalApplication = result + result.bindings, err = NewBindings(appOptions.Bind) + if err != nil { + println("Fatal error in application initialisation: ", err.Error()) + os.Exit(1) + } + err = result.bindings.AddPlugins(appOptions.Plugins) + if err != nil { + println("Fatal error in application initialisation: ", err.Error()) + os.Exit(1) + } + + result.plugins = NewPluginManager(appOptions.Plugins, srv) + err = result.plugins.Init() + if err != nil { + println("Fatal error in application initialisation: ", err.Error()) + os.Exit(1) + } + return result } @@ -318,20 +336,6 @@ func (a *App) Run() error { } }() - var err error - a.bindings, err = NewBindings(a.options.Bind) - if err != nil { - return err - } - - a.plugins = NewPluginManager(a.options.Plugins) - err = a.plugins.Init() - if err != nil { - return err - } - - a.bindings.AddPlugins(a.options.Plugins) - // run windows for _, window := range a.windows { go window.run() @@ -348,7 +352,7 @@ func (a *App) Run() error { // set the application Icon a.impl.setIcon(a.options.Icon) - err = a.impl.run() + err := a.impl.run() if err != nil { return err } diff --git a/v3/pkg/application/bindings.go b/v3/pkg/application/bindings.go index 362ec9a96..9fe604c70 100644 --- a/v3/pkg/application/bindings.go +++ b/v3/pkg/application/bindings.go @@ -2,10 +2,11 @@ package application import ( "fmt" - "github.com/samber/lo" "reflect" "runtime" "strings" + + "github.com/samber/lo" ) type CallOptions struct { @@ -174,7 +175,7 @@ func (b *Bindings) getMethods(value interface{}) ([]*BoundMethod, error) { if isFunction(value) { name := runtime.FuncForPC(reflect.ValueOf(value).Pointer()).Name() - return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct.", name) + return nil, fmt.Errorf("%s is a function, not a pointer to a struct. Wails v2 has deprecated the binding of functions. Please wrap your functions up in a struct and bind a pointer to that struct", name) } return nil, fmt.Errorf("not a pointer to a struct") diff --git a/v3/pkg/application/plugins.go b/v3/pkg/application/plugins.go index c12ef0c8e..f60dcd44a 100644 --- a/v3/pkg/application/plugins.go +++ b/v3/pkg/application/plugins.go @@ -1,5 +1,7 @@ package application +import "github.com/wailsapp/wails/v2/pkg/assetserver" + type Plugin interface { Name() string Init(app *App) error @@ -9,14 +11,15 @@ type Plugin interface { } type PluginManager struct { - plugins map[string]Plugin + plugins map[string]Plugin + assetServer *assetserver.AssetServer } -func NewPluginManager(plugins map[string]Plugin) *PluginManager { +func NewPluginManager(plugins map[string]Plugin, assetServer *assetserver.AssetServer) *PluginManager { result := &PluginManager{ - plugins: plugins, + plugins: plugins, + assetServer: assetServer, } - globalApplication.OnWindowCreation(result.onWindowCreation) return result } @@ -28,7 +31,7 @@ func (p *PluginManager) Init() error { } injectJS := plugin.InjectJS() if injectJS != "" { - + p.assetServer.AddPluginScript(plugin.Name(), injectJS) } globalApplication.info("Plugin '%s' initialised", plugin.Name()) } @@ -41,12 +44,3 @@ func (p *PluginManager) Shutdown() { globalApplication.info("Plugin '%s' shutdown", plugin.Name()) } } - -func (p *PluginManager) onWindowCreation(window *WebviewWindow) { - for _, plugin := range p.plugins { - injectJS := plugin.InjectJS() - if injectJS != "" { - window.ExecJS(injectJS) - } - } -} diff --git a/v3/plugins/README.md b/v3/plugins/README.md deleted file mode 100644 index 4edb0f81e..000000000 --- a/v3/plugins/README.md +++ /dev/null @@ -1,29 +0,0 @@ -# Plugins - -Plugins are a way to extend the functionality of your Wails application. - -## Creating a plugin - -Plugins are standard Go structure that adhere to the following interface: - -```go -type Plugin interface { - Name() string - Init(*application.App) error - Shutdown() - CallableByJS() []string -} -``` - -The `Name()` method returns the name of the plugin. This is used for logging purposes. - -The `Init(*application.App) error` method is called when the plugin is loaded. The `*application.App` -parameter is the application that the plugin is being loaded into. Any errors will prevent -the application from starting. - -The `Shutdown()` method is called when the application is shutting down. - -The `CallableByJS()` method returns a list of exported functions that can be called from -the frontend. These method names must exactly match the names of the methods exported -by the plugin. - diff --git a/v3/plugins/TODO.md b/v3/plugins/TODO.md deleted file mode 100644 index 690966caf..000000000 --- a/v3/plugins/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -# TODO - -- [ ] Add InjectCSS() to the plugin API? \ No newline at end of file diff --git a/v3/plugins/browser/plugin.go b/v3/plugins/browser/plugin.go index 918f8bf33..85925cd70 100644 --- a/v3/plugins/browser/plugin.go +++ b/v3/plugins/browser/plugin.go @@ -33,6 +33,10 @@ func (p *Plugin) CallableByJS() []string { } } +func (p *Plugin) InjectJS() string { + return "" +} + // ---------------- Plugin Methods ---------------- func (p *Plugin) OpenURL(url string) error { diff --git a/v3/plugins/kvstore/kvstore.go b/v3/plugins/kvstore/kvstore.go index 116fb4eb0..2fb9d49f2 100644 --- a/v3/plugins/kvstore/kvstore.go +++ b/v3/plugins/kvstore/kvstore.go @@ -2,11 +2,12 @@ package kvstore import ( "encoding/json" - "github.com/wailsapp/wails/v3/pkg/application" - "github.com/wailsapp/wails/v3/pkg/logger" "io" "os" "sync" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/logger" ) type KeyValueStore struct { @@ -18,6 +19,10 @@ type KeyValueStore struct { app *application.App } +func (kvs *KeyValueStore) InjectJS() string { + return "" +} + type Config struct { Filename string AutoSave bool @@ -67,6 +72,10 @@ func (kvs *KeyValueStore) CallableByJS() []string { } } +func (p *Plugin) InjectJS() string { + return "" +} + // ---------------- Plugin Methods ---------------- func (kvs *KeyValueStore) open(filename string) (err error) { diff --git a/v3/plugins/log/plugin.go b/v3/plugins/log/plugin.go index 794456d02..704c842f9 100644 --- a/v3/plugins/log/plugin.go +++ b/v3/plugins/log/plugin.go @@ -1,18 +1,23 @@ package log import ( + _ "embed" "fmt" - "github.com/wailsapp/wails/v3/pkg/application" "io" "os" + + "github.com/wailsapp/wails/v3/pkg/application" ) +//go:embed plugin.js +var pluginJS string + // ---------------- Plugin Setup ---------------- // This is the main plugin struct. It can be named anything you like. // It must implement the application.Plugin interface. // Both the Init() and Shutdown() methods are called synchronously when the app starts and stops. -type LogLevel int +type LogLevel = float64 const ( Trace LogLevel = iota + 1 @@ -55,6 +60,7 @@ func NewPluginWithConfig(config *Config) *Plugin { } return &Plugin{ config: config, + level: config.Level, } } @@ -88,9 +94,14 @@ func (p *Plugin) CallableByJS() []string { "Warning", "Error", "Fatal", + "SetLevel", } } +func (p *Plugin) InjectJS() string { + return pluginJS +} + // ---------------- Plugin Methods ---------------- // Plugin methods are just normal Go methods. You can add as many as you like. // The only requirement is that they are exported (start with a capital letter). @@ -98,7 +109,7 @@ func (p *Plugin) CallableByJS() []string { // See https://golang.org/pkg/encoding/json/#Marshal for more information. func (p *Plugin) write(prefix string, level LogLevel, message string, args ...any) { - if level >= p.config.Level { + if level >= p.level { if !p.config.DisablePrefix { message = prefix + " " + message } @@ -134,5 +145,8 @@ func (p *Plugin) Fatal(message string, args ...any) { } func (p *Plugin) SetLevel(newLevel LogLevel) { + if newLevel == 0 { + newLevel = Debug + } p.level = newLevel } diff --git a/v3/plugins/log/plugin.js b/v3/plugins/log/plugin.js index a5e952549..16811d5d1 100644 --- a/v3/plugins/log/plugin.js +++ b/v3/plugins/log/plugin.js @@ -8,7 +8,7 @@ * @param args {...any} - The arguments for the log message. * @returns {Promise} */ -export function Trace(input, ...args) { +function Trace(input, ...args) { return wails.Plugin("log", "Trace", input, ...args); } @@ -19,7 +19,7 @@ export function Trace(input, ...args) { * @returns {Promise} */ -export function Debug(input, ...args) { +function Debug(input, ...args) { return wails.Plugin("log", "Debug", input, ...args); } @@ -29,7 +29,7 @@ export function Debug(input, ...args) { * @param args {...any} - The arguments for the log message. * @returns {Promise} */ -export function Info(input, ...args) { +function Info(input, ...args) { return wails.Plugin("log", "Info", input, ...args); } @@ -39,7 +39,7 @@ export function Info(input, ...args) { * @param args {...any} - The arguments for the log message. * @returns {Promise} */ -export function Warning(input, ...args) { +function Warning(input, ...args) { return wails.Plugin("log", "Warning", input, ...args); } @@ -49,7 +49,7 @@ export function Warning(input, ...args) { * @param args {...any} - The arguments for the log message. * @returns {Promise} */ -export function Error(input, ...args) { +function Error(input, ...args) { return wails.Plugin("log", "Error", input, ...args); } @@ -59,6 +59,40 @@ export function Error(input, ...args) { * @param args {...any} - The arguments for the log message. * @returns {Promise} */ -export function Fatal(input, ...args) { +function Fatal(input, ...args) { return wails.Plugin("log", "Fatal", input, ...args); -} \ No newline at end of file +} + +/** + * SetLevel sets the logging level + * @param level {Level} The log level to set + * @returns {Promise} + */ +function SetLevel(level) { + return wails.Plugin("log", "SetLevel", level); +} + +/** + * Log Level. + * @readonly + * @enum {number} + */ +let Level = { + Trace: 1, + Debug: 2, + Info: 3, + Warning: 4, + Error: 5, + Fatal: 6, +}; + +window.Logger = { + Trace, + Debug, + Info, + Warning, + Error, + Fatal, + SetLevel, + Level, +}