mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-07 00:02:39 +08:00
more cleanup
This commit is contained in:
parent
ee885fea44
commit
4af058bd02
@ -1,10 +1,25 @@
|
|||||||
|
// 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: Falls back between D-Bus, notify-send, or other methods and does not support text inputs
|
||||||
package notifications
|
package notifications
|
||||||
|
|
||||||
import "sync"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
// Service represents the notifications service
|
// Service represents the notifications service
|
||||||
type Service struct {
|
type Service struct {
|
||||||
// notificationResponseCallback is called when a notification response is received
|
// notificationResponseCallback is called when a notification result is received.
|
||||||
// Only one callback can be assigned at a time.
|
// Only one callback can be assigned at a time.
|
||||||
notificationResultCallback func(result NotificationResult)
|
notificationResultCallback func(result NotificationResult)
|
||||||
|
|
||||||
@ -93,3 +108,15 @@ func (ns *Service) handleNotificationResult(result NotificationResult) {
|
|||||||
callback(result)
|
callback(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
@ -56,7 +56,7 @@ func CheckBundleIdentifier() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// RequestNotificationAuthorization requests permission for notifications.
|
// RequestNotificationAuthorization requests permission for notifications.
|
||||||
// Default timeout is 5 minutes
|
// Default timeout is 15 minutes
|
||||||
func (ns *Service) RequestNotificationAuthorization() (bool, error) {
|
func (ns *Service) RequestNotificationAuthorization() (bool, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*900)
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*900)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@ -94,6 +94,10 @@ func (ns *Service) CheckNotificationAuthorization() (bool, error) {
|
|||||||
|
|
||||||
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
|
||||||
func (ns *Service) SendNotification(options NotificationOptions) error {
|
func (ns *Service) SendNotification(options NotificationOptions) error {
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -139,6 +143,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
|
|||||||
// A NotificationCategory must be registered with RegisterNotificationCategory first. The `CategoryID` must match the registered category.
|
// 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.
|
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||||
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@ -243,7 +251,7 @@ func (ns *Service) RemoveNotificationCategory(categoryId string) error {
|
|||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
return result.Error
|
return result.Error
|
||||||
}
|
}
|
||||||
return fmt.Errorf("category registration failed")
|
return fmt.Errorf("category removal failed")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
|
@ -106,37 +106,43 @@ void checkNotificationAuthorization(int channelID) {
|
|||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
|
// Helper function to create notification content
|
||||||
ensureDelegateInitialized();
|
UNMutableNotificationContent* createNotificationContent(const char *title, const char *subtitle,
|
||||||
|
const char *body, const char *data_json) {
|
||||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
|
||||||
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
||||||
NSString *nsSubtitle = [NSString stringWithUTF8String:subtitle];
|
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
|
||||||
NSString *nsBody = [NSString stringWithUTF8String:body];
|
NSString *nsBody = [NSString stringWithUTF8String:body];
|
||||||
|
|
||||||
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
|
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) {
|
if (data_json) {
|
||||||
NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
|
NSString *dataJsonStr = [NSString stringWithUTF8String:data_json];
|
||||||
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
NSData *jsonData = [dataJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
||||||
if (error) {
|
if (!error && parsedData) {
|
||||||
NSString *errorMsg = [NSString stringWithFormat:@"Error: %@", [error localizedDescription]];
|
content.userInfo = parsedData;
|
||||||
captureResult(channelID, false, [errorMsg UTF8String]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (parsedData) {
|
|
||||||
[customData addEntriesFromDictionary:parsedData];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendNotification(int channelID, const char *identifier, const char *title, const char *subtitle, const char *body, const char *data_json) {
|
||||||
|
ensureDelegateInitialized();
|
||||||
|
|
||||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||||
|
|
||||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||||
content.title = nsTitle;
|
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
|
||||||
content.subtitle = nsSubtitle;
|
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
|
||||||
content.body = nsBody;
|
|
||||||
content.sound = [UNNotificationSound defaultSound];
|
|
||||||
|
|
||||||
if (customData.count > 0) {
|
if (customData.count > 0) {
|
||||||
content.userInfo = customData;
|
content.userInfo = customData;
|
||||||
@ -157,35 +163,16 @@ void sendNotification(int channelID, const char *identifier, const char *title,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle,
|
void sendNotificationWithActions(int channelID, const char *identifier, const char *title, const char *subtitle,
|
||||||
const char *body, const char *categoryId, const char *actions_json) {
|
const char *body, const char *categoryId, const char *data_json) {
|
||||||
ensureDelegateInitialized();
|
ensureDelegateInitialized();
|
||||||
|
|
||||||
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
|
||||||
NSString *nsTitle = [NSString stringWithUTF8String:title];
|
|
||||||
NSString *nsSubtitle = subtitle ? [NSString stringWithUTF8String:subtitle] : @"";
|
|
||||||
NSString *nsBody = [NSString stringWithUTF8String:body];
|
|
||||||
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
|
||||||
|
|
||||||
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
|
|
||||||
if (actions_json) {
|
|
||||||
NSString *actionsJsonStr = [NSString stringWithUTF8String:actions_json];
|
|
||||||
NSData *jsonData = [actionsJsonStr dataUsingEncoding:NSUTF8StringEncoding];
|
|
||||||
NSError *error = nil;
|
|
||||||
NSDictionary *parsedData = [NSJSONSerialization JSONObjectWithData:jsonData options:0 error:&error];
|
|
||||||
if (!error && parsedData) {
|
|
||||||
[customData addEntriesFromDictionary:parsedData];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
|
||||||
|
|
||||||
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
|
NSString *nsIdentifier = [NSString stringWithUTF8String:identifier];
|
||||||
content.title = nsTitle;
|
NSString *nsCategoryId = [NSString stringWithUTF8String:categoryId];
|
||||||
if (![nsSubtitle isEqualToString:@""]) {
|
UNMutableNotificationContent *content = createNotificationContent(title, subtitle, body, data_json);
|
||||||
content.subtitle = nsSubtitle;
|
NSMutableDictionary *customData = [NSMutableDictionary dictionary];
|
||||||
}
|
|
||||||
content.body = nsBody;
|
|
||||||
content.sound = [UNNotificationSound defaultSound];
|
|
||||||
content.categoryIdentifier = nsCategoryId;
|
content.categoryIdentifier = nsCategoryId;
|
||||||
|
|
||||||
if (customData.count > 0) {
|
if (customData.count > 0) {
|
||||||
|
@ -415,6 +415,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
|
|||||||
return errors.New("notification service not initialized")
|
return errors.New("notification service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notifier.Lock()
|
notifier.Lock()
|
||||||
defer notifier.Unlock()
|
defer notifier.Unlock()
|
||||||
|
|
||||||
@ -454,6 +458,10 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
|
|||||||
return errors.New("notification service not initialized")
|
return errors.New("notification service not initialized")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
notificationLock.RLock()
|
notificationLock.RLock()
|
||||||
category, exists := notificationCategories[options.CategoryID]
|
category, exists := notificationCategories[options.CategoryID]
|
||||||
notificationLock.RUnlock()
|
notificationLock.RUnlock()
|
||||||
|
@ -128,6 +128,10 @@ func (ns *Service) CheckNotificationAuthorization() bool {
|
|||||||
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
|
// SendNotification sends a basic notification with a name, title, and body. All other options are ignored on Windows.
|
||||||
// (subtitle and category id are only available on macOS)
|
// (subtitle and category id are only available on macOS)
|
||||||
func (ns *Service) SendNotification(options NotificationOptions) error {
|
func (ns *Service) SendNotification(options NotificationOptions) error {
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := saveIconToDir(); err != nil {
|
if err := saveIconToDir(); err != nil {
|
||||||
fmt.Printf("Error saving icon: %v\n", err)
|
fmt.Printf("Error saving icon: %v\n", err)
|
||||||
}
|
}
|
||||||
@ -140,9 +144,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
|
|||||||
|
|
||||||
if options.Data != nil {
|
if options.Data != nil {
|
||||||
encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data)
|
encodedPayload, err := encodePayload(DefaultActionIdentifier, options.Data)
|
||||||
if err == nil {
|
if err != nil {
|
||||||
n.ActivationArguments = encodedPayload
|
return fmt.Errorf("failed to encode notification data: %w", err)
|
||||||
}
|
}
|
||||||
|
n.ActivationArguments = encodedPayload
|
||||||
}
|
}
|
||||||
|
|
||||||
return n.Push()
|
return n.Push()
|
||||||
@ -153,6 +158,10 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
|
|||||||
// If a NotificationCategory is not registered a basic notification will be sent.
|
// If a NotificationCategory is not registered a basic notification will be sent.
|
||||||
// (subtitle and category id are only available on macOS)
|
// (subtitle and category id are only available on macOS)
|
||||||
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
|
||||||
|
if err := validateNotificationOptions(options); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := saveIconToDir(); err != nil {
|
if err := saveIconToDir(); err != nil {
|
||||||
fmt.Printf("Error saving icon: %v\n", err)
|
fmt.Printf("Error saving icon: %v\n", err)
|
||||||
}
|
}
|
||||||
@ -191,15 +200,15 @@ func (ns *Service) SendNotificationWithActions(options NotificationOptions) erro
|
|||||||
n.ActivationArguments, _ = encodePayload(n.ActivationArguments, options.Data)
|
n.ActivationArguments, _ = encodePayload(n.ActivationArguments, options.Data)
|
||||||
|
|
||||||
for index := range n.Actions {
|
for index := range n.Actions {
|
||||||
n.Actions[index].Arguments, _ = encodePayload(n.Actions[index].Arguments, options.Data)
|
encodedPayload, err := encodePayload(n.Actions[index].Arguments, options.Data)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to encode notification data: %w", err)
|
||||||
|
}
|
||||||
|
n.Actions[index].Arguments = encodedPayload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err := n.Push()
|
return n.Push()
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
|
||||||
@ -344,20 +353,25 @@ func loadCategoriesFromRegistry() error {
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == registry.ErrNotExist {
|
if err == registry.ErrNotExist {
|
||||||
|
// Not an error, no saved categories
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return err
|
return fmt.Errorf("failed to open registry key: %w", err)
|
||||||
}
|
}
|
||||||
defer key.Close()
|
defer key.Close()
|
||||||
|
|
||||||
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
|
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
categories := make(map[string]NotificationCategory)
|
||||||
if err := json.Unmarshal([]byte(data), &categories); err != nil {
|
if err := json.Unmarshal([]byte(data), &categories); err != nil {
|
||||||
return err
|
return fmt.Errorf("failed to parse notification categories from registry: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationCategoriesLock.Lock()
|
notificationCategoriesLock.Lock()
|
||||||
|
Loading…
Reference in New Issue
Block a user