mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-11 14:39:30 +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"
|
"git.sr.ht/~jackmordaunt/go-toast/v2"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/wailsapp/wails/v3/pkg/application"
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
var NotificationLock sync.RWMutex
|
var (
|
||||||
var NotificationCategories = make(map[string]NotificationCategory)
|
NotificationLock sync.RWMutex
|
||||||
var Icon []byte
|
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
|
// NotificationPayload combines the action ID and user data into a single structure
|
||||||
type NotificationPayload struct {
|
type NotificationPayload struct {
|
||||||
@ -39,17 +51,20 @@ func New() *Service {
|
|||||||
// ServiceStartup is called when the service is loaded
|
// ServiceStartup is called when the service is loaded
|
||||||
// Sets an activation callback to emit an event when notifications are interacted with.
|
// Sets an activation callback to emit an event when notifications are interacted with.
|
||||||
func (ns *Service) ServiceStartup(ctx context.Context, options application.ServiceOptions) error {
|
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 {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
AppGUID = guid
|
||||||
|
|
||||||
|
IconPath = filepath.Join(os.TempDir(), AppName+guid+".png")
|
||||||
|
|
||||||
toast.SetAppData(toast.AppData{
|
toast.SetAppData(toast.AppData{
|
||||||
AppID: appName,
|
AppID: AppName,
|
||||||
GUID: guid,
|
GUID: guid,
|
||||||
IconPath: filepath.Join(os.TempDir(), appName+guid+".png"),
|
IconPath: IconPath,
|
||||||
})
|
})
|
||||||
|
|
||||||
toast.SetActivationCallback(func(args string, data []toast.UserData) {
|
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.
|
// 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 len(Icon) > 0 {
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n := toast.Notification{
|
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.
|
// 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 len(Icon) > 0 {
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationLock.RLock()
|
NotificationLock.RLock()
|
||||||
@ -236,11 +247,6 @@ func (ns *Service) RemoveNotification(identifier string) error {
|
|||||||
return nil
|
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
|
// encodePayload combines an action ID and user data into a single encoded string
|
||||||
func encodePayload(actionID string, data map[string]interface{}) (string, error) {
|
func encodePayload(actionID string, data map[string]interface{}) (string, error) {
|
||||||
payload := NotificationPayload{
|
payload := NotificationPayload{
|
||||||
@ -287,25 +293,16 @@ func parseNotificationResponse(response string) (action string, data string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func saveIconToDir() error {
|
func saveIconToDir() error {
|
||||||
options := application.Get().Config()
|
icon, err := application.NewIconFromResource(w32.GetModuleHandle(""), uint16(3))
|
||||||
appName := options.Name
|
|
||||||
|
|
||||||
guid, err := getGUID(appName)
|
|
||||||
if err != nil {
|
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 saveHIconAsPNG(icon, IconPath)
|
||||||
|
|
||||||
return os.WriteFile(iconPath, Icon, 0644)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveCategoriesToRegistry() error {
|
func saveCategoriesToRegistry() error {
|
||||||
appName := application.Get().Config().Name
|
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
|
||||||
if appName == "" {
|
|
||||||
return fmt.Errorf("failed to save categories to registry: empty executable name")
|
|
||||||
}
|
|
||||||
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
|
|
||||||
|
|
||||||
key, _, err := registry.CreateKey(
|
key, _, err := registry.CreateKey(
|
||||||
registry.CURRENT_USER,
|
registry.CURRENT_USER,
|
||||||
@ -324,15 +321,11 @@ func saveCategoriesToRegistry() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return key.SetStringValue("Categories", string(data))
|
return key.SetStringValue(NotificationCategoriesRegistryKey, string(data))
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadCategoriesFromRegistry() error {
|
func loadCategoriesFromRegistry() error {
|
||||||
appName := application.Get().Config().Name
|
registryPath := fmt.Sprintf(NotificationCategoriesRegistryPath, AppName)
|
||||||
if appName == "" {
|
|
||||||
return fmt.Errorf("failed to save categories to registry: empty executable name")
|
|
||||||
}
|
|
||||||
registryPath := fmt.Sprintf(`SOFTWARE\%s\NotificationCategories`, appName)
|
|
||||||
|
|
||||||
key, err := registry.OpenKey(
|
key, err := registry.OpenKey(
|
||||||
registry.CURRENT_USER,
|
registry.CURRENT_USER,
|
||||||
@ -347,7 +340,7 @@ func loadCategoriesFromRegistry() error {
|
|||||||
}
|
}
|
||||||
defer key.Close()
|
defer key.Close()
|
||||||
|
|
||||||
data, _, err := key.GetStringValue("Categories")
|
data, _, err := key.GetStringValue(NotificationCategoriesRegistryKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -373,12 +366,12 @@ func getUserText(data []toast.UserData) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func getGUID(name string) (string, error) {
|
func getGUID() (string, error) {
|
||||||
keyPath := `Software\Classes\AppUserModelId\` + name
|
keyPath := ToastRegistryPath + AppName
|
||||||
|
|
||||||
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
|
k, err := registry.OpenKey(registry.CURRENT_USER, keyPath, registry.QUERY_VALUE)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
guid, _, err := k.GetStringValue("CustomActivator")
|
guid, _, err := k.GetStringValue(ToastRegistryGuidKey)
|
||||||
k.Close()
|
k.Close()
|
||||||
if err == nil && guid != "" {
|
if err == nil && guid != "" {
|
||||||
return guid, nil
|
return guid, nil
|
||||||
@ -393,7 +386,7 @@ func getGUID(name string) (string, error) {
|
|||||||
}
|
}
|
||||||
defer k.Close()
|
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)
|
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