[client][ui] added accessible tray icons (#3335)

Added accessible tray icons with:
- dark mode support on Windows and Linux, kudos to @burgosz for the PoC
- template icon support on MacOS
Also added appropriate connecting status icons
This commit is contained in:
Karsa 2025-02-18 02:21:44 +01:00 committed by GitHub
parent 8fb5a9ce11
commit f67e56d3b9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 149 additions and 33 deletions

View File

@ -53,7 +53,7 @@ nfpms:
contents: contents:
- src: client/ui/netbird.desktop - src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-connected.png - src: client/ui/netbird.png
dst: /usr/share/pixmaps/netbird.png dst: /usr/share/pixmaps/netbird.png
dependencies: dependencies:
- netbird - netbird
@ -70,7 +70,7 @@ nfpms:
contents: contents:
- src: client/ui/netbird.desktop - src: client/ui/netbird.desktop
dst: /usr/share/applications/netbird.desktop dst: /usr/share/applications/netbird.desktop
- src: client/ui/netbird-systemtray-connected.png - src: client/ui/netbird.png
dst: /usr/share/pixmaps/netbird.png dst: /usr/share/pixmaps/netbird.png
dependencies: dependencies:
- netbird - netbird

View File

@ -21,6 +21,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/app" "fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"fyne.io/systray" "fyne.io/systray"
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
@ -90,6 +91,14 @@ func main() {
} }
client := newServiceClient(daemonAddr, a, showSettings, showRoutes) client := newServiceClient(daemonAddr, a, showSettings, showRoutes)
settingsChangeChan := make(chan fyne.Settings)
a.Settings().AddChangeListener(settingsChangeChan)
go func() {
for range settingsChangeChan {
client.updateIcon()
}
}()
if showSettings || showRoutes { if showSettings || showRoutes {
a.Run() a.Run()
} else { } else {
@ -106,46 +115,108 @@ func main() {
} }
} }
//go:embed netbird.ico
var iconAboutICO []byte
//go:embed netbird.png
var iconAboutPNG []byte
//go:embed netbird-systemtray-connected.ico //go:embed netbird-systemtray-connected.ico
var iconConnectedICO []byte var iconConnectedICO []byte
//go:embed netbird-systemtray-connected.png //go:embed netbird-systemtray-connected.png
var iconConnectedPNG []byte var iconConnectedPNG []byte
//go:embed netbird-systemtray-connected-macos.png
var iconConnectedMacOS []byte
//go:embed netbird-systemtray-connected-dark.ico
var iconConnectedDarkICO []byte
//go:embed netbird-systemtray-connected-dark.png
var iconConnectedDarkPNG []byte
//go:embed netbird-systemtray-disconnected.ico //go:embed netbird-systemtray-disconnected.ico
var iconDisconnectedICO []byte var iconDisconnectedICO []byte
//go:embed netbird-systemtray-disconnected.png //go:embed netbird-systemtray-disconnected.png
var iconDisconnectedPNG []byte var iconDisconnectedPNG []byte
//go:embed netbird-systemtray-disconnected-macos.png
var iconDisconnectedMacOS []byte
//go:embed netbird-systemtray-update-disconnected.ico //go:embed netbird-systemtray-update-disconnected.ico
var iconUpdateDisconnectedICO []byte var iconUpdateDisconnectedICO []byte
//go:embed netbird-systemtray-update-disconnected.png //go:embed netbird-systemtray-update-disconnected.png
var iconUpdateDisconnectedPNG []byte var iconUpdateDisconnectedPNG []byte
//go:embed netbird-systemtray-update-disconnected-macos.png
var iconUpdateDisconnectedMacOS []byte
//go:embed netbird-systemtray-update-disconnected-dark.ico
var iconUpdateDisconnectedDarkICO []byte
//go:embed netbird-systemtray-update-disconnected-dark.png
var iconUpdateDisconnectedDarkPNG []byte
//go:embed netbird-systemtray-update-connected.ico //go:embed netbird-systemtray-update-connected.ico
var iconUpdateConnectedICO []byte var iconUpdateConnectedICO []byte
//go:embed netbird-systemtray-update-connected.png //go:embed netbird-systemtray-update-connected.png
var iconUpdateConnectedPNG []byte var iconUpdateConnectedPNG []byte
//go:embed netbird-systemtray-update-cloud.ico //go:embed netbird-systemtray-update-connected-macos.png
var iconUpdateCloudICO []byte var iconUpdateConnectedMacOS []byte
//go:embed netbird-systemtray-update-cloud.png //go:embed netbird-systemtray-update-connected-dark.ico
var iconUpdateCloudPNG []byte var iconUpdateConnectedDarkICO []byte
//go:embed netbird-systemtray-update-connected-dark.png
var iconUpdateConnectedDarkPNG []byte
//go:embed netbird-systemtray-connecting.ico
var iconConnectingICO []byte
//go:embed netbird-systemtray-connecting.png
var iconConnectingPNG []byte
//go:embed netbird-systemtray-connecting-macos.png
var iconConnectingMacOS []byte
//go:embed netbird-systemtray-connecting-dark.ico
var iconConnectingDarkICO []byte
//go:embed netbird-systemtray-connecting-dark.png
var iconConnectingDarkPNG []byte
//go:embed netbird-systemtray-error.ico
var iconErrorICO []byte
//go:embed netbird-systemtray-error.png
var iconErrorPNG []byte
//go:embed netbird-systemtray-error-macos.png
var iconErrorMacOS []byte
//go:embed netbird-systemtray-error-dark.ico
var iconErrorDarkICO []byte
//go:embed netbird-systemtray-error-dark.png
var iconErrorDarkPNG []byte
type serviceClient struct { type serviceClient struct {
ctx context.Context ctx context.Context
addr string addr string
conn proto.DaemonServiceClient conn proto.DaemonServiceClient
icAbout []byte
icConnected []byte icConnected []byte
icDisconnected []byte icDisconnected []byte
icUpdateConnected []byte icUpdateConnected []byte
icUpdateDisconnected []byte icUpdateDisconnected []byte
icUpdateCloud []byte icConnecting []byte
icError []byte
// systray menu items // systray menu items
mStatus *systray.MenuItem mStatus *systray.MenuItem
@ -214,20 +285,7 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
update: version.NewUpdate(), update: version.NewUpdate(),
} }
if runtime.GOOS == "windows" { s.setNewIcons()
s.icConnected = iconConnectedICO
s.icDisconnected = iconDisconnectedICO
s.icUpdateConnected = iconUpdateConnectedICO
s.icUpdateDisconnected = iconUpdateDisconnectedICO
s.icUpdateCloud = iconUpdateCloudICO
} else {
s.icConnected = iconConnectedPNG
s.icDisconnected = iconDisconnectedPNG
s.icUpdateConnected = iconUpdateConnectedPNG
s.icUpdateDisconnected = iconUpdateDisconnectedPNG
s.icUpdateCloud = iconUpdateCloudPNG
}
if showSettings { if showSettings {
s.showSettingsUI() s.showSettingsUI()
@ -239,6 +297,63 @@ func newServiceClient(addr string, a fyne.App, showSettings bool, showRoutes boo
return s return s
} }
func (s *serviceClient) setNewIcons() {
if runtime.GOOS == "windows" {
s.icAbout = iconAboutICO
if s.app.Settings().ThemeVariant() == theme.VariantDark {
s.icConnected = iconConnectedDarkICO
s.icDisconnected = iconDisconnectedICO
s.icUpdateConnected = iconUpdateConnectedDarkICO
s.icUpdateDisconnected = iconUpdateDisconnectedDarkICO
s.icConnecting = iconConnectingDarkICO
s.icError = iconErrorDarkICO
} else {
s.icConnected = iconConnectedICO
s.icDisconnected = iconDisconnectedICO
s.icUpdateConnected = iconUpdateConnectedICO
s.icUpdateDisconnected = iconUpdateDisconnectedICO
s.icConnecting = iconConnectingICO
s.icError = iconErrorICO
}
} else {
s.icAbout = iconAboutPNG
if s.app.Settings().ThemeVariant() == theme.VariantDark {
s.icConnected = iconConnectedDarkPNG
s.icDisconnected = iconDisconnectedPNG
s.icUpdateConnected = iconUpdateConnectedDarkPNG
s.icUpdateDisconnected = iconUpdateDisconnectedDarkPNG
s.icConnecting = iconConnectingDarkPNG
s.icError = iconErrorDarkPNG
} else {
s.icConnected = iconConnectedPNG
s.icDisconnected = iconDisconnectedPNG
s.icUpdateConnected = iconUpdateConnectedPNG
s.icUpdateDisconnected = iconUpdateDisconnectedPNG
s.icConnecting = iconConnectingPNG
s.icError = iconErrorPNG
}
}
}
func (s *serviceClient) updateIcon() {
s.setNewIcons()
s.updateIndicationLock.Lock()
if s.connected {
if s.isUpdateIconActive {
systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
} else {
systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
}
} else {
if s.isUpdateIconActive {
systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
} else {
systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
}
}
s.updateIndicationLock.Unlock()
}
func (s *serviceClient) showSettingsUI() { func (s *serviceClient) showSettingsUI() {
// add settings window UI elements. // add settings window UI elements.
s.wSettings = s.app.NewWindow("NetBird Settings") s.wSettings = s.app.NewWindow("NetBird Settings")
@ -376,8 +491,10 @@ func (s *serviceClient) login() error {
} }
func (s *serviceClient) menuUpClick() error { func (s *serviceClient) menuUpClick() error {
systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
systray.SetTemplateIcon(iconErrorMacOS, s.icError)
log.Errorf("get client: %v", err) log.Errorf("get client: %v", err)
return err return err
} }
@ -407,6 +524,7 @@ func (s *serviceClient) menuUpClick() error {
} }
func (s *serviceClient) menuDownClick() error { func (s *serviceClient) menuDownClick() error {
systray.SetTemplateIcon(iconConnectingMacOS, s.icConnecting)
conn, err := s.getSrvClient(defaultFailTimeout) conn, err := s.getSrvClient(defaultFailTimeout)
if err != nil { if err != nil {
log.Errorf("get client: %v", err) log.Errorf("get client: %v", err)
@ -458,9 +576,9 @@ func (s *serviceClient) updateStatus() error {
s.connected = true s.connected = true
s.sendNotification = true s.sendNotification = true
if s.isUpdateIconActive { if s.isUpdateIconActive {
systray.SetIcon(s.icUpdateConnected) systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
} else { } else {
systray.SetIcon(s.icConnected) systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
} }
systray.SetTooltip("NetBird (Connected)") systray.SetTooltip("NetBird (Connected)")
s.mStatus.SetTitle("Connected") s.mStatus.SetTitle("Connected")
@ -482,11 +600,9 @@ func (s *serviceClient) updateStatus() error {
s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion) s.isUpdateIconActive = s.update.SetDaemonVersion(status.DaemonVersion)
if !s.isUpdateIconActive { if !s.isUpdateIconActive {
if systrayIconState { if systrayIconState {
systray.SetIcon(s.icConnected) systray.SetTemplateIcon(iconConnectedMacOS, s.icConnected)
s.mAbout.SetIcon(s.icConnected)
} else { } else {
systray.SetIcon(s.icDisconnected) systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
s.mAbout.SetIcon(s.icDisconnected)
} }
} }
@ -517,9 +633,9 @@ func (s *serviceClient) updateStatus() error {
func (s *serviceClient) setDisconnectedStatus() { func (s *serviceClient) setDisconnectedStatus() {
s.connected = false s.connected = false
if s.isUpdateIconActive { if s.isUpdateIconActive {
systray.SetIcon(s.icUpdateDisconnected) systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
} else { } else {
systray.SetIcon(s.icDisconnected) systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
} }
systray.SetTooltip("NetBird (Disconnected)") systray.SetTooltip("NetBird (Disconnected)")
s.mStatus.SetTitle("Disconnected") s.mStatus.SetTitle("Disconnected")
@ -529,7 +645,7 @@ func (s *serviceClient) setDisconnectedStatus() {
} }
func (s *serviceClient) onTrayReady() { func (s *serviceClient) onTrayReady() {
systray.SetIcon(s.icDisconnected) systray.SetTemplateIcon(iconDisconnectedMacOS, s.icDisconnected)
systray.SetTooltip("NetBird") systray.SetTooltip("NetBird")
// setup systray menu items // setup systray menu items
@ -554,7 +670,7 @@ func (s *serviceClient) onTrayReady() {
systray.AddSeparator() systray.AddSeparator()
s.mAbout = systray.AddMenuItem("About", "About") s.mAbout = systray.AddMenuItem("About", "About")
s.mAbout.SetIcon(s.icDisconnected) s.mAbout.SetIcon(s.icAbout)
versionString := normalizedVersion(version.NetbirdVersion()) versionString := normalizedVersion(version.NetbirdVersion())
s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString)) s.mVersionUI = s.mAbout.AddSubMenuItem(fmt.Sprintf("GUI: %s", versionString), fmt.Sprintf("GUI Version: %s", versionString))
s.mVersionUI.Disable() s.mVersionUI.Disable()
@ -771,9 +887,9 @@ func (s *serviceClient) onUpdateAvailable() {
s.isUpdateIconActive = true s.isUpdateIconActive = true
if s.connected { if s.connected {
systray.SetIcon(s.icUpdateConnected) systray.SetTemplateIcon(iconUpdateConnectedMacOS, s.icUpdateConnected)
} else { } else {
systray.SetIcon(s.icUpdateDisconnected) systray.SetTemplateIcon(iconUpdateDisconnectedMacOS, s.icUpdateDisconnected)
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
client/ui/netbird.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB