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:
parent
207b162544
commit
3bdb3ddba3
@ -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)
|
||||
}
|
||||
|
||||
|
173
v3/pkg/services/notifications/notifications_windows_icon.go
Normal file
173
v3/pkg/services/notifications/notifications_windows_icon.go
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user