diff --git a/docs/src/content/docs/changelog.mdx b/docs/src/content/docs/changelog.mdx index d34e8660b..67f832ea4 100644 --- a/docs/src/content/docs/changelog.mdx +++ b/docs/src/content/docs/changelog.mdx @@ -78,9 +78,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Gin support by [Lea Anthony](https://github.com/leaanthony) in [PR](https://github.com/wailsapp/wails/pull/3537) based on the original work of [@AnalogJ](https://github.com/AnalogJ) in PR[https://github.com/wailsapp/wails/pull/3537] - Fix auto save and password auto save always enabled by [@oSethoum](https://github.com/osethoum) in [#4134](https://github.com/wailsapp/wails/pull/4134) - Add `SetMenu()` on window to allow for setting a menu on a window by [@leaanthony](https://github.com/leaanthony) -- Add Notification support by [@popaprozac] in [#4098](https://github.com/wailsapp/wails/pull/4098) +- Add Notification support by [@popaprozac](https://github.com/popaprozac) in [#4098](https://github.com/wailsapp/wails/pull/4098) -  Add File Association support for mac by [@wimaha](https://github.com/wimaha) in [#4177](https://github.com/wailsapp/wails/pull/4177) - Add `wails3 tool version` for semantic version bumping by [@leaanthony](https://github.com/leaanthony) +- Add badging support for macOS and Windows by [@popaprozac](https://github.com/popaprozac) in [#](https://github.com/wailsapp/wails/pull/4234) ### Fixed - Fixed Windows+Linux Edit Menu issues by [@leaanthony](https://github.com/leaanthony) in [#3f78a3a](https://github.com/wailsapp/wails/commit/3f78a3a8ce7837e8b32242c8edbbed431c68c062) @@ -155,7 +156,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Built-in service creation functions with options are now consistently called `NewWithConfig` by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) - `Select` method on `sqlite` service is now named `Query` for consistency with Go APIs by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067) - Templates: moved runtime to "dependencies", organized package.json files by [@IanVS](https://github.com/IanVS) in [#4133](https://github.com/wailsapp/wails/pull/4133) -- Creates and ad-hoc signs app bundles in dev to enable certain macOS APIs by [@popaprozac] in [#4171](https://github.com/wailsapp/wails/pull/4171) +- Creates and ad-hoc signs app bundles in dev to enable certain macOS APIs by [@popaprozac](https://github.com/popaprozac) in [#4171](https://github.com/wailsapp/wails/pull/4171) ## v3.0.0-alpha.9 - 2025-01-13 diff --git a/docs/src/content/docs/learn/badges.mdx b/docs/src/content/docs/learn/badges.mdx new file mode 100644 index 000000000..3effaf7cc --- /dev/null +++ b/docs/src/content/docs/learn/badges.mdx @@ -0,0 +1,195 @@ +--- +title: Badges +--- + +import { Tabs, TabItem } from "@astrojs/starlight/components"; + +## Introduction + +Wails provides a cross-platform badge service for desktop applications. This service allows you to display badges on your application tile or dock icon, which is useful for indicating unread messages, notifications, or other status information. + +## Basic Usage + +### Creating the Service + +First, initialize the badge service: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" + +// Create a new badge service +badgeService := badge.New() + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +### Creating the Service with Custom Options (Windows Only) + +On Windows, you can customize the badge appearance with various options: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" +import "image/color" + +// Create a badge service with custom options +options := badge.Options{ + TextColour: color.RGBA{255, 255, 255, 255}, // White text + BackgroundColour: color.RGBA{0, 0, 255, 255}, // Blue background + FontName: "consolab.ttf", // Bold Consolas font + FontSize: 20, // Font size for single character + SmallFontSize: 14, // Font size for multiple characters +} + +badgeService := badge.NewWithOptions(options) + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +## Badge Operations + +### Setting a Badge + +Set a badge on the application tile/dock icon: + +```go +// Set a default badge +badgeService.SetBadge("") + +// Set a numeric badge +badgeService.SetBadge("3") + +// Set a text badge +badgeService.SetBadge("New") +``` + +### Setting a Custom Badge + +Set a badge on the application tile/dock icon with one-off options applied: + +#### Go +```go +options := badge.Options{ + BackgroundColour: color.RGBA{0, 255, 255, 255}, + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: color.RGBA{0, 0, 0, 255}, +} + +// Set a default badge +badgeService.SetCustomBadge("", options) + +// Set a numeric badge +badgeService.SetCustomBadge("3", options) + +// Set a text badge +badgeService.SetCustomBadge("New", options) +``` + +### Removing a Badge + +Remove the badge from the application icon: + +```go +badgeService.RemoveBadge() +``` + +## Platform Considerations + + + + + On macOS, badges: + + - Are displayed directly on the dock icon + - Support text values + - Automatically handle dark/light mode appearance + - Use the standard macOS dock badge styling + - Automatically handle label overflow + - Do not support customization options (any options passed to NewWithOptions will be ignored) + - Will display "●" as the default badge if an empty label is provided + + + + + + On Windows, badges: + + - Are displayed as an overlay icon in the taskbar + - Support text values + - Allow customization of colors, font, and font sizes + - Adapt to Windows theme settings + - Require the application to have a window + - Use smaller font size automatically for badges with multiple characters + - Do not handle label overflow + - Support the following customization options: + - TextColour: Text color (default: white) + - BackgroundColour: Badge background color (default: red) + - FontName: Font file name (default: "segoeuib.ttf") + - FontSize: Font size for single character (default: 18) + - SmallFontSize: Font size for multiple characters (default: 14) + + + + + + On Linux: + + - Badge functionality is not available + + + + +## Best Practices + +1. Use badges sparingly: + - Too many badge updates can distract users + - Reserve badges for important notifications + +2. Keep badge text short: + - Numeric badges are most effective + - On macOS, text badges should be brief + +3. For Windows customization: + - Ensure high contrast between text and background colors + - Test with different text lengths as font size decreases with length + - Use common system fonts to ensure availability + +## API Reference + +### Service Management +| Method | Description | +|--------------------------------------------|-------------------------------------------------------| +| `New()` | Creates a new badge service with default options | +| `NewWithOptions(options Options)` | Creates a new badge service with custom options (Windows only, options are ignored on macOS) | + +### Badge Operations +| Method | Description | +|----------------------------------------------|------------------------------------------------------------| +| `SetBadge(label string) error` | Sets a badge with the specified label | +| `SetCustomBadge(label string, options Options) error` | Sets a badge with the specified label and custom styling options (Windows only) | +| `RemoveBadge() error` | Removes the badge from the application icon | +### Structs and Types + +```go +// Options for customizing badge appearance (Windows only) +type Options struct { + TextColour color.RGBA // Color of the badge text + BackgroundColour color.RGBA // Color of the badge background + FontName string // Font file name (e.g., "segoeuib.ttf") + FontSize int // Font size for single character + SmallFontSize int // Font size for multiple characters +} +``` diff --git a/v3/examples/badge-custom/README.md b/v3/examples/badge-custom/README.md new file mode 100644 index 000000000..ab4c5a3fb --- /dev/null +++ b/v3/examples/badge-custom/README.md @@ -0,0 +1,128 @@ +# Welcome to Your New Wails3 Project! +Now that you have your project set up, it's time to explore the custom badge features that Wails3 offers on **Windows**. + +## Exploring Custom Badge Features + +### Creating the Service with Custom Options (Windows Only) + +On Windows, you can customize the badge appearance with various options: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" +import "image/color" + +// Create a badge service with custom options +options := badge.Options{ + TextColour: color.RGBA{255, 255, 255, 255}, // White text + BackgroundColour: color.RGBA{0, 0, 255, 255}, // Green background + FontName: "consolab.ttf", // Bold Consolas font + FontSize: 20, // Font size for single character + SmallFontSize: 14, // Font size for multiple characters +} + +badgeService := badge.NewWithOptions(options) + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +## Badge Operations + +### Setting a Badge + +Set a badge on the application tile/dock icon with the global options applied: + +#### Go +```go +// Set a default badge +badgeService.SetBadge("") + +// Set a numeric badge +badgeService.SetBadge("3") + +// Set a text badge +badgeService.SetBadge("New") +``` + +#### JS +```js +import {SetBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +// Set a default badge +SetBadge("") + +// Set a numeric badge +SetBadge("3") + +// Set a text badge +SetBadge("New") +``` + +### Setting a Custom Badge + +Set a badge on the application tile/dock icon with one-off options applied: + +#### Go +```go +// Set a default badge +badgeService.SetCustomBadge("") + +// Set a numeric badge +badgeService.SetCustomBadge("3") + +// Set a text badge +badgeService.SetCustomBadge("New") +``` + +#### JS +```js +import {SetCustomBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +const options = { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255, + }), + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255, + }), +} + +// Set a default badge +SetCustomBadge("", options) + +// Set a numeric badge +SetCustomBadge("3", options) + +// Set a text badge +SetCustomBadge("New", options) +``` + +### Removing a Badge + +Remove the badge from the application icon: + +#### Go +```go +badgeService.RemoveBadge() +``` + +#### JS +```js +import {RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +RemoveBadge() +``` \ No newline at end of file diff --git a/v3/examples/badge-custom/Taskfile.yml b/v3/examples/badge-custom/Taskfile.yml new file mode 100644 index 000000000..d1bcfaacf --- /dev/null +++ b/v3/examples/badge-custom/Taskfile.yml @@ -0,0 +1,34 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + +vars: + APP_NAME: "badge" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + diff --git a/v3/examples/badge-custom/build/Taskfile.yml b/v3/examples/badge-custom/build/Taskfile.yml new file mode 100644 index 000000000..5f3517efc --- /dev/null +++ b/v3/examples/badge-custom/build/Taskfile.yml @@ -0,0 +1,86 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules/* + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (PRODUCTION={{.PRODUCTION}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + vars: + BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/v3/examples/badge-custom/build/appicon.png b/v3/examples/badge-custom/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v3/examples/badge-custom/build/appicon.png differ diff --git a/v3/examples/badge-custom/build/config.yml b/v3/examples/badge-custom/build/config.yml new file mode 100644 index 000000000..aaa3240fb --- /dev/null +++ b/v3/examples/badge-custom/build/config.yml @@ -0,0 +1,63 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + git_ignore: true + executes: + - cmd: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/v3/examples/badge-custom/build/darwin/Info.dev.plist b/v3/examples/badge-custom/build/darwin/Info.dev.plist new file mode 100644 index 000000000..4caddf720 --- /dev/null +++ b/v3/examples/badge-custom/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © now, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/v3/examples/badge-custom/build/darwin/Info.plist b/v3/examples/badge-custom/build/darwin/Info.plist new file mode 100644 index 000000000..0dc90b2e7 --- /dev/null +++ b/v3/examples/badge-custom/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © now, My Company + + \ No newline at end of file diff --git a/v3/examples/badge-custom/build/darwin/Taskfile.yml b/v3/examples/badge-custom/build/darwin/Taskfile.yml new file mode 100644 index 000000000..f0791fea9 --- /dev/null +++ b/v3/examples/badge-custom/build/darwin/Taskfile.yml @@ -0,0 +1,81 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Creates a production build of the application + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app + + run: + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS + - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/v3/examples/badge-custom/build/darwin/icons.icns b/v3/examples/badge-custom/build/darwin/icons.icns new file mode 100644 index 000000000..1b5bd4c86 Binary files /dev/null and b/v3/examples/badge-custom/build/darwin/icons.icns differ diff --git a/v3/examples/badge-custom/build/linux/Taskfile.yml b/v3/examples/badge-custom/build/linux/Taskfile.yml new file mode 100644 index 000000000..560cc9c92 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/Taskfile.yml @@ -0,0 +1,119 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Linux + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application for Linux + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + vars: + PRODUCTION: "true" + - task: generate:dotdesktop + cmds: + - cp {{.APP_BINARY}} {{.APP_NAME}} + - cp ../../appicon.png appicon.png + - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '../../appicon.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: 'appicon' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/v3/examples/badge-custom/build/linux/appimage/build.sh b/v3/examples/badge-custom/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/v3/examples/badge-custom/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/v3/examples/badge-custom/build/linux/nfpm/nfpm.yaml b/v3/examples/badge-custom/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..5b7ea9d02 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,50 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "badge" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "My Product Description" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/badge" + dst: "/usr/local/bin/badge" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/badge.png" + - src: "./build/linux/badge.desktop" + dst: "/usr/share/applications/badge.desktop" + +depends: + - gtk3 + - libwebkit2gtk + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" +# scripts: +# preinstall: ./build/linux/nfpm/scripts/preinstall.sh +# postinstall: ./build/linux/nfpm/scripts/postinstall.sh +# preremove: ./build/linux/nfpm/scripts/preremove.sh +# postremove: ./build/linux/nfpm/scripts/postremove.sh diff --git a/v3/examples/badge-custom/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/badge-custom/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge-custom/build/linux/nfpm/scripts/postremove.sh b/v3/examples/badge-custom/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge-custom/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/badge-custom/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge-custom/build/linux/nfpm/scripts/preremove.sh b/v3/examples/badge-custom/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge-custom/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge-custom/build/windows/Taskfile.yml b/v3/examples/badge-custom/build/windows/Taskfile.yml new file mode 100644 index 000000000..534f4fb31 --- /dev/null +++ b/v3/examples/badge-custom/build/windows/Taskfile.yml @@ -0,0 +1,63 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Windows + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: windows + CGO_ENABLED: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application into a `.exe` bundle + cmds: + - task: create:nsis:installer + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/v3/examples/badge-custom/build/windows/icon.ico b/v3/examples/badge-custom/build/windows/icon.ico new file mode 100644 index 000000000..bfa0690b7 Binary files /dev/null and b/v3/examples/badge-custom/build/windows/icon.ico differ diff --git a/v3/examples/badge-custom/build/windows/info.json b/v3/examples/badge-custom/build/windows/info.json new file mode 100644 index 000000000..850b2b5b0 --- /dev/null +++ b/v3/examples/badge-custom/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "My Product Description", + "LegalCopyright": "© now, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/v3/examples/badge-custom/build/windows/nsis/project.nsi b/v3/examples/badge-custom/build/windows/nsis/project.nsi new file mode 100644 index 000000000..985b8e207 --- /dev/null +++ b/v3/examples/badge-custom/build/windows/nsis/project.nsi @@ -0,0 +1,112 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "badge" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v3/examples/badge-custom/build/windows/nsis/wails_tools.nsh b/v3/examples/badge-custom/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..6fc10ab79 --- /dev/null +++ b/v3/examples/badge-custom/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,212 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "badge" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "© now, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend \ No newline at end of file diff --git a/v3/examples/badge-custom/build/windows/wails.exe.manifest b/v3/examples/badge-custom/build/windows/wails.exe.manifest new file mode 100644 index 000000000..1d8992a3d --- /dev/null +++ b/v3/examples/badge-custom/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/Inter Font License.txt b/v3/examples/badge-custom/frontend/Inter Font License.txt new file mode 100644 index 000000000..b525cbf3a --- /dev/null +++ b/v3/examples/badge-custom/frontend/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts new file mode 100644 index 000000000..3c5d48043 --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts @@ -0,0 +1,11 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as Service from "./service.js"; +export { + Service +}; + +export { + Options +} from "./models.js"; diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts new file mode 100644 index 000000000..67ed264c0 --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/models.ts @@ -0,0 +1,58 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as color$0 from "../../../../../../../image/color/models.js"; + +export class Options { + "TextColour": color$0.RGBA; + "BackgroundColour": color$0.RGBA; + "FontName": string; + "FontSize": number; + "SmallFontSize": number; + + /** Creates a new Options instance. */ + constructor($$source: Partial = {}) { + if (!("TextColour" in $$source)) { + this["TextColour"] = (new color$0.RGBA()); + } + if (!("BackgroundColour" in $$source)) { + this["BackgroundColour"] = (new color$0.RGBA()); + } + if (!("FontName" in $$source)) { + this["FontName"] = ""; + } + if (!("FontSize" in $$source)) { + this["FontSize"] = 0; + } + if (!("SmallFontSize" in $$source)) { + this["SmallFontSize"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new Options instance from a string or object. + */ + static createFrom($$source: any = {}): Options { + const $$createField0_0 = $$createType0; + const $$createField1_0 = $$createType0; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("TextColour" in $$parsedSource) { + $$parsedSource["TextColour"] = $$createField0_0($$parsedSource["TextColour"]); + } + if ("BackgroundColour" in $$parsedSource) { + $$parsedSource["BackgroundColour"] = $$createField1_0($$parsedSource["BackgroundColour"]); + } + return new Options($$parsedSource as Partial); + } +} + +// Private type creation functions +const $$createType0 = color$0.RGBA.createFrom; diff --git a/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts new file mode 100644 index 000000000..dca83282b --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts @@ -0,0 +1,33 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Service represents the notifications service + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime"; + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as $models from "./models.js"; + +/** + * RemoveBadge removes the badge label from the application icon. + */ +export function RemoveBadge(): $CancellablePromise { + return $Call.ByID(2633565570); +} + +/** + * SetBadge sets the badge label on the application icon. + */ +export function SetBadge(label: string): $CancellablePromise { + return $Call.ByID(3052354152, label); +} + +export function SetCustomBadge(label: string, options: $models.Options): $CancellablePromise { + return $Call.ByID(921166821, label, options); +} diff --git a/v3/examples/badge-custom/frontend/bindings/image/color/index.ts b/v3/examples/badge-custom/frontend/bindings/image/color/index.ts new file mode 100644 index 000000000..97b507b08 --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/image/color/index.ts @@ -0,0 +1,6 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +export { + RGBA +} from "./models.js"; diff --git a/v3/examples/badge-custom/frontend/bindings/image/color/models.ts b/v3/examples/badge-custom/frontend/bindings/image/color/models.ts new file mode 100644 index 000000000..0d4eab56d --- /dev/null +++ b/v3/examples/badge-custom/frontend/bindings/image/color/models.ts @@ -0,0 +1,46 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import { Create as $Create } from "@wailsio/runtime"; + +/** + * RGBA represents a traditional 32-bit alpha-premultiplied color, having 8 + * bits for each of red, green, blue and alpha. + * + * An alpha-premultiplied color component C has been scaled by alpha (A), so + * has valid values 0 <= C <= A. + */ +export class RGBA { + "R": number; + "G": number; + "B": number; + "A": number; + + /** Creates a new RGBA instance. */ + constructor($$source: Partial = {}) { + if (!("R" in $$source)) { + this["R"] = 0; + } + if (!("G" in $$source)) { + this["G"] = 0; + } + if (!("B" in $$source)) { + this["B"] = 0; + } + if (!("A" in $$source)) { + this["A"] = 0; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new RGBA instance from a string or object. + */ + static createFrom($$source: any = {}): RGBA { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new RGBA($$parsedSource as Partial); + } +} diff --git a/v3/examples/badge-custom/frontend/dist/Inter-Medium.ttf b/v3/examples/badge-custom/frontend/dist/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge-custom/frontend/dist/Inter-Medium.ttf differ diff --git a/v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js b/v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js new file mode 100644 index 000000000..98bdf5437 --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/assets/index-DHsC0KxN.js @@ -0,0 +1,1415 @@ +var __defProp = Object.defineProperty; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); +(function polyfill() { + const relList = document.createElement("link").relList; + if (relList && relList.supports && relList.supports("modulepreload")) { + return; + } + for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { + processPreload(link); + } + new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== "childList") { + continue; + } + for (const node of mutation.addedNodes) { + if (node.tagName === "LINK" && node.rel === "modulepreload") + processPreload(node); + } + } + }).observe(document, { childList: true, subtree: true }); + function getFetchOpts(link) { + const fetchOpts = {}; + if (link.integrity) fetchOpts.integrity = link.integrity; + if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; + if (link.crossOrigin === "use-credentials") + fetchOpts.credentials = "include"; + else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; + else fetchOpts.credentials = "same-origin"; + return fetchOpts; + } + function processPreload(link) { + if (link.ep) + return; + link.ep = true; + const fetchOpts = getFetchOpts(link); + fetch(link.href, fetchOpts); + } +})(); +const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +function nanoid(size = 21) { + let id = ""; + let i = size | 0; + while (i--) { + id += urlAlphabet[Math.random() * 64 | 0]; + } + return id; +} +const runtimeURL = window.location.origin + "/wails/runtime"; +const objectNames = Object.freeze({ + Call: 0, + Clipboard: 1, + Application: 2, + Events: 3, + ContextMenu: 4, + Dialog: 5, + Window: 6, + Screens: 7, + System: 8, + Browser: 9, + CancelCall: 10 +}); +let clientId = nanoid(); +function newRuntimeCaller(object, windowName = "") { + return function(method, args = null) { + return runtimeCallWithID(object, method, windowName, args); + }; +} +async function runtimeCallWithID(objectID, method, windowName, args) { + var _a2, _b; + let url = new URL(runtimeURL); + url.searchParams.append("object", objectID.toString()); + url.searchParams.append("method", method.toString()); + if (args) { + url.searchParams.append("args", JSON.stringify(args)); + } + let headers = { + ["x-wails-client-id"]: clientId + }; + if (windowName) { + headers["x-wails-window-name"] = windowName; + } + let response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(await response.text()); + } + if (((_b = (_a2 = response.headers.get("Content-Type")) === null || _a2 === void 0 ? void 0 : _a2.indexOf("application/json")) !== null && _b !== void 0 ? _b : -1) !== -1) { + return response.json(); + } else { + return response.text(); + } +} +newRuntimeCaller(objectNames.System); +const _invoke = function() { + var _a2, _b, _c, _d, _e; + try { + if ((_b = (_a2 = window.chrome) === null || _a2 === void 0 ? void 0 : _a2.webview) === null || _b === void 0 ? void 0 : _b.postMessage) { + return window.chrome.webview.postMessage.bind(window.chrome.webview); + } else if ((_e = (_d = (_c = window.webkit) === null || _c === void 0 ? void 0 : _c.messageHandlers) === null || _d === void 0 ? void 0 : _d["external"]) === null || _e === void 0 ? void 0 : _e.postMessage) { + return window.webkit.messageHandlers["external"].postMessage.bind(window.webkit.messageHandlers["external"]); + } + } catch (e) { + } + console.warn("\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n", "background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;", "background: transparent;", "color: #ffffff; font-style: italic; font-weight: bold;"); + return null; +}(); +function invoke(msg) { + _invoke === null || _invoke === void 0 ? void 0 : _invoke(msg); +} +function IsWindows() { + return window._wails.environment.OS === "windows"; +} +function IsDebug() { + return Boolean(window._wails.environment.Debug); +} +function canTrackButtons() { + return new MouseEvent("mousedown").buttons === 0; +} +function eventTarget(event) { + var _a2; + if (event.target instanceof HTMLElement) { + return event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + return (_a2 = event.target.parentElement) !== null && _a2 !== void 0 ? _a2 : document.body; + } else { + return document.body; + } +} +document.addEventListener("DOMContentLoaded", () => { +}); +window.addEventListener("contextmenu", contextMenuHandler); +const call$2 = newRuntimeCaller(objectNames.ContextMenu); +const ContextMenuOpen = 0; +function openContextMenu(id, x, y, data) { + void call$2(ContextMenuOpen, { id, x, y, data }); +} +function contextMenuHandler(event) { + const target = eventTarget(event); + const customContextMenu = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu").trim(); + if (customContextMenu) { + event.preventDefault(); + const data = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu-data"); + openContextMenu(customContextMenu, event.clientX, event.clientY, data); + } else { + processDefaultContextMenu(event, target); + } +} +function processDefaultContextMenu(event, target) { + if (IsDebug()) { + return; + } + switch (window.getComputedStyle(target).getPropertyValue("--default-contextmenu").trim()) { + case "show": + return; + case "hide": + event.preventDefault(); + return; + } + if (target.isContentEditable) { + return; + } + const selection = window.getSelection(); + const hasSelection = selection && selection.toString().length > 0; + if (hasSelection) { + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const rects = range.getClientRects(); + for (let j = 0; j < rects.length; j++) { + const rect = rects[j]; + if (document.elementFromPoint(rect.left, rect.top) === target) { + return; + } + } + } + } + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + if (hasSelection || !target.readOnly && !target.disabled) { + return; + } + } + event.preventDefault(); +} +function GetFlag(key) { + try { + return window._wails.flags[key]; + } catch (e) { + throw new Error("Unable to retrieve flag '" + key + "': " + e, { cause: e }); + } +} +let canDrag = false; +let dragging = false; +let resizable = false; +let canResize = false; +let resizing = false; +let resizeEdge = ""; +let defaultCursor = "auto"; +let buttons = 0; +const buttonsTracked = canTrackButtons(); +window._wails = window._wails || {}; +window._wails.setResizable = (value) => { + resizable = value; + if (!resizable) { + canResize = resizing = false; + setResize(); + } +}; +window.addEventListener("mousedown", update, { capture: true }); +window.addEventListener("mousemove", update, { capture: true }); +window.addEventListener("mouseup", update, { capture: true }); +for (const ev of ["click", "contextmenu", "dblclick"]) { + window.addEventListener(ev, suppressEvent, { capture: true }); +} +function suppressEvent(event) { + if (dragging || resizing) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } +} +const MouseDown = 0; +const MouseUp = 1; +const MouseMove = 2; +function update(event) { + let eventType, eventButtons = event.buttons; + switch (event.type) { + case "mousedown": + eventType = MouseDown; + if (!buttonsTracked) { + eventButtons = buttons | 1 << event.button; + } + break; + case "mouseup": + eventType = MouseUp; + if (!buttonsTracked) { + eventButtons = buttons & ~(1 << event.button); + } + break; + default: + eventType = MouseMove; + if (!buttonsTracked) { + eventButtons = buttons; + } + break; + } + let released = buttons & ~eventButtons; + let pressed = eventButtons & ~buttons; + buttons = eventButtons; + if (eventType === MouseDown && !(pressed & event.button)) { + released |= 1 << event.button; + pressed |= 1 << event.button; + } + if (eventType !== MouseMove && resizing || dragging && (eventType === MouseDown || event.button !== 0)) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + if (released & 1) { + primaryUp(); + } + if (pressed & 1) { + primaryDown(event); + } + if (eventType === MouseMove) { + onMouseMove(event); + } +} +function primaryDown(event) { + canDrag = false; + canResize = false; + if (!IsWindows()) { + if (event.type === "mousedown" && event.button === 0 && event.detail !== 1) { + return; + } + } + if (resizeEdge) { + canResize = true; + return; + } + const target = eventTarget(event); + const style = window.getComputedStyle(target); + canDrag = style.getPropertyValue("--wails-draggable").trim() === "drag" && (event.offsetX - parseFloat(style.paddingLeft) < target.clientWidth && event.offsetY - parseFloat(style.paddingTop) < target.clientHeight); +} +function primaryUp(event) { + canDrag = false; + dragging = false; + canResize = false; + resizing = false; +} +const cursorForEdge = Object.freeze({ + "se-resize": "nwse-resize", + "sw-resize": "nesw-resize", + "nw-resize": "nwse-resize", + "ne-resize": "nesw-resize", + "w-resize": "ew-resize", + "n-resize": "ns-resize", + "s-resize": "ns-resize", + "e-resize": "ew-resize" +}); +function setResize(edge) { + if (edge) { + if (!resizeEdge) { + defaultCursor = document.body.style.cursor; + } + document.body.style.cursor = cursorForEdge[edge]; + } else if (!edge && resizeEdge) { + document.body.style.cursor = defaultCursor; + } + resizeEdge = edge || ""; +} +function onMouseMove(event) { + if (canResize && resizeEdge) { + resizing = true; + invoke("wails:resize:" + resizeEdge); + } else if (canDrag) { + dragging = true; + invoke("wails:drag"); + } + if (dragging || resizing) { + canDrag = canResize = false; + return; + } + if (!resizable || !IsWindows()) { + if (resizeEdge) { + setResize(); + } + return; + } + const resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; + const resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; + const cornerExtra = GetFlag("resizeCornerExtra") || 10; + const rightBorder = window.outerWidth - event.clientX < resizeHandleWidth; + const leftBorder = event.clientX < resizeHandleWidth; + const topBorder = event.clientY < resizeHandleHeight; + const bottomBorder = window.outerHeight - event.clientY < resizeHandleHeight; + const rightCorner = window.outerWidth - event.clientX < resizeHandleWidth + cornerExtra; + const leftCorner = event.clientX < resizeHandleWidth + cornerExtra; + const topCorner = event.clientY < resizeHandleHeight + cornerExtra; + const bottomCorner = window.outerHeight - event.clientY < resizeHandleHeight + cornerExtra; + if (!leftCorner && !topCorner && !bottomCorner && !rightCorner) { + setResize(); + } else if (rightCorner && bottomCorner) + setResize("se-resize"); + else if (leftCorner && bottomCorner) + setResize("sw-resize"); + else if (leftCorner && topCorner) + setResize("nw-resize"); + else if (topCorner && rightCorner) + setResize("ne-resize"); + else if (leftBorder) + setResize("w-resize"); + else if (topBorder) + setResize("n-resize"); + else if (bottomBorder) + setResize("s-resize"); + else if (rightBorder) + setResize("e-resize"); + else + setResize(); +} +var fnToStr = Function.prototype.toString; +var reflectApply = typeof Reflect === "object" && Reflect !== null && Reflect.apply; +var badArrayLike; +var isCallableMarker; +if (typeof reflectApply === "function" && typeof Object.defineProperty === "function") { + try { + badArrayLike = Object.defineProperty({}, "length", { + get: function() { + throw isCallableMarker; + } + }); + isCallableMarker = {}; + reflectApply(function() { + throw 42; + }, null, badArrayLike); + } catch (_) { + if (_ !== isCallableMarker) { + reflectApply = null; + } + } +} else { + reflectApply = null; +} +var constructorRegex = /^\s*class\b/; +var isES6ClassFn = function isES6ClassFunction(value) { + try { + var fnStr = fnToStr.call(value); + return constructorRegex.test(fnStr); + } catch (e) { + return false; + } +}; +var tryFunctionObject = function tryFunctionToStr(value) { + try { + if (isES6ClassFn(value)) { + return false; + } + fnToStr.call(value); + return true; + } catch (e) { + return false; + } +}; +var toStr = Object.prototype.toString; +var objectClass = "[object Object]"; +var fnClass = "[object Function]"; +var genClass = "[object GeneratorFunction]"; +var ddaClass = "[object HTMLAllCollection]"; +var ddaClass2 = "[object HTML document.all class]"; +var ddaClass3 = "[object HTMLCollection]"; +var hasToStringTag = typeof Symbol === "function" && !!Symbol.toStringTag; +var isIE68 = !(0 in [,]); +var isDDA = function isDocumentDotAll() { + return false; +}; +if (typeof document === "object") { + var all = document.all; + if (toStr.call(all) === toStr.call(document.all)) { + isDDA = function isDocumentDotAll2(value) { + if ((isIE68 || !value) && (typeof value === "undefined" || typeof value === "object")) { + try { + var str = toStr.call(value); + return (str === ddaClass || str === ddaClass2 || str === ddaClass3 || str === objectClass) && value("") == null; + } catch (e) { + } + } + return false; + }; + } +} +function isCallableRefApply(value) { + if (isDDA(value)) { + return true; + } + if (!value) { + return false; + } + if (typeof value !== "function" && typeof value !== "object") { + return false; + } + try { + reflectApply(value, null, badArrayLike); + } catch (e) { + if (e !== isCallableMarker) { + return false; + } + } + return !isES6ClassFn(value) && tryFunctionObject(value); +} +function isCallableNoRefApply(value) { + if (isDDA(value)) { + return true; + } + if (!value) { + return false; + } + if (typeof value !== "function" && typeof value !== "object") { + return false; + } + if (hasToStringTag) { + return tryFunctionObject(value); + } + if (isES6ClassFn(value)) { + return false; + } + var strClass = toStr.call(value); + if (strClass !== fnClass && strClass !== genClass && !/^\[object HTML/.test(strClass)) { + return false; + } + return tryFunctionObject(value); +} +const isCallable = reflectApply ? isCallableRefApply : isCallableNoRefApply; +var _a; +class CancelError extends Error { + /** + * Constructs a new `CancelError` instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message, options) { + super(message, options); + this.name = "CancelError"; + } +} +class CancelledRejectionError extends Error { + /** + * Constructs a new `CancelledRejectionError` instance. + * @param promise - The promise that caused the error originally. + * @param reason - The rejection reason. + * @param info - An optional informative message specifying the circumstances in which the error was thrown. + * Defaults to the string `"Unhandled rejection in cancelled promise."`. + */ + constructor(promise, reason, info) { + super((info !== null && info !== void 0 ? info : "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason }); + this.promise = promise; + this.name = "CancelledRejectionError"; + } +} +const barrierSym = Symbol("barrier"); +const cancelImplSym = Symbol("cancelImpl"); +const species = (_a = Symbol.species) !== null && _a !== void 0 ? _a : Symbol("speciesPolyfill"); +class CancellablePromise extends Promise { + /** + * Creates a new `CancellablePromise`. + * + * @param executor - A callback used to initialize the promise. This callback is passed two arguments: + * a `resolve` callback used to resolve the promise with a value + * or the result of another promise (possibly cancellable), + * and a `reject` callback used to reject the promise with a provided reason or error. + * If the value provided to the `resolve` callback is a thenable _and_ cancellable object + * (it has a `then` _and_ a `cancel` method), + * cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. + * If any one of the two callbacks is called _after_ the promise has been cancelled, + * the provided values will be cancelled and resolved as usual, + * but their results will be discarded. + * However, if the resolution process ultimately ends up in a rejection + * that is not due to cancellation, the rejection reason + * will be wrapped in a {@link CancelledRejectionError} + * and bubbled up as an unhandled rejection. + * @param oncancelled - It is the caller's responsibility to ensure that any operation + * started by the executor is properly halted upon cancellation. + * This optional callback can be used to that purpose. + * It will be called _synchronously_ with a cancellation cause + * when cancellation is requested, _after_ the promise has already rejected + * with a {@link CancelError}, but _before_ + * any {@link then}/{@link catch}/{@link finally} callback runs. + * If the callback returns a thenable, the promise returned from {@link cancel} + * will only fulfill after the former has settled. + * Unhandled exceptions or rejections from the callback will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as unhandled rejections. + * If the `resolve` callback is called before cancellation with a cancellable promise, + * cancellation requests on this promise will be diverted to that promise, + * and the original `oncancelled` callback will be discarded. + */ + constructor(executor, oncancelled) { + let resolve; + let reject; + super((res, rej) => { + resolve = res; + reject = rej; + }); + if (this.constructor[species] !== Promise) { + throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property."); + } + let promise = { + promise: this, + resolve, + reject, + get oncancelled() { + return oncancelled !== null && oncancelled !== void 0 ? oncancelled : null; + }, + set oncancelled(cb) { + oncancelled = cb !== null && cb !== void 0 ? cb : void 0; + } + }; + const state = { + get root() { + return state; + }, + resolving: false, + settled: false + }; + void Object.defineProperties(this, { + [barrierSym]: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + [cancelImplSym]: { + configurable: false, + enumerable: false, + writable: false, + value: cancellerFor(promise, state) + } + }); + const rejector = rejectorFor(promise, state); + try { + executor(resolverFor(promise, state), rejector); + } catch (err) { + if (state.resolving) { + console.log("Unhandled exception in CancellablePromise executor.", err); + } else { + rejector(err); + } + } + } + /** + * Cancels immediately the execution of the operation associated with this promise. + * The promise rejects with a {@link CancelError} instance as reason, + * with the {@link CancelError#cause} property set to the given argument, if any. + * + * Has no effect if called after the promise has already settled; + * repeated calls in particular are safe, but only the first one + * will set the cancellation cause. + * + * The `CancelError` exception _need not_ be handled explicitly _on the promises that are being cancelled:_ + * cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. + * Therefore, the following idioms are all equally correct: + * ```ts + * new CancellablePromise((resolve, reject) => { ... }).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); + * ``` + * Whenever some cancelled promise in a chain rejects with a `CancelError` + * with the same cancellation cause as itself, the error will be discarded silently. + * However, the `CancelError` _will still be delivered_ to all attached rejection handlers + * added by {@link then} and related methods: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * cancellable.then(() => { ... }).catch(console.log); + * cancellable.cancel(); // A CancelError is printed to the console. + * ``` + * If the `CancelError` is not handled downstream by the time it reaches + * a _non-cancelled_ promise, it _will_ trigger an unhandled rejection event, + * just like normal rejections would: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch... + * cancellable.cancel(); // Unhandled rejection event on chained! + * ``` + * Therefore, it is important to either cancel whole promise chains from their tail, + * as shown in the correct idioms above, or take care of handling errors everywhere. + * + * @returns A cancellable promise that _fulfills_ after the cancel callback (if any) + * and all handlers attached up to the call to cancel have run. + * If the cancel callback returns a thenable, the promise returned by `cancel` + * will also wait for that thenable to settle. + * This enables callers to wait for the cancelled operation to terminate + * without being forced to handle potential errors at the call site. + * ```ts + * cancellable.cancel().then(() => { + * // Cleanup finished, it's safe to do something else. + * }, (err) => { + * // Unreachable: the promise returned from cancel will never reject. + * }); + * ``` + * Note that the returned promise will _not_ handle implicitly any rejection + * that might have occurred already in the cancelled chain. + * It will just track whether registered handlers have been executed or not. + * Therefore, unhandled rejections will never be silently handled by calling cancel. + */ + cancel(cause) { + return new CancellablePromise((resolve) => { + Promise.all([ + this[cancelImplSym](new CancelError("Promise cancelled.", { cause })), + currentBarrier(this) + ]).then(() => resolve(), () => resolve()); + }); + } + /** + * Binds promise cancellation to the abort event of the given {@link AbortSignal}. + * If the signal has already aborted, the promise will be cancelled immediately. + * When either condition is verified, the cancellation cause will be set + * to the signal's abort reason (see {@link AbortSignal#reason}). + * + * Has no effect if called (or if the signal aborts) _after_ the promise has already settled. + * Only the first signal to abort will set the cancellation cause. + * + * For more details about the cancellation process, + * see {@link cancel} and the `CancellablePromise` constructor. + * + * This method enables `await`ing cancellable promises without having + * to store them for future cancellation, e.g.: + * ```ts + * await longRunningOperation().cancelOn(signal); + * ``` + * instead of: + * ```ts + * let promiseToBeCancelled = longRunningOperation(); + * await promiseToBeCancelled; + * ``` + * + * @returns This promise, for method chaining. + */ + cancelOn(signal) { + if (signal.aborted) { + void this.cancel(signal.reason); + } else { + signal.addEventListener("abort", () => void this.cancel(signal.reason), { capture: true }); + } + return this; + } + /** + * Attaches callbacks for the resolution and/or rejection of the `CancellablePromise`. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A `CancellablePromise` for the completion of whichever callback is executed. + * The returned promise is hooked up to propagate cancellation requests up the chain, but not down: + * + * - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError` + * and the returned promise _will resolve regularly_ with its result; + * - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_ + * the `onrejected` handler will still be invoked with the parent's `CancelError`, + * but its result will be discarded + * and the returned promise will reject with a `CancelError` as well. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If either callback returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + */ + then(onfulfilled, onrejected, oncancelled) { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.then called on an invalid object."); + } + if (!isCallable(onfulfilled)) { + onfulfilled = identity; + } + if (!isCallable(onrejected)) { + onrejected = thrower; + } + if (onfulfilled === identity && onrejected == thrower) { + return new CancellablePromise((resolve) => resolve(this)); + } + const barrier = {}; + this[barrierSym] = barrier; + return new CancellablePromise((resolve, reject) => { + void super.then((value) => { + var _a2; + if (this[barrierSym] === barrier) { + this[barrierSym] = null; + } + (_a2 = barrier.resolve) === null || _a2 === void 0 ? void 0 : _a2.call(barrier); + try { + resolve(onfulfilled(value)); + } catch (err) { + reject(err); + } + }, (reason) => { + var _a2; + if (this[barrierSym] === barrier) { + this[barrierSym] = null; + } + (_a2 = barrier.resolve) === null || _a2 === void 0 ? void 0 : _a2.call(barrier); + try { + resolve(onrejected(reason)); + } catch (err) { + reject(err); + } + }); + }, async (cause) => { + try { + return oncancelled === null || oncancelled === void 0 ? void 0 : oncancelled(cause); + } finally { + await this.cancel(cause); + } + }); + } + /** + * Attaches a callback for only the rejection of the Promise. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * It is equivalent to + * ```ts + * cancellablePromise.then(undefined, onrejected, oncancelled); + * ``` + * and the same caveats apply. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onrejected` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + catch(onrejected, oncancelled) { + return this.then(void 0, onrejected, oncancelled); + } + /** + * Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The + * resolved value cannot be accessed or modified from the callback. + * The returned promise will settle in the same state as the original one + * after the provided callback has completed execution, + * unless the callback throws or returns a rejecting promise, + * in which case the returned promise will reject as well. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * Once the parent promise settles, the `onfinally` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * This method is implemented in terms of {@link then} and the same caveats apply. + * It is polyfilled, hence available in every OS/webview version. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onfinally` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + finally(onfinally, oncancelled) { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.finally called on an invalid object."); + } + if (!isCallable(onfinally)) { + return this.then(onfinally, onfinally, oncancelled); + } + return this.then((value) => CancellablePromise.resolve(onfinally()).then(() => value), (reason) => CancellablePromise.resolve(onfinally()).then(() => { + throw reason; + }), oncancelled); + } + /** + * We use the `[Symbol.species]` static property, if available, + * to disable the built-in automatic subclassing features from {@link Promise}. + * It is critical for performance reasons that extenders do not override this. + * Once the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing + * is either accepted or retired, this implementation will have to be revised accordingly. + * + * @ignore + * @internal + */ + static get [species]() { + return Promise; + } + static all(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.all(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static allSettled(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.allSettled(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static any(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.any(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static race(values) { + let collected = Array.from(values); + const promise = new CancellablePromise((resolve, reject) => { + void Promise.race(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + /** + * Creates a new cancelled CancellablePromise for the provided cause. + * + * @group Static Methods + */ + static cancel(cause) { + const p = new CancellablePromise(() => { + }); + p.cancel(cause); + return p; + } + /** + * Creates a new CancellablePromise that cancels + * after the specified timeout, with the provided cause. + * + * If the {@link AbortSignal.timeout} factory method is available, + * it is used to base the timeout on _active_ time rather than _elapsed_ time. + * Otherwise, `timeout` falls back to {@link setTimeout}. + * + * @group Static Methods + */ + static timeout(milliseconds, cause) { + const promise = new CancellablePromise(() => { + }); + if (AbortSignal && typeof AbortSignal === "function" && AbortSignal.timeout && typeof AbortSignal.timeout === "function") { + AbortSignal.timeout(milliseconds).addEventListener("abort", () => void promise.cancel(cause)); + } else { + setTimeout(() => void promise.cancel(cause), milliseconds); + } + return promise; + } + static sleep(milliseconds, value) { + return new CancellablePromise((resolve) => { + setTimeout(() => resolve(value), milliseconds); + }); + } + /** + * Creates a new rejected CancellablePromise for the provided reason. + * + * @group Static Methods + */ + static reject(reason) { + return new CancellablePromise((_, reject) => reject(reason)); + } + static resolve(value) { + if (value instanceof CancellablePromise) { + return value; + } + return new CancellablePromise((resolve) => resolve(value)); + } + /** + * Creates a new CancellablePromise and returns it in an object, along with its resolve and reject functions + * and a getter/setter for the cancellation callback. + * + * This method is polyfilled, hence available in every OS/webview version. + * + * @group Static Methods + */ + static withResolvers() { + let result = { oncancelled: null }; + result.promise = new CancellablePromise((resolve, reject) => { + result.resolve = resolve; + result.reject = reject; + }, (cause) => { + var _a2; + (_a2 = result.oncancelled) === null || _a2 === void 0 ? void 0 : _a2.call(result, cause); + }); + return result; + } +} +function cancellerFor(promise, state) { + let cancellationPromise = void 0; + return (reason) => { + if (!state.settled) { + state.settled = true; + state.reason = reason; + promise.reject(reason); + void Promise.prototype.then.call(promise.promise, void 0, (err) => { + if (err !== reason) { + throw err; + } + }); + } + if (!state.reason || !promise.oncancelled) { + return; + } + cancellationPromise = new Promise((resolve) => { + try { + resolve(promise.oncancelled(state.reason.cause)); + } catch (err) { + Promise.reject(new CancelledRejectionError(promise.promise, err, "Unhandled exception in oncancelled callback.")); + } + }).catch((reason2) => { + Promise.reject(new CancelledRejectionError(promise.promise, reason2, "Unhandled rejection in oncancelled callback.")); + }); + promise.oncancelled = null; + return cancellationPromise; + }; +} +function resolverFor(promise, state) { + return (value) => { + if (state.resolving) { + return; + } + state.resolving = true; + if (value === promise.promise) { + if (state.settled) { + return; + } + state.settled = true; + promise.reject(new TypeError("A promise cannot be resolved with itself.")); + return; + } + if (value != null && (typeof value === "object" || typeof value === "function")) { + let then; + try { + then = value.then; + } catch (err) { + state.settled = true; + promise.reject(err); + return; + } + if (isCallable(then)) { + try { + let cancel = value.cancel; + if (isCallable(cancel)) { + const oncancelled = (cause) => { + Reflect.apply(cancel, value, [cause]); + }; + if (state.reason) { + void cancellerFor(Object.assign(Object.assign({}, promise), { oncancelled }), state)(state.reason); + } else { + promise.oncancelled = oncancelled; + } + } + } catch (_a2) { + } + const newState = { + root: state.root, + resolving: false, + get settled() { + return this.root.settled; + }, + set settled(value2) { + this.root.settled = value2; + }, + get reason() { + return this.root.reason; + } + }; + const rejector = rejectorFor(promise, newState); + try { + Reflect.apply(then, value, [resolverFor(promise, newState), rejector]); + } catch (err) { + rejector(err); + } + return; + } + } + if (state.settled) { + return; + } + state.settled = true; + promise.resolve(value); + }; +} +function rejectorFor(promise, state) { + return (reason) => { + if (state.resolving) { + return; + } + state.resolving = true; + if (state.settled) { + try { + if (reason instanceof CancelError && state.reason instanceof CancelError && Object.is(reason.cause, state.reason.cause)) { + return; + } + } catch (_a2) { + } + void Promise.reject(new CancelledRejectionError(promise.promise, reason)); + } else { + state.settled = true; + promise.reject(reason); + } + }; +} +function cancelAll(parent, values, cause) { + const results = []; + for (const value of values) { + let cancel; + try { + if (!isCallable(value.then)) { + continue; + } + cancel = value.cancel; + if (!isCallable(cancel)) { + continue; + } + } catch (_a2) { + continue; + } + let result; + try { + result = Reflect.apply(cancel, value, [cause]); + } catch (err) { + Promise.reject(new CancelledRejectionError(parent, err, "Unhandled exception in cancel method.")); + continue; + } + if (!result) { + continue; + } + results.push((result instanceof Promise ? result : Promise.resolve(result)).catch((reason) => { + Promise.reject(new CancelledRejectionError(parent, reason, "Unhandled rejection in cancel method.")); + })); + } + return Promise.all(results); +} +function identity(x) { + return x; +} +function thrower(reason) { + throw reason; +} +function errorMessage(err) { + try { + if (err instanceof Error || typeof err !== "object" || err.toString !== Object.prototype.toString) { + return "" + err; + } + } catch (_a2) { + } + try { + return JSON.stringify(err); + } catch (_b) { + } + try { + return Object.prototype.toString.call(err); + } catch (_c) { + } + return ""; +} +function currentBarrier(promise) { + var _a2; + let pwr = (_a2 = promise[barrierSym]) !== null && _a2 !== void 0 ? _a2 : {}; + if (!("promise" in pwr)) { + Object.assign(pwr, promiseWithResolvers()); + } + if (promise[barrierSym] == null) { + pwr.resolve(); + promise[barrierSym] = pwr; + } + return pwr.promise; +} +let promiseWithResolvers = Promise.withResolvers; +if (promiseWithResolvers && typeof promiseWithResolvers === "function") { + promiseWithResolvers = promiseWithResolvers.bind(Promise); +} else { + promiseWithResolvers = function() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; +} +window._wails = window._wails || {}; +window._wails.callResultHandler = resultHandler; +window._wails.callErrorHandler = errorHandler; +const call$1 = newRuntimeCaller(objectNames.Call); +const cancelCall = newRuntimeCaller(objectNames.CancelCall); +const callResponses = /* @__PURE__ */ new Map(); +const CallBinding = 0; +const CancelMethod = 0; +class RuntimeError extends Error { + /** + * Constructs a new RuntimeError instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message, options) { + super(message, options); + this.name = "RuntimeError"; + } +} +function resultHandler(id, data, isJSON) { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + if (!data) { + resolvers.resolve(void 0); + } else if (!isJSON) { + resolvers.resolve(data); + } else { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } +} +function errorHandler(id, data, isJSON) { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + if (!isJSON) { + resolvers.reject(new Error(data)); + } else { + let error; + try { + error = JSON.parse(data); + } catch (err) { + resolvers.reject(new TypeError("could not parse error: " + err.message, { cause: err })); + return; + } + let options = {}; + if (error.cause) { + options.cause = error.cause; + } + let exception; + switch (error.kind) { + case "ReferenceError": + exception = new ReferenceError(error.message, options); + break; + case "TypeError": + exception = new TypeError(error.message, options); + break; + case "RuntimeError": + exception = new RuntimeError(error.message, options); + break; + default: + exception = new Error(error.message, options); + break; + } + resolvers.reject(exception); + } +} +function getAndDeleteResponse(id) { + const response = callResponses.get(id); + callResponses.delete(id); + return response; +} +function generateID() { + let result; + do { + result = nanoid(); + } while (callResponses.has(result)); + return result; +} +function Call(options) { + const id = generateID(); + const result = CancellablePromise.withResolvers(); + callResponses.set(id, { resolve: result.resolve, reject: result.reject }); + const request = call$1(CallBinding, Object.assign({ "call-id": id }, options)); + let running = false; + request.then(() => { + running = true; + }, (err) => { + callResponses.delete(id); + result.reject(err); + }); + const cancel = () => { + callResponses.delete(id); + return cancelCall(CancelMethod, { "call-id": id }).catch((err) => { + console.error("Error while requesting binding call cancellation:", err); + }); + }; + result.oncancelled = () => { + if (running) { + return cancel(); + } else { + return request.then(cancel); + } + }; + return result.promise; +} +function ByID(methodID, ...args) { + return Call({ methodID, args }); +} +const eventListeners = /* @__PURE__ */ new Map(); +class Listener { + constructor(eventName, callback, maxCallbacks) { + this.eventName = eventName; + this.callback = callback; + this.maxCallbacks = maxCallbacks || -1; + } + dispatch(data) { + try { + this.callback(data); + } catch (err) { + console.error(err); + } + if (this.maxCallbacks === -1) + return false; + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + } +} +function listenerOff(listener) { + let listeners = eventListeners.get(listener.eventName); + if (!listeners) { + return; + } + listeners = listeners.filter((l) => l !== listener); + if (listeners.length === 0) { + eventListeners.delete(listener.eventName); + } else { + eventListeners.set(listener.eventName, listeners); + } +} +window._wails = window._wails || {}; +window._wails.dispatchWailsEvent = dispatchWailsEvent; +const call = newRuntimeCaller(objectNames.Events); +const EmitMethod = 0; +class WailsEvent { + constructor(name, data = null) { + this.name = name; + this.data = data; + } +} +function dispatchWailsEvent(event) { + let listeners = eventListeners.get(event.name); + if (!listeners) { + return; + } + let wailsEvent = new WailsEvent(event.name, event.data); + if ("sender" in event) { + wailsEvent.sender = event.sender; + } + listeners = listeners.filter((listener) => !listener.dispatch(wailsEvent)); + if (listeners.length === 0) { + eventListeners.delete(event.name); + } else { + eventListeners.set(event.name, listeners); + } +} +function OnMultiple(eventName, callback, maxCallbacks) { + let listeners = eventListeners.get(eventName) || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + listeners.push(thisListener); + eventListeners.set(eventName, listeners); + return () => listenerOff(thisListener); +} +function On(eventName, callback) { + return OnMultiple(eventName, callback, -1); +} +function Emit(event) { + return call(EmitMethod, event); +} +window._wails = window._wails || {}; +window._wails.invoke = invoke; +invoke("wails:runtime:ready"); +function RemoveBadge() { + return ByID(2633565570); +} +function SetBadge(label) { + return ByID(3052354152, label); +} +function SetCustomBadge(label, options) { + return ByID(921166821, label, options); +} +class RGBA { + /** Creates a new RGBA instance. */ + constructor($$source = {}) { + __publicField(this, "R"); + __publicField(this, "G"); + __publicField(this, "B"); + __publicField(this, "A"); + if (!("R" in $$source)) { + this["R"] = 0; + } + if (!("G" in $$source)) { + this["G"] = 0; + } + if (!("B" in $$source)) { + this["B"] = 0; + } + if (!("A" in $$source)) { + this["A"] = 0; + } + Object.assign(this, $$source); + } + /** + * Creates a new RGBA instance from a string or object. + */ + static createFrom($$source = {}) { + let $$parsedSource = typeof $$source === "string" ? JSON.parse($$source) : $$source; + return new RGBA($$parsedSource); + } +} +const setCustomButton = document.getElementById("set-custom"); +const setButton = document.getElementById("set"); +const removeButton = document.getElementById("remove"); +const setButtonUsingGo = document.getElementById("set-go"); +const removeButtonUsingGo = document.getElementById("remove-go"); +const labelElement = document.getElementById("label"); +const timeElement = document.getElementById("time"); +setCustomButton.addEventListener("click", () => { + console.log("click!"); + let label = labelElement.value; + SetCustomBadge(label, { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255 + }), + FontName: "arialb.ttf", + // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255 + }) + }); +}); +setButton.addEventListener("click", () => { + let label = labelElement.value; + SetBadge(label); +}); +removeButton.addEventListener("click", () => { + RemoveBadge(); +}); +setButtonUsingGo.addEventListener("click", () => { + let label = labelElement.value; + void Emit({ + name: "set:badge", + data: label + }); +}); +removeButtonUsingGo.addEventListener("click", () => { + void Emit({ name: "remove:badge", data: null }); +}); +On("time", (time) => { + timeElement.innerText = time.data; +}); diff --git a/v3/examples/badge-custom/frontend/dist/index.html b/v3/examples/badge-custom/frontend/dist/index.html new file mode 100644 index 000000000..70886cf91 --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/index.html @@ -0,0 +1,39 @@ + + + + + + + + Wails App + + + +
+ +

Wails + Typescript

+
Set a badge label below 👇
+
+
+ + + + + + +
+
+ +
+ + diff --git a/v3/examples/badge-custom/frontend/dist/style.css b/v3/examples/badge-custom/frontend/dist/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/dist/typescript.svg b/v3/examples/badge-custom/frontend/dist/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge-custom/frontend/dist/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/dist/wails.png b/v3/examples/badge-custom/frontend/dist/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge-custom/frontend/dist/wails.png differ diff --git a/v3/examples/badge-custom/frontend/index.html b/v3/examples/badge-custom/frontend/index.html new file mode 100644 index 000000000..e4a9ec4a2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/index.html @@ -0,0 +1,39 @@ + + + + + + + + Wails App + + +
+ +

Wails + Typescript

+
Set a badge label below 👇
+
+
+ + + + + + +
+
+ +
+ + + diff --git a/v3/examples/badge-custom/frontend/package.json b/v3/examples/badge-custom/frontend/package.json new file mode 100644 index 000000000..4d675f189 --- /dev/null +++ b/v3/examples/badge-custom/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0", + "@wailsio/runtime": "latest" + } +} \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf b/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge-custom/frontend/public/Inter-Medium.ttf differ diff --git a/v3/examples/badge-custom/frontend/public/style.css b/v3/examples/badge-custom/frontend/public/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge-custom/frontend/public/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/public/typescript.svg b/v3/examples/badge-custom/frontend/public/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge-custom/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge-custom/frontend/public/wails.png b/v3/examples/badge-custom/frontend/public/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge-custom/frontend/public/wails.png differ diff --git a/v3/examples/badge-custom/frontend/src/main.ts b/v3/examples/badge-custom/frontend/src/main.ts new file mode 100644 index 000000000..82ab24eef --- /dev/null +++ b/v3/examples/badge-custom/frontend/src/main.ts @@ -0,0 +1,59 @@ +import {Events} from "@wailsio/runtime"; +import {SetBadge, RemoveBadge, SetCustomBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; +import { RGBA } from "../bindings/image/color/models"; + +const setCustomButton = document.getElementById('set-custom')! as HTMLButtonElement; +const setButton = document.getElementById('set')! as HTMLButtonElement; +const removeButton = document.getElementById('remove')! as HTMLButtonElement; +const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement; +const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonElement; +const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement; +const timeElement = document.getElementById('time')! as HTMLDivElement; + +setCustomButton.addEventListener('click', () => { + console.log("click!") + let label = (labelElement as HTMLInputElement).value + SetCustomBadge(label, { + BackgroundColour: RGBA.createFrom({ + R: 0, + G: 255, + B: 255, + A: 255, + }), + FontName: "arialb.ttf", // System font + FontSize: 16, + SmallFontSize: 10, + TextColour: RGBA.createFrom({ + R: 0, + G: 0, + B: 0, + A: 255, + }), + }); +}) + +setButton.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + SetBadge(label); +}); + +removeButton.addEventListener('click', () => { + RemoveBadge(); +}); + +setButtonUsingGo.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + void Events.Emit({ + name: "set:badge", + data: label, + }) +}) + +removeButtonUsingGo.addEventListener('click', () => { + void Events.Emit({name:"remove:badge", data: null}) +}) + +Events.On('time', (time: {data: any}) => { + timeElement.innerText = time.data; +}); + diff --git a/v3/examples/badge-custom/frontend/src/vite-env.d.ts b/v3/examples/badge-custom/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v3/examples/badge-custom/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v3/examples/badge-custom/frontend/tsconfig.json b/v3/examples/badge-custom/frontend/tsconfig.json new file mode 100644 index 000000000..c267ecf24 --- /dev/null +++ b/v3/examples/badge-custom/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v3/examples/badge-custom/main.go b/v3/examples/badge-custom/main.go new file mode 100644 index 000000000..299d2bca8 --- /dev/null +++ b/v3/examples/badge-custom/main.go @@ -0,0 +1,102 @@ +package main + +import ( + "embed" + _ "embed" + "image/color" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/badge" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + + badgeService := badge.NewWithOptions(badge.Options{ + TextColour: color.RGBA{255, 255, 204, 255}, + BackgroundColour: color.RGBA{16, 124, 16, 255}, + FontName: "consolab.ttf", + FontSize: 20, + SmallFontSize: 14, + }) + + app := application.New(application.Options{ + Name: "badge", + Description: "A demo of using raw HTML & CSS", + Services: []application.Service{ + application.NewService(badgeService), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + app.OnEvent("remove:badge", func(event *application.CustomEvent) { + err := badgeService.RemoveBadge() + if err != nil { + log.Fatal(err) + } + }) + + app.OnEvent("set:badge", func(event *application.CustomEvent) { + text := event.Data.(string) + err := badgeService.SetBadge(text) + if err != nil { + log.Fatal(err) + } + }) + + // Create a goroutine that emits an event containing the current time every second. + // The frontend can listen to this event and update the UI accordingly. + go func() { + for { + now := time.Now().Format(time.RFC1123) + app.EmitEvent("time", now) + time.Sleep(time.Second) + } + }() + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/badge/README.md b/v3/examples/badge/README.md new file mode 100644 index 000000000..abb7b9653 --- /dev/null +++ b/v3/examples/badge/README.md @@ -0,0 +1,71 @@ +# Welcome to Your New Wails3 Project! +Now that you have your project set up, it's time to explore the basic badge features that Wails3 offers on **macOS** and **Windows**. + +## Exploring Badge Features + +### Creating the Service + +First, initialize the badge service: + +```go +import "github.com/wailsapp/wails/v3/pkg/application" +import "github.com/wailsapp/wails/v3/pkg/services/badge" + +// Create a new badge service +badgeService := badge.New() + +// Register the service with the application +app := application.New(application.Options{ + Services: []application.Service{ + application.NewService(badgeService), + }, +}) +``` + +## Badge Operations + +### Setting a Badge + +Set a badge on the application tile/dock icon: + +#### Go +```go +// Set a default badge +badgeService.SetBadge("") + +// Set a numeric badge +badgeService.SetBadge("3") + +// Set a text badge +badgeService.SetBadge("New") +``` + +#### JS +```js +import {SetBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +// Set a default badge +SetBadge("") + +// Set a numeric badge +SetBadge("3") + +// Set a text badge +SetBadge("New") +``` + +### Removing a Badge + +Remove the badge from the application icon: + +#### Go +```go +badgeService.RemoveBadge() +``` + +#### JS +```js +import {RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +RemoveBadge() +``` \ No newline at end of file diff --git a/v3/examples/badge/Taskfile.yml b/v3/examples/badge/Taskfile.yml new file mode 100644 index 000000000..d1bcfaacf --- /dev/null +++ b/v3/examples/badge/Taskfile.yml @@ -0,0 +1,34 @@ +version: '3' + +includes: + common: ./build/Taskfile.yml + windows: ./build/windows/Taskfile.yml + darwin: ./build/darwin/Taskfile.yml + linux: ./build/linux/Taskfile.yml + +vars: + APP_NAME: "badge" + BIN_DIR: "bin" + VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}' + +tasks: + build: + summary: Builds the application + cmds: + - task: "{{OS}}:build" + + package: + summary: Packages a production build of the application + cmds: + - task: "{{OS}}:package" + + run: + summary: Runs the application + cmds: + - task: "{{OS}}:run" + + dev: + summary: Runs the application in development mode + cmds: + - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}} + diff --git a/v3/examples/badge/build/Taskfile.yml b/v3/examples/badge/build/Taskfile.yml new file mode 100644 index 000000000..5f3517efc --- /dev/null +++ b/v3/examples/badge/build/Taskfile.yml @@ -0,0 +1,86 @@ +version: '3' + +tasks: + go:mod:tidy: + summary: Runs `go mod tidy` + internal: true + cmds: + - go mod tidy + + install:frontend:deps: + summary: Install frontend dependencies + dir: frontend + sources: + - package.json + - package-lock.json + generates: + - node_modules/* + preconditions: + - sh: npm version + msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/" + cmds: + - npm install + + build:frontend: + label: build:frontend (PRODUCTION={{.PRODUCTION}}) + summary: Build the frontend project + dir: frontend + sources: + - "**/*" + generates: + - dist/**/* + deps: + - task: install:frontend:deps + - task: generate:bindings + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + cmds: + - npm run {{.BUILD_COMMAND}} -q + env: + PRODUCTION: '{{.PRODUCTION | default "false"}}' + vars: + BUILD_COMMAND: '{{if eq .PRODUCTION "true"}}build{{else}}build:dev{{end}}' + + + generate:bindings: + label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}}) + summary: Generates bindings for the frontend + deps: + - task: go:mod:tidy + sources: + - "**/*.[jt]s" + - exclude: frontend/**/* + - frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output + - "**/*.go" + - go.mod + - go.sum + generates: + - frontend/bindings/**/* + cmds: + - wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts + + generate:icons: + summary: Generates Windows `.ico` and Mac `.icns` files from an image + dir: build + sources: + - "appicon.png" + generates: + - "darwin/icons.icns" + - "windows/icon.ico" + cmds: + - wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico + + dev:frontend: + summary: Runs the frontend in development mode + dir: frontend + deps: + - task: install:frontend:deps + cmds: + - npm run dev -- --port {{.VITE_PORT}} --strictPort + + update:build-assets: + summary: Updates the build assets + dir: build + cmds: + - wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir . diff --git a/v3/examples/badge/build/appicon.png b/v3/examples/badge/build/appicon.png new file mode 100644 index 000000000..63617fe4f Binary files /dev/null and b/v3/examples/badge/build/appicon.png differ diff --git a/v3/examples/badge/build/config.yml b/v3/examples/badge/build/config.yml new file mode 100644 index 000000000..aaa3240fb --- /dev/null +++ b/v3/examples/badge/build/config.yml @@ -0,0 +1,63 @@ +# This file contains the configuration for this project. +# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets. +# Note that this will overwrite any changes you have made to the assets. +version: '3' + +# This information is used to generate the build assets. +info: + companyName: "My Company" # The name of the company + productName: "My Product" # The name of the application + productIdentifier: "com.mycompany.myproduct" # The unique product identifier + description: "A program that does X" # The application description + copyright: "(c) 2025, My Company" # Copyright text + comments: "Some Product Comments" # Comments + version: "0.0.1" # The application version + +# Dev mode configuration +dev_mode: + root_path: . + log_level: warn + debounce: 1000 + ignore: + dir: + - .git + - node_modules + - frontend + - bin + file: + - .DS_Store + - .gitignore + - .gitkeep + watched_extension: + - "*.go" + git_ignore: true + executes: + - cmd: wails3 task common:install:frontend:deps + type: once + - cmd: wails3 task common:dev:frontend + type: background + - cmd: go mod tidy + type: blocking + - cmd: wails3 task build + type: blocking + - cmd: wails3 task run + type: primary + +# File Associations +# More information at: https://v3.wails.io/noit/done/yet +fileAssociations: +# - ext: wails +# name: Wails +# description: Wails Application File +# iconName: wailsFileIcon +# role: Editor +# - ext: jpg +# name: JPEG +# description: Image File +# iconName: jpegFileIcon +# role: Editor +# mimeType: image/jpeg # (optional) + +# Other data +other: + - name: My Other Data \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Info.dev.plist b/v3/examples/badge/build/darwin/Info.dev.plist new file mode 100644 index 000000000..4caddf720 --- /dev/null +++ b/v3/examples/badge/build/darwin/Info.dev.plist @@ -0,0 +1,32 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © now, My Company + NSAppTransportSecurity + + NSAllowsLocalNetworking + + + + \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Info.plist b/v3/examples/badge/build/darwin/Info.plist new file mode 100644 index 000000000..0dc90b2e7 --- /dev/null +++ b/v3/examples/badge/build/darwin/Info.plist @@ -0,0 +1,27 @@ + + + + CFBundlePackageType + APPL + CFBundleName + My Product + CFBundleExecutable + badge + CFBundleIdentifier + com.wails.badge + CFBundleVersion + 0.1.0 + CFBundleGetInfoString + This is a comment + CFBundleShortVersionString + 0.1.0 + CFBundleIconFile + icons + LSMinimumSystemVersion + 10.15.0 + NSHighResolutionCapable + true + NSHumanReadableCopyright + © now, My Company + + \ No newline at end of file diff --git a/v3/examples/badge/build/darwin/Taskfile.yml b/v3/examples/badge/build/darwin/Taskfile.yml new file mode 100644 index 000000000..f0791fea9 --- /dev/null +++ b/v3/examples/badge/build/darwin/Taskfile.yml @@ -0,0 +1,81 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Creates a production build of the application + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}' + OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}' + env: + GOOS: darwin + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + CGO_CFLAGS: "-mmacosx-version-min=10.15" + CGO_LDFLAGS: "-mmacosx-version-min=10.15" + MACOSX_DEPLOYMENT_TARGET: "10.15" + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + build:universal: + summary: Builds darwin universal binary (arm64 + amd64) + deps: + - task: build + vars: + ARCH: amd64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" + - task: build + vars: + ARCH: arm64 + OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + cmds: + - lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + - rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64" + + package: + summary: Packages a production build of the application into a `.app` bundle + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:app:bundle + + package:universal: + summary: Packages darwin universal binary (arm64 + amd64) + deps: + - task: build:universal + cmds: + - task: create:app:bundle + + + create:app:bundle: + summary: Creates an `.app` bundle + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS + - cp build/darwin/Info.plist {{.BIN_DIR}}/{{.APP_NAME}}.app/Contents + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.app + + run: + cmds: + - mkdir -p {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/{MacOS,Resources} + - cp build/darwin/icons.icns {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources + - cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS + - cp build/darwin/Info.dev.plist {{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist + - codesign --force --deep --sign - {{.BIN_DIR}}/{{.APP_NAME}}.dev.app + - '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}' diff --git a/v3/examples/badge/build/darwin/icons.icns b/v3/examples/badge/build/darwin/icons.icns new file mode 100644 index 000000000..1b5bd4c86 Binary files /dev/null and b/v3/examples/badge/build/darwin/icons.icns differ diff --git a/v3/examples/badge/build/linux/Taskfile.yml b/v3/examples/badge/build/linux/Taskfile.yml new file mode 100644 index 000000000..560cc9c92 --- /dev/null +++ b/v3/examples/badge/build/linux/Taskfile.yml @@ -0,0 +1,119 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Linux + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}} + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: linux + CGO_ENABLED: 1 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application for Linux + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: create:appimage + - task: create:deb + - task: create:rpm + - task: create:aur + + create:appimage: + summary: Creates an AppImage + dir: build/linux/appimage + deps: + - task: build + vars: + PRODUCTION: "true" + - task: generate:dotdesktop + cmds: + - cp {{.APP_BINARY}} {{.APP_NAME}} + - cp ../../appicon.png appicon.png + - wails3 generate appimage -binary {{.APP_NAME}} -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build + vars: + APP_NAME: '{{.APP_NAME}}' + APP_BINARY: '../../../bin/{{.APP_NAME}}' + ICON: '../../appicon.png' + DESKTOP_FILE: '../{{.APP_NAME}}.desktop' + OUTPUT_DIR: '../../../bin' + + create:deb: + summary: Creates a deb package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:deb + + create:rpm: + summary: Creates a rpm package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:rpm + + create:aur: + summary: Creates a arch linux packager package + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + - task: generate:dotdesktop + - task: generate:aur + + generate:deb: + summary: Creates a deb package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:rpm: + summary: Creates a rpm package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:aur: + summary: Creates a arch linux packager package + cmds: + - wails3 tool package -name {{.APP_NAME}} -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin + + generate:dotdesktop: + summary: Generates a `.desktop` file + dir: build + cmds: + - mkdir -p {{.ROOT_DIR}}/build/linux/appimage + - wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop -categories "{{.CATEGORIES}}" + vars: + APP_NAME: '{{.APP_NAME}}' + EXEC: '{{.APP_NAME}}' + ICON: 'appicon' + CATEGORIES: 'Development;' + OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}' diff --git a/v3/examples/badge/build/linux/appimage/build.sh b/v3/examples/badge/build/linux/appimage/build.sh new file mode 100644 index 000000000..85901c34e --- /dev/null +++ b/v3/examples/badge/build/linux/appimage/build.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# Copyright (c) 2018-Present Lea Anthony +# SPDX-License-Identifier: MIT + +# Fail script on any error +set -euxo pipefail + +# Define variables +APP_DIR="${APP_NAME}.AppDir" + +# Create AppDir structure +mkdir -p "${APP_DIR}/usr/bin" +cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/" +cp "${ICON_PATH}" "${APP_DIR}/" +cp "${DESKTOP_FILE}" "${APP_DIR}/" + +if [[ $(uname -m) == *x86_64* ]]; then + # Download linuxdeploy and make it executable + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage + chmod +x linuxdeploy-x86_64.AppImage + + # Run linuxdeploy to bundle the application + ./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage +else + # Download linuxdeploy and make it executable (arm64) + wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage + chmod +x linuxdeploy-aarch64.AppImage + + # Run linuxdeploy to bundle the application (arm64) + ./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage +fi + +# Rename the generated AppImage +mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage" + diff --git a/v3/examples/badge/build/linux/nfpm/nfpm.yaml b/v3/examples/badge/build/linux/nfpm/nfpm.yaml new file mode 100644 index 000000000..5b7ea9d02 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/nfpm.yaml @@ -0,0 +1,50 @@ +# Feel free to remove those if you don't want/need to use them. +# Make sure to check the documentation at https://nfpm.goreleaser.com +# +# The lines below are called `modelines`. See `:help modeline` + +name: "badge" +arch: ${GOARCH} +platform: "linux" +version: "0.1.0" +section: "default" +priority: "extra" +maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> +description: "My Product Description" +vendor: "My Company" +homepage: "https://wails.io" +license: "MIT" +release: "1" + +contents: + - src: "./bin/badge" + dst: "/usr/local/bin/badge" + - src: "./build/appicon.png" + dst: "/usr/share/icons/hicolor/128x128/apps/badge.png" + - src: "./build/linux/badge.desktop" + dst: "/usr/share/applications/badge.desktop" + +depends: + - gtk3 + - libwebkit2gtk + +# replaces: +# - foobar +# provides: +# - bar +# depends: +# - gtk3 +# - libwebkit2gtk +# recommends: +# - whatever +# suggests: +# - something-else +# conflicts: +# - not-foo +# - not-bar +# changelog: "changelog.yaml" +# scripts: +# preinstall: ./build/linux/nfpm/scripts/preinstall.sh +# postinstall: ./build/linux/nfpm/scripts/postinstall.sh +# preremove: ./build/linux/nfpm/scripts/preremove.sh +# postremove: ./build/linux/nfpm/scripts/postremove.sh diff --git a/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh b/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/postinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh b/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/postremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh b/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/preinstall.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh b/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh new file mode 100644 index 000000000..a9bf588e2 --- /dev/null +++ b/v3/examples/badge/build/linux/nfpm/scripts/preremove.sh @@ -0,0 +1 @@ +#!/bin/bash diff --git a/v3/examples/badge/build/windows/Taskfile.yml b/v3/examples/badge/build/windows/Taskfile.yml new file mode 100644 index 000000000..534f4fb31 --- /dev/null +++ b/v3/examples/badge/build/windows/Taskfile.yml @@ -0,0 +1,63 @@ +version: '3' + +includes: + common: ../Taskfile.yml + +tasks: + build: + summary: Builds the application for Windows + deps: + - task: common:go:mod:tidy + - task: common:build:frontend + vars: + BUILD_FLAGS: + ref: .BUILD_FLAGS + PRODUCTION: + ref: .PRODUCTION + - task: common:generate:icons + cmds: + - task: generate:syso + - go build {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}.exe + - cmd: powershell Remove-item *.syso + platforms: [windows] + - cmd: rm -f *.syso + platforms: [linux, darwin] + vars: + BUILD_FLAGS: '{{if eq .PRODUCTION "true"}}-tags production -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{else}}-buildvcs=false -gcflags=all="-l"{{end}}' + env: + GOOS: windows + CGO_ENABLED: 0 + GOARCH: '{{.ARCH | default ARCH}}' + PRODUCTION: '{{.PRODUCTION | default "false"}}' + + package: + summary: Packages a production build of the application into a `.exe` bundle + cmds: + - task: create:nsis:installer + + generate:syso: + summary: Generates Windows `.syso` file + dir: build + cmds: + - wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso + vars: + ARCH: '{{.ARCH | default ARCH}}' + + create:nsis:installer: + summary: Creates an NSIS installer + dir: build/windows/nsis + deps: + - task: build + vars: + PRODUCTION: "true" + cmds: + # Create the Microsoft WebView2 bootstrapper if it doesn't exist + - wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis" + - makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi + vars: + ARCH: '{{.ARCH | default ARCH}}' + ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}' + + run: + cmds: + - '{{.BIN_DIR}}/{{.APP_NAME}}.exe' diff --git a/v3/examples/badge/build/windows/icon.ico b/v3/examples/badge/build/windows/icon.ico new file mode 100644 index 000000000..bfa0690b7 Binary files /dev/null and b/v3/examples/badge/build/windows/icon.ico differ diff --git a/v3/examples/badge/build/windows/info.json b/v3/examples/badge/build/windows/info.json new file mode 100644 index 000000000..850b2b5b0 --- /dev/null +++ b/v3/examples/badge/build/windows/info.json @@ -0,0 +1,15 @@ +{ + "fixed": { + "file_version": "0.1.0" + }, + "info": { + "0000": { + "ProductVersion": "0.1.0", + "CompanyName": "My Company", + "FileDescription": "My Product Description", + "LegalCopyright": "© now, My Company", + "ProductName": "My Product", + "Comments": "This is a comment" + } + } +} \ No newline at end of file diff --git a/v3/examples/badge/build/windows/nsis/project.nsi b/v3/examples/badge/build/windows/nsis/project.nsi new file mode 100644 index 000000000..985b8e207 --- /dev/null +++ b/v3/examples/badge/build/windows/nsis/project.nsi @@ -0,0 +1,112 @@ +Unicode true + +#### +## Please note: Template replacements don't work in this file. They are provided with default defines like +## mentioned underneath. +## If the keyword is not defined, "wails_tools.nsh" will populate them. +## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually +## from outside of Wails for debugging and development of the installer. +## +## For development first make a wails nsis build to populate the "wails_tools.nsh": +## > wails build --target windows/amd64 --nsis +## Then you can call makensis on this file with specifying the path to your binary: +## For a AMD64 only installer: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe +## For a ARM64 only installer: +## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe +## For a installer with both architectures: +## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe +#### +## The following information is taken from the wails_tools.nsh file, but they can be overwritten here. +#### +## !define INFO_PROJECTNAME "my-project" # Default "badge" +## !define INFO_COMPANYNAME "My Company" # Default "My Company" +## !define INFO_PRODUCTNAME "My Product Name" # Default "My Product" +## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.1.0" +## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© now, My Company" +### +## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe" +## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +#### +## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html +#### +## Include the wails tools +#### +!include "wails_tools.nsh" + +# The version information for this two must consist of 4 parts +VIProductVersion "${INFO_PRODUCTVERSION}.0" +VIFileVersion "${INFO_PRODUCTVERSION}.0" + +VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}" +VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer" +VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}" +VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}" +VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}" + +# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware +ManifestDPIAware true + +!include "MUI.nsh" + +!define MUI_ICON "..\icon.ico" +!define MUI_UNICON "..\icon.ico" +# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314 +!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps +!define MUI_ABORTWARNING # This will warn the user if they exit from the installer. + +!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page. +# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer +!insertmacro MUI_PAGE_DIRECTORY # In which folder install page. +!insertmacro MUI_PAGE_INSTFILES # Installing page. +!insertmacro MUI_PAGE_FINISH # Finished installation page. + +!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page + +!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer + +## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1 +#!uninstfinalize 'signtool --file "%1"' +#!finalize 'signtool --file "%1"' + +Name "${INFO_PRODUCTNAME}" +OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file. +InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder). +ShowInstDetails show # This will always show the installation details. + +Function .onInit + !insertmacro wails.checkArchitecture +FunctionEnd + +Section + !insertmacro wails.setShellContext + + !insertmacro wails.webview2runtime + + SetOutPath $INSTDIR + + !insertmacro wails.files + + CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}" + + !insertmacro wails.associateFiles + + !insertmacro wails.writeUninstaller +SectionEnd + +Section "uninstall" + !insertmacro wails.setShellContext + + RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath + + RMDir /r $INSTDIR + + Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" + Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk" + + !insertmacro wails.unassociateFiles + + !insertmacro wails.deleteUninstaller +SectionEnd diff --git a/v3/examples/badge/build/windows/nsis/wails_tools.nsh b/v3/examples/badge/build/windows/nsis/wails_tools.nsh new file mode 100644 index 000000000..6fc10ab79 --- /dev/null +++ b/v3/examples/badge/build/windows/nsis/wails_tools.nsh @@ -0,0 +1,212 @@ +# DO NOT EDIT - Generated automatically by `wails build` + +!include "x64.nsh" +!include "WinVer.nsh" +!include "FileFunc.nsh" + +!ifndef INFO_PROJECTNAME + !define INFO_PROJECTNAME "badge" +!endif +!ifndef INFO_COMPANYNAME + !define INFO_COMPANYNAME "My Company" +!endif +!ifndef INFO_PRODUCTNAME + !define INFO_PRODUCTNAME "My Product" +!endif +!ifndef INFO_PRODUCTVERSION + !define INFO_PRODUCTVERSION "0.1.0" +!endif +!ifndef INFO_COPYRIGHT + !define INFO_COPYRIGHT "© now, My Company" +!endif +!ifndef PRODUCT_EXECUTABLE + !define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe" +!endif +!ifndef UNINST_KEY_NAME + !define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}" +!endif +!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}" + +!ifndef REQUEST_EXECUTION_LEVEL + !define REQUEST_EXECUTION_LEVEL "admin" +!endif + +RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}" + +!ifdef ARG_WAILS_AMD64_BINARY + !define SUPPORTS_AMD64 +!endif + +!ifdef ARG_WAILS_ARM64_BINARY + !define SUPPORTS_ARM64 +!endif + +!ifdef SUPPORTS_AMD64 + !ifdef SUPPORTS_ARM64 + !define ARCH "amd64_arm64" + !else + !define ARCH "amd64" + !endif +!else + !ifdef SUPPORTS_ARM64 + !define ARCH "arm64" + !else + !error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY" + !endif +!endif + +!macro wails.checkArchitecture + !ifndef WAILS_WIN10_REQUIRED + !define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later." + !endif + + !ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED + !define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}" + !endif + + ${If} ${AtLeastWin10} + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + Goto ok + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + Goto ok + ${EndIf} + !endif + + IfSilent silentArch notSilentArch + silentArch: + SetErrorLevel 65 + Abort + notSilentArch: + MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}" + Quit + ${else} + IfSilent silentWin notSilentWin + silentWin: + SetErrorLevel 64 + Abort + notSilentWin: + MessageBox MB_OK "${WAILS_WIN10_REQUIRED}" + Quit + ${EndIf} + + ok: +!macroend + +!macro wails.files + !ifdef SUPPORTS_AMD64 + ${if} ${IsNativeAMD64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}" + ${EndIf} + !endif + + !ifdef SUPPORTS_ARM64 + ${if} ${IsNativeARM64} + File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}" + ${EndIf} + !endif +!macroend + +!macro wails.writeUninstaller + WriteUninstaller "$INSTDIR\uninstall.exe" + + SetRegView 64 + WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}" + WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}" + WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\"" + WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S" + + ${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2 + IntFmt $0 "0x%08X" $0 + WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0" +!macroend + +!macro wails.deleteUninstaller + Delete "$INSTDIR\uninstall.exe" + + SetRegView 64 + DeleteRegKey HKLM "${UNINST_KEY}" +!macroend + +!macro wails.setShellContext + ${If} ${REQUEST_EXECUTION_LEVEL} == "admin" + SetShellVarContext all + ${else} + SetShellVarContext current + ${EndIf} +!macroend + +# Install webview2 by launching the bootstrapper +# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment +!macro wails.webview2runtime + !ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT + !define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime" + !endif + + SetRegView 64 + # If the admin key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + + ${If} ${REQUEST_EXECUTION_LEVEL} == "user" + # If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed + ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv" + ${If} $0 != "" + Goto ok + ${EndIf} + ${EndIf} + + SetDetailsPrint both + DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}" + SetDetailsPrint listonly + + InitPluginsDir + CreateDirectory "$pluginsdir\webview2bootstrapper" + SetOutPath "$pluginsdir\webview2bootstrapper" + File "MicrosoftEdgeWebview2Setup.exe" + ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install' + + SetDetailsPrint both + ok: +!macroend + +# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b +!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" "" + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0" + + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}" + + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open" + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}` + WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}` +!macroend + +!macro APP_UNASSOCIATE EXT FILECLASS + ; Backup the previously associated file class + ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup` + WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0" + + DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}` +!macroend + +!macro wails.associateFiles + ; Create file associations + +!macroend + +!macro wails.unassociateFiles + ; Delete app associations + +!macroend \ No newline at end of file diff --git a/v3/examples/badge/build/windows/wails.exe.manifest b/v3/examples/badge/build/windows/wails.exe.manifest new file mode 100644 index 000000000..1d8992a3d --- /dev/null +++ b/v3/examples/badge/build/windows/wails.exe.manifest @@ -0,0 +1,15 @@ + + + + + + + + + + + true/pm + permonitorv2,permonitor + + + \ No newline at end of file diff --git a/v3/examples/badge/frontend/Inter Font License.txt b/v3/examples/badge/frontend/Inter Font License.txt new file mode 100644 index 000000000..b525cbf3a --- /dev/null +++ b/v3/examples/badge/frontend/Inter Font License.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts new file mode 100644 index 000000000..fd900b4cd --- /dev/null +++ b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/index.ts @@ -0,0 +1,7 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +import * as Service from "./service.js"; +export { + Service +}; diff --git a/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts new file mode 100644 index 000000000..abd6cd0a3 --- /dev/null +++ b/v3/examples/badge/frontend/bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service.ts @@ -0,0 +1,21 @@ +// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL +// This file is automatically generated. DO NOT EDIT + +/** + * Service represents the notifications service + * @module + */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import {Call as $Call, Create as $Create} from "@wailsio/runtime"; + +export function RemoveBadge(): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(2633565570) as any; + return $resultPromise; +} + +export function SetBadge(label: string): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3052354152, label) as any; + return $resultPromise; +} diff --git a/v3/examples/badge/frontend/dist/Inter-Medium.ttf b/v3/examples/badge/frontend/dist/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge/frontend/dist/Inter-Medium.ttf differ diff --git a/v3/examples/badge/frontend/dist/assets/index-sXwpgKSV.js b/v3/examples/badge/frontend/dist/assets/index-sXwpgKSV.js new file mode 100644 index 000000000..72bf73d69 --- /dev/null +++ b/v3/examples/badge/frontend/dist/assets/index-sXwpgKSV.js @@ -0,0 +1,1359 @@ +(function polyfill() { + const relList = document.createElement("link").relList; + if (relList && relList.supports && relList.supports("modulepreload")) { + return; + } + for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { + processPreload(link); + } + new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== "childList") { + continue; + } + for (const node of mutation.addedNodes) { + if (node.tagName === "LINK" && node.rel === "modulepreload") + processPreload(node); + } + } + }).observe(document, { childList: true, subtree: true }); + function getFetchOpts(link) { + const fetchOpts = {}; + if (link.integrity) fetchOpts.integrity = link.integrity; + if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; + if (link.crossOrigin === "use-credentials") + fetchOpts.credentials = "include"; + else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; + else fetchOpts.credentials = "same-origin"; + return fetchOpts; + } + function processPreload(link) { + if (link.ep) + return; + link.ep = true; + const fetchOpts = getFetchOpts(link); + fetch(link.href, fetchOpts); + } +})(); +const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict"; +function nanoid(size = 21) { + let id = ""; + let i = size | 0; + while (i--) { + id += urlAlphabet[Math.random() * 64 | 0]; + } + return id; +} +const runtimeURL = window.location.origin + "/wails/runtime"; +const objectNames = Object.freeze({ + Call: 0, + Clipboard: 1, + Application: 2, + Events: 3, + ContextMenu: 4, + Dialog: 5, + Window: 6, + Screens: 7, + System: 8, + Browser: 9, + CancelCall: 10 +}); +let clientId = nanoid(); +function newRuntimeCaller(object, windowName = "") { + return function(method, args = null) { + return runtimeCallWithID(object, method, windowName, args); + }; +} +async function runtimeCallWithID(objectID, method, windowName, args) { + var _a2, _b; + let url = new URL(runtimeURL); + url.searchParams.append("object", objectID.toString()); + url.searchParams.append("method", method.toString()); + if (args) { + url.searchParams.append("args", JSON.stringify(args)); + } + let headers = { + ["x-wails-client-id"]: clientId + }; + if (windowName) { + headers["x-wails-window-name"] = windowName; + } + let response = await fetch(url, { headers }); + if (!response.ok) { + throw new Error(await response.text()); + } + if (((_b = (_a2 = response.headers.get("Content-Type")) === null || _a2 === void 0 ? void 0 : _a2.indexOf("application/json")) !== null && _b !== void 0 ? _b : -1) !== -1) { + return response.json(); + } else { + return response.text(); + } +} +newRuntimeCaller(objectNames.System); +const _invoke = function() { + var _a2, _b, _c, _d, _e; + try { + if ((_b = (_a2 = window.chrome) === null || _a2 === void 0 ? void 0 : _a2.webview) === null || _b === void 0 ? void 0 : _b.postMessage) { + return window.chrome.webview.postMessage.bind(window.chrome.webview); + } else if ((_e = (_d = (_c = window.webkit) === null || _c === void 0 ? void 0 : _c.messageHandlers) === null || _d === void 0 ? void 0 : _d["external"]) === null || _e === void 0 ? void 0 : _e.postMessage) { + return window.webkit.messageHandlers["external"].postMessage.bind(window.webkit.messageHandlers["external"]); + } + } catch (e) { + } + console.warn("\n%c⚠️ Browser Environment Detected %c\n\n%cOnly UI previews are available in the browser. For full functionality, please run the application in desktop mode.\nMore information at: https://v3.wails.io/learn/build/#using-a-browser-for-development\n", "background: #ffffff; color: #000000; font-weight: bold; padding: 4px 8px; border-radius: 4px; border: 2px solid #000000;", "background: transparent;", "color: #ffffff; font-style: italic; font-weight: bold;"); + return null; +}(); +function invoke(msg) { + _invoke === null || _invoke === void 0 ? void 0 : _invoke(msg); +} +function IsWindows() { + return window._wails.environment.OS === "windows"; +} +function IsDebug() { + return Boolean(window._wails.environment.Debug); +} +function canTrackButtons() { + return new MouseEvent("mousedown").buttons === 0; +} +function eventTarget(event) { + var _a2; + if (event.target instanceof HTMLElement) { + return event.target; + } else if (!(event.target instanceof HTMLElement) && event.target instanceof Node) { + return (_a2 = event.target.parentElement) !== null && _a2 !== void 0 ? _a2 : document.body; + } else { + return document.body; + } +} +document.addEventListener("DOMContentLoaded", () => { +}); +window.addEventListener("contextmenu", contextMenuHandler); +const call$2 = newRuntimeCaller(objectNames.ContextMenu); +const ContextMenuOpen = 0; +function openContextMenu(id, x, y, data) { + void call$2(ContextMenuOpen, { id, x, y, data }); +} +function contextMenuHandler(event) { + const target = eventTarget(event); + const customContextMenu = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu").trim(); + if (customContextMenu) { + event.preventDefault(); + const data = window.getComputedStyle(target).getPropertyValue("--custom-contextmenu-data"); + openContextMenu(customContextMenu, event.clientX, event.clientY, data); + } else { + processDefaultContextMenu(event, target); + } +} +function processDefaultContextMenu(event, target) { + if (IsDebug()) { + return; + } + switch (window.getComputedStyle(target).getPropertyValue("--default-contextmenu").trim()) { + case "show": + return; + case "hide": + event.preventDefault(); + return; + } + if (target.isContentEditable) { + return; + } + const selection = window.getSelection(); + const hasSelection = selection && selection.toString().length > 0; + if (hasSelection) { + for (let i = 0; i < selection.rangeCount; i++) { + const range = selection.getRangeAt(i); + const rects = range.getClientRects(); + for (let j = 0; j < rects.length; j++) { + const rect = rects[j]; + if (document.elementFromPoint(rect.left, rect.top) === target) { + return; + } + } + } + } + if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) { + if (hasSelection || !target.readOnly && !target.disabled) { + return; + } + } + event.preventDefault(); +} +function GetFlag(key) { + try { + return window._wails.flags[key]; + } catch (e) { + throw new Error("Unable to retrieve flag '" + key + "': " + e, { cause: e }); + } +} +let canDrag = false; +let dragging = false; +let resizable = false; +let canResize = false; +let resizing = false; +let resizeEdge = ""; +let defaultCursor = "auto"; +let buttons = 0; +const buttonsTracked = canTrackButtons(); +window._wails = window._wails || {}; +window._wails.setResizable = (value) => { + resizable = value; + if (!resizable) { + canResize = resizing = false; + setResize(); + } +}; +window.addEventListener("mousedown", update, { capture: true }); +window.addEventListener("mousemove", update, { capture: true }); +window.addEventListener("mouseup", update, { capture: true }); +for (const ev of ["click", "contextmenu", "dblclick"]) { + window.addEventListener(ev, suppressEvent, { capture: true }); +} +function suppressEvent(event) { + if (dragging || resizing) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } +} +const MouseDown = 0; +const MouseUp = 1; +const MouseMove = 2; +function update(event) { + let eventType, eventButtons = event.buttons; + switch (event.type) { + case "mousedown": + eventType = MouseDown; + if (!buttonsTracked) { + eventButtons = buttons | 1 << event.button; + } + break; + case "mouseup": + eventType = MouseUp; + if (!buttonsTracked) { + eventButtons = buttons & ~(1 << event.button); + } + break; + default: + eventType = MouseMove; + if (!buttonsTracked) { + eventButtons = buttons; + } + break; + } + let released = buttons & ~eventButtons; + let pressed = eventButtons & ~buttons; + buttons = eventButtons; + if (eventType === MouseDown && !(pressed & event.button)) { + released |= 1 << event.button; + pressed |= 1 << event.button; + } + if (eventType !== MouseMove && resizing || dragging && (eventType === MouseDown || event.button !== 0)) { + event.stopImmediatePropagation(); + event.stopPropagation(); + event.preventDefault(); + } + if (released & 1) { + primaryUp(); + } + if (pressed & 1) { + primaryDown(event); + } + if (eventType === MouseMove) { + onMouseMove(event); + } +} +function primaryDown(event) { + canDrag = false; + canResize = false; + if (!IsWindows()) { + if (event.type === "mousedown" && event.button === 0 && event.detail !== 1) { + return; + } + } + if (resizeEdge) { + canResize = true; + return; + } + const target = eventTarget(event); + const style = window.getComputedStyle(target); + canDrag = style.getPropertyValue("--wails-draggable").trim() === "drag" && (event.offsetX - parseFloat(style.paddingLeft) < target.clientWidth && event.offsetY - parseFloat(style.paddingTop) < target.clientHeight); +} +function primaryUp(event) { + canDrag = false; + dragging = false; + canResize = false; + resizing = false; +} +const cursorForEdge = Object.freeze({ + "se-resize": "nwse-resize", + "sw-resize": "nesw-resize", + "nw-resize": "nwse-resize", + "ne-resize": "nesw-resize", + "w-resize": "ew-resize", + "n-resize": "ns-resize", + "s-resize": "ns-resize", + "e-resize": "ew-resize" +}); +function setResize(edge) { + if (edge) { + if (!resizeEdge) { + defaultCursor = document.body.style.cursor; + } + document.body.style.cursor = cursorForEdge[edge]; + } else if (!edge && resizeEdge) { + document.body.style.cursor = defaultCursor; + } + resizeEdge = edge || ""; +} +function onMouseMove(event) { + if (canResize && resizeEdge) { + resizing = true; + invoke("wails:resize:" + resizeEdge); + } else if (canDrag) { + dragging = true; + invoke("wails:drag"); + } + if (dragging || resizing) { + canDrag = canResize = false; + return; + } + if (!resizable || !IsWindows()) { + if (resizeEdge) { + setResize(); + } + return; + } + const resizeHandleHeight = GetFlag("system.resizeHandleHeight") || 5; + const resizeHandleWidth = GetFlag("system.resizeHandleWidth") || 5; + const cornerExtra = GetFlag("resizeCornerExtra") || 10; + const rightBorder = window.outerWidth - event.clientX < resizeHandleWidth; + const leftBorder = event.clientX < resizeHandleWidth; + const topBorder = event.clientY < resizeHandleHeight; + const bottomBorder = window.outerHeight - event.clientY < resizeHandleHeight; + const rightCorner = window.outerWidth - event.clientX < resizeHandleWidth + cornerExtra; + const leftCorner = event.clientX < resizeHandleWidth + cornerExtra; + const topCorner = event.clientY < resizeHandleHeight + cornerExtra; + const bottomCorner = window.outerHeight - event.clientY < resizeHandleHeight + cornerExtra; + if (!leftCorner && !topCorner && !bottomCorner && !rightCorner) { + setResize(); + } else if (rightCorner && bottomCorner) + setResize("se-resize"); + else if (leftCorner && bottomCorner) + setResize("sw-resize"); + else if (leftCorner && topCorner) + setResize("nw-resize"); + else if (topCorner && rightCorner) + setResize("ne-resize"); + else if (leftBorder) + setResize("w-resize"); + else if (topBorder) + setResize("n-resize"); + else if (bottomBorder) + setResize("s-resize"); + else if (rightBorder) + setResize("e-resize"); + else + setResize(); +} +var fnToStr = Function.prototype.toString; +var reflectApply = typeof Reflect === "object" && Reflect !== null && Reflect.apply; +var badArrayLike; +var isCallableMarker; +if (typeof reflectApply === "function" && typeof Object.defineProperty === "function") { + try { + badArrayLike = Object.defineProperty({}, "length", { + get: function() { + throw isCallableMarker; + } + }); + isCallableMarker = {}; + reflectApply(function() { + throw 42; + }, null, badArrayLike); + } catch (_) { + if (_ !== isCallableMarker) { + reflectApply = null; + } + } +} else { + reflectApply = null; +} +var constructorRegex = /^\s*class\b/; +var isES6ClassFn = function isES6ClassFunction(value) { + try { + var fnStr = fnToStr.call(value); + return constructorRegex.test(fnStr); + } catch (e) { + return false; + } +}; +var tryFunctionObject = function tryFunctionToStr(value) { + try { + if (isES6ClassFn(value)) { + return false; + } + fnToStr.call(value); + return true; + } catch (e) { + return false; + } +}; +var toStr = Object.prototype.toString; +var objectClass = "[object Object]"; +var fnClass = "[object Function]"; +var genClass = "[object GeneratorFunction]"; +var ddaClass = "[object HTMLAllCollection]"; +var ddaClass2 = "[object HTML document.all class]"; +var ddaClass3 = "[object HTMLCollection]"; +var hasToStringTag = typeof Symbol === "function" && !!Symbol.toStringTag; +var isIE68 = !(0 in [,]); +var isDDA = function isDocumentDotAll() { + return false; +}; +if (typeof document === "object") { + var all = document.all; + if (toStr.call(all) === toStr.call(document.all)) { + isDDA = function isDocumentDotAll2(value) { + if ((isIE68 || !value) && (typeof value === "undefined" || typeof value === "object")) { + try { + var str = toStr.call(value); + return (str === ddaClass || str === ddaClass2 || str === ddaClass3 || str === objectClass) && value("") == null; + } catch (e) { + } + } + return false; + }; + } +} +function isCallableRefApply(value) { + if (isDDA(value)) { + return true; + } + if (!value) { + return false; + } + if (typeof value !== "function" && typeof value !== "object") { + return false; + } + try { + reflectApply(value, null, badArrayLike); + } catch (e) { + if (e !== isCallableMarker) { + return false; + } + } + return !isES6ClassFn(value) && tryFunctionObject(value); +} +function isCallableNoRefApply(value) { + if (isDDA(value)) { + return true; + } + if (!value) { + return false; + } + if (typeof value !== "function" && typeof value !== "object") { + return false; + } + if (hasToStringTag) { + return tryFunctionObject(value); + } + if (isES6ClassFn(value)) { + return false; + } + var strClass = toStr.call(value); + if (strClass !== fnClass && strClass !== genClass && !/^\[object HTML/.test(strClass)) { + return false; + } + return tryFunctionObject(value); +} +const isCallable = reflectApply ? isCallableRefApply : isCallableNoRefApply; +var _a; +class CancelError extends Error { + /** + * Constructs a new `CancelError` instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message, options) { + super(message, options); + this.name = "CancelError"; + } +} +class CancelledRejectionError extends Error { + /** + * Constructs a new `CancelledRejectionError` instance. + * @param promise - The promise that caused the error originally. + * @param reason - The rejection reason. + * @param info - An optional informative message specifying the circumstances in which the error was thrown. + * Defaults to the string `"Unhandled rejection in cancelled promise."`. + */ + constructor(promise, reason, info) { + super((info !== null && info !== void 0 ? info : "Unhandled rejection in cancelled promise.") + " Reason: " + errorMessage(reason), { cause: reason }); + this.promise = promise; + this.name = "CancelledRejectionError"; + } +} +const barrierSym = Symbol("barrier"); +const cancelImplSym = Symbol("cancelImpl"); +const species = (_a = Symbol.species) !== null && _a !== void 0 ? _a : Symbol("speciesPolyfill"); +class CancellablePromise extends Promise { + /** + * Creates a new `CancellablePromise`. + * + * @param executor - A callback used to initialize the promise. This callback is passed two arguments: + * a `resolve` callback used to resolve the promise with a value + * or the result of another promise (possibly cancellable), + * and a `reject` callback used to reject the promise with a provided reason or error. + * If the value provided to the `resolve` callback is a thenable _and_ cancellable object + * (it has a `then` _and_ a `cancel` method), + * cancellation requests will be forwarded to that object and the oncancelled will not be invoked anymore. + * If any one of the two callbacks is called _after_ the promise has been cancelled, + * the provided values will be cancelled and resolved as usual, + * but their results will be discarded. + * However, if the resolution process ultimately ends up in a rejection + * that is not due to cancellation, the rejection reason + * will be wrapped in a {@link CancelledRejectionError} + * and bubbled up as an unhandled rejection. + * @param oncancelled - It is the caller's responsibility to ensure that any operation + * started by the executor is properly halted upon cancellation. + * This optional callback can be used to that purpose. + * It will be called _synchronously_ with a cancellation cause + * when cancellation is requested, _after_ the promise has already rejected + * with a {@link CancelError}, but _before_ + * any {@link then}/{@link catch}/{@link finally} callback runs. + * If the callback returns a thenable, the promise returned from {@link cancel} + * will only fulfill after the former has settled. + * Unhandled exceptions or rejections from the callback will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as unhandled rejections. + * If the `resolve` callback is called before cancellation with a cancellable promise, + * cancellation requests on this promise will be diverted to that promise, + * and the original `oncancelled` callback will be discarded. + */ + constructor(executor, oncancelled) { + let resolve; + let reject; + super((res, rej) => { + resolve = res; + reject = rej; + }); + if (this.constructor[species] !== Promise) { + throw new TypeError("CancellablePromise does not support transparent subclassing. Please refrain from overriding the [Symbol.species] static property."); + } + let promise = { + promise: this, + resolve, + reject, + get oncancelled() { + return oncancelled !== null && oncancelled !== void 0 ? oncancelled : null; + }, + set oncancelled(cb) { + oncancelled = cb !== null && cb !== void 0 ? cb : void 0; + } + }; + const state = { + get root() { + return state; + }, + resolving: false, + settled: false + }; + void Object.defineProperties(this, { + [barrierSym]: { + configurable: false, + enumerable: false, + writable: true, + value: null + }, + [cancelImplSym]: { + configurable: false, + enumerable: false, + writable: false, + value: cancellerFor(promise, state) + } + }); + const rejector = rejectorFor(promise, state); + try { + executor(resolverFor(promise, state), rejector); + } catch (err) { + if (state.resolving) { + console.log("Unhandled exception in CancellablePromise executor.", err); + } else { + rejector(err); + } + } + } + /** + * Cancels immediately the execution of the operation associated with this promise. + * The promise rejects with a {@link CancelError} instance as reason, + * with the {@link CancelError#cause} property set to the given argument, if any. + * + * Has no effect if called after the promise has already settled; + * repeated calls in particular are safe, but only the first one + * will set the cancellation cause. + * + * The `CancelError` exception _need not_ be handled explicitly _on the promises that are being cancelled:_ + * cancelling a promise with no attached rejection handler does not trigger an unhandled rejection event. + * Therefore, the following idioms are all equally correct: + * ```ts + * new CancellablePromise((resolve, reject) => { ... }).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).cancel(); + * new CancellablePromise((resolve, reject) => { ... }).then(...).catch(...).cancel(); + * ``` + * Whenever some cancelled promise in a chain rejects with a `CancelError` + * with the same cancellation cause as itself, the error will be discarded silently. + * However, the `CancelError` _will still be delivered_ to all attached rejection handlers + * added by {@link then} and related methods: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * cancellable.then(() => { ... }).catch(console.log); + * cancellable.cancel(); // A CancelError is printed to the console. + * ``` + * If the `CancelError` is not handled downstream by the time it reaches + * a _non-cancelled_ promise, it _will_ trigger an unhandled rejection event, + * just like normal rejections would: + * ```ts + * let cancellable = new CancellablePromise((resolve, reject) => { ... }); + * let chained = cancellable.then(() => { ... }).then(() => { ... }); // No catch... + * cancellable.cancel(); // Unhandled rejection event on chained! + * ``` + * Therefore, it is important to either cancel whole promise chains from their tail, + * as shown in the correct idioms above, or take care of handling errors everywhere. + * + * @returns A cancellable promise that _fulfills_ after the cancel callback (if any) + * and all handlers attached up to the call to cancel have run. + * If the cancel callback returns a thenable, the promise returned by `cancel` + * will also wait for that thenable to settle. + * This enables callers to wait for the cancelled operation to terminate + * without being forced to handle potential errors at the call site. + * ```ts + * cancellable.cancel().then(() => { + * // Cleanup finished, it's safe to do something else. + * }, (err) => { + * // Unreachable: the promise returned from cancel will never reject. + * }); + * ``` + * Note that the returned promise will _not_ handle implicitly any rejection + * that might have occurred already in the cancelled chain. + * It will just track whether registered handlers have been executed or not. + * Therefore, unhandled rejections will never be silently handled by calling cancel. + */ + cancel(cause) { + return new CancellablePromise((resolve) => { + Promise.all([ + this[cancelImplSym](new CancelError("Promise cancelled.", { cause })), + currentBarrier(this) + ]).then(() => resolve(), () => resolve()); + }); + } + /** + * Binds promise cancellation to the abort event of the given {@link AbortSignal}. + * If the signal has already aborted, the promise will be cancelled immediately. + * When either condition is verified, the cancellation cause will be set + * to the signal's abort reason (see {@link AbortSignal#reason}). + * + * Has no effect if called (or if the signal aborts) _after_ the promise has already settled. + * Only the first signal to abort will set the cancellation cause. + * + * For more details about the cancellation process, + * see {@link cancel} and the `CancellablePromise` constructor. + * + * This method enables `await`ing cancellable promises without having + * to store them for future cancellation, e.g.: + * ```ts + * await longRunningOperation().cancelOn(signal); + * ``` + * instead of: + * ```ts + * let promiseToBeCancelled = longRunningOperation(); + * await promiseToBeCancelled; + * ``` + * + * @returns This promise, for method chaining. + */ + cancelOn(signal) { + if (signal.aborted) { + void this.cancel(signal.reason); + } else { + signal.addEventListener("abort", () => void this.cancel(signal.reason), { capture: true }); + } + return this; + } + /** + * Attaches callbacks for the resolution and/or rejection of the `CancellablePromise`. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A `CancellablePromise` for the completion of whichever callback is executed. + * The returned promise is hooked up to propagate cancellation requests up the chain, but not down: + * + * - if the parent promise is cancelled, the `onrejected` handler will be invoked with a `CancelError` + * and the returned promise _will resolve regularly_ with its result; + * - conversely, if the returned promise is cancelled, _the parent promise is cancelled too;_ + * the `onrejected` handler will still be invoked with the parent's `CancelError`, + * but its result will be discarded + * and the returned promise will reject with a `CancelError` as well. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If either callback returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + */ + then(onfulfilled, onrejected, oncancelled) { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.then called on an invalid object."); + } + if (!isCallable(onfulfilled)) { + onfulfilled = identity; + } + if (!isCallable(onrejected)) { + onrejected = thrower; + } + if (onfulfilled === identity && onrejected == thrower) { + return new CancellablePromise((resolve) => resolve(this)); + } + const barrier = {}; + this[barrierSym] = barrier; + return new CancellablePromise((resolve, reject) => { + void super.then((value) => { + var _a2; + if (this[barrierSym] === barrier) { + this[barrierSym] = null; + } + (_a2 = barrier.resolve) === null || _a2 === void 0 ? void 0 : _a2.call(barrier); + try { + resolve(onfulfilled(value)); + } catch (err) { + reject(err); + } + }, (reason) => { + var _a2; + if (this[barrierSym] === barrier) { + this[barrierSym] = null; + } + (_a2 = barrier.resolve) === null || _a2 === void 0 ? void 0 : _a2.call(barrier); + try { + resolve(onrejected(reason)); + } catch (err) { + reject(err); + } + }); + }, async (cause) => { + try { + return oncancelled === null || oncancelled === void 0 ? void 0 : oncancelled(cause); + } finally { + await this.cancel(cause); + } + }); + } + /** + * Attaches a callback for only the rejection of the Promise. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * When the parent promise rejects or is cancelled, the `onrejected` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * It is equivalent to + * ```ts + * cancellablePromise.then(undefined, onrejected, oncancelled); + * ``` + * and the same caveats apply. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onrejected` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + catch(onrejected, oncancelled) { + return this.then(void 0, onrejected, oncancelled); + } + /** + * Attaches a callback that is invoked when the CancellablePromise is settled (fulfilled or rejected). The + * resolved value cannot be accessed or modified from the callback. + * The returned promise will settle in the same state as the original one + * after the provided callback has completed execution, + * unless the callback throws or returns a rejecting promise, + * in which case the returned promise will reject as well. + * + * The optional `oncancelled` argument will be invoked when the returned promise is cancelled, + * with the same semantics as the `oncancelled` argument of the constructor. + * Once the parent promise settles, the `onfinally` callback will run, + * _even after the returned promise has been cancelled:_ + * in that case, should it reject or throw, the reason will be wrapped + * in a {@link CancelledRejectionError} and bubbled up as an unhandled rejection. + * + * This method is implemented in terms of {@link then} and the same caveats apply. + * It is polyfilled, hence available in every OS/webview version. + * + * @returns A Promise for the completion of the callback. + * Cancellation requests on the returned promise + * will propagate up the chain to the parent promise, + * but not in the other direction. + * + * The promise returned from {@link cancel} will fulfill only after all attached handlers + * up the entire promise chain have been run. + * + * If `onfinally` returns a cancellable promise, + * cancellation requests will be diverted to it, + * and the specified `oncancelled` callback will be discarded. + * See {@link then} for more details. + */ + finally(onfinally, oncancelled) { + if (!(this instanceof CancellablePromise)) { + throw new TypeError("CancellablePromise.prototype.finally called on an invalid object."); + } + if (!isCallable(onfinally)) { + return this.then(onfinally, onfinally, oncancelled); + } + return this.then((value) => CancellablePromise.resolve(onfinally()).then(() => value), (reason) => CancellablePromise.resolve(onfinally()).then(() => { + throw reason; + }), oncancelled); + } + /** + * We use the `[Symbol.species]` static property, if available, + * to disable the built-in automatic subclassing features from {@link Promise}. + * It is critical for performance reasons that extenders do not override this. + * Once the proposal at https://github.com/tc39/proposal-rm-builtin-subclassing + * is either accepted or retired, this implementation will have to be revised accordingly. + * + * @ignore + * @internal + */ + static get [species]() { + return Promise; + } + static all(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.all(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static allSettled(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.allSettled(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static any(values) { + let collected = Array.from(values); + const promise = collected.length === 0 ? CancellablePromise.resolve(collected) : new CancellablePromise((resolve, reject) => { + void Promise.any(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + static race(values) { + let collected = Array.from(values); + const promise = new CancellablePromise((resolve, reject) => { + void Promise.race(collected).then(resolve, reject); + }, (cause) => cancelAll(promise, collected, cause)); + return promise; + } + /** + * Creates a new cancelled CancellablePromise for the provided cause. + * + * @group Static Methods + */ + static cancel(cause) { + const p = new CancellablePromise(() => { + }); + p.cancel(cause); + return p; + } + /** + * Creates a new CancellablePromise that cancels + * after the specified timeout, with the provided cause. + * + * If the {@link AbortSignal.timeout} factory method is available, + * it is used to base the timeout on _active_ time rather than _elapsed_ time. + * Otherwise, `timeout` falls back to {@link setTimeout}. + * + * @group Static Methods + */ + static timeout(milliseconds, cause) { + const promise = new CancellablePromise(() => { + }); + if (AbortSignal && typeof AbortSignal === "function" && AbortSignal.timeout && typeof AbortSignal.timeout === "function") { + AbortSignal.timeout(milliseconds).addEventListener("abort", () => void promise.cancel(cause)); + } else { + setTimeout(() => void promise.cancel(cause), milliseconds); + } + return promise; + } + static sleep(milliseconds, value) { + return new CancellablePromise((resolve) => { + setTimeout(() => resolve(value), milliseconds); + }); + } + /** + * Creates a new rejected CancellablePromise for the provided reason. + * + * @group Static Methods + */ + static reject(reason) { + return new CancellablePromise((_, reject) => reject(reason)); + } + static resolve(value) { + if (value instanceof CancellablePromise) { + return value; + } + return new CancellablePromise((resolve) => resolve(value)); + } + /** + * Creates a new CancellablePromise and returns it in an object, along with its resolve and reject functions + * and a getter/setter for the cancellation callback. + * + * This method is polyfilled, hence available in every OS/webview version. + * + * @group Static Methods + */ + static withResolvers() { + let result = { oncancelled: null }; + result.promise = new CancellablePromise((resolve, reject) => { + result.resolve = resolve; + result.reject = reject; + }, (cause) => { + var _a2; + (_a2 = result.oncancelled) === null || _a2 === void 0 ? void 0 : _a2.call(result, cause); + }); + return result; + } +} +function cancellerFor(promise, state) { + let cancellationPromise = void 0; + return (reason) => { + if (!state.settled) { + state.settled = true; + state.reason = reason; + promise.reject(reason); + void Promise.prototype.then.call(promise.promise, void 0, (err) => { + if (err !== reason) { + throw err; + } + }); + } + if (!state.reason || !promise.oncancelled) { + return; + } + cancellationPromise = new Promise((resolve) => { + try { + resolve(promise.oncancelled(state.reason.cause)); + } catch (err) { + Promise.reject(new CancelledRejectionError(promise.promise, err, "Unhandled exception in oncancelled callback.")); + } + }).catch((reason2) => { + Promise.reject(new CancelledRejectionError(promise.promise, reason2, "Unhandled rejection in oncancelled callback.")); + }); + promise.oncancelled = null; + return cancellationPromise; + }; +} +function resolverFor(promise, state) { + return (value) => { + if (state.resolving) { + return; + } + state.resolving = true; + if (value === promise.promise) { + if (state.settled) { + return; + } + state.settled = true; + promise.reject(new TypeError("A promise cannot be resolved with itself.")); + return; + } + if (value != null && (typeof value === "object" || typeof value === "function")) { + let then; + try { + then = value.then; + } catch (err) { + state.settled = true; + promise.reject(err); + return; + } + if (isCallable(then)) { + try { + let cancel = value.cancel; + if (isCallable(cancel)) { + const oncancelled = (cause) => { + Reflect.apply(cancel, value, [cause]); + }; + if (state.reason) { + void cancellerFor(Object.assign(Object.assign({}, promise), { oncancelled }), state)(state.reason); + } else { + promise.oncancelled = oncancelled; + } + } + } catch (_a2) { + } + const newState = { + root: state.root, + resolving: false, + get settled() { + return this.root.settled; + }, + set settled(value2) { + this.root.settled = value2; + }, + get reason() { + return this.root.reason; + } + }; + const rejector = rejectorFor(promise, newState); + try { + Reflect.apply(then, value, [resolverFor(promise, newState), rejector]); + } catch (err) { + rejector(err); + } + return; + } + } + if (state.settled) { + return; + } + state.settled = true; + promise.resolve(value); + }; +} +function rejectorFor(promise, state) { + return (reason) => { + if (state.resolving) { + return; + } + state.resolving = true; + if (state.settled) { + try { + if (reason instanceof CancelError && state.reason instanceof CancelError && Object.is(reason.cause, state.reason.cause)) { + return; + } + } catch (_a2) { + } + void Promise.reject(new CancelledRejectionError(promise.promise, reason)); + } else { + state.settled = true; + promise.reject(reason); + } + }; +} +function cancelAll(parent, values, cause) { + const results = []; + for (const value of values) { + let cancel; + try { + if (!isCallable(value.then)) { + continue; + } + cancel = value.cancel; + if (!isCallable(cancel)) { + continue; + } + } catch (_a2) { + continue; + } + let result; + try { + result = Reflect.apply(cancel, value, [cause]); + } catch (err) { + Promise.reject(new CancelledRejectionError(parent, err, "Unhandled exception in cancel method.")); + continue; + } + if (!result) { + continue; + } + results.push((result instanceof Promise ? result : Promise.resolve(result)).catch((reason) => { + Promise.reject(new CancelledRejectionError(parent, reason, "Unhandled rejection in cancel method.")); + })); + } + return Promise.all(results); +} +function identity(x) { + return x; +} +function thrower(reason) { + throw reason; +} +function errorMessage(err) { + try { + if (err instanceof Error || typeof err !== "object" || err.toString !== Object.prototype.toString) { + return "" + err; + } + } catch (_a2) { + } + try { + return JSON.stringify(err); + } catch (_b) { + } + try { + return Object.prototype.toString.call(err); + } catch (_c) { + } + return ""; +} +function currentBarrier(promise) { + var _a2; + let pwr = (_a2 = promise[barrierSym]) !== null && _a2 !== void 0 ? _a2 : {}; + if (!("promise" in pwr)) { + Object.assign(pwr, promiseWithResolvers()); + } + if (promise[barrierSym] == null) { + pwr.resolve(); + promise[barrierSym] = pwr; + } + return pwr.promise; +} +let promiseWithResolvers = Promise.withResolvers; +if (promiseWithResolvers && typeof promiseWithResolvers === "function") { + promiseWithResolvers = promiseWithResolvers.bind(Promise); +} else { + promiseWithResolvers = function() { + let resolve; + let reject; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; + }; +} +window._wails = window._wails || {}; +window._wails.callResultHandler = resultHandler; +window._wails.callErrorHandler = errorHandler; +const call$1 = newRuntimeCaller(objectNames.Call); +const cancelCall = newRuntimeCaller(objectNames.CancelCall); +const callResponses = /* @__PURE__ */ new Map(); +const CallBinding = 0; +const CancelMethod = 0; +class RuntimeError extends Error { + /** + * Constructs a new RuntimeError instance. + * @param message - The error message. + * @param options - Options to be forwarded to the Error constructor. + */ + constructor(message, options) { + super(message, options); + this.name = "RuntimeError"; + } +} +function resultHandler(id, data, isJSON) { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + if (!data) { + resolvers.resolve(void 0); + } else if (!isJSON) { + resolvers.resolve(data); + } else { + try { + resolvers.resolve(JSON.parse(data)); + } catch (err) { + resolvers.reject(new TypeError("could not parse result: " + err.message, { cause: err })); + } + } +} +function errorHandler(id, data, isJSON) { + const resolvers = getAndDeleteResponse(id); + if (!resolvers) { + return; + } + if (!isJSON) { + resolvers.reject(new Error(data)); + } else { + let error; + try { + error = JSON.parse(data); + } catch (err) { + resolvers.reject(new TypeError("could not parse error: " + err.message, { cause: err })); + return; + } + let options = {}; + if (error.cause) { + options.cause = error.cause; + } + let exception; + switch (error.kind) { + case "ReferenceError": + exception = new ReferenceError(error.message, options); + break; + case "TypeError": + exception = new TypeError(error.message, options); + break; + case "RuntimeError": + exception = new RuntimeError(error.message, options); + break; + default: + exception = new Error(error.message, options); + break; + } + resolvers.reject(exception); + } +} +function getAndDeleteResponse(id) { + const response = callResponses.get(id); + callResponses.delete(id); + return response; +} +function generateID() { + let result; + do { + result = nanoid(); + } while (callResponses.has(result)); + return result; +} +function Call(options) { + const id = generateID(); + const result = CancellablePromise.withResolvers(); + callResponses.set(id, { resolve: result.resolve, reject: result.reject }); + const request = call$1(CallBinding, Object.assign({ "call-id": id }, options)); + let running = false; + request.then(() => { + running = true; + }, (err) => { + callResponses.delete(id); + result.reject(err); + }); + const cancel = () => { + callResponses.delete(id); + return cancelCall(CancelMethod, { "call-id": id }).catch((err) => { + console.error("Error while requesting binding call cancellation:", err); + }); + }; + result.oncancelled = () => { + if (running) { + return cancel(); + } else { + return request.then(cancel); + } + }; + return result.promise; +} +function ByID(methodID, ...args) { + return Call({ methodID, args }); +} +const eventListeners = /* @__PURE__ */ new Map(); +class Listener { + constructor(eventName, callback, maxCallbacks) { + this.eventName = eventName; + this.callback = callback; + this.maxCallbacks = maxCallbacks || -1; + } + dispatch(data) { + try { + this.callback(data); + } catch (err) { + console.error(err); + } + if (this.maxCallbacks === -1) + return false; + this.maxCallbacks -= 1; + return this.maxCallbacks === 0; + } +} +function listenerOff(listener) { + let listeners = eventListeners.get(listener.eventName); + if (!listeners) { + return; + } + listeners = listeners.filter((l) => l !== listener); + if (listeners.length === 0) { + eventListeners.delete(listener.eventName); + } else { + eventListeners.set(listener.eventName, listeners); + } +} +window._wails = window._wails || {}; +window._wails.dispatchWailsEvent = dispatchWailsEvent; +const call = newRuntimeCaller(objectNames.Events); +const EmitMethod = 0; +class WailsEvent { + constructor(name, data = null) { + this.name = name; + this.data = data; + } +} +function dispatchWailsEvent(event) { + let listeners = eventListeners.get(event.name); + if (!listeners) { + return; + } + let wailsEvent = new WailsEvent(event.name, event.data); + if ("sender" in event) { + wailsEvent.sender = event.sender; + } + listeners = listeners.filter((listener) => !listener.dispatch(wailsEvent)); + if (listeners.length === 0) { + eventListeners.delete(event.name); + } else { + eventListeners.set(event.name, listeners); + } +} +function OnMultiple(eventName, callback, maxCallbacks) { + let listeners = eventListeners.get(eventName) || []; + const thisListener = new Listener(eventName, callback, maxCallbacks); + listeners.push(thisListener); + eventListeners.set(eventName, listeners); + return () => listenerOff(thisListener); +} +function On(eventName, callback) { + return OnMultiple(eventName, callback, -1); +} +function Emit(event) { + return call(EmitMethod, event); +} +window._wails = window._wails || {}; +window._wails.invoke = invoke; +invoke("wails:runtime:ready"); +function RemoveBadge() { + let $resultPromise = ByID(2633565570); + return $resultPromise; +} +function SetBadge(label) { + let $resultPromise = ByID(3052354152, label); + return $resultPromise; +} +const setButton = document.getElementById("set"); +const removeButton = document.getElementById("remove"); +const setButtonUsingGo = document.getElementById("set-go"); +const removeButtonUsingGo = document.getElementById("remove-go"); +const labelElement = document.getElementById("label"); +const timeElement = document.getElementById("time"); +setButton.addEventListener("click", () => { + let label = labelElement.value; + SetBadge(label); +}); +removeButton.addEventListener("click", () => { + RemoveBadge(); +}); +setButtonUsingGo.addEventListener("click", () => { + let label = labelElement.value; + void Emit({ + name: "set:badge", + data: label + }); +}); +removeButtonUsingGo.addEventListener("click", () => { + void Emit({ name: "remove:badge", data: null }); +}); +On("time", (time) => { + timeElement.innerText = time.data; +}); diff --git a/v3/examples/badge/frontend/dist/index.html b/v3/examples/badge/frontend/dist/index.html new file mode 100644 index 000000000..a543f93cd --- /dev/null +++ b/v3/examples/badge/frontend/dist/index.html @@ -0,0 +1,38 @@ + + + + + + + + Wails App + + + +
+ +

Wails + Typescript

+
Set a badge label below 👇
+
+
+ + + + + +
+
+ +
+ + diff --git a/v3/examples/badge/frontend/dist/style.css b/v3/examples/badge/frontend/dist/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge/frontend/dist/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge/frontend/dist/typescript.svg b/v3/examples/badge/frontend/dist/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge/frontend/dist/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge/frontend/dist/wails.png b/v3/examples/badge/frontend/dist/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge/frontend/dist/wails.png differ diff --git a/v3/examples/badge/frontend/index.html b/v3/examples/badge/frontend/index.html new file mode 100644 index 000000000..616cb4c0f --- /dev/null +++ b/v3/examples/badge/frontend/index.html @@ -0,0 +1,38 @@ + + + + + + + + Wails App + + +
+ +

Wails + Typescript

+
Set a badge label below 👇
+
+
+ + + + + +
+
+ +
+ + + diff --git a/v3/examples/badge/frontend/package.json b/v3/examples/badge/frontend/package.json new file mode 100644 index 000000000..4d675f189 --- /dev/null +++ b/v3/examples/badge/frontend/package.json @@ -0,0 +1,17 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build:dev": "tsc && vite build --minify false --mode development", + "build": "tsc && vite build --mode production", + "preview": "vite preview" + }, + "devDependencies": { + "typescript": "^4.9.3", + "vite": "^5.0.0", + "@wailsio/runtime": "latest" + } +} \ No newline at end of file diff --git a/v3/examples/badge/frontend/public/Inter-Medium.ttf b/v3/examples/badge/frontend/public/Inter-Medium.ttf new file mode 100644 index 000000000..a01f3777a Binary files /dev/null and b/v3/examples/badge/frontend/public/Inter-Medium.ttf differ diff --git a/v3/examples/badge/frontend/public/style.css b/v3/examples/badge/frontend/public/style.css new file mode 100644 index 000000000..6ce81cad2 --- /dev/null +++ b/v3/examples/badge/frontend/public/style.css @@ -0,0 +1,155 @@ +:root { + font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", + "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", + sans-serif; + font-size: 16px; + line-height: 24px; + font-weight: 400; + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: rgba(27, 38, 54, 1); + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -webkit-text-size-adjust: 100%; + user-select: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; +} + +@font-face { + font-family: "Inter"; + font-style: normal; + font-weight: 400; + src: local(""), + url("./Inter-Medium.ttf") format("truetype"); +} + +h3 { + font-size: 3em; + line-height: 1.1; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} + +a:hover { + color: #535bf2; +} + +button { + line-height: 30px; + border-radius: 3px; + border: none; + margin: 0 0 0 20px; + padding: 0 8px; + cursor: pointer; +} + +.result { + height: 20px; + line-height: 20px; +} + +body { + margin: 0; + display: flex; + place-items: center; + place-content: center; + min-width: 320px; + min-height: 100vh; +} + +.container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +#app { + max-width: 1280px; + margin: 0 auto; + padding: 2rem; + text-align: center; +} + +.logo { + height: 6em; + padding: 1.5em; + will-change: filter; +} + +.logo:hover { + filter: drop-shadow(0 0 2em #e80000aa); +} + +.logo.vanilla:hover { + filter: drop-shadow(0 0 2em #f7df1eaa); +} + +.result { + height: 20px; + line-height: 20px; + margin: 1.5rem auto; + text-align: center; +} + +.footer { + margin-top: 1rem; + align-content: center; + text-align: center; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + + a:hover { + color: #747bff; + } + + button { + background-color: #f9f9f9; + } +} + + +.input-box .btn:hover { + background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%); + color: #333333; +} + +.input-box .input { + border: none; + border-radius: 3px; + outline: none; + height: 30px; + line-height: 30px; + padding: 0 10px; + color: black; + background-color: rgba(240, 240, 240, 1); + -webkit-font-smoothing: antialiased; +} + +.input-box .input:hover { + border: none; + background-color: rgba(255, 255, 255, 1); +} + +.input-box .input:focus { + border: none; + background-color: rgba(255, 255, 255, 1); +} \ No newline at end of file diff --git a/v3/examples/badge/frontend/public/typescript.svg b/v3/examples/badge/frontend/public/typescript.svg new file mode 100644 index 000000000..d91c910cc --- /dev/null +++ b/v3/examples/badge/frontend/public/typescript.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/v3/examples/badge/frontend/public/wails.png b/v3/examples/badge/frontend/public/wails.png new file mode 100644 index 000000000..8bdf42483 Binary files /dev/null and b/v3/examples/badge/frontend/public/wails.png differ diff --git a/v3/examples/badge/frontend/src/main.ts b/v3/examples/badge/frontend/src/main.ts new file mode 100644 index 000000000..593582b4a --- /dev/null +++ b/v3/examples/badge/frontend/src/main.ts @@ -0,0 +1,35 @@ +import {Events} from "@wailsio/runtime"; +import {SetBadge, RemoveBadge} from "../bindings/github.com/wailsapp/wails/v3/pkg/services/badge/service"; + +const setButton = document.getElementById('set')! as HTMLButtonElement; +const removeButton = document.getElementById('remove')! as HTMLButtonElement; +const setButtonUsingGo = document.getElementById('set-go')! as HTMLButtonElement; +const removeButtonUsingGo = document.getElementById('remove-go')! as HTMLButtonElement; +const labelElement : HTMLInputElement = document.getElementById('label')! as HTMLInputElement; +const timeElement = document.getElementById('time')! as HTMLDivElement; + +setButton.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + SetBadge(label); +}); + +removeButton.addEventListener('click', () => { + RemoveBadge(); +}); + +setButtonUsingGo.addEventListener('click', () => { + let label = (labelElement as HTMLInputElement).value + void Events.Emit({ + name: "set:badge", + data: label, + }) +}) + +removeButtonUsingGo.addEventListener('click', () => { + void Events.Emit({name:"remove:badge", data: null}) +}) + +Events.On('time', (time: {data: any}) => { + timeElement.innerText = time.data; +}); + diff --git a/v3/examples/badge/frontend/src/vite-env.d.ts b/v3/examples/badge/frontend/src/vite-env.d.ts new file mode 100644 index 000000000..11f02fe2a --- /dev/null +++ b/v3/examples/badge/frontend/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/v3/examples/badge/frontend/tsconfig.json b/v3/examples/badge/frontend/tsconfig.json new file mode 100644 index 000000000..c267ecf24 --- /dev/null +++ b/v3/examples/badge/frontend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ESNext", "DOM"], + "moduleResolution": "Node", + "strict": true, + "resolveJsonModule": true, + "isolatedModules": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/v3/examples/badge/main.go b/v3/examples/badge/main.go new file mode 100644 index 000000000..f1fe9afa1 --- /dev/null +++ b/v3/examples/badge/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "embed" + _ "embed" + "log" + "time" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/services/badge" +) + +// Wails uses Go's `embed` package to embed the frontend files into the binary. +// Any files in the frontend/dist folder will be embedded into the binary and +// made available to the frontend. +// See https://pkg.go.dev/embed for more information. + +//go:embed all:frontend/dist +var assets embed.FS + +// main function serves as the application's entry point. It initializes the application, creates a window, +// and starts a goroutine that emits a time-based event every second. It subsequently runs the application and +// logs any error that might occur. +func main() { + // Create a new Wails application by providing the necessary options. + // Variables 'Name' and 'Description' are for application metadata. + // 'Assets' configures the asset server with the 'FS' variable pointing to the frontend files. + // 'Bind' is a list of Go struct instances. The frontend has access to the methods of these instances. + // 'Mac' options tailor the application when running an macOS. + + badgeService := badge.New() + + app := application.New(application.Options{ + Name: "badge", + Description: "A demo of using raw HTML & CSS", + Services: []application.Service{ + application.NewService(badgeService), + }, + Assets: application.AssetOptions{ + Handler: application.AssetFileServerFS(assets), + }, + Mac: application.MacOptions{ + ApplicationShouldTerminateAfterLastWindowClosed: true, + }, + }) + + // Create a new window with the necessary options. + // 'Title' is the title of the window. + // 'Mac' options tailor the window when running on macOS. + // 'BackgroundColour' is the background colour of the window. + // 'URL' is the URL that will be loaded into the webview. + app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{ + Title: "Window 1", + Mac: application.MacWindow{ + InvisibleTitleBarHeight: 50, + Backdrop: application.MacBackdropTranslucent, + TitleBar: application.MacTitleBarHiddenInset, + }, + BackgroundColour: application.NewRGB(27, 38, 54), + URL: "/", + }) + + app.OnEvent("remove:badge", func(event *application.CustomEvent) { + err := badgeService.RemoveBadge() + if err != nil { + log.Fatal(err) + } + }) + + app.OnEvent("set:badge", func(event *application.CustomEvent) { + text := event.Data.(string) + err := badgeService.SetBadge(text) + if err != nil { + log.Fatal(err) + } + }) + + // Create a goroutine that emits an event containing the current time every second. + // The frontend can listen to this event and update the UI accordingly. + go func() { + for { + now := time.Now().Format(time.RFC1123) + app.EmitEvent("time", now) + time.Sleep(time.Second) + } + }() + + // Run the application. This blocks until the application has been exited. + err := app.Run() + + // If an error occurred while running the application, log it and exit. + if err != nil { + log.Fatal(err) + } +} diff --git a/v3/examples/dev/go.mod b/v3/examples/dev/go.mod index 93ce87758..34bf45c16 100644 --- a/v3/examples/dev/go.mod +++ b/v3/examples/dev/go.mod @@ -35,13 +35,13 @@ require ( github.com/samber/lo v1.49.1 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/stretchr/testify v1.10.0 // indirect - github.com/wailsapp/go-webview2 v1.0.19 // indirect + github.com/wailsapp/go-webview2 v1.0.21 // indirect github.com/wailsapp/mimetype v1.4.1 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.33.0 // indirect + golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/v3/examples/dev/go.sum b/v3/examples/dev/go.sum index dc4098614..6ae42958f 100644 --- a/v3/examples/dev/go.sum +++ b/v3/examples/dev/go.sum @@ -138,6 +138,7 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/wailsapp/go-webview2 v1.0.9 h1:lrU+q0cf1wgLdR69rN+ZnRtMJNaJRrcQ4ELxoO7/xjs= github.com/wailsapp/go-webview2 v1.0.9/go.mod h1:Uk2BePfCRzttBBjFrBmqKGJd41P6QIHeV9kTgIeOZNo= github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= +github.com/wailsapp/go-webview2 v1.0.21/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc= github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs= github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o= github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= @@ -153,6 +154,7 @@ golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDf golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME= golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= @@ -167,6 +169,7 @@ golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -191,6 +194,7 @@ golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= diff --git a/v3/go.mod b/v3/go.mod index 947f0495f..8d0e4935c 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -136,7 +136,7 @@ require ( golang.org/x/crypto v0.36.0 // indirect golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac - golang.org/x/image v0.24.0 // indirect + golang.org/x/image v0.24.0 golang.org/x/mod v0.24.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sync v0.12.0 // indirect diff --git a/v3/pkg/services/badge/badge.go b/v3/pkg/services/badge/badge.go new file mode 100644 index 000000000..8d1d926d1 --- /dev/null +++ b/v3/pkg/services/badge/badge.go @@ -0,0 +1,60 @@ +package badge + +import ( + "context" + "image/color" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type platformBadge interface { + // Lifecycle methods + Startup(ctx context.Context, options application.ServiceOptions) error + Shutdown() error + + SetBadge(label string) error + SetCustomBadge(label string, options Options) error + RemoveBadge() error +} + +// Service represents the notifications service +type Service struct { + impl platformBadge +} + +type Options struct { + TextColour color.RGBA + BackgroundColour color.RGBA + FontName string + FontSize int + SmallFontSize int +} + +// ServiceName returns the name of the service. +func (b *Service) ServiceName() string { + return "github.com/wailsapp/wails/v3/services/badge" +} + +// ServiceStartup is called when the service is loaded. +func (b *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error { + return b.impl.Startup(ctx, options) +} + +// ServiceShutdown is called when the service is unloaded. +func (b *Service) ServiceShutdown() error { + return b.impl.Shutdown() +} + +// SetBadge sets the badge label on the application icon. +func (b *Service) SetBadge(label string) error { + return b.impl.SetBadge(label) +} + +func (b *Service) SetCustomBadge(label string, options Options) error { + return b.impl.SetCustomBadge(label, options) +} + +// RemoveBadge removes the badge label from the application icon. +func (b *Service) RemoveBadge() error { + return b.impl.RemoveBadge() +} diff --git a/v3/pkg/services/badge/badge_darwin.go b/v3/pkg/services/badge/badge_darwin.go new file mode 100644 index 000000000..75bf04e7c --- /dev/null +++ b/v3/pkg/services/badge/badge_darwin.go @@ -0,0 +1,74 @@ +//go:build darwin + +package badge + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa +#import + +static void setBadge(const char *label) { + NSString *nsLabel = nil; + if (label != NULL) { + nsLabel = [NSString stringWithUTF8String:label]; + } + [[NSApp dockTile] setBadgeLabel:nsLabel]; + [[NSApp dockTile] display]; +} +*/ +import "C" +import ( + "context" + "unsafe" + + "github.com/wailsapp/wails/v3/pkg/application" +) + +type darwinBadge struct{} + +// Creates a new Badge Service. +func New() *Service { + return &Service{ + impl: &darwinBadge{}, + } +} + +// NewWithOptions creates a new badge service with the given options. +// Currently, options are not available on macOS and are ignored. +// (Windows-specific) +func NewWithOptions(options Options) *Service { + return New() +} + +func (d *darwinBadge) Startup(ctx context.Context, options application.ServiceOptions) error { + return nil +} + +func (d *darwinBadge) Shutdown() error { + return nil +} + +// SetBadge sets the badge label on the application icon. +func (d *darwinBadge) SetBadge(label string) error { + var cLabel *C.char + if label != "" { + cLabel = C.CString(label) + defer C.free(unsafe.Pointer(cLabel)) + } else { + cLabel = C.CString("●") // Default badge character + } + C.setBadge(cLabel) + return nil +} + +// SetCustomBadge is not supported on macOS, SetBadge is called instead. +// (Windows-specific) +func (d *darwinBadge) SetCustomBadge(label string, options Options) error { + return d.SetBadge(label) +} + +// RemoveBadge removes the badge label from the application icon. +func (d *darwinBadge) RemoveBadge() error { + C.setBadge(nil) + return nil +} diff --git a/v3/pkg/services/badge/badge_windows.go b/v3/pkg/services/badge/badge_windows.go new file mode 100644 index 000000000..ae09edc94 --- /dev/null +++ b/v3/pkg/services/badge/badge_windows.go @@ -0,0 +1,374 @@ +//go:build windows + +package badge + +import ( + "bytes" + "context" + "image" + "image/color" + "image/png" + "os" + + "github.com/wailsapp/wails/v3/pkg/application" + "github.com/wailsapp/wails/v3/pkg/w32" + "golang.org/x/image/font" + "golang.org/x/image/font/opentype" + "golang.org/x/image/math/fixed" +) + +type windowsBadge struct { + taskbar *w32.ITaskbarList3 + badgeImg *image.RGBA + badgeSize int + fontManager *FontManager + options Options +} + +var defaultOptions = Options{ + TextColour: color.RGBA{255, 255, 255, 255}, + BackgroundColour: color.RGBA{255, 0, 0, 255}, + FontName: "segoeuib.ttf", + FontSize: 18, + SmallFontSize: 14, +} + +// Creates a new Badge Service. +func New() *Service { + return &Service{ + impl: &windowsBadge{ + options: defaultOptions, + }, + } +} + +// NewWithOptions creates a new badge service with the given options. +func NewWithOptions(options Options) *Service { + return &Service{ + impl: &windowsBadge{ + options: options, + }, + } +} + +func (w *windowsBadge) Startup(ctx context.Context, options application.ServiceOptions) error { + taskbar, err := w32.NewTaskbarList3() + if err != nil { + return err + } + w.taskbar = taskbar + w.fontManager = NewFontManager() + + return nil +} + +func (w *windowsBadge) Shutdown() error { + if w.taskbar != nil { + w.taskbar.Release() + } + + return nil +} + +// SetBadge sets the badge label on the application icon. +func (w *windowsBadge) SetBadge(label string) error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.CurrentWindow() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + w.createBadge() + + var hicon w32.HICON + if label == "" { + hicon, err = w.createBadgeIcon() + if err != nil { + return err + } + } else { + hicon, err = w.createBadgeIconWithText(label) + if err != nil { + return err + } + } + defer w32.DestroyIcon(hicon) + + return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) +} + +// SetCustomBadge sets the badge label on the application icon with one-off options. +func (w *windowsBadge) SetCustomBadge(label string, options Options) error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.CurrentWindow() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + const badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, badgeSize, badgeSize)) + + backgroundColour := options.BackgroundColour + radius := badgeSize / 2 + centerX, centerY := radius, radius + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(radius*radius) { + img.Set(x, y, backgroundColour) + } + } + } + + var hicon w32.HICON + if label == "" { + hicon, err = createBadgeIcon(badgeSize, img, options) + if err != nil { + return err + } + } else { + hicon, err = createBadgeIconWithText(w, label, badgeSize, img, options) + if err != nil { + return err + } + } + defer w32.DestroyIcon(hicon) + + return w.taskbar.SetOverlayIcon(hwnd, hicon, nil) +} + +// RemoveBadge removes the badge label from the application icon. +func (w *windowsBadge) RemoveBadge() error { + if w.taskbar == nil { + return nil + } + + app := application.Get() + if app == nil { + return nil + } + + window := app.CurrentWindow() + if window == nil { + return nil + } + + hwnd, err := window.NativeWindowHandle() + if err != nil { + return err + } + + return w.taskbar.SetOverlayIcon(hwnd, 0, nil) +} + +// createBadgeIcon creates a badge icon with the specified size and color. +func (w *windowsBadge) createBadgeIcon() (w32.HICON, error) { + radius := w.badgeSize / 2 + centerX, centerY := radius, radius + innerRadius := w.badgeSize / 5 + + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + w.badgeImg.Set(x, y, w.options.TextColour) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + +func createBadgeIcon(badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + radius := badgeSize / 2 + centerX, centerY := radius, radius + innerRadius := badgeSize / 5 + + for y := 0; y < badgeSize; y++ { + for x := 0; x < badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(innerRadius*innerRadius) { + img.Set(x, y, options.TextColour) + } + } + } + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + hicon, err := w32.CreateSmallHIconFromImage(buf.Bytes()) + return hicon, err +} + +// createBadgeIconWithText creates a badge icon with the specified text. +func (w *windowsBadge) createBadgeIconWithText(label string) (w32.HICON, error) { + fontPath := w.fontManager.FindFontOrDefault(w.options.FontName) + if fontPath == "" { + return w.createBadgeIcon() + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return w.createBadgeIcon() + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return w.createBadgeIcon() + } + + fontSize := float64(w.options.FontSize) + if len(label) > 1 { + fontSize = float64(w.options.SmallFontSize) + } + + // Get DPI of the current screen + screen := w32.GetDesktopWindow() + dpi := w32.GetDpiForWindow(screen) + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: float64(dpi), + Hinting: font.HintingFull, + }) + if err != nil { + return w.createBadgeIcon() + } + defer face.Close() + + d := &font.Drawer{ + Dst: w.badgeImg, + Src: image.NewUniform(w.options.TextColour), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((w.badgeSize-textWidth)/2, int(float64(w.badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, w.badgeImg); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + +func createBadgeIconWithText(w *windowsBadge, label string, badgeSize int, img *image.RGBA, options Options) (w32.HICON, error) { + fontPath := w.fontManager.FindFontOrDefault(options.FontName) + if fontPath == "" { + return createBadgeIcon(badgeSize, img, options) + } + + fontBytes, err := os.ReadFile(fontPath) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + ttf, err := opentype.Parse(fontBytes) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + + fontSize := float64(options.FontSize) + if len(label) > 1 { + fontSize = float64(options.SmallFontSize) + } + + // Get DPI of the current screen + screen := w32.GetDesktopWindow() + dpi := w32.GetDpiForWindow(screen) + + face, err := opentype.NewFace(ttf, &opentype.FaceOptions{ + Size: fontSize, + DPI: float64(dpi), + Hinting: font.HintingFull, + }) + if err != nil { + return createBadgeIcon(badgeSize, img, options) + } + defer face.Close() + + d := &font.Drawer{ + Dst: img, + Src: image.NewUniform(options.TextColour), + Face: face, + } + + textWidth := d.MeasureString(label).Ceil() + d.Dot = fixed.P((badgeSize-textWidth)/2, int(float64(badgeSize)/2+fontSize/2)) + d.DrawString(label) + + var buf bytes.Buffer + if err := png.Encode(&buf, img); err != nil { + return 0, err + } + + return w32.CreateSmallHIconFromImage(buf.Bytes()) +} + +// createBadge creates a circular badge with the specified background color. +func (w *windowsBadge) createBadge() { + w.badgeSize = 32 + + img := image.NewRGBA(image.Rect(0, 0, w.badgeSize, w.badgeSize)) + + backgroundColour := w.options.BackgroundColour + radius := w.badgeSize / 2 + centerX, centerY := radius, radius + + for y := 0; y < w.badgeSize; y++ { + for x := 0; x < w.badgeSize; x++ { + dx := float64(x - centerX) + dy := float64(y - centerY) + + if dx*dx+dy*dy < float64(radius*radius) { + img.Set(x, y, backgroundColour) + } + } + } + + w.badgeImg = img +} diff --git a/v3/pkg/services/badge/font.go b/v3/pkg/services/badge/font.go new file mode 100644 index 000000000..430a9c14d --- /dev/null +++ b/v3/pkg/services/badge/font.go @@ -0,0 +1,153 @@ +//go:build windows + +package badge + +import ( + "errors" + "os" + "path/filepath" + "strings" + "sync" + + "golang.org/x/sys/windows/registry" +) + +// FontManager handles font discovery on Windows with minimal caching +type FontManager struct { + fontCache map[string]string // Maps only requested font filenames to paths + fontDirs []string // Directories to search for fonts + mu sync.RWMutex // Mutex for thread-safe access to the cache + registryPaths []string // Registry paths to search for fonts +} + +// NewFontManager creates a new FontManager instance +func NewFontManager() *FontManager { + return &FontManager{ + fontCache: make(map[string]string), + fontDirs: []string{ + filepath.Join(os.Getenv("windir"), "Fonts"), + filepath.Join(os.Getenv("localappdata"), "Microsoft", "Windows", "Fonts"), + }, + registryPaths: []string{ + `SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts`, + }, + } +} + +// FindFont searches for a font by filename and returns its full path +// Only caches fonts that are found +func (fm *FontManager) FindFont(fontFilename string) (string, error) { + fontKey := strings.ToLower(fontFilename) + + // Check if already in cache + fm.mu.RLock() + if path, exists := fm.fontCache[fontKey]; exists { + fm.mu.RUnlock() + return path, nil + } + fm.mu.RUnlock() + + // If not in cache, search for the font + fontPath, err := fm.searchForFont(fontFilename) + if err != nil { + return "", err + } + + // Add to cache only if found + fm.mu.Lock() + fm.fontCache[fontKey] = fontPath + fm.mu.Unlock() + + return fontPath, nil +} + +// searchForFont looks for a font in all known locations +func (fm *FontManager) searchForFont(fontFilename string) (string, error) { + fontFileLower := strings.ToLower(fontFilename) + + // 1. Direct file check in font directories (fastest approach) + for _, dir := range fm.fontDirs { + fontPath := filepath.Join(dir, fontFilename) + if fileExists(fontPath) { + return fontPath, nil + } + } + + // 2. Search in registry (can find fonts with different paths) + // System fonts (HKEY_LOCAL_MACHINE) + for _, regPath := range fm.registryPaths { + k, err := registry.OpenKey(registry.LOCAL_MACHINE, regPath, registry.QUERY_VALUE) + if err == nil { + defer k.Close() + + // Look for the specific font in registry values + fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[0]) + if found { + return fontPath, nil + } + } + } + + // 3. User fonts (HKEY_CURRENT_USER) + for _, regPath := range fm.registryPaths { + k, err := registry.OpenKey(registry.CURRENT_USER, regPath, registry.QUERY_VALUE) + if err == nil { + defer k.Close() + + // Look for the specific font in registry values + fontPath, found := fm.findFontInRegistry(k, fontFileLower, fm.fontDirs[1]) + if found { + return fontPath, nil + } + } + } + + return "", errors.New("font not found: " + fontFilename) +} + +// findFontInRegistry searches for a specific font in a registry key +func (fm *FontManager) findFontInRegistry(k registry.Key, fontFileLower string, defaultDir string) (string, bool) { + valueNames, err := k.ReadValueNames(0) + if err != nil { + return "", false + } + + for _, name := range valueNames { + value, _, err := k.GetStringValue(name) + if err != nil { + continue + } + + // Check if this registry entry corresponds to our font + valueLower := strings.ToLower(value) + if strings.HasSuffix(valueLower, fontFileLower) { + // If it's a relative path, assume it's in the default font directory + if !strings.Contains(value, "\\") { + value = filepath.Join(defaultDir, value) + } + + if fileExists(value) { + return value, true + } + } + } + + return "", false +} + +func (fm *FontManager) FindFontOrDefault(name string) string { + fontsToFind := []string{name, "segoeuib.ttf", "arialbd.ttf"} + for _, font := range fontsToFind { + path, err := fm.FindFont(font) + if err == nil { + return path + } + } + return "" +} + +// Helper functions +func fileExists(path string) bool { + info, err := os.Stat(path) + return err == nil && !info.IsDir() +} diff --git a/v3/pkg/w32/ole32.go b/v3/pkg/w32/ole32.go index 8e7c20d4a..a7d204912 100644 --- a/v3/pkg/w32/ole32.go +++ b/v3/pkg/w32/ole32.go @@ -7,9 +7,10 @@ package w32 import ( - "github.com/wailsapp/go-webview2/pkg/combridge" "syscall" "unsafe" + + "github.com/wailsapp/go-webview2/pkg/combridge" ) var ( @@ -19,6 +20,7 @@ var ( procCoInitialize = modole32.NewProc("CoInitialize") procOleInitialize = modole32.NewProc("OleInitialize") procCoUninitialize = modole32.NewProc("CoUninitialize") + procCoCreateInstance = modole32.NewProc("CoCreateInstance") procCreateStreamOnHGlobal = modole32.NewProc("CreateStreamOnHGlobal") procRegisterDragDrop = modole32.NewProc("RegisterDragDrop") procRevokeDragDrop = modole32.NewProc("RevokeDragDrop") @@ -49,6 +51,26 @@ func CoUninitialize() { procCoUninitialize.Call() } +func CoCreateInstance(clsid *syscall.GUID, dwClsContext uintptr, riid *syscall.GUID, ppv uintptr) HRESULT { + ret, _, _ := procCoCreateInstance.Call( + uintptr(unsafe.Pointer(clsid)), + 0, + uintptr(dwClsContext), + uintptr(unsafe.Pointer(riid)), + uintptr(ppv)) + + switch uint32(ret) { + case E_INVALIDARG: + panic("CoCreateInstance failed with E_INVALIDARG") + case E_OUTOFMEMORY: + panic("CoCreateInstance failed with E_OUTOFMEMORY") + case E_UNEXPECTED: + panic("CoCreateInstance failed with E_UNEXPECTED") + } + + return HRESULT(ret) +} + func CreateStreamOnHGlobal(hGlobal HGLOBAL, fDeleteOnRelease bool) *IStream { stream := new(IStream) ret, _, _ := procCreateStreamOnHGlobal.Call( diff --git a/v3/pkg/w32/taskbar.go b/v3/pkg/w32/taskbar.go new file mode 100644 index 000000000..7f2a697d6 --- /dev/null +++ b/v3/pkg/w32/taskbar.go @@ -0,0 +1,95 @@ +//go:build windows + +package w32 + +import ( + "syscall" + "unsafe" +) + +var ( + CLSID_TaskbarList = syscall.GUID{Data1: 0x56FDF344, Data2: 0xFD6D, Data3: 0x11D0, Data4: [8]byte{0x95, 0x8A, 0x00, 0x60, 0x97, 0xC9, 0xA0, 0x90}} + IID_ITaskbarList3 = syscall.GUID{Data1: 0xEA1AFB91, Data2: 0x9E28, Data3: 0x4B86, Data4: [8]byte{0x90, 0xE9, 0x9E, 0x9F, 0x8A, 0x5E, 0xEF, 0xAF}} +) + +// ITaskbarList3 interface for Windows taskbar functionality +type ITaskbarList3 struct { + lpVtbl *taskbarList3Vtbl +} + +type taskbarList3Vtbl struct { + QueryInterface uintptr + AddRef uintptr + Release uintptr + HrInit uintptr + AddTab uintptr + DeleteTab uintptr + ActivateTab uintptr + SetActiveAlt uintptr + MarkFullscreenWindow uintptr + SetProgressValue uintptr + SetProgressState uintptr + RegisterTab uintptr + UnregisterTab uintptr + SetTabOrder uintptr + SetTabActive uintptr + ThumbBarAddButtons uintptr + ThumbBarUpdateButtons uintptr + ThumbBarSetImageList uintptr + SetOverlayIcon uintptr + SetThumbnailTooltip uintptr + SetThumbnailClip uintptr +} + +// NewTaskbarList3 creates a new instance of ITaskbarList3 +func NewTaskbarList3() (*ITaskbarList3, error) { + const COINIT_APARTMENTTHREADED = 0x2 + + if hrInit := CoInitializeEx(COINIT_APARTMENTTHREADED); hrInit != 0 && hrInit != 0x1 { + return nil, syscall.Errno(hrInit) + } + + var taskbar *ITaskbarList3 + hr := CoCreateInstance( + &CLSID_TaskbarList, + CLSCTX_INPROC_SERVER, + &IID_ITaskbarList3, + uintptr(unsafe.Pointer(&taskbar)), + ) + + if hr != 0 { + CoUninitialize() + return nil, syscall.Errno(hr) + } + + if r, _, _ := syscall.SyscallN(taskbar.lpVtbl.HrInit, uintptr(unsafe.Pointer(taskbar))); r != 0 { + syscall.SyscallN(taskbar.lpVtbl.Release, uintptr(unsafe.Pointer(taskbar))) + CoUninitialize() + return nil, syscall.Errno(r) + } + + return taskbar, nil +} + +// SetOverlayIcon sets an overlay icon on the taskbar +func (t *ITaskbarList3) SetOverlayIcon(hwnd HWND, hIcon HICON, description *uint16) error { + ret, _, _ := syscall.SyscallN( + t.lpVtbl.SetOverlayIcon, + uintptr(unsafe.Pointer(t)), + uintptr(hwnd), + uintptr(hIcon), + uintptr(unsafe.Pointer(description)), + ) + if ret != 0 { + return syscall.Errno(ret) + } + return nil +} + +// Release releases the ITaskbarList3 interface +func (t *ITaskbarList3) Release() { + if t != nil { + syscall.SyscallN(t.lpVtbl.Release, uintptr(unsafe.Pointer(t))) + CoUninitialize() + } +}