5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-08 06:50:56 +08:00

use app icon

This commit is contained in:
Zach Botterman 2025-02-26 12:30:31 -08:00
parent 207b162544
commit 3bdb3ddba3
2 changed files with 210 additions and 44 deletions

View File

@ -15,12 +15,24 @@ import (
"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"
)
var NotificationLock sync.RWMutex
var NotificationCategories = make(map[string]NotificationCategory)
var Icon []byte
var (
NotificationLock sync.RWMutex
NotificationCategories = make(map[string]NotificationCategory)
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 {
@ -39,17 +51,20 @@ func New() *Service {
// ServiceStartup is called when the service is loaded
// Sets an activation callback to emit an event when notifications are interacted with.
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
appName := application.Get().Config().Name
AppName = application.Get().Config().Name
guid, err := getGUID(appName)
guid, err := getGUID()
if err != nil {
return err
}
AppGUID = guid
IconPath = filepath.Join(os.TempDir(), AppName+guid+".png")
toast.SetAppData(toast.AppData{
AppID: appName,
AppID: AppName,
GUID: guid,
IconPath: filepath.Join(os.TempDir(), appName+guid+".png"),
IconPath: IconPath,
})
toast.SetActivationCallback(func(args string, data []toast.UserData) {
@ -103,10 +118,8 @@ func (ns *Service) CheckNotificationAuthorization() bool {
// 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)
func (ns *Service) SendNotification(options NotificationOptions) error {
if len(Icon) > 0 {
if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err)
}
if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err)
}
n := toast.Notification{
@ -130,10 +143,8 @@ func (ns *Service) SendNotification(options NotificationOptions) error {
// If a NotificationCategory is not registered a basic notification will be sent.
// (subtitle and category id are only available on macOS)
func (ns *Service) SendNotificationWithActions(options NotificationOptions) error {
if len(Icon) > 0 {
if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err)
}
if err := saveIconToDir(); err != nil {
fmt.Printf("Error saving icon: %v\n", err)
}
NotificationLock.RLock()
@ -236,11 +247,6 @@ func (ns *Service) RemoveNotification(identifier string) error {
return nil
}
// SetIcon sets the notifications icon.
func (ns *Service) SetIcon(icon []byte) {
Icon = icon
}
// encodePayload combines an action ID and user data into a single encoded string
func encodePayload(actionID string, data map[string]interface{}) (string, error) {
payload := NotificationPayload{
@ -287,25 +293,16 @@ func parseNotificationResponse(response string) (action string, data string) {
}
func saveIconToDir() error {
options := application.Get().Config()
appName := options.Name
guid, err := getGUID(appName)
icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3))
if err != nil {
return fmt.Errorf("failed to retrieve application guid from registry")
return fmt.Errorf("failed to retrieve application icon")
}
iconPath := filepath.Join(os.TempDir(), appName+guid+".png")
return os.WriteFile(iconPath, Icon, 0644)
return saveHIconAsPNG(icon, IconPath)
}
func saveCategoriesToRegistry() error {
appName := application.Get().Config().Name
if appName == "" {
return fmt.Errorf("failed to save categories to registry: empty executable name")
}
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
key, _, err := registry.CreateKey(
registry.CURRENT_USER,
@ -324,15 +321,11 @@ func saveCategoriesToRegistry() error {
return err
}
return key.SetStringValue("Categories", string(data))
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
}
func loadCategoriesFromRegistry() error {
appName := application.Get().Config().Name
if appName == "" {
return fmt.Errorf("failed to save categories to registry: empty executable name")
}
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
key, err := registry.OpenKey(
registry.CURRENT_USER,
@ -347,7 +340,7 @@ func loadCategoriesFromRegistry() error {
}
defer key.Close()
data, _, err := key.GetStringValue("Categories")
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
if err != nil {
return err
}
@ -373,12 +366,12 @@ func getUserText(data []toast.UserData) (string, bool) {
return "", false
}
func getGUID(name string) (string, error) {
keyPath := `Software\Classes\AppUserModelId\` + name
func getGUID() (string, error) {
keyPath := ToastRegistryPath + AppName
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
if err == nil {
guid, _, err := k.GetStringValue("CustomActivator")
guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
k.Close()
if err == nil && guid != "" {
return guid, nil
@ -393,7 +386,7 @@ func getGUID(name string) (string, error) {
}
defer k.Close()
if err := k.SetStringValue("CustomActivator", guid); err != nil {
if err := k.SetStringValue(ToastRegistryGuidKey, guid); err != nil {
return "", fmt.Errorf("failed to write GUID to registry: %w", err)
}

View File

@ -0,0 +1,173 @@
//go:build windows
package notifications
import (
"image"
"image/color"
"image/png"
"os"
"syscall"
"unsafe"
"github.com/wailsapp/wails/v3/pkg/w32"
)
// Windows API constants
const (
SRCCOPY = 0x00CC0020
BI_RGB = 0
DIB_RGB_COLORS = 0
)
// Windows structures
type ICONINFO struct {
FIcon int32
XHotspot int32
YHotspot int32
HbmMask syscall.Handle
HbmColor syscall.Handle
}
type BITMAP struct {
BmType int32
BmWidth int32
BmHeight int32
BmWidthBytes int32
BmPlanes uint16
BmBitsPixel uint16
BmBits uintptr
}
type BITMAPINFOHEADER struct {
BiSize uint32
BiWidth int32
BiHeight int32
BiPlanes uint16
BiBitCount uint16
BiCompression uint32
BiSizeImage uint32
BiXPelsPerMeter int32
BiYPelsPerMeter int32
BiClrUsed uint32
BiClrImportant uint32
}
type RGBQUAD struct {
RgbBlue byte
RgbGreen byte
RgbRed byte
RgbReserved byte
}
type BITMAPINFO struct {
BmiHeader BITMAPINFOHEADER
BmiColors [1]RGBQUAD
}
func saveHIconAsPNG(hIcon w32.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)
}