5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-04 16:59:31 +08:00
wails/v3/pkg/services/notifications/notifications_linux.go
2025-02-25 15:59:27 -08:00

289 lines
7.6 KiB
Go

//go:build linux
package notifications
import (
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"sync"
"git.sr.ht/~whereswaldon/shout"
"github.com/godbus/dbus/v5"
"github.com/wailsapp/wails/v3/pkg/application"
)
var NotificationLock sync.RWMutex
var NotificationCategories = make(map[string]NotificationCategory)
var Notifier shout.Notifier
// Creates a new Notifications Service.
func New() *Service {
if NotificationService == nil {
NotificationService = &Service{}
}
return NotificationService
}
// ServiceStartup is called when the service is loaded
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
if err := loadCategories(); err != nil {
fmt.Printf("Failed to load notification categories: %v\n", err)
}
conn, err := dbus.SessionBus()
if err != nil {
return fmt.Errorf("failed to connect to D-Bus session bus: %v", err)
}
appName := application.Get().Config().Name
var iconPath string
Notifier, err = shout.NewNotifier(conn, appName, iconPath, func(notificationID, action string, platformData map[string]dbus.Variant, target, notifierResponse dbus.Variant, err error) {
if err != nil {
return
}
response := NotificationResponse{
ID: notificationID,
ActionIdentifier: action,
}
if action == "" {
response.ActionIdentifier = DefaultActionIdentifier
}
if target.Signature().String() == "s" {
var targetStr string
if err := target.Store(&targetStr); err == nil {
var userInfo map[string]interface{}
if err := json.Unmarshal([]byte(targetStr), &userInfo); err == nil {
response.UserInfo = userInfo
}
}
}
if notifierResponse.Signature().String() == "s" {
var userText string
if err := notifierResponse.Store(&userText); err == nil {
response.UserText = userText
}
}
if NotificationService != nil {
NotificationService.handleNotificationResponse(response)
}
})
if err != nil {
return fmt.Errorf("failed to create notifier: %v", err)
}
return nil
}
// ServiceShutdown is called when the service is unloaded
func (ns *Service) ServiceShutdown() error {
return saveCategories()
}
// CheckBundleIdentifier is a Linux stub that always returns true.
// (bundle identifiers are macOS-specific)
func CheckBundleIdentifier() bool {
return true
}
// RequestUserNotificationAuthorization is a Linux stub that always returns true, nil.
// (user authorization is macOS-specific)
func (ns *Service) RequestUserNotificationAuthorization() (bool, error) {
return true, nil
}
// CheckNotificationAuthorization is a Linux stub that always returns true.
// (user authorization is macOS-specific)
func (ns *Service) CheckNotificationAuthorization() (bool, error) {
return true, nil
}
// SendNotification sends a basic notification with a unique identifier, title, subtitle, and body.
func (ns *Service) SendNotification(options NotificationOptions) error {
notification := shout.Notification{
Title: options.Title,
Body: options.Body,
Priority: shout.Normal,
DefaultAction: "default",
}
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err == nil {
notification.DefaultActionTarget = dbus.MakeVariant(string(jsonData))
}
}
return Notifier.Send(options.ID, notification)
}
// SendNotificationWithActions sends a notification with additional actions and inputs.
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
NotificationLock.RLock()
category, exists := NotificationCategories[options.CategoryID]
NotificationLock.RUnlock()
if !exists {
return ns.SendNotification(options)
}
notification := shout.Notification{
Title: options.Title,
Body: options.Body,
Priority: shout.Normal,
DefaultAction: "default",
}
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err == nil {
notification.DefaultActionTarget = dbus.MakeVariant(string(jsonData))
}
}
for _, action := range category.Actions {
notification.Buttons = append(notification.Buttons, shout.Button{
Label: action.Title,
Action: action.ID,
Target: "", // Will be set below if we have user data
})
}
if options.Data != nil {
jsonData, err := json.Marshal(options.Data)
if err == nil {
for i := range notification.Buttons {
notification.Buttons[i].Target = string(jsonData)
}
}
}
return Notifier.Send(options.ID, notification)
}
// RegisterNotificationCategory registers a new NotificationCategory to be used with SendNotificationWithActions.
func (ns *Service) RegisterNotificationCategory(category NotificationCategory) error {
NotificationLock.Lock()
NotificationCategories[category.ID] = category
NotificationLock.Unlock()
return saveCategories()
}
// RemoveNotificationCategory removes a previously registered NotificationCategory.
func (ns *Service) RemoveNotificationCategory(categoryId string) error {
NotificationLock.Lock()
delete(NotificationCategories, categoryId)
NotificationLock.Unlock()
return saveCategories()
}
// RemoveAllPendingNotifications is a Linux stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveAllPendingNotifications() error {
return nil
}
// RemovePendingNotification is a Linux stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemovePendingNotification(_ string) error {
return nil
}
// RemoveAllDeliveredNotifications is a Linux stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveAllDeliveredNotifications() error {
return nil
}
// RemoveDeliveredNotification is a Linux stub that always returns nil.
// (macOS-specific)
func (ns *Service) RemoveDeliveredNotification(_ string) error {
return nil
}
// RemoveNotification removes a notification by ID (Linux-specific)
func (ns *Service) RemoveNotification(identifier string) error {
return Notifier.Remove(identifier)
}
// getConfigFilePath returns the path to the configuration file for storing notification categories
func getConfigFilePath() (string, error) {
configDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("failed to get user config directory: %v", err)
}
appName := "Wails Application"
appConfigDir := filepath.Join(configDir, appName)
if err := os.MkdirAll(appConfigDir, 0755); err != nil {
return "", fmt.Errorf("failed to create config directory: %v", err)
}
return filepath.Join(appConfigDir, "notification-categories.json"), nil
}
// saveCategories saves the notification categories to a file.
func saveCategories() error {
filePath, err := getConfigFilePath()
if err != nil {
return err
}
NotificationLock.RLock()
data, err := json.Marshal(NotificationCategories)
NotificationLock.RUnlock()
if err != nil {
return fmt.Errorf("failed to marshal notification categories: %v", err)
}
if err := os.WriteFile(filePath, data, 0644); err != nil {
return fmt.Errorf("failed to write notification categories to file: %v", err)
}
return nil
}
// loadCategories loads notification categories from a file.
func loadCategories() error {
filePath, err := getConfigFilePath()
if err != nil {
return err
}
if _, err := os.Stat(filePath); os.IsNotExist(err) {
return nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("failed to read notification categories file: %v", err)
}
if len(data) == 0 {
return nil
}
categories := make(map[string]NotificationCategory)
if err := json.Unmarshal(data, &categories); err != nil {
return fmt.Errorf("failed to unmarshal notification categories: %v", err)
}
NotificationLock.Lock()
NotificationCategories = categories
NotificationLock.Unlock()
return nil
}