mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 01:00:21 +08:00
Merge pull request #4098 from popaprozac/notifications_darwin
[v3] Notifications API
This commit is contained in:
commit
afb4bd933d
@ -76,6 +76,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add cancellation support for query methods on `sqlite` service by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
|
||||
- Add prepared statement support to `sqlite` service with JS bindings by [@fbbdev](https://github.com/fbbdev) in [#4067](https://github.com/wailsapp/wails/pull/4067)
|
||||
- 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 File Association support for mac by [@wimaha](https://github.com/wimaha) in [#4177](https://github.com/wailsapp/wails/pull/4177)
|
||||
|
||||
### Fixed
|
||||
|
304
docs/src/content/docs/learn/notifications.mdx
Normal file
304
docs/src/content/docs/learn/notifications.mdx
Normal file
@ -0,0 +1,304 @@
|
||||
---
|
||||
title: Notifications
|
||||
---
|
||||
|
||||
import { Tabs, TabItem } from "@astrojs/starlight/components";
|
||||
|
||||
## Introduction
|
||||
|
||||
Wails provides a comprehensive cross-platform notification system for desktop applications. This service allows you to display native system notifications, with support for interactive elements like action buttons and text input fields.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Creating the Service
|
||||
|
||||
First, initialize the notifications service:
|
||||
|
||||
```go
|
||||
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||
import "github.com/wailsapp/wails/v3/services/notifications"
|
||||
|
||||
// Create a new notification service
|
||||
notifier := notifications.New()
|
||||
|
||||
//Register the service with the application
|
||||
app := application.New(application.Options{
|
||||
Services: []application.Service{
|
||||
application.NewService(notifier),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## Notification Authorization
|
||||
|
||||
Notifications on macOS require user authorization. Request and check authorization:
|
||||
|
||||
```go
|
||||
authorized, err := notifier.CheckNotificationAuthorization()
|
||||
if err != nil {
|
||||
// Handle authorization error
|
||||
}
|
||||
if authorized {
|
||||
// Send notifications
|
||||
} else {
|
||||
// Request authorization
|
||||
authorized, err = notifier.RequestNotificationAuthorization()
|
||||
}
|
||||
```
|
||||
On Windows and Linux this always returns `true`.
|
||||
|
||||
## Notification Types
|
||||
|
||||
### Basic Notifications
|
||||
|
||||
Send a basic notification with a unique id, title, optional subtitle (macOS and Linux), and body text to users:
|
||||
|
||||
```go
|
||||
notifier.SendNotification(notifications.NotificationOptions{
|
||||
ID: "unique-id",
|
||||
Title: "New Calendar Invite",
|
||||
Subtitle: "From: Jane Doe", // Optional
|
||||
Body: "Tap to view the event",
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
### Interactive Notifications
|
||||
Send a notification with action buttons and text inputs. These notifications require a notification category to be resgistered first:
|
||||
|
||||
```go
|
||||
// Define a unique category id
|
||||
categoryID := "unique-category-id"
|
||||
|
||||
// Define a category with actions
|
||||
category := notifications.NotificationCategory{
|
||||
ID: categoryID,
|
||||
Actions: []notifications.NotificationAction{
|
||||
{
|
||||
ID: "OPEN",
|
||||
Title: "Open",
|
||||
},
|
||||
{
|
||||
ID: "ARCHIVE",
|
||||
Title: "Archive",
|
||||
Destructive: true, /* macOS-specific */
|
||||
},
|
||||
},
|
||||
HasReplyField: true,
|
||||
ReplyPlaceholder: "message...",
|
||||
ReplyButtonTitle: "Reply",
|
||||
}
|
||||
|
||||
// Register the category
|
||||
notifier.RegisterNotificationCategory(category)
|
||||
|
||||
// Send an interactive notification with the actions registered in the provided category
|
||||
notifier.SendNotificationWithActions(notifications.NotificationOptions{
|
||||
ID: "unique-id",
|
||||
Title: "New Message",
|
||||
Subtitle: "From: Jane Doe",
|
||||
Body: "Are you able to make it?",
|
||||
CategoryID: categoryID,
|
||||
})
|
||||
```
|
||||
|
||||
## Notification Responses
|
||||
|
||||
Process user interactions with notifications:
|
||||
|
||||
```go
|
||||
notifier.OnNotificationResponse(func(result notifications.NotificationResult) {
|
||||
response := result.Response
|
||||
fmt.Printf("Notification %s was actioned with: %s\n", response.ID, response.ActionIdentifier)
|
||||
|
||||
if response.ActionIdentifier == "TEXT_REPLY" {
|
||||
fmt.Printf("User replied: %s\n", response.UserText)
|
||||
}
|
||||
|
||||
if data, ok := response.UserInfo["sender"].(string); ok {
|
||||
fmt.Printf("Original sender: %s\n", data)
|
||||
}
|
||||
|
||||
// Emit an event to the frontend
|
||||
app.EmitEvent("notification", result.Response)
|
||||
})
|
||||
```
|
||||
|
||||
## Notification Customisation
|
||||
|
||||
### Custom Metadata
|
||||
|
||||
Basic and interactive notifications can include custom data:
|
||||
|
||||
```go
|
||||
notifier.SendNotification(notifications.NotificationOptions{
|
||||
ID: "unique-id",
|
||||
Title: "New Calendar Invite",
|
||||
Subtitle: "From: Jane Doe", // Optional
|
||||
Body: "Tap to view the event",
|
||||
Data: map[string]interface{}{
|
||||
"sender": "jane.doe@example.com",
|
||||
"timestamp": "2025-03-10T15:30:00Z",
|
||||
}
|
||||
})
|
||||
|
||||
```
|
||||
|
||||
## Platform Considerations
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="macOS" icon="fa-brands:apple">
|
||||
|
||||
On macOS, notifications:
|
||||
|
||||
- Require user authorization
|
||||
- Require the app to be notorized for distribution
|
||||
- Use system-standard notification appearances
|
||||
- Support `subtitle`
|
||||
- Support user text input
|
||||
- Support the `Destructive` action option
|
||||
- Automatically handle dark/light mode
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Windows" icon="fa-brands:windows">
|
||||
|
||||
On Windows, notifications:
|
||||
|
||||
- Use Windows system toast styles
|
||||
- Adapt to Windows theme settings
|
||||
- Support user text input
|
||||
- Support high DPI displays
|
||||
- Do not support `subtitle`
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem label="Linux" icon="fa-brands:linux">
|
||||
|
||||
On Linux, dialog behaviour depends on the desktop environment:
|
||||
|
||||
- Use native notifications when available
|
||||
- Follow desktop environment theme
|
||||
- Position according to desktop environment rules
|
||||
- Support `subtitle`
|
||||
- Do not support user text input
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. Check and request for authorization:
|
||||
- macOS requires user authorization
|
||||
|
||||
2. Provide clear and concise notifications:
|
||||
- Use descriptive titles, subtitles, text, and action titles
|
||||
|
||||
3. Handle dialog responses appropriately:
|
||||
- Check for errors in notification responses
|
||||
- Provide feedback for user actions
|
||||
|
||||
4. Consider platform conventions:
|
||||
- Follow platform-specific notification patterns
|
||||
- Respect system settings
|
||||
|
||||
## Examples
|
||||
|
||||
Explore this example:
|
||||
|
||||
- [Notifications](/examples/notifications)
|
||||
|
||||
## API Reference
|
||||
|
||||
### Service Management
|
||||
| Method | Description |
|
||||
|--------------------------------------------|-------------------------------------------------------|
|
||||
| `New()` | Creates a new notifications service |
|
||||
|
||||
### Notification Authorization
|
||||
| Method | Description |
|
||||
|----------------------------------------------|------------------------------------------------------------|
|
||||
| `RequestNotificationAuthorization()` | Requests permission to display notifications (macOS) |
|
||||
| `CheckNotificationAuthorization()` | Checks current notification authorization status (macOS) |
|
||||
|
||||
### Sending Notifications
|
||||
| Method | Description |
|
||||
|------------------------------------------------------------|---------------------------------------------------|
|
||||
| `SendNotification(options NotificationOptions)` | Sends a basic notification |
|
||||
| `SendNotificationWithActions(options NotificationOptions)` | Sends an interactive notification with actions |
|
||||
|
||||
### Notification Categories
|
||||
| Method | Description |
|
||||
|---------------------------------------------------------------|---------------------------------------------------|
|
||||
| `RegisterNotificationCategory(category NotificationCategory)` | Registers a reusable notification category |
|
||||
| `RemoveNotificationCategory(categoryID string)` | Removes a previously registered category |
|
||||
|
||||
### Managing Notifications
|
||||
| Method | Description |
|
||||
|-------------------------------------------------|---------------------------------------------------------------------|
|
||||
| `RemoveAllPendingNotifications()` | Removes all pending notifications (macOS and Linux only) |
|
||||
| `RemovePendingNotification(identifier string)` | Removes a specific pending notification (macOS and Linux only) |
|
||||
| `RemoveAllDeliveredNotifications()` | Removes all delivered notifications (macOS and Linux only) |
|
||||
| `RemoveDeliveredNotification(identifier string)`| Removes a specific delivered notification (macOS and Linux only) |
|
||||
| `RemoveNotification(identifier string)` | Removes a notification (Linux-specific) |
|
||||
|
||||
### Event Handling
|
||||
| Method | Description |
|
||||
|--------------------------------------------------------------------|-------------------------------------------------|
|
||||
| `OnNotificationResponse(callback func(result NotificationResult))` | Registers a callback for notification responses |
|
||||
|
||||
### Structs and Types
|
||||
|
||||
#### NotificationOptions
|
||||
```go
|
||||
type NotificationOptions struct {
|
||||
ID string // Unique identifier for the notification
|
||||
Title string // Main notification title
|
||||
Subtitle string // Subtitle text (macOS and Linux only)
|
||||
Body string // Main notification content
|
||||
CategoryID string // Category identifier for interactive notifications
|
||||
Data map[string]interface{} // Custom data to associate with the notification
|
||||
}
|
||||
```
|
||||
|
||||
#### NotificationCategory
|
||||
```go
|
||||
type NotificationCategory struct {
|
||||
ID string // Unique identifier for the category
|
||||
Actions []NotificationAction // Button actions for the notification
|
||||
HasReplyField bool // Whether to include a text input field
|
||||
ReplyPlaceholder string // Placeholder text for the input field
|
||||
ReplyButtonTitle string // Text for the reply button
|
||||
}
|
||||
```
|
||||
|
||||
#### NotificationAction
|
||||
```go
|
||||
type NotificationAction struct {
|
||||
ID string // Unique identifier for the action
|
||||
Title string // Button text
|
||||
Destructive bool // Whether the action is destructive (macOS-specific)
|
||||
}
|
||||
```
|
||||
|
||||
#### NotificationResponse
|
||||
```go
|
||||
type NotificationResponse struct {
|
||||
ID string // Notification identifier
|
||||
ActionIdentifier string // Action that was triggered
|
||||
CategoryID string // Category of the notification
|
||||
Title string // Title of the notification
|
||||
Subtitle string // Subtitle of the notification
|
||||
Body string // Body text of the notification
|
||||
UserText string // Text entered by the user
|
||||
UserInfo map[string]interface{} // Custom data from the notification
|
||||
}
|
||||
```
|
||||
|
||||
#### NotificationResult
|
||||
```go
|
||||
type NotificationResult struct {
|
||||
Response NotificationResponse // Response data
|
||||
Error error // Any error that occurred
|
||||
}
|
||||
```
|
59
v3/examples/notifications/README.md
Normal file
59
v3/examples/notifications/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Welcome to Your New Wails3 Project!
|
||||
|
||||
Congratulations on generating your Wails3 application! This README will guide you through the next steps to get your project up and running.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Navigate to your project directory in the terminal.
|
||||
|
||||
2. To run your application in development mode, use the following command:
|
||||
|
||||
```
|
||||
wails3 dev
|
||||
```
|
||||
|
||||
This will start your application and enable hot-reloading for both frontend and backend changes.
|
||||
|
||||
3. To build your application for production, use:
|
||||
|
||||
```
|
||||
wails3 build
|
||||
```
|
||||
|
||||
This will create a production-ready executable in the `build` directory.
|
||||
|
||||
## Exploring Wails3 Features
|
||||
|
||||
Now that you have your project set up, it's time to explore the features that Wails3 offers:
|
||||
|
||||
1. **Check out the examples**: The best way to learn is by example. Visit the `examples` directory in the `v3/examples` directory to see various sample applications.
|
||||
|
||||
2. **Run an example**: To run any of the examples, navigate to the example's directory and use:
|
||||
|
||||
```
|
||||
go run .
|
||||
```
|
||||
|
||||
Note: Some examples may be under development during the alpha phase.
|
||||
|
||||
3. **Explore the documentation**: Visit the [Wails3 documentation](https://v3.wails.io/) for in-depth guides and API references.
|
||||
|
||||
4. **Join the community**: Have questions or want to share your progress? Join the [Wails Discord](https://discord.gg/JDdSxwjhGf) or visit the [Wails discussions on GitHub](https://github.com/wailsapp/wails/discussions).
|
||||
|
||||
## Project Structure
|
||||
|
||||
Take a moment to familiarize yourself with your project structure:
|
||||
|
||||
- `frontend/`: Contains your frontend code (HTML, CSS, JavaScript/TypeScript)
|
||||
- `main.go`: The entry point of your Go backend
|
||||
- `app.go`: Define your application structure and methods here
|
||||
- `wails.json`: Configuration file for your Wails project
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Modify the frontend in the `frontend/` directory to create your desired UI.
|
||||
2. Add backend functionality in `main.go`.
|
||||
3. Use `wails3 dev` to see your changes in real-time.
|
||||
4. When ready, build your application with `wails3 build`.
|
||||
|
||||
Happy coding with Wails3! If you encounter any issues or have questions, don't hesitate to consult the documentation or reach out to the Wails community.
|
34
v3/examples/notifications/Taskfile.yml
Normal file
34
v3/examples/notifications/Taskfile.yml
Normal file
@ -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: "Notifications\\ Demo"
|
||||
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}}
|
||||
|
86
v3/examples/notifications/build/Taskfile.yml
Normal file
86
v3/examples/notifications/build/Taskfile.yml
Normal file
@ -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 .
|
BIN
v3/examples/notifications/build/appicon.png
Normal file
BIN
v3/examples/notifications/build/appicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 130 KiB |
62
v3/examples/notifications/build/config.yml
Normal file
62
v3/examples/notifications/build/config.yml
Normal file
@ -0,0 +1,62 @@
|
||||
# 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: "v0.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
|
||||
|
||||
# Other data
|
||||
other:
|
||||
- name: My Other Data
|
32
v3/examples/notifications/build/darwin/Info.dev.plist
Normal file
32
v3/examples/notifications/build/darwin/Info.dev.plist
Normal file
@ -0,0 +1,32 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Notifications Demo</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.notifications-demo</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© now, My Company</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
27
v3/examples/notifications/build/darwin/Info.plist
Normal file
27
v3/examples/notifications/build/darwin/Info.plist
Normal file
@ -0,0 +1,27 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>My Product</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Notifications Demo</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.notifications-demo</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>This is a comment</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icons</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.15.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>© now, My Company</string>
|
||||
</dict>
|
||||
</plist>
|
80
v3/examples/notifications/build/darwin/Taskfile.yml
Normal file
80
v3/examples/notifications/build/darwin/Taskfile.yml
Normal file
@ -0,0 +1,80 @@
|
||||
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
|
||||
|
||||
run:
|
||||
cmds:
|
||||
- mkdir -p {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/{MacOS,Resources}
|
||||
- cp build/darwin/icons.icns {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Resources
|
||||
- cp {{.BIN_DIR}}/{{.APP_NAME}} {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS
|
||||
- cp build/darwin/Info.dev.plist {{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/Info.plist
|
||||
- codesign --force --deep --sign - {{.BIN_DIR}}/dev/{{.APP_NAME}}.app
|
||||
- '{{.BIN_DIR}}/dev/{{.APP_NAME}}.app/Contents/MacOS/{{.APP_NAME}}'
|
BIN
v3/examples/notifications/build/darwin/icons.icns
Normal file
BIN
v3/examples/notifications/build/darwin/icons.icns
Normal file
Binary file not shown.
119
v3/examples/notifications/build/linux/Taskfile.yml
Normal file
119
v3/examples/notifications/build/linux/Taskfile.yml
Normal file
@ -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}}'
|
35
v3/examples/notifications/build/linux/appimage/build.sh
Normal file
35
v3/examples/notifications/build/linux/appimage/build.sh
Normal file
@ -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"
|
||||
|
50
v3/examples/notifications/build/linux/nfpm/nfpm.yaml
Normal file
50
v3/examples/notifications/build/linux/nfpm/nfpm.yaml
Normal file
@ -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: "notifications"
|
||||
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/notifications"
|
||||
dst: "/usr/local/bin/notifications"
|
||||
- src: "./build/appicon.png"
|
||||
dst: "/usr/share/icons/hicolor/128x128/apps/notifications.png"
|
||||
- src: "./build/linux/notifications.desktop"
|
||||
dst: "/usr/share/applications/notifications.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
|
@ -0,0 +1 @@
|
||||
#!/bin/bash
|
@ -0,0 +1 @@
|
||||
#!/bin/bash
|
@ -0,0 +1 @@
|
||||
#!/bin/bash
|
@ -0,0 +1 @@
|
||||
#!/bin/bash
|
63
v3/examples/notifications/build/windows/Taskfile.yml
Normal file
63
v3/examples/notifications/build/windows/Taskfile.yml
Normal file
@ -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'
|
BIN
v3/examples/notifications/build/windows/icon.ico
Normal file
BIN
v3/examples/notifications/build/windows/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
15
v3/examples/notifications/build/windows/info.json
Normal file
15
v3/examples/notifications/build/windows/info.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
112
v3/examples/notifications/build/windows/nsis/project.nsi
Normal file
112
v3/examples/notifications/build/windows/nsis/project.nsi
Normal file
@ -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 "notifications"
|
||||
## !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
|
212
v3/examples/notifications/build/windows/nsis/wails_tools.nsh
Normal file
212
v3/examples/notifications/build/windows/nsis/wails_tools.nsh
Normal file
@ -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 "notifications"
|
||||
!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
|
15
v3/examples/notifications/build/windows/wails.exe.manifest
Normal file
15
v3/examples/notifications/build/windows/wails.exe.manifest
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.notifications" version="0.1.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
93
v3/examples/notifications/frontend/Inter Font License.txt
Normal file
93
v3/examples/notifications/frontend/Inter Font License.txt
Normal file
@ -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.
|
@ -0,0 +1,13 @@
|
||||
// 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 {
|
||||
NotificationAction,
|
||||
NotificationCategory,
|
||||
NotificationOptions
|
||||
} from "./models.js";
|
@ -0,0 +1,107 @@
|
||||
// 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";
|
||||
|
||||
/**
|
||||
* NotificationAction represents an action button for a notification.
|
||||
*/
|
||||
export class NotificationAction {
|
||||
"id"?: string;
|
||||
"title"?: string;
|
||||
|
||||
/**
|
||||
* (macOS-specific)
|
||||
*/
|
||||
"destructive"?: boolean;
|
||||
|
||||
/** Creates a new NotificationAction instance. */
|
||||
constructor($$source: Partial<NotificationAction> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationAction instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationAction {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationCategory groups actions for notifications.
|
||||
*/
|
||||
export class NotificationCategory {
|
||||
"id"?: string;
|
||||
"actions"?: NotificationAction[];
|
||||
"hasReplyField"?: boolean;
|
||||
"replyPlaceholder"?: string;
|
||||
"replyButtonTitle"?: string;
|
||||
|
||||
/** Creates a new NotificationCategory instance. */
|
||||
constructor($$source: Partial<NotificationCategory> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationCategory instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationCategory {
|
||||
const $$createField1_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("actions" in $$parsedSource) {
|
||||
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
|
||||
}
|
||||
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationOptions contains configuration for a notification
|
||||
*/
|
||||
export class NotificationOptions {
|
||||
"id": string;
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* (macOS and Linux only)
|
||||
*/
|
||||
"subtitle"?: string;
|
||||
"body"?: string;
|
||||
"categoryId"?: string;
|
||||
"data"?: { [_: string]: any };
|
||||
|
||||
/** Creates a new NotificationOptions instance. */
|
||||
constructor($$source: Partial<NotificationOptions> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationOptions instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationOptions {
|
||||
const $$createField5_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("data" in $$parsedSource) {
|
||||
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
|
||||
}
|
||||
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = NotificationAction.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
@ -0,0 +1,62 @@
|
||||
// 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";
|
||||
|
||||
export function CheckNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2789931702);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise<void> {
|
||||
return $Call.ByID(2679064664, category);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(384520397);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(1423986276);
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(149440045, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3702062929, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(229511469, categoryID);
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3872412470, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public methods that delegate to the implementation.
|
||||
*/
|
||||
export function RequestNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(729898933);
|
||||
}
|
||||
|
||||
export function SendNotification(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(2246903123, options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(1615199806, options);
|
||||
}
|
30
v3/examples/notifications/frontend/index.html
Normal file
30
v3/examples/notifications/frontend/index.html
Normal file
@ -0,0 +1,30 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/wails.png"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<link rel="stylesheet" href="/style.css"/>
|
||||
<title>Wails App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div>
|
||||
<a data-wml-openURL="https://wails.io">
|
||||
<img src="/wails.png" class="logo" alt="Wails logo"/>
|
||||
</a>
|
||||
<a data-wml-openURL="https://www.typescriptlang.org/">
|
||||
<img src="/typescript.svg" class="logo vanilla" alt="Typescript logo"/>
|
||||
</a>
|
||||
</div>
|
||||
<h1>Wails + Typescript + Desktop Notifications</h1>
|
||||
<h3>Send notifications 👇</h3>
|
||||
<div class="controls">
|
||||
<button class="btn" id="basic">Basic</button>
|
||||
<button class="btn" id="complex">Complex</button>
|
||||
</div>
|
||||
<div class="footer" id="response"></div>
|
||||
</div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
935
v3/examples/notifications/frontend/package-lock.json
generated
Normal file
935
v3/examples/notifications/frontend/package-lock.json
generated
Normal file
@ -0,0 +1,935 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@wailsio/runtime": "latest",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
|
||||
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
|
||||
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
|
||||
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
|
||||
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
|
||||
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
|
||||
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
|
||||
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
|
||||
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
|
||||
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.37.0.tgz",
|
||||
"integrity": "sha512-l7StVw6WAa8l3vA1ov80jyetOAEo1FtHvZDbzXDO/02Sq/QVvqlHkYoFwDJPIMj0GKiistsBudfx5tGFnwYWDQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.37.0.tgz",
|
||||
"integrity": "sha512-6U3SlVyMxezt8Y+/iEBcbp945uZjJwjZimu76xoG7tO1av9VO691z8PkhzQ85ith2I8R2RddEPeSfcbyPfD4hA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.37.0.tgz",
|
||||
"integrity": "sha512-+iTQ5YHuGmPt10NTzEyMPbayiNTcOZDWsbxZYR1ZnmLnZxG17ivrPSWFO9j6GalY0+gV3Jtwrrs12DBscxnlYA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.37.0.tgz",
|
||||
"integrity": "sha512-m8W2UbxLDcmRKVjgl5J/k4B8d7qX2EcJve3Sut7YGrQoPtCIQGPH5AMzuFvYRWZi0FVS0zEY4c8uttPfX6bwYQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.37.0.tgz",
|
||||
"integrity": "sha512-FOMXGmH15OmtQWEt174v9P1JqqhlgYge/bUjIbiVD1nI1NeJ30HYT9SJlZMqdo1uQFyt9cz748F1BHghWaDnVA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.37.0.tgz",
|
||||
"integrity": "sha512-SZMxNttjPKvV14Hjck5t70xS3l63sbVwl98g3FlVVx2YIDmfUIy29jQrsw06ewEYQ8lQSuY9mpAPlmgRD2iSsA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.37.0.tgz",
|
||||
"integrity": "sha512-hhAALKJPidCwZcj+g+iN+38SIOkhK2a9bqtJR+EtyxrKKSt1ynCBeqrQy31z0oWU6thRZzdx53hVgEbRkuI19w==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.37.0.tgz",
|
||||
"integrity": "sha512-jUb/kmn/Gd8epbHKEqkRAxq5c2EwRt0DqhSGWjPFxLeFvldFdHQs/n8lQ9x85oAeVb6bHcS8irhTJX2FCOd8Ag==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-oNrJxcQT9IcbcmKlkF+Yz2tmOxZgG9D9GRq+1OE6XCQwCVwxixYAa38Z8qqPzQvzt1FCfmrHX03E0pWoXm1DqA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.37.0.tgz",
|
||||
"integrity": "sha512-pfxLBMls+28Ey2enpX3JvjEjaJMBX5XlPCZNGxj4kdJyHduPBXtxYeb8alo0a7bqOoWZW2uKynhHxF/MWoHaGQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-yCE0NnutTC/7IGUq/PUHmoeZbIwq3KRh02e9SfFh7Vmc1Z7atuJRYWhRME5fKgT8aS20mwi1RyChA23qSyRGpA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-NxcICptHk06E2Lh3a4Pu+2PEdZ6ahNHuK7o6Np9zcWkrBMuv21j10SQDJW3C9Yf/A/P7cutWoC/DptNLVsZ0VQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-PpWwHMPCVpFZLTfLq7EWJWvrmEuLdGn1GMYcm5MV7PaRgwCEYJAwiN94uBuZev0/J/hFIIJCsYw4nLmXA9J7Pw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.37.0.tgz",
|
||||
"integrity": "sha512-DTNwl6a3CfhGTAOYZ4KtYbdS8b+275LSLqJVJIrPa5/JuIufWWZ/QFvkxp52gpmguN95eujrM68ZG+zVxa8zHA==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-hZDDU5fgWvDdHFuExN1gBOhCuzo/8TMpidfOR+1cPZJflcEzXdCy1LjnklQdW8/Et9sryOPJAKAQRw8Jq7Tg+A==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.37.0.tgz",
|
||||
"integrity": "sha512-pKivGpgJM5g8dwj0ywBwe/HeVAUSuVVJhUTa/URXjxvoyTT/AxsLTAbkHkDHG7qQxLoW2s3apEIl26uUe08LVQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.37.0.tgz",
|
||||
"integrity": "sha512-E2lPrLKE8sQbY/2bEkVTGDEk4/49UYRVWgj90MY8yPjpnGBQ+Xi1Qnr7b7UIWw1NOggdFQFOLZ8+5CzCiz143w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.37.0.tgz",
|
||||
"integrity": "sha512-Jm7biMazjNzTU4PrQtr7VS8ibeys9Pn29/1bm4ph7CP2kf21950LgN+BaE2mJ1QujnvOc6p54eWWiVvn05SOBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.37.0.tgz",
|
||||
"integrity": "sha512-e3/1SFm1OjefWICB2Ucstg2dxYDkDTZGDYgwufcbsxTHyqQps1UQf33dFEChBNmeSsTOyrjw2JJq0zbG5GF6RA==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.37.0.tgz",
|
||||
"integrity": "sha512-LWbXUBwn/bcLx2sSsqy7pK5o+Nr+VCoRoAohfJ5C/aBio9nfJmGQqHAhU6pwxV/RmyTk5AqdySma7uwWGlmeuA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@wailsio/runtime": {
|
||||
"version": "3.0.0-alpha.66",
|
||||
"resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.66.tgz",
|
||||
"integrity": "sha512-ENLu8rn1griL1gFHJqkq1i+BVxrrA0JPJHYneUJYuf/s54kjuQViW0RKDEe/WTDo56ABpfykrd/T8OYpPUyXUw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.21.5",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
|
||||
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.21.5",
|
||||
"@esbuild/android-arm": "0.21.5",
|
||||
"@esbuild/android-arm64": "0.21.5",
|
||||
"@esbuild/android-x64": "0.21.5",
|
||||
"@esbuild/darwin-arm64": "0.21.5",
|
||||
"@esbuild/darwin-x64": "0.21.5",
|
||||
"@esbuild/freebsd-arm64": "0.21.5",
|
||||
"@esbuild/freebsd-x64": "0.21.5",
|
||||
"@esbuild/linux-arm": "0.21.5",
|
||||
"@esbuild/linux-arm64": "0.21.5",
|
||||
"@esbuild/linux-ia32": "0.21.5",
|
||||
"@esbuild/linux-loong64": "0.21.5",
|
||||
"@esbuild/linux-mips64el": "0.21.5",
|
||||
"@esbuild/linux-ppc64": "0.21.5",
|
||||
"@esbuild/linux-riscv64": "0.21.5",
|
||||
"@esbuild/linux-s390x": "0.21.5",
|
||||
"@esbuild/linux-x64": "0.21.5",
|
||||
"@esbuild/netbsd-x64": "0.21.5",
|
||||
"@esbuild/openbsd-x64": "0.21.5",
|
||||
"@esbuild/sunos-x64": "0.21.5",
|
||||
"@esbuild/win32-arm64": "0.21.5",
|
||||
"@esbuild/win32-ia32": "0.21.5",
|
||||
"@esbuild/win32-x64": "0.21.5"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.37.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.37.0.tgz",
|
||||
"integrity": "sha512-iAtQy/L4QFU+rTJ1YUjXqJOJzuwEghqWzCEYD2FEghT7Gsy1VdABntrO4CLopA5IkflTyqNiLNwPcOJ3S7UKLg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "1.0.6"
|
||||
},
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.37.0",
|
||||
"@rollup/rollup-android-arm64": "4.37.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.37.0",
|
||||
"@rollup/rollup-darwin-x64": "4.37.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.37.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.37.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.37.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.37.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.37.0",
|
||||
"@rollup/rollup-linux-loongarch64-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-powerpc64le-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.37.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.37.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.37.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.37.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.37.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.37.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.15",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.15.tgz",
|
||||
"integrity": "sha512-6ANcZRivqL/4WtwPGTKNaosuNJr5tWiftOC7liM7G9+rMb8+oeJeyzymDu4rTN93seySBmbjSfsS3Vzr19KNtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.21.3",
|
||||
"postcss": "^8.4.43",
|
||||
"rollup": "^4.20.0"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.0.0 || >=20.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/vitejs/vite?sponsor=1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"less": "*",
|
||||
"lightningcss": "^1.21.0",
|
||||
"sass": "*",
|
||||
"sass-embedded": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"lightningcss": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"sass-embedded": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
17
v3/examples/notifications/frontend/package.json
Normal file
17
v3/examples/notifications/frontend/package.json
Normal file
@ -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"
|
||||
}
|
||||
}
|
BIN
v3/examples/notifications/frontend/public/Inter-Medium.ttf
Normal file
BIN
v3/examples/notifications/frontend/public/Inter-Medium.ttf
Normal file
Binary file not shown.
131
v3/examples/notifications/frontend/public/style.css
Normal file
131
v3/examples/notifications/frontend/public/style.css
Normal file
@ -0,0 +1,131 @@
|
||||
: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");
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 1em;
|
||||
}
|
||||
|
||||
button {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
margin: 0 0 0 20px;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
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;
|
||||
padding: 3em;
|
||||
}
|
||||
|
||||
h1, h3 {
|
||||
line-height: 1.1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 1rem;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer table {
|
||||
font-size: 12px;
|
||||
border-collapse: collapse;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.footer table, th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
1
v3/examples/notifications/frontend/public/typescript.svg
Normal file
1
v3/examples/notifications/frontend/public/typescript.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 256"><path fill="#007ACC" d="M0 128v128h256V0H0z"></path><path fill="#FFF" d="m56.612 128.85l-.081 10.483h33.32v94.68h23.568v-94.68h33.321v-10.28c0-5.69-.122-10.444-.284-10.566c-.122-.162-20.4-.244-44.983-.203l-44.74.122l-.121 10.443Zm149.955-10.742c6.501 1.625 11.459 4.51 16.01 9.224c2.357 2.52 5.851 7.111 6.136 8.208c.08.325-11.053 7.802-17.798 11.988c-.244.162-1.22-.894-2.317-2.52c-3.291-4.795-6.745-6.867-12.028-7.233c-7.76-.528-12.759 3.535-12.718 10.321c0 1.992.284 3.17 1.097 4.795c1.707 3.536 4.876 5.649 14.832 9.956c18.326 7.883 26.168 13.084 31.045 20.48c5.445 8.249 6.664 21.415 2.966 31.208c-4.063 10.646-14.14 17.879-28.323 20.276c-4.388.772-14.79.65-19.504-.203c-10.28-1.828-20.033-6.908-26.047-13.572c-2.357-2.6-6.949-9.387-6.664-9.874c.122-.163 1.178-.813 2.356-1.504c1.138-.65 5.446-3.129 9.509-5.485l7.355-4.267l1.544 2.276c2.154 3.29 6.867 7.801 9.712 9.305c8.167 4.307 19.383 3.698 24.909-1.26c2.357-2.153 3.332-4.388 3.332-7.68c0-2.966-.366-4.266-1.91-6.501c-1.99-2.845-6.054-5.242-17.595-10.24c-13.206-5.69-18.895-9.224-24.096-14.832c-3.007-3.25-5.852-8.452-7.03-12.8c-.975-3.617-1.22-12.678-.447-16.335c2.723-12.76 12.353-21.659 26.25-24.3c4.51-.853 14.994-.528 19.424.569Z"></path></svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
v3/examples/notifications/frontend/public/wails.png
Normal file
BIN
v3/examples/notifications/frontend/public/wails.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
95
v3/examples/notifications/frontend/src/main.ts
Normal file
95
v3/examples/notifications/frontend/src/main.ts
Normal file
@ -0,0 +1,95 @@
|
||||
import { Events } from "@wailsio/runtime";
|
||||
import * as Notifications from "../bindings/github.com/wailsapp/wails/v3/pkg/services/notifications";
|
||||
|
||||
document.querySelector("#basic")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const authorized = await Notifications.Service.CheckNotificationAuthorization();
|
||||
if (authorized) {
|
||||
await Notifications.Service.SendNotification({
|
||||
id: crypto.randomUUID(),
|
||||
title: "Notification Title",
|
||||
subtitle: "Subtitle on macOS and Linux",
|
||||
body: "Body text of notification.",
|
||||
data: {
|
||||
"user-id": "user-123",
|
||||
"message-id": "msg-123",
|
||||
"timestamp": Date.now(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
document.querySelector("#complex")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const authorized = await Notifications.Service.CheckNotificationAuthorization();
|
||||
if (authorized) {
|
||||
const CategoryID = "frontend-notification-id";
|
||||
|
||||
await Notifications.Service.RegisterNotificationCategory({
|
||||
id: CategoryID,
|
||||
actions: [
|
||||
{ id: "VIEW", title: "View" },
|
||||
{ id: "MARK_READ", title: "Mark as read" },
|
||||
{ id: "DELETE", title: "Delete", destructive: true },
|
||||
],
|
||||
hasReplyField: true,
|
||||
replyPlaceholder: "Message...",
|
||||
replyButtonTitle: "Reply",
|
||||
});
|
||||
|
||||
await Notifications.Service.SendNotificationWithActions({
|
||||
id: crypto.randomUUID(),
|
||||
title: "Notification Title",
|
||||
subtitle: "Subtitle on macOS and Linux",
|
||||
body: "Body text of notification.",
|
||||
categoryId: CategoryID,
|
||||
data: {
|
||||
"user-id": "user-123",
|
||||
"message-id": "msg-123",
|
||||
"timestamp": Date.now(),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
console.warn("Notifications are not authorized.\n You can attempt to request again or let the user know in the UI.\n");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
||||
const unlisten = Events.On("notification:action", (response) => {
|
||||
console.info(`Recieved a ${response.name} event`);
|
||||
const { userInfo, ...base } = response.data[0];
|
||||
console.info("Notification Response:");
|
||||
console.table(base);
|
||||
console.info("Notification Response Metadata:");
|
||||
console.table(userInfo);
|
||||
const table = `
|
||||
<h5>Notification Response</h5>
|
||||
<table>
|
||||
<thead>
|
||||
${Object.keys(base).map(key => `<th>${key}</th>`).join("")}
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.values(base).map(value => `<td>${value}</td>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
<h5>Notification Metadata</h5>
|
||||
<table>
|
||||
<thead>
|
||||
${Object.keys(userInfo).map(key => `<th>${key}</th>`).join("")}
|
||||
</thead>
|
||||
<tbody>
|
||||
${Object.values(userInfo).map(value => `<td>${value}</td>`).join("")}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
const footer = document.querySelector("#response");
|
||||
if (footer) footer.innerHTML = table;
|
||||
});
|
||||
|
||||
window.onbeforeunload = () => unlisten();
|
1
v3/examples/notifications/frontend/src/vite-env.d.ts
vendored
Normal file
1
v3/examples/notifications/frontend/src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
20
v3/examples/notifications/frontend/tsconfig.json
Normal file
20
v3/examples/notifications/frontend/tsconfig.json
Normal file
@ -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"]
|
||||
}
|
53
v3/examples/notifications/go.mod
Normal file
53
v3/examples/notifications/go.mod
Normal file
@ -0,0 +1,53 @@
|
||||
module notifications
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.1
|
||||
|
||||
require github.com/wailsapp/wails/v3 v3.0.0-dev
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.1 // indirect
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/adrg/xdg v0.5.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.2 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/go-git/go-git/v5 v5.13.2 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lmittmann/tint v1.0.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // 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.36.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
||||
|
||||
replace github.com/wailsapp/wails/v3 => ../wails/v3
|
154
v3/examples/notifications/main.go
Normal file
154
v3/examples/notifications/main.go
Normal file
@ -0,0 +1,154 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
"github.com/wailsapp/wails/v3/pkg/services/notifications"
|
||||
)
|
||||
|
||||
// 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 Notification Service
|
||||
ns := notifications.New()
|
||||
|
||||
// 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.
|
||||
app := application.New(application.Options{
|
||||
Name: "Notifications Demo",
|
||||
Description: "A demo of using desktop notifications with Wails",
|
||||
Services: []application.Service{
|
||||
application.NewService(ns),
|
||||
},
|
||||
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.OnApplicationEvent(events.Common.ApplicationStarted, func(event *application.ApplicationEvent) {
|
||||
// Create a goroutine that spawns desktop notifications from Go
|
||||
go func() {
|
||||
var authorized bool
|
||||
var err error
|
||||
authorized, err = ns.CheckNotificationAuthorization()
|
||||
if err != nil {
|
||||
println(fmt.Errorf("checking app notification authorization failed: %s", err))
|
||||
}
|
||||
|
||||
if !authorized {
|
||||
authorized, err = ns.RequestNotificationAuthorization()
|
||||
if err != nil {
|
||||
println(fmt.Errorf("requesting app notification authorization failed: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if authorized {
|
||||
ns.OnNotificationResponse(func(result notifications.NotificationResult) {
|
||||
if result.Error != nil {
|
||||
println(fmt.Errorf("parsing notification result failed: %s", result.Error))
|
||||
} else {
|
||||
fmt.Printf("Response: %+v\n", result.Response)
|
||||
println("Sending response to frontend...")
|
||||
app.EmitEvent("notification:action", result.Response)
|
||||
}
|
||||
})
|
||||
|
||||
err = ns.SendNotification(notifications.NotificationOptions{
|
||||
ID: "uuid-basic-1",
|
||||
Title: "Notification Title",
|
||||
Subtitle: "Subtitle on macOS and Linux",
|
||||
Body: "Body text of notification.",
|
||||
Data: map[string]interface{}{
|
||||
"user-id": "user-123",
|
||||
"message-id": "msg-123",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
println(fmt.Errorf("sending basic notification failed: %s", err))
|
||||
}
|
||||
|
||||
// Delay before sending next notification
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
const CategoryID = "backend-notification-id"
|
||||
|
||||
err = ns.RegisterNotificationCategory(notifications.NotificationCategory{
|
||||
ID: CategoryID,
|
||||
Actions: []notifications.NotificationAction{
|
||||
{ID: "VIEW", Title: "View"},
|
||||
{ID: "MARK_READ", Title: "Mark as read"},
|
||||
{ID: "DELETE", Title: "Delete", Destructive: true},
|
||||
},
|
||||
HasReplyField: true,
|
||||
ReplyPlaceholder: "Message...",
|
||||
ReplyButtonTitle: "Reply",
|
||||
})
|
||||
if err != nil {
|
||||
println(fmt.Errorf("creating notification category failed: %s", err))
|
||||
}
|
||||
|
||||
err = ns.SendNotificationWithActions(notifications.NotificationOptions{
|
||||
ID: "uuid-with-actions-1",
|
||||
Title: "Actions Notification Title",
|
||||
Subtitle: "Subtitle on macOS and Linux",
|
||||
Body: "Body text of notification with actions.",
|
||||
CategoryID: CategoryID,
|
||||
Data: map[string]interface{}{
|
||||
"user-id": "user-123",
|
||||
"message-id": "msg-123",
|
||||
"timestamp": time.Now().Unix(),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
println(fmt.Errorf("sending notification with actions failed: %s", err))
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ module github.com/wailsapp/wails/v3
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3
|
||||
github.com/Masterminds/semver v1.5.0
|
||||
github.com/adrg/xdg v0.5.3
|
||||
github.com/atterpac/refresh v0.8.6
|
||||
|
@ -8,6 +8,8 @@ atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs=
|
||||
atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU=
|
||||
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
|
||||
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
|
@ -345,6 +345,10 @@ type App struct {
|
||||
singleInstanceManager *singleInstanceManager
|
||||
}
|
||||
|
||||
func (a *App) Config() Options {
|
||||
return a.options
|
||||
}
|
||||
|
||||
func (a *App) handleWarning(msg string) {
|
||||
if a.options.WarningHandler != nil {
|
||||
a.options.WarningHandler(msg)
|
||||
|
216
v3/pkg/services/notifications/notifications.go
Normal file
216
v3/pkg/services/notifications/notifications.go
Normal file
@ -0,0 +1,216 @@
|
||||
// Package notifications provides cross-platform notification capabilities for desktop applications.
|
||||
// It supports macOS, Windows, and Linux with a consistent API while handling platform-specific
|
||||
// differences internally. Key features include:
|
||||
// - Basic notifications with title, subtitle, and body
|
||||
// - Interactive notifications with buttons and actions
|
||||
// - Notification categories for reusing configurations
|
||||
// - User feedback handling with a unified callback system
|
||||
//
|
||||
// Platform-specific notes:
|
||||
// - macOS: Requires a properly bundled and signed application
|
||||
// - Windows: Uses Windows Toast notifications
|
||||
// - Linux: Uses D-Bus and does not support text inputs
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type platformNotifier interface {
|
||||
// Lifecycle methods
|
||||
Startup(ctx context.Context, options application.ServiceOptions) error
|
||||
Shutdown() error
|
||||
|
||||
// Core notification methods
|
||||
RequestNotificationAuthorization() (bool, error)
|
||||
CheckNotificationAuthorization() (bool, error)
|
||||
SendNotification(options NotificationOptions) error
|
||||
SendNotificationWithActions(options NotificationOptions) error
|
||||
|
||||
// Category management
|
||||
RegisterNotificationCategory(category NotificationCategory) error
|
||||
RemoveNotificationCategory(categoryID string) error
|
||||
|
||||
// Notification management
|
||||
RemoveAllPendingNotifications() error
|
||||
RemovePendingNotification(identifier string) error
|
||||
RemoveAllDeliveredNotifications() error
|
||||
RemoveDeliveredNotification(identifier string) error
|
||||
RemoveNotification(identifier string) error
|
||||
}
|
||||
|
||||
// Service represents the notifications service
|
||||
type Service struct {
|
||||
impl platformNotifier
|
||||
|
||||
// notificationResponseCallback is called when a notification result is received.
|
||||
// Only one callback can be assigned at a time.
|
||||
notificationResultCallback func(result NotificationResult)
|
||||
|
||||
callbackLock sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
notificationServiceOnce sync.Once
|
||||
NotificationService *Service
|
||||
notificationServiceLock sync.RWMutex
|
||||
)
|
||||
|
||||
// NotificationAction represents an action button for a notification.
|
||||
type NotificationAction struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Destructive bool `json:"destructive,omitempty"` // (macOS-specific)
|
||||
}
|
||||
|
||||
// NotificationCategory groups actions for notifications.
|
||||
type NotificationCategory struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Actions []NotificationAction `json:"actions,omitempty"`
|
||||
HasReplyField bool `json:"hasReplyField,omitempty"`
|
||||
ReplyPlaceholder string `json:"replyPlaceholder,omitempty"`
|
||||
ReplyButtonTitle string `json:"replyButtonTitle,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationOptions contains configuration for a notification
|
||||
type NotificationOptions struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
|
||||
Body string `json:"body,omitempty"`
|
||||
CategoryID string `json:"categoryId,omitempty"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
const DefaultActionIdentifier = "DEFAULT_ACTION"
|
||||
|
||||
// NotificationResponse represents the response sent by interacting with a notification.
|
||||
type NotificationResponse struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
ActionIdentifier string `json:"actionIdentifier,omitempty"`
|
||||
CategoryID string `json:"categoryIdentifier,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Subtitle string `json:"subtitle,omitempty"` // (macOS and Linux only)
|
||||
Body string `json:"body,omitempty"`
|
||||
UserText string `json:"userText,omitempty"`
|
||||
UserInfo map[string]interface{} `json:"userInfo,omitempty"`
|
||||
}
|
||||
|
||||
// NotificationResult represents the result of a notification response,
|
||||
// returning the response or any errors that occurred.
|
||||
type NotificationResult struct {
|
||||
Response NotificationResponse
|
||||
Error error
|
||||
}
|
||||
|
||||
// ServiceName returns the name of the service.
|
||||
func (ns *Service) ServiceName() string {
|
||||
return "github.com/wailsapp/wails/v3/services/notifications"
|
||||
}
|
||||
|
||||
// OnNotificationResponse registers a callback function that will be called when
|
||||
// a notification response is received from the user.
|
||||
//
|
||||
//wails:ignore
|
||||
func (ns *Service) OnNotificationResponse(callback func(result NotificationResult)) {
|
||||
ns.callbackLock.Lock()
|
||||
defer ns.callbackLock.Unlock()
|
||||
|
||||
ns.notificationResultCallback = callback
|
||||
}
|
||||
|
||||
// handleNotificationResponse is an internal method to handle notification responses
|
||||
// and invoke the registered callback if one exists.
|
||||
func (ns *Service) handleNotificationResult(result NotificationResult) {
|
||||
ns.callbackLock.RLock()
|
||||
callback := ns.notificationResultCallback
|
||||
ns.callbackLock.RUnlock()
|
||||
|
||||
if callback != nil {
|
||||
callback(result)
|
||||
}
|
||||
}
|
||||
|
||||
// ServiceStartup is called when the service is loaded.
|
||||
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
||||
return ns.impl.Startup(ctx, options)
|
||||
}
|
||||
|
||||
// ServiceShutdown is called when the service is unloaded.
|
||||
func (ns *Service) ServiceShutdown() error {
|
||||
return ns.impl.Shutdown()
|
||||
}
|
||||
|
||||
// Public methods that delegate to the implementation.
|
||||
func (ns *Service) RequestNotificationAuthorization() (bool, error) {
|
||||
return ns.impl.RequestNotificationAuthorization()
|
||||
}
|
||||
|
||||
func (ns *Service) CheckNotificationAuthorization() (bool, error) {
|
||||
return ns.impl.CheckNotificationAuthorization()
|
||||
}
|
||||
|
||||
func (ns *Service) SendNotification(options NotificationOptions) error {
|
||||
if err := validateNotificationOptions(options); err != nil {
|
||||
return err
|
||||
}
|
||||
return ns.impl.SendNotification(options)
|
||||
}
|
||||
|
||||
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
||||
if err := validateNotificationOptions(options); err != nil {
|
||||
return err
|
||||
}
|
||||
return ns.impl.SendNotificationWithActions(options)
|
||||
}
|
||||
|
||||
func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
return ns.impl.RegisterNotificationCategory(category)
|
||||
}
|
||||
|
||||
func (ns *Service) RemoveNotificationCategory(categoryID string) error {
|
||||
return ns.impl.RemoveNotificationCategory(categoryID)
|
||||
}
|
||||
|
||||
func (ns *Service) RemoveAllPendingNotifications() error {
|
||||
return ns.impl.RemoveAllPendingNotifications()
|
||||
}
|
||||
|
||||
func (ns *Service) RemovePendingNotification(identifier string) error {
|
||||
return ns.impl.RemovePendingNotification(identifier)
|
||||
}
|
||||
|
||||
func (ns *Service) RemoveAllDeliveredNotifications() error {
|
||||
return ns.impl.RemoveAllDeliveredNotifications()
|
||||
}
|
||||
|
||||
func (ns *Service) RemoveDeliveredNotification(identifier string) error {
|
||||
return ns.impl.RemoveDeliveredNotification(identifier)
|
||||
}
|
||||
|
||||
func (ns *Service) RemoveNotification(identifier string) error {
|
||||
return ns.impl.RemoveNotification(identifier)
|
||||
}
|
||||
|
||||
func getNotificationService() *Service {
|
||||
notificationServiceLock.RLock()
|
||||
defer notificationServiceLock.RUnlock()
|
||||
return NotificationService
|
||||
}
|
||||
|
||||
// validateNotificationOptions validates an ID and Title are provided for notifications.
|
||||
func validateNotificationOptions(options NotificationOptions) error {
|
||||
if options.ID == "" {
|
||||
return fmt.Errorf("notification ID cannot be empty")
|
||||
}
|
||||
|
||||
if options.Title == "" {
|
||||
return fmt.Errorf("notification title cannot be empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
423
v3/pkg/services/notifications/notifications_darwin.go
Normal file
423
v3/pkg/services/notifications/notifications_darwin.go
Normal file
@ -0,0 +1,423 @@
|
||||
//go:build darwin
|
||||
|
||||
package notifications
|
||||
|
||||
/*
|
||||
#cgo CFLAGS:-x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
#cgo LDFLAGS: -framework UserNotifications
|
||||
#endif
|
||||
|
||||
#import "./notifications_darwin.h"
|
||||
*/
|
||||
import "C"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type darwinNotifier struct {
|
||||
channels map[int]chan notificationChannel
|
||||
channelsLock sync.Mutex
|
||||
nextChannelID int
|
||||
}
|
||||
|
||||
type notificationChannel struct {
|
||||
Success bool
|
||||
Error error
|
||||
}
|
||||
|
||||
type ChannelHandler interface {
|
||||
GetChannel(id int) (chan notificationChannel, bool)
|
||||
}
|
||||
|
||||
const AppleDefaultActionIdentifier = "com.apple.UNNotificationDefaultActionIdentifier"
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
// Your app must be packaged and signed for this feature to work.
|
||||
func New() *Service {
|
||||
notificationServiceOnce.Do(func() {
|
||||
impl := &darwinNotifier{
|
||||
channels: make(map[int]chan notificationChannel),
|
||||
nextChannelID: 0,
|
||||
}
|
||||
|
||||
NotificationService = &Service{
|
||||
impl: impl,
|
||||
}
|
||||
})
|
||||
|
||||
return NotificationService
|
||||
}
|
||||
|
||||
func (dn *darwinNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
|
||||
if !isNotificationAvailable() {
|
||||
return fmt.Errorf("notifications are not available on this system")
|
||||
}
|
||||
if !checkBundleIdentifier() {
|
||||
return fmt.Errorf("notifications require a valid bundle identifier")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dn *darwinNotifier) Shutdown() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isNotificationAvailable checks if notifications are available on the system.
|
||||
func isNotificationAvailable() bool {
|
||||
return bool(C.isNotificationAvailable())
|
||||
}
|
||||
|
||||
func checkBundleIdentifier() bool {
|
||||
return bool(C.checkBundleIdentifier())
|
||||
}
|
||||
|
||||
// RequestNotificationAuthorization requests permission for notifications.
|
||||
// Default timeout is 3 minutes
|
||||
func (dn *darwinNotifier) RequestNotificationAuthorization() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
|
||||
C.requestNotificationAuthorization(C.int(id))
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
return result.Success, result.Error
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return false, fmt.Errorf("notification authorization timed out after 3 minutes: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// CheckNotificationAuthorization checks current notification permission status.
|
||||
func (dn *darwinNotifier) CheckNotificationAuthorization() (bool, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
|
||||
C.checkNotificationAuthorization(C.int(id))
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
return result.Success, result.Error
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return false, fmt.Errorf("notification authorization timed out after 15s: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
func (dn *darwinNotifier) SendNotification(options NotificationOptions) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cIdentifier := C.CString(options.ID)
|
||||
cTitle := C.CString(options.Title)
|
||||
cSubtitle := C.CString(options.Subtitle)
|
||||
cBody := C.CString(options.Body)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
defer C.free(unsafe.Pointer(cTitle))
|
||||
defer C.free(unsafe.Pointer(cSubtitle))
|
||||
defer C.free(unsafe.Pointer(cBody))
|
||||
|
||||
var cDataJSON *C.char
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification data: %w", err)
|
||||
}
|
||||
cDataJSON = C.CString(string(jsonData))
|
||||
defer C.free(unsafe.Pointer(cDataJSON))
|
||||
}
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
C.sendNotification(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cDataJSON)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("sending notification failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions and inputs.
|
||||
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
|
||||
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||
func (dn *darwinNotifier) SendNotificationWithActions(options NotificationOptions) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cIdentifier := C.CString(options.ID)
|
||||
cTitle := C.CString(options.Title)
|
||||
cSubtitle := C.CString(options.Subtitle)
|
||||
cBody := C.CString(options.Body)
|
||||
cCategoryID := C.CString(options.CategoryID)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
defer C.free(unsafe.Pointer(cTitle))
|
||||
defer C.free(unsafe.Pointer(cSubtitle))
|
||||
defer C.free(unsafe.Pointer(cBody))
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
var cDataJSON *C.char
|
||||
if options.Data != nil {
|
||||
jsonData, err := json.Marshal(options.Data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification data: %w", err)
|
||||
}
|
||||
cDataJSON = C.CString(string(jsonData))
|
||||
defer C.free(unsafe.Pointer(cDataJSON))
|
||||
}
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
C.sendNotificationWithActions(C.int(id), cIdentifier, cTitle, cSubtitle, cBody, cCategoryID, cDataJSON)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("sending notification failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return fmt.Errorf("sending notification timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
// Registering a category with the same name as a previously registered NotificationCategory will override it.
|
||||
func (dn *darwinNotifier) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cCategoryID := C.CString(category.ID)
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
actionsJSON, err := json.Marshal(category.Actions)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification category: %w", err)
|
||||
}
|
||||
cActionsJSON := C.CString(string(actionsJSON))
|
||||
defer C.free(unsafe.Pointer(cActionsJSON))
|
||||
|
||||
var cReplyPlaceholder, cReplyButtonTitle *C.char
|
||||
if category.HasReplyField {
|
||||
cReplyPlaceholder = C.CString(category.ReplyPlaceholder)
|
||||
cReplyButtonTitle = C.CString(category.ReplyButtonTitle)
|
||||
defer C.free(unsafe.Pointer(cReplyPlaceholder))
|
||||
defer C.free(unsafe.Pointer(cReplyButtonTitle))
|
||||
}
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
C.registerNotificationCategory(C.int(id), cCategoryID, cActionsJSON, C.bool(category.HasReplyField),
|
||||
cReplyPlaceholder, cReplyButtonTitle)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("category registration failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return fmt.Errorf("category registration timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory remove a previously registered NotificationCategory.
|
||||
func (dn *darwinNotifier) RemoveNotificationCategory(categoryId string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
cCategoryID := C.CString(categoryId)
|
||||
defer C.free(unsafe.Pointer(cCategoryID))
|
||||
|
||||
id, resultCh := dn.registerChannel()
|
||||
C.removeNotificationCategory(C.int(id), cCategoryID)
|
||||
|
||||
select {
|
||||
case result := <-resultCh:
|
||||
if !result.Success {
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
return fmt.Errorf("category removal failed")
|
||||
}
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
dn.cleanupChannel(id)
|
||||
return fmt.Errorf("category removal timed out: %w", ctx.Err())
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications removes all pending notifications.
|
||||
func (dn *darwinNotifier) RemoveAllPendingNotifications() error {
|
||||
C.removeAllPendingNotifications()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification removes a pending notification matching the unique identifier.
|
||||
func (dn *darwinNotifier) RemovePendingNotification(identifier string) error {
|
||||
cIdentifier := C.CString(identifier)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
C.removePendingNotification(cIdentifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications removes all delivered notifications.
|
||||
func (dn *darwinNotifier) RemoveAllDeliveredNotifications() error {
|
||||
C.removeAllDeliveredNotifications()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification removes a delivered notification matching the unique identifier.
|
||||
func (dn *darwinNotifier) RemoveDeliveredNotification(identifier string) error {
|
||||
cIdentifier := C.CString(identifier)
|
||||
defer C.free(unsafe.Pointer(cIdentifier))
|
||||
C.removeDeliveredNotification(cIdentifier)
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotification is a macOS stub that always returns nil.
|
||||
// Use one of the following instead:
|
||||
// RemoveAllPendingNotifications
|
||||
// RemovePendingNotification
|
||||
// RemoveAllDeliveredNotifications
|
||||
// RemoveDeliveredNotification
|
||||
// (Linux-specific)
|
||||
func (dn *darwinNotifier) RemoveNotification(identifier string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//export captureResult
|
||||
func captureResult(channelID C.int, success C.bool, errorMsg *C.char) {
|
||||
ns := getNotificationService()
|
||||
if ns == nil {
|
||||
return
|
||||
}
|
||||
|
||||
handler, ok := ns.impl.(ChannelHandler)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
resultCh, exists := handler.GetChannel(int(channelID))
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
if errorMsg != nil {
|
||||
err = fmt.Errorf("%s", C.GoString(errorMsg))
|
||||
}
|
||||
|
||||
resultCh <- notificationChannel{
|
||||
Success: bool(success),
|
||||
Error: err,
|
||||
}
|
||||
|
||||
close(resultCh)
|
||||
}
|
||||
|
||||
//export didReceiveNotificationResponse
|
||||
func didReceiveNotificationResponse(jsonPayload *C.char, err *C.char) {
|
||||
result := NotificationResult{}
|
||||
|
||||
if err != nil {
|
||||
errMsg := C.GoString(err)
|
||||
result.Error = fmt.Errorf("notification response error: %s", errMsg)
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if jsonPayload == nil {
|
||||
result.Error = fmt.Errorf("received nil JSON payload in notification response")
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
payload := C.GoString(jsonPayload)
|
||||
|
||||
var response NotificationResponse
|
||||
if err := json.Unmarshal([]byte(payload), &response); err != nil {
|
||||
result.Error = fmt.Errorf("failed to unmarshal notification response: %w", err)
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if response.ActionIdentifier == AppleDefaultActionIdentifier {
|
||||
response.ActionIdentifier = DefaultActionIdentifier
|
||||
}
|
||||
|
||||
result.Response = response
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
func (dn *darwinNotifier) registerChannel() (int, chan notificationChannel) {
|
||||
dn.channelsLock.Lock()
|
||||
defer dn.channelsLock.Unlock()
|
||||
|
||||
id := dn.nextChannelID
|
||||
dn.nextChannelID++
|
||||
|
||||
resultCh := make(chan notificationChannel, 1)
|
||||
|
||||
dn.channels[id] = resultCh
|
||||
return id, resultCh
|
||||
}
|
||||
|
||||
func (dn *darwinNotifier) GetChannel(id int) (chan notificationChannel, bool) {
|
||||
dn.channelsLock.Lock()
|
||||
defer dn.channelsLock.Unlock()
|
||||
|
||||
ch, exists := dn.channels[id]
|
||||
if exists {
|
||||
delete(dn.channels, id)
|
||||
}
|
||||
return ch, exists
|
||||
}
|
||||
|
||||
func (dn *darwinNotifier) cleanupChannel(id int) {
|
||||
dn.channelsLock.Lock()
|
||||
defer dn.channelsLock.Unlock()
|
||||
|
||||
if ch, exists := dn.channels[id]; exists {
|
||||
delete(dn.channels, id)
|
||||
close(ch)
|
||||
}
|
||||
}
|
21
v3/pkg/services/notifications/notifications_darwin.h
Normal file
21
v3/pkg/services/notifications/notifications_darwin.h
Normal file
@ -0,0 +1,21 @@
|
||||
//go:build darwin
|
||||
|
||||
#ifndef NOTIFICATIONS_DARWIN_H
|
||||
#define NOTIFICATIONS_DARWIN_H
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
bool checkBundleIdentifier(void);
|
||||
bool isNotificationAvailable(void);
|
||||
void requestNotificationAuthorization(int channelID);
|
||||
void checkNotificationAuthorization(int channelID);
|
||||
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json);
|
||||
void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *categoryId, const char *actions_json);
|
||||
void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField, const char *replyPlaceholder, const char *replyButtonTitle);
|
||||
void removeNotificationCategory(int channelID, const char *categoryId);
|
||||
void removeAllPendingNotifications(void);
|
||||
void removePendingNotification(const char *identifier);
|
||||
void removeAllDeliveredNotifications(void);
|
||||
void removeDeliveredNotification(const char *identifier);
|
||||
|
||||
#endif /* NOTIFICATIONS_DARWIN_H */
|
377
v3/pkg/services/notifications/notifications_darwin.m
Normal file
377
v3/pkg/services/notifications/notifications_darwin.m
Normal file
@ -0,0 +1,377 @@
|
||||
#import "notifications_darwin.h"
|
||||
#include <Foundation/Foundation.h>
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 110000
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#endif
|
||||
|
||||
bool isNotificationAvailable(void) {
|
||||
if (@available(macOS 11.0, *)) {
|
||||
return YES;
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
bool checkBundleIdentifier(void) {
|
||||
NSBundle *main = [NSBundle mainBundle];
|
||||
if (main.bundleIdentifier == nil) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
extern void captureResult(int channelID, bool success, const char* error);
|
||||
extern void didReceiveNotificationResponse(const char *jsonPayload, const char* error);
|
||||
|
||||
@interface NotificationsDelegate : NSObject <UNUserNotificationCenterDelegate>
|
||||
@end
|
||||
|
||||
@implementation NotificationsDelegate
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
willPresentNotification:(UNNotification *)notification
|
||||
withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler {
|
||||
UNNotificationPresentationOptions options = 0;
|
||||
|
||||
if (@available(macOS 11.0, *)) {
|
||||
// These options are only available in macOS 11.0+
|
||||
options = UNNotificationPresentationOptionList |
|
||||
UNNotificationPresentationOptionBanner |
|
||||
UNNotificationPresentationOptionSound;
|
||||
}
|
||||
|
||||
completionHandler(options);
|
||||
}
|
||||
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center
|
||||
didReceiveNotificationResponse:(UNNotificationResponse *)response
|
||||
withCompletionHandler:(void (^)(void))completionHandler {
|
||||
|
||||
NSMutableDictionary *payload = [NSMutableDictionary dictionary];
|
||||
|
||||
[payload setObject:response.notification.request.identifier forKey:@"id"];
|
||||
[payload setObject:response.actionIdentifier forKey:@"actionIdentifier"];
|
||||
[payload setObject:response.notification.request.content.title ?: @"" forKey:@"title"];
|
||||
[payload setObject:response.notification.request.content.body ?: @"" forKey:@"body"];
|
||||
|
||||
if (response.notification.request.content.categoryIdentifier) {
|
||||
[payload setObject:response.notification.request.content.categoryIdentifier forKey:@"categoryIdentifier"];
|
||||
}
|
||||
|
||||
if (response.notification.request.content.subtitle) {
|
||||
[payload setObject:response.notification.request.content.subtitle forKey:@"subtitle"];
|
||||
}
|
||||
|
||||
if (response.notification.request.content.userInfo) {
|
||||
[payload setObject:response.notification.request.content.userInfo forKey:@"userInfo"];
|
||||
}
|
||||
|
||||
if ([response isKindOfClass:[UNTextInputNotificationResponse class]]) {
|
||||
UNTextInputNotificationResponse *textResponse = (UNTextInputNotificationResponse *)response;
|
||||
[payload setObject:textResponse.userText forKey:@"userText"];
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:payload options:0 error:&error];
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
didReceiveNotificationResponse(NULL, [errorMsg UTF8String]);
|
||||
} else {
|
||||
NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
|
||||
didReceiveNotificationResponse([jsonString UTF8String], NULL);
|
||||
}
|
||||
|
||||
completionHandler();
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static NotificationsDelegate *delegateInstance = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
static BOOL ensureDelegateInitialized(void) {
|
||||
__block BOOL success = YES;
|
||||
|
||||
dispatch_once(&onceToken, ^{
|
||||
delegateInstance = [[NotificationsDelegate alloc] init];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
center.delegate = delegateInstance;
|
||||
});
|
||||
|
||||
if (!delegateInstance) {
|
||||
success = NO;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
void requestNotificationAuthorization(int channelID) {
|
||||
if (!ensureDelegateInitialized()) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
UNAuthorizationOptions options = UNAuthorizationOptionAlert | UNAuthorizationOptionSound | UNAuthorizationOptionBadge;
|
||||
|
||||
[center requestAuthorizationWithOptions:options completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
} else {
|
||||
captureResult(channelID, granted, NULL);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void checkNotificationAuthorization(int channelID) {
|
||||
if (!ensureDelegateInitialized()) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) {
|
||||
BOOL isAuthorized = (settings.authorizationStatus == UNAuthorizationStatusAuthorized);
|
||||
captureResult(channelID, isAuthorized, NULL);
|
||||
}];
|
||||
}
|
||||
|
||||
// Helper function to create notification content
|
||||
UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle,
|
||||
const char *body, const char *data_json, NSError **contentError) {
|
||||
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
||||
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
|
||||
NSString *nsBody = [NSString stringWithUTF8String:body];
|
||||
|
||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
||||
content.title = nsTitle;
|
||||
if (![nsSubtitle isEqualToString:@""]) {
|
||||
content.subtitle = nsSubtitle;
|
||||
}
|
||||
content.body = nsBody;
|
||||
content.sound = [UNNotificationSound defaultSound];
|
||||
|
||||
// Parse JSON data if provided
|
||||
if (data_json) {
|
||||
NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
|
||||
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
if (!error && parsedData) {
|
||||
content.userInfo = parsedData;
|
||||
} else if (error) {
|
||||
*contentError = error;
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
|
||||
if (!ensureDelegateInitialized()) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
|
||||
NSError *contentError = nil;
|
||||
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError);
|
||||
|
||||
if (contentError) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNTimeIntervalNotificationTrigger *trigger = nil;
|
||||
|
||||
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
|
||||
|
||||
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
} else {
|
||||
captureResult(channelID, true, NULL);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle,
|
||||
const char *body, const char *categoryId, const char *data_json) {
|
||||
if (!ensureDelegateInitialized()) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
|
||||
NSError *contentError = nil;
|
||||
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json, &contentError);
|
||||
|
||||
if (contentError) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [contentError localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
content.categoryIdentifier = nsCategoryId;
|
||||
|
||||
UNTimeIntervalNotificationTrigger *trigger = nil;
|
||||
|
||||
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:nsIdentifier content:content trigger:trigger];
|
||||
|
||||
[center addNotificationRequest:request withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
} else {
|
||||
captureResult(channelID, true, NULL);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void registerNotificationCategory(int channelID, const char *categoryId, const char *actions_json, bool hasReplyField,
|
||||
const char *replyPlaceholder, const char *replyButtonTitle) {
|
||||
if (!ensureDelegateInitialized()) {
|
||||
NSString *errorMsg = @"Notification delegate has been lost. Reinitialize the notification service.";
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
NSString *actionsJsonStr = actions_json ? [NSString stringWithUTF8String:actions_json] : @"[]";
|
||||
|
||||
NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||
NSError *error = nil;
|
||||
NSArray *actionsArray = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||
|
||||
if (error) {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray *actions = [NSMutableArray array];
|
||||
|
||||
for (NSDictionary *actionDict in actionsArray) {
|
||||
NSString *actionId = actionDict[@"id"];
|
||||
NSString *actionTitle = actionDict[@"title"];
|
||||
BOOL destructive = [actionDict[@"destructive"] boolValue];
|
||||
|
||||
if (actionId && actionTitle) {
|
||||
UNNotificationActionOptions options = UNNotificationActionOptionNone;
|
||||
if (destructive) options |= UNNotificationActionOptionDestructive;
|
||||
|
||||
UNNotificationAction *action = [UNNotificationAction
|
||||
actionWithIdentifier:actionId
|
||||
title:actionTitle
|
||||
options:options];
|
||||
[actions addObject:action];
|
||||
}
|
||||
}
|
||||
|
||||
if (hasReplyField && replyPlaceholder && replyButtonTitle) {
|
||||
NSString *placeholder = [NSString stringWithUTF8String:replyPlaceholder];
|
||||
NSString *buttonTitle = [NSString stringWithUTF8String:replyButtonTitle];
|
||||
|
||||
UNTextInputNotificationAction *textAction =
|
||||
[UNTextInputNotificationAction actionWithIdentifier:@"TEXT_REPLY"
|
||||
title:buttonTitle
|
||||
options:UNNotificationActionOptionNone
|
||||
textInputButtonTitle:buttonTitle
|
||||
textInputPlaceholder:placeholder];
|
||||
[actions addObject:textAction];
|
||||
}
|
||||
|
||||
UNNotificationCategory *newCategory = [UNNotificationCategory
|
||||
categoryWithIdentifier:nsCategoryId
|
||||
actions:actions
|
||||
intentIdentifiers:@[]
|
||||
options:UNNotificationCategoryOptionNone];
|
||||
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
|
||||
NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
|
||||
|
||||
// Remove existing category with same ID if it exists
|
||||
UNNotificationCategory *existingCategory = nil;
|
||||
for (UNNotificationCategory *category in updatedCategories) {
|
||||
if ([category.identifier isEqualToString:nsCategoryId]) {
|
||||
existingCategory = category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingCategory) {
|
||||
[updatedCategories removeObject:existingCategory];
|
||||
}
|
||||
|
||||
// Add the new category
|
||||
[updatedCategories addObject:newCategory];
|
||||
[center setNotificationCategories:updatedCategories];
|
||||
|
||||
captureResult(channelID, true, NULL);
|
||||
}];
|
||||
}
|
||||
|
||||
void removeNotificationCategory(int channelID, const char *categoryId) {
|
||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
|
||||
[center getNotificationCategoriesWithCompletionHandler:^(NSSet<UNNotificationCategory *> *categories) {
|
||||
NSMutableSet *updatedCategories = [NSMutableSet setWithSet:categories];
|
||||
|
||||
// Find and remove the category with matching identifier
|
||||
UNNotificationCategory *categoryToRemove = nil;
|
||||
for (UNNotificationCategory *category in updatedCategories) {
|
||||
if ([category.identifier isEqualToString:nsCategoryId]) {
|
||||
categoryToRemove = category;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (categoryToRemove) {
|
||||
[updatedCategories removeObject:categoryToRemove];
|
||||
[center setNotificationCategories:updatedCategories];
|
||||
captureResult(channelID, true, NULL);
|
||||
} else {
|
||||
NSString *errorMsg = [NSString stringWithFormat:@"Category '%@' not found", nsCategoryId];
|
||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
void removeAllPendingNotifications(void) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeAllPendingNotificationRequests];
|
||||
}
|
||||
|
||||
void removePendingNotification(const char *identifier) {
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removePendingNotificationRequestsWithIdentifiers:@[nsIdentifier]];
|
||||
}
|
||||
|
||||
void removeAllDeliveredNotifications(void) {
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeAllDeliveredNotifications];
|
||||
}
|
||||
|
||||
void removeDeliveredNotification(const char *identifier) {
|
||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||
[center removeDeliveredNotificationsWithIdentifiers:@[nsIdentifier]];
|
||||
}
|
565
v3/pkg/services/notifications/notifications_linux.go
Normal file
565
v3/pkg/services/notifications/notifications_linux.go
Normal file
@ -0,0 +1,565 @@
|
||||
//go:build linux
|
||||
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
type linuxNotifier struct {
|
||||
conn *dbus.Conn
|
||||
categories map[string]NotificationCategory
|
||||
categoriesLock sync.RWMutex
|
||||
notifications map[uint32]*notificationData
|
||||
notificationsLock sync.RWMutex
|
||||
appName string
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type notificationData struct {
|
||||
ID string
|
||||
Title string
|
||||
Subtitle string
|
||||
Body string
|
||||
CategoryID string
|
||||
Data map[string]interface{}
|
||||
DBusID uint32
|
||||
ActionMap map[string]string
|
||||
}
|
||||
|
||||
const (
|
||||
dbusNotificationInterface = "org.freedesktop.Notifications"
|
||||
dbusNotificationPath = "/org/freedesktop/Notifications"
|
||||
)
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
func New() *Service {
|
||||
notificationServiceOnce.Do(func() {
|
||||
impl := &linuxNotifier{
|
||||
categories: make(map[string]NotificationCategory),
|
||||
notifications: make(map[uint32]*notificationData),
|
||||
}
|
||||
|
||||
NotificationService = &Service{
|
||||
impl: impl,
|
||||
}
|
||||
})
|
||||
|
||||
return NotificationService
|
||||
}
|
||||
|
||||
// Startup is called when the service is loaded.
|
||||
func (ln *linuxNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
|
||||
ln.appName = application.Get().Config().Name
|
||||
|
||||
conn, err := dbus.ConnectSessionBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to session bus: %w", err)
|
||||
}
|
||||
ln.conn = conn
|
||||
|
||||
if err := ln.loadCategories(); err != nil {
|
||||
fmt.Printf("Failed to load notification categories: %v\n", err)
|
||||
}
|
||||
|
||||
var signalCtx context.Context
|
||||
signalCtx, ln.cancel = context.WithCancel(context.Background())
|
||||
|
||||
if err := ln.setupSignalHandling(signalCtx); err != nil {
|
||||
return fmt.Errorf("failed to set up notification signal handling: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown will save categories and close the D-Bus connection when the service unloads.
|
||||
func (ln *linuxNotifier) Shutdown() error {
|
||||
if ln.cancel != nil {
|
||||
ln.cancel()
|
||||
}
|
||||
|
||||
if err := ln.saveCategories(); err != nil {
|
||||
fmt.Printf("Failed to save notification categories: %v\n", err)
|
||||
}
|
||||
|
||||
if ln.conn != nil {
|
||||
return ln.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestNotificationAuthorization is a Linux stub that always returns true, nil.
|
||||
// (authorization is macOS-specific)
|
||||
func (ln *linuxNotifier) RequestNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckNotificationAuthorization is a Linux stub that always returns true.
|
||||
// (authorization is macOS-specific)
|
||||
func (ln *linuxNotifier) CheckNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||
func (ln *linuxNotifier) SendNotification(options NotificationOptions) error {
|
||||
hints := map[string]dbus.Variant{}
|
||||
|
||||
body := options.Body
|
||||
if options.Subtitle != "" {
|
||||
body = options.Subtitle + "\n" + body
|
||||
}
|
||||
|
||||
defaultActionID := "default"
|
||||
actions := []string{defaultActionID, "Default"}
|
||||
|
||||
actionMap := map[string]string{
|
||||
defaultActionID: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
|
||||
|
||||
if options.Data != nil {
|
||||
userData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
hints["x-user-data"] = dbus.MakeVariant(string(userData))
|
||||
}
|
||||
}
|
||||
|
||||
// Call the Notify method on the D-Bus interface
|
||||
obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(
|
||||
dbusNotificationInterface+".Notify",
|
||||
0,
|
||||
ln.appName,
|
||||
uint32(0),
|
||||
"", // Icon
|
||||
options.Title,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
int32(-1),
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to send notification: %w", call.Err)
|
||||
}
|
||||
|
||||
var dbusID uint32
|
||||
if err := call.Store(&dbusID); err != nil {
|
||||
return fmt.Errorf("failed to store notification ID: %w", err)
|
||||
}
|
||||
|
||||
notification := ¬ificationData{
|
||||
ID: options.ID,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
Data: options.Data,
|
||||
DBusID: dbusID,
|
||||
ActionMap: actionMap,
|
||||
}
|
||||
|
||||
ln.notificationsLock.Lock()
|
||||
ln.notifications[dbusID] = notification
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions.
|
||||
func (ln *linuxNotifier) SendNotificationWithActions(options NotificationOptions) error {
|
||||
ln.categoriesLock.RLock()
|
||||
category, exists := ln.categories[options.CategoryID]
|
||||
ln.categoriesLock.RUnlock()
|
||||
|
||||
if options.CategoryID == "" || !exists {
|
||||
// Fall back to basic notification
|
||||
return ln.SendNotification(options)
|
||||
}
|
||||
|
||||
body := options.Body
|
||||
if options.Subtitle != "" {
|
||||
body = options.Subtitle + "\n" + body
|
||||
}
|
||||
|
||||
var actions []string
|
||||
actionMap := make(map[string]string)
|
||||
|
||||
defaultActionID := "default"
|
||||
actions = append(actions, defaultActionID, "Default")
|
||||
actionMap[defaultActionID] = DefaultActionIdentifier
|
||||
|
||||
for _, action := range category.Actions {
|
||||
actions = append(actions, action.ID, action.Title)
|
||||
actionMap[action.ID] = action.ID
|
||||
}
|
||||
|
||||
hints := map[string]dbus.Variant{}
|
||||
|
||||
hints["x-notification-id"] = dbus.MakeVariant(options.ID)
|
||||
|
||||
hints["x-category-id"] = dbus.MakeVariant(options.CategoryID)
|
||||
|
||||
if options.Data != nil {
|
||||
userData, err := json.Marshal(options.Data)
|
||||
if err == nil {
|
||||
hints["x-user-data"] = dbus.MakeVariant(string(userData))
|
||||
}
|
||||
}
|
||||
|
||||
obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(
|
||||
dbusNotificationInterface+".Notify",
|
||||
0,
|
||||
ln.appName,
|
||||
uint32(0),
|
||||
"", // Icon
|
||||
options.Title,
|
||||
body,
|
||||
actions,
|
||||
hints,
|
||||
int32(-1),
|
||||
)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to send notification: %w", call.Err)
|
||||
}
|
||||
|
||||
var dbusID uint32
|
||||
if err := call.Store(&dbusID); err != nil {
|
||||
return fmt.Errorf("failed to store notification ID: %w", err)
|
||||
}
|
||||
|
||||
notification := ¬ificationData{
|
||||
ID: options.ID,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
CategoryID: options.CategoryID,
|
||||
Data: options.Data,
|
||||
DBusID: dbusID,
|
||||
ActionMap: actionMap,
|
||||
}
|
||||
|
||||
ln.notificationsLock.Lock()
|
||||
ln.notifications[dbusID] = notification
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
func (ln *linuxNotifier) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
ln.categoriesLock.Lock()
|
||||
ln.categories[category.ID] = category
|
||||
ln.categoriesLock.Unlock()
|
||||
|
||||
if err := ln.saveCategories(); err != nil {
|
||||
fmt.Printf("Failed to save notification categories: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (ln *linuxNotifier) RemoveNotificationCategory(categoryId string) error {
|
||||
ln.categoriesLock.Lock()
|
||||
delete(ln.categories, categoryId)
|
||||
ln.categoriesLock.Unlock()
|
||||
|
||||
if err := ln.saveCategories(); err != nil {
|
||||
fmt.Printf("Failed to save notification categories: %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications attempts to remove all active notifications.
|
||||
func (ln *linuxNotifier) RemoveAllPendingNotifications() error {
|
||||
ln.notificationsLock.Lock()
|
||||
dbusIDs := make([]uint32, 0, len(ln.notifications))
|
||||
for id := range ln.notifications {
|
||||
dbusIDs = append(dbusIDs, id)
|
||||
}
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
for _, id := range dbusIDs {
|
||||
ln.closeNotification(id)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification removes a pending notification.
|
||||
func (ln *linuxNotifier) RemovePendingNotification(identifier string) error {
|
||||
var dbusID uint32
|
||||
found := false
|
||||
|
||||
ln.notificationsLock.Lock()
|
||||
for id, notif := range ln.notifications {
|
||||
if notif.ID == identifier {
|
||||
dbusID = id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
if !found {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ln.closeNotification(dbusID)
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications functionally equivalent to RemoveAllPendingNotification on Linux.
|
||||
func (ln *linuxNotifier) RemoveAllDeliveredNotifications() error {
|
||||
return ln.RemoveAllPendingNotifications()
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification functionally equivalent RemovePendingNotification on Linux.
|
||||
func (ln *linuxNotifier) RemoveDeliveredNotification(identifier string) error {
|
||||
return ln.RemovePendingNotification(identifier)
|
||||
}
|
||||
|
||||
// RemoveNotification removes a notification by identifier.
|
||||
func (ln *linuxNotifier) RemoveNotification(identifier string) error {
|
||||
return ln.RemovePendingNotification(identifier)
|
||||
}
|
||||
|
||||
// Helper method to close a notification.
|
||||
func (ln *linuxNotifier) closeNotification(id uint32) error {
|
||||
obj := ln.conn.Object(dbusNotificationInterface, dbusNotificationPath)
|
||||
call := obj.Call(dbusNotificationInterface+".CloseNotification", 0, id)
|
||||
|
||||
if call.Err != nil {
|
||||
return fmt.Errorf("failed to close notification: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ln *linuxNotifier) getConfigDir() (string, error) {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user config directory: %w", err)
|
||||
}
|
||||
|
||||
appConfigDir := filepath.Join(configDir, ln.appName)
|
||||
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create app config directory: %w", err)
|
||||
}
|
||||
|
||||
return appConfigDir, nil
|
||||
}
|
||||
|
||||
// Save notification categories.
|
||||
func (ln *linuxNotifier) saveCategories() error {
|
||||
configDir, err := ln.getConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
categoriesFile := filepath.Join(configDir, "notification-categories.json")
|
||||
|
||||
ln.categoriesLock.RLock()
|
||||
categoriesData, err := json.MarshalIndent(ln.categories, "", " ")
|
||||
ln.categoriesLock.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal notification categories: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(categoriesFile, categoriesData, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write notification categories to disk: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Load notification categories.
|
||||
func (ln *linuxNotifier) loadCategories() error {
|
||||
configDir, err := ln.getConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
categoriesFile := filepath.Join(configDir, "notification-categories.json")
|
||||
|
||||
if _, err := os.Stat(categoriesFile); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
categoriesData, err := os.ReadFile(categoriesFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read notification categories from disk: %w", err)
|
||||
}
|
||||
|
||||
categories := make(map[string]NotificationCategory)
|
||||
if err := json.Unmarshal(categoriesData, &categories); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal notification categories: %w", err)
|
||||
}
|
||||
|
||||
ln.categoriesLock.Lock()
|
||||
ln.categories = categories
|
||||
ln.categoriesLock.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Setup signal handling for notification actions.
|
||||
func (ln *linuxNotifier) setupSignalHandling(ctx context.Context) error {
|
||||
if err := ln.conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(dbusNotificationInterface),
|
||||
dbus.WithMatchMember("ActionInvoked"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ln.conn.AddMatchSignal(
|
||||
dbus.WithMatchInterface(dbusNotificationInterface),
|
||||
dbus.WithMatchMember("NotificationClosed"),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := make(chan *dbus.Signal, 10)
|
||||
ln.conn.Signal(c)
|
||||
|
||||
go ln.handleSignals(ctx, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle incoming D-Bus signals.
|
||||
func (ln *linuxNotifier) handleSignals(ctx context.Context, c chan *dbus.Signal) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case signal, ok := <-c:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch signal.Name {
|
||||
case dbusNotificationInterface + ".ActionInvoked":
|
||||
ln.handleActionInvoked(signal)
|
||||
case dbusNotificationInterface + ".NotificationClosed":
|
||||
ln.handleNotificationClosed(signal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle ActionInvoked signal.
|
||||
func (ln *linuxNotifier) handleActionInvoked(signal *dbus.Signal) {
|
||||
if len(signal.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
dbusID, ok := signal.Body[0].(uint32)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
actionID, ok := signal.Body[1].(string)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
ln.notificationsLock.Lock()
|
||||
notification, exists := ln.notifications[dbusID]
|
||||
if exists {
|
||||
delete(ln.notifications, dbusID)
|
||||
}
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
appActionID, ok := notification.ActionMap[actionID]
|
||||
if !ok {
|
||||
appActionID = actionID
|
||||
}
|
||||
|
||||
response := NotificationResponse{
|
||||
ID: notification.ID,
|
||||
ActionIdentifier: appActionID,
|
||||
Title: notification.Title,
|
||||
Subtitle: notification.Subtitle,
|
||||
Body: notification.Body,
|
||||
CategoryID: notification.CategoryID,
|
||||
UserInfo: notification.Data,
|
||||
}
|
||||
|
||||
result := NotificationResult{
|
||||
Response: response,
|
||||
}
|
||||
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle NotificationClosed signal.
|
||||
// Reason codes:
|
||||
// 1 - expired timeout
|
||||
// 2 - dismissed by user (click on X)
|
||||
// 3 - closed by CloseNotification call
|
||||
// 4 - undefined/reserved
|
||||
func (ln *linuxNotifier) handleNotificationClosed(signal *dbus.Signal) {
|
||||
if len(signal.Body) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
dbusID, ok := signal.Body[0].(uint32)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
reason, ok := signal.Body[1].(uint32)
|
||||
if !ok {
|
||||
reason = 0 // Unknown reason
|
||||
}
|
||||
|
||||
ln.notificationsLock.Lock()
|
||||
notification, exists := ln.notifications[dbusID]
|
||||
if exists {
|
||||
delete(ln.notifications, dbusID)
|
||||
}
|
||||
ln.notificationsLock.Unlock()
|
||||
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
if reason == 2 {
|
||||
response := NotificationResponse{
|
||||
ID: notification.ID,
|
||||
ActionIdentifier: DefaultActionIdentifier,
|
||||
Title: notification.Title,
|
||||
Subtitle: notification.Subtitle,
|
||||
Body: notification.Body,
|
||||
CategoryID: notification.CategoryID,
|
||||
UserInfo: notification.Data,
|
||||
}
|
||||
|
||||
result := NotificationResult{
|
||||
Response: response,
|
||||
}
|
||||
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
}
|
||||
}
|
433
v3/pkg/services/notifications/notifications_windows.go
Normal file
433
v3/pkg/services/notifications/notifications_windows.go
Normal file
@ -0,0 +1,433 @@
|
||||
//go:build windows
|
||||
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
type windowsNotifier struct {
|
||||
categories map[string]NotificationCategory
|
||||
categoriesLock sync.RWMutex
|
||||
appName string
|
||||
appGUID string
|
||||
iconPath string
|
||||
}
|
||||
|
||||
const (
|
||||
ToastRegistryPath = `Software\Classes\AppUserModelId\`
|
||||
ToastRegistryGuidKey = "CustomActivator"
|
||||
NotificationCategoriesRegistryPath = `SOFTWARE\%s\NotificationCategories`
|
||||
NotificationCategoriesRegistryKey = "Categories"
|
||||
)
|
||||
|
||||
// NotificationPayload combines the action ID and user data into a single structure
|
||||
type NotificationPayload struct {
|
||||
Action string `json:"action"`
|
||||
Options NotificationOptions `json:"payload,omitempty"`
|
||||
}
|
||||
|
||||
// Creates a new Notifications Service.
|
||||
func New() *Service {
|
||||
notificationServiceOnce.Do(func() {
|
||||
impl := &windowsNotifier{
|
||||
categories: make(map[string]NotificationCategory),
|
||||
}
|
||||
|
||||
NotificationService = &Service{
|
||||
impl: impl,
|
||||
}
|
||||
})
|
||||
|
||||
return NotificationService
|
||||
}
|
||||
|
||||
// Startup is called when the service is loaded
|
||||
// Sets an activation callback to emit an event when notifications are interacted with.
|
||||
func (wn *windowsNotifier) Startup(ctx context.Context, options application.ServiceOptions) error {
|
||||
wn.categoriesLock.Lock()
|
||||
defer wn.categoriesLock.Unlock()
|
||||
|
||||
wn.appName = application.Get().Config().Name
|
||||
|
||||
guid, err := wn.getGUID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wn.appGUID = guid
|
||||
|
||||
wn.iconPath = filepath.Join(os.TempDir(), wn.appName+wn.appGUID+".png")
|
||||
|
||||
toast.SetAppData(toast.AppData{
|
||||
AppID: wn.appName,
|
||||
GUID: guid,
|
||||
IconPath: wn.iconPath,
|
||||
})
|
||||
|
||||
toast.SetActivationCallback(func(args string, data []toast.UserData) {
|
||||
result := NotificationResult{}
|
||||
|
||||
actionIdentifier, options, err := parseNotificationResponse(args)
|
||||
|
||||
if err != nil {
|
||||
result.Error = err
|
||||
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Subtitle is retained but was not shown with the notification
|
||||
response := NotificationResponse{
|
||||
ID: options.ID,
|
||||
ActionIdentifier: actionIdentifier,
|
||||
Title: options.Title,
|
||||
Subtitle: options.Subtitle,
|
||||
Body: options.Body,
|
||||
CategoryID: options.CategoryID,
|
||||
UserInfo: options.Data,
|
||||
}
|
||||
|
||||
if userText, found := wn.getUserText(data); found {
|
||||
response.UserText = userText
|
||||
}
|
||||
|
||||
result.Response = response
|
||||
if ns := getNotificationService(); ns != nil {
|
||||
ns.handleNotificationResult(result)
|
||||
}
|
||||
})
|
||||
|
||||
return wn.loadCategoriesFromRegistry()
|
||||
}
|
||||
|
||||
// Shutdown will attempt to save the categories to the registry when the service unloads
|
||||
func (wn *windowsNotifier) Shutdown() error {
|
||||
wn.categoriesLock.Lock()
|
||||
defer wn.categoriesLock.Unlock()
|
||||
|
||||
return wn.saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RequestNotificationAuthorization is a Windows stub that always returns true, nil.
|
||||
// (user authorization is macOS-specific)
|
||||
func (wn *windowsNotifier) RequestNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// CheckNotificationAuthorization is a Windows stub that always returns true.
|
||||
// (user authorization is macOS-specific)
|
||||
func (wn *windowsNotifier) CheckNotificationAuthorization() (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
|
||||
// (subtitle is only available on macOS and Linux)
|
||||
func (wn *windowsNotifier) SendNotification(options NotificationOptions) error {
|
||||
if err := wn.saveIconToDir(); err != nil {
|
||||
fmt.Printf("Error saving icon: %v\n", err)
|
||||
}
|
||||
|
||||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
if options.Data != nil {
|
||||
encodedPayload, err := wn.encodePayload(DefaultActionIdentifier, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.ActivationArguments = encodedPayload
|
||||
}
|
||||
|
||||
return n.Push()
|
||||
}
|
||||
|
||||
// SendNotificationWithActions sends a notification with additional actions and inputs.
|
||||
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
|
||||
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||
// (subtitle is only available on macOS and Linux)
|
||||
func (wn *windowsNotifier) SendNotificationWithActions(options NotificationOptions) error {
|
||||
if err := wn.saveIconToDir(); err != nil {
|
||||
fmt.Printf("Error saving icon: %v\n", err)
|
||||
}
|
||||
|
||||
wn.categoriesLock.RLock()
|
||||
nCategory, categoryExists := wn.categories[options.CategoryID]
|
||||
wn.categoriesLock.RUnlock()
|
||||
|
||||
if options.CategoryID == "" || !categoryExists {
|
||||
fmt.Printf("Category '%s' not found, sending basic notification without actions\n", options.CategoryID)
|
||||
}
|
||||
|
||||
n := toast.Notification{
|
||||
Title: options.Title,
|
||||
Body: options.Body,
|
||||
ActivationArguments: DefaultActionIdentifier,
|
||||
}
|
||||
|
||||
for _, action := range nCategory.Actions {
|
||||
n.Actions = append(n.Actions, toast.Action{
|
||||
Content: action.Title,
|
||||
Arguments: action.ID,
|
||||
})
|
||||
}
|
||||
|
||||
if nCategory.HasReplyField {
|
||||
n.Inputs = append(n.Inputs, toast.Input{
|
||||
ID: "userText",
|
||||
Placeholder: nCategory.ReplyPlaceholder,
|
||||
})
|
||||
|
||||
n.Actions = append(n.Actions, toast.Action{
|
||||
Content: nCategory.ReplyButtonTitle,
|
||||
Arguments: "TEXT_REPLY",
|
||||
InputID: "userText",
|
||||
})
|
||||
}
|
||||
|
||||
if options.Data != nil {
|
||||
encodedPayload, err := wn.encodePayload(n.ActivationArguments, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.ActivationArguments = encodedPayload
|
||||
|
||||
for index := range n.Actions {
|
||||
encodedPayload, err := wn.encodePayload(n.Actions[index].Arguments, options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode notification payload: %w", err)
|
||||
}
|
||||
n.Actions[index].Arguments = encodedPayload
|
||||
}
|
||||
}
|
||||
|
||||
return n.Push()
|
||||
}
|
||||
|
||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||
// Registering a category with the same name as a previously registered NotificationCategory will override it.
|
||||
func (wn *windowsNotifier) RegisterNotificationCategory(category NotificationCategory) error {
|
||||
wn.categoriesLock.Lock()
|
||||
defer wn.categoriesLock.Unlock()
|
||||
|
||||
wn.categories[category.ID] = NotificationCategory{
|
||||
ID: category.ID,
|
||||
Actions: category.Actions,
|
||||
HasReplyField: bool(category.HasReplyField),
|
||||
ReplyPlaceholder: category.ReplyPlaceholder,
|
||||
ReplyButtonTitle: category.ReplyButtonTitle,
|
||||
}
|
||||
|
||||
return wn.saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RemoveNotificationCategory removes a previously registered NotificationCategory.
|
||||
func (wn *windowsNotifier) RemoveNotificationCategory(categoryId string) error {
|
||||
wn.categoriesLock.Lock()
|
||||
defer wn.categoriesLock.Unlock()
|
||||
|
||||
delete(wn.categories, categoryId)
|
||||
|
||||
return wn.saveCategoriesToRegistry()
|
||||
}
|
||||
|
||||
// RemoveAllPendingNotifications is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (wn *windowsNotifier) RemoveAllPendingNotifications() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemovePendingNotification is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (wn *windowsNotifier) RemovePendingNotification(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveAllDeliveredNotifications is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (wn *windowsNotifier) RemoveAllDeliveredNotifications() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDeliveredNotification is a Windows stub that always returns nil.
|
||||
// (macOS and Linux only)
|
||||
func (wn *windowsNotifier) RemoveDeliveredNotification(_ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveNotification is a Windows stub that always returns nil.
|
||||
// (Linux-specific)
|
||||
func (wn *windowsNotifier) RemoveNotification(identifier string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// encodePayload combines an action ID and user data into a single encoded string
|
||||
func (wn *windowsNotifier) encodePayload(actionID string, options NotificationOptions) (string, error) {
|
||||
payload := NotificationPayload{
|
||||
Action: actionID,
|
||||
Options: options,
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return actionID, err
|
||||
}
|
||||
|
||||
encodedPayload := base64.StdEncoding.EncodeToString(jsonData)
|
||||
return encodedPayload, nil
|
||||
}
|
||||
|
||||
// decodePayload extracts the action ID and user data from an encoded payload
|
||||
func decodePayload(encodedString string) (string, NotificationOptions, error) {
|
||||
jsonData, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
return encodedString, NotificationOptions{}, fmt.Errorf("failed to decode base64 payload: %w", err)
|
||||
}
|
||||
|
||||
var payload NotificationPayload
|
||||
if err := json.Unmarshal(jsonData, &payload); err != nil {
|
||||
return encodedString, NotificationOptions{}, fmt.Errorf("failed to unmarshal notification payload: %w", err)
|
||||
}
|
||||
|
||||
return payload.Action, payload.Options, nil
|
||||
}
|
||||
|
||||
// parseNotificationResponse updated to use structured payload decoding
|
||||
func parseNotificationResponse(response string) (action string, options NotificationOptions, err error) {
|
||||
actionID, options, err := decodePayload(response)
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: Failed to decode notification response: %v\n", err)
|
||||
return response, NotificationOptions{}, err
|
||||
}
|
||||
|
||||
return actionID, options, nil
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) saveIconToDir() error {
|
||||
icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to retrieve application icon: %w", err)
|
||||
}
|
||||
|
||||
return w32.SaveHIconAsPNG(icon, wn.iconPath)
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) saveCategoriesToRegistry() error {
|
||||
// We assume lock is held by caller
|
||||
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName)
|
||||
|
||||
key, _, err := registry.CreateKey(
|
||||
registry.CURRENT_USER,
|
||||
registryPath,
|
||||
registry.ALL_ACCESS,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
data, err := json.Marshal(wn.categories)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) loadCategoriesFromRegistry() error {
|
||||
// We assume lock is held by caller
|
||||
|
||||
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, wn.appName)
|
||||
|
||||
key, err := registry.OpenKey(
|
||||
registry.CURRENT_USER,
|
||||
registryPath,
|
||||
registry.QUERY_VALUE,
|
||||
)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
// Not an error, no saved categories
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to open registry key: %w", err)
|
||||
}
|
||||
defer key.Close()
|
||||
|
||||
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
|
||||
if err != nil {
|
||||
if err == registry.ErrNotExist {
|
||||
// No value yet, but key exists
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read categories from registry: %w", err)
|
||||
}
|
||||
|
||||
categories := make(map[string]NotificationCategory)
|
||||
if err := json.Unmarshal([]byte(data), &categories); err != nil {
|
||||
return fmt.Errorf("failed to parse notification categories from registry: %w", err)
|
||||
}
|
||||
|
||||
wn.categories = categories
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) getUserText(data []toast.UserData) (string, bool) {
|
||||
for _, d := range data {
|
||||
if d.Key == "userText" {
|
||||
return d.Value, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) getGUID() (string, error) {
|
||||
keyPath := ToastRegistryPath + wn.appName
|
||||
|
||||
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
|
||||
if err == nil {
|
||||
guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
|
||||
k.Close()
|
||||
if err == nil && guid != "" {
|
||||
return guid, nil
|
||||
}
|
||||
}
|
||||
|
||||
guid := wn.generateGUID()
|
||||
|
||||
k, _, err = registry.CreateKey(registry.CURRENT_USER, keyPath, registry.WRITE)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create registry key: %w", err)
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil {
|
||||
return "", fmt.Errorf("failed to write GUID to registry: %w", err)
|
||||
}
|
||||
|
||||
return guid, nil
|
||||
}
|
||||
|
||||
func (wn *windowsNotifier) generateGUID() string {
|
||||
guid := uuid.New()
|
||||
return fmt.Sprintf("{%s}", guid.String())
|
||||
}
|
@ -6,8 +6,11 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
"image/png"
|
||||
"os"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
@ -90,6 +93,121 @@ func CreateLargeHIconFromImage(fileData []byte) (HICON, error) {
|
||||
return HICON(icon), err
|
||||
}
|
||||
|
||||
type ICONINFO struct {
|
||||
FIcon int32
|
||||
XHotspot int32
|
||||
YHotspot int32
|
||||
HbmMask syscall.Handle
|
||||
HbmColor syscall.Handle
|
||||
}
|
||||
|
||||
func SaveHIconAsPNG(hIcon HICON, filePath string) error {
|
||||
// Load necessary DLLs
|
||||
user32 := syscall.NewLazyDLL("user32.dll")
|
||||
gdi32 := syscall.NewLazyDLL("gdi32.dll")
|
||||
|
||||
// Get procedures
|
||||
getIconInfo := user32.NewProc("GetIconInfo")
|
||||
getObject := gdi32.NewProc("GetObjectW")
|
||||
createCompatibleDC := gdi32.NewProc("CreateCompatibleDC")
|
||||
selectObject := gdi32.NewProc("SelectObject")
|
||||
getDIBits := gdi32.NewProc("GetDIBits")
|
||||
deleteObject := gdi32.NewProc("DeleteObject")
|
||||
deleteDC := gdi32.NewProc("DeleteDC")
|
||||
|
||||
// Get icon info
|
||||
var iconInfo ICONINFO
|
||||
ret, _, err := getIconInfo.Call(
|
||||
uintptr(hIcon),
|
||||
uintptr(unsafe.Pointer(&iconInfo)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
defer deleteObject.Call(uintptr(iconInfo.HbmMask))
|
||||
defer deleteObject.Call(uintptr(iconInfo.HbmColor))
|
||||
|
||||
// Get bitmap info
|
||||
var bmp BITMAP
|
||||
ret, _, err = getObject.Call(
|
||||
uintptr(iconInfo.HbmColor),
|
||||
unsafe.Sizeof(bmp),
|
||||
uintptr(unsafe.Pointer(&bmp)),
|
||||
)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create DC
|
||||
hdc, _, _ := createCompatibleDC.Call(0)
|
||||
if hdc == 0 {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
defer deleteDC.Call(hdc)
|
||||
|
||||
// Select bitmap into DC
|
||||
oldBitmap, _, _ := selectObject.Call(hdc, uintptr(iconInfo.HbmColor))
|
||||
defer selectObject.Call(hdc, oldBitmap)
|
||||
|
||||
// Prepare bitmap info header
|
||||
var bi BITMAPINFO
|
||||
bi.BmiHeader.BiSize = uint32(unsafe.Sizeof(bi.BmiHeader))
|
||||
bi.BmiHeader.BiWidth = bmp.BmWidth
|
||||
bi.BmiHeader.BiHeight = bmp.BmHeight
|
||||
bi.BmiHeader.BiPlanes = 1
|
||||
bi.BmiHeader.BiBitCount = 32
|
||||
bi.BmiHeader.BiCompression = BI_RGB
|
||||
|
||||
// Allocate memory for bitmap bits
|
||||
width, height := int(bmp.BmWidth), int(bmp.BmHeight)
|
||||
bufferSize := width * height * 4
|
||||
bits := make([]byte, bufferSize)
|
||||
|
||||
// Get bitmap bits
|
||||
ret, _, err = getDIBits.Call(
|
||||
hdc,
|
||||
uintptr(iconInfo.HbmColor),
|
||||
0,
|
||||
uintptr(bmp.BmHeight),
|
||||
uintptr(unsafe.Pointer(&bits[0])),
|
||||
uintptr(unsafe.Pointer(&bi)),
|
||||
DIB_RGB_COLORS,
|
||||
)
|
||||
if ret == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create Go image
|
||||
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Convert DIB to RGBA
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// DIB is bottom-up, so we need to invert Y
|
||||
dibIndex := ((height-1-y)*width + x) * 4
|
||||
|
||||
// BGRA to RGBA
|
||||
b := bits[dibIndex]
|
||||
g := bits[dibIndex+1]
|
||||
r := bits[dibIndex+2]
|
||||
a := bits[dibIndex+3]
|
||||
|
||||
// Set pixel in the image
|
||||
img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
|
||||
}
|
||||
}
|
||||
|
||||
// Create output file
|
||||
outFile, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// Encode and save the image
|
||||
return png.Encode(outFile, img)
|
||||
}
|
||||
|
||||
func SetWindowIcon(hwnd HWND, icon HICON) {
|
||||
SendMessage(hwnd, WM_SETICON, ICON_SMALL, uintptr(icon))
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user