Stabilize the tray icon and locale handling on Windows while aligning provider and alias management panels with the refreshed desktop branding.
330 lines
8.1 KiB
Go
330 lines
8.1 KiB
Go
//go:build desktop_wails
|
|
|
|
package desktop
|
|
|
|
import (
|
|
"context"
|
|
_ "embed"
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/systray"
|
|
frontendassets "github.com/Apale7/opencode-provider-switch/frontend"
|
|
"github.com/Apale7/opencode-provider-switch/internal/app"
|
|
)
|
|
|
|
//go:embed assets/icon.ico
|
|
var trayIcon []byte
|
|
|
|
// Tray wires resident-mode controls into a native system tray.
|
|
type Tray struct {
|
|
service *app.Service
|
|
|
|
mu sync.Mutex
|
|
ctx context.Context
|
|
prefs app.DesktopPrefsView
|
|
language string
|
|
registered bool
|
|
ready bool
|
|
running bool
|
|
quitting bool
|
|
|
|
statusItem *systray.MenuItem
|
|
showItem *systray.MenuItem
|
|
hideItem *systray.MenuItem
|
|
startItem *systray.MenuItem
|
|
stopItem *systray.MenuItem
|
|
quitItem *systray.MenuItem
|
|
}
|
|
|
|
func NewTray(service *app.Service) *Tray {
|
|
return &Tray{service: service}
|
|
}
|
|
|
|
func (t *Tray) Attach(ctx context.Context) {
|
|
t.mu.Lock()
|
|
t.ctx = ctx
|
|
if t.registered {
|
|
t.mu.Unlock()
|
|
return
|
|
}
|
|
t.registered = true
|
|
t.mu.Unlock()
|
|
|
|
go func() {
|
|
runtime.LockOSThread()
|
|
defer runtime.UnlockOSThread()
|
|
systray.Run(t.onReady, t.onExit)
|
|
}()
|
|
}
|
|
|
|
func (t *Tray) Detach() {
|
|
t.mu.Lock()
|
|
t.ctx = nil
|
|
registered := t.registered
|
|
t.mu.Unlock()
|
|
if registered {
|
|
systray.Quit()
|
|
}
|
|
}
|
|
|
|
func (t *Tray) Sync(ctx context.Context, prefs app.DesktopPrefsView) {
|
|
t.mu.Lock()
|
|
if ctx != nil {
|
|
t.ctx = ctx
|
|
}
|
|
t.prefs = prefs
|
|
t.language = trayLanguage(prefs.Language)
|
|
ready := t.ready
|
|
statusItem := t.statusItem
|
|
showItem := t.showItem
|
|
hideItem := t.hideItem
|
|
startItem := t.startItem
|
|
stopItem := t.stopItem
|
|
quitItem := t.quitItem
|
|
labels := t.trayLabelsLocked()
|
|
t.mu.Unlock()
|
|
if ready && statusItem != nil && showItem != nil && hideItem != nil && startItem != nil && stopItem != nil && quitItem != nil {
|
|
systray.SetTitle(labels.appTitle)
|
|
systray.SetTooltip(labels.appTooltip)
|
|
showItem.SetTitle(labels.openWindow)
|
|
showItem.SetTooltip(labels.openWindowHint)
|
|
hideItem.SetTitle(labels.hideWindow)
|
|
hideItem.SetTooltip(labels.hideWindowHint)
|
|
startItem.SetTitle(labels.startProxy)
|
|
startItem.SetTooltip(labels.startProxyHint)
|
|
stopItem.SetTitle(labels.stopProxy)
|
|
stopItem.SetTooltip(labels.stopProxyHint)
|
|
quitItem.SetTitle(labels.quit)
|
|
quitItem.SetTooltip(labels.quitHint)
|
|
}
|
|
t.refresh()
|
|
}
|
|
|
|
func (t *Tray) RefreshProxyStatus(ctx context.Context) {
|
|
t.mu.Lock()
|
|
if ctx != nil {
|
|
t.ctx = ctx
|
|
}
|
|
t.mu.Unlock()
|
|
t.refresh()
|
|
}
|
|
|
|
func (t *Tray) BeforeClose(ctx context.Context) (bool, error) {
|
|
t.mu.Lock()
|
|
t.ctx = ctx
|
|
quitting := t.quitting
|
|
minimize := t.prefs.MinimizeToTray
|
|
t.mu.Unlock()
|
|
if quitting || !minimize {
|
|
return false, nil
|
|
}
|
|
return true, hideWindow(ctx)
|
|
}
|
|
|
|
func (t *Tray) onReady() {
|
|
labels := t.labels()
|
|
if len(trayIcon) > 0 {
|
|
systray.SetIcon(trayIcon)
|
|
}
|
|
systray.SetTitle(labels.appTitle)
|
|
systray.SetTooltip(labels.appTooltip)
|
|
systray.SetOnTapped(func() {
|
|
_ = t.withContext(showWindow)
|
|
})
|
|
|
|
t.mu.Lock()
|
|
t.ready = true
|
|
t.running = true
|
|
t.statusItem = systray.AddMenuItem(labels.proxyChecking, labels.proxyStatusHint)
|
|
t.statusItem.Disable()
|
|
systray.AddSeparator()
|
|
t.showItem = systray.AddMenuItem(labels.openWindow, labels.openWindowHint)
|
|
t.hideItem = systray.AddMenuItem(labels.hideWindow, labels.hideWindowHint)
|
|
systray.AddSeparator()
|
|
t.startItem = systray.AddMenuItem(labels.startProxy, labels.startProxyHint)
|
|
t.stopItem = systray.AddMenuItem(labels.stopProxy, labels.stopProxyHint)
|
|
systray.AddSeparator()
|
|
t.quitItem = systray.AddMenuItem(labels.quit, labels.quitHint)
|
|
t.mu.Unlock()
|
|
|
|
go t.loop()
|
|
t.refresh()
|
|
}
|
|
|
|
func (t *Tray) onExit() {
|
|
t.mu.Lock()
|
|
t.ready = false
|
|
t.running = false
|
|
t.registered = false
|
|
t.statusItem = nil
|
|
t.showItem = nil
|
|
t.hideItem = nil
|
|
t.startItem = nil
|
|
t.stopItem = nil
|
|
t.quitItem = nil
|
|
t.mu.Unlock()
|
|
}
|
|
|
|
func (t *Tray) loop() {
|
|
for {
|
|
t.mu.Lock()
|
|
showItem := t.showItem
|
|
hideItem := t.hideItem
|
|
startItem := t.startItem
|
|
stopItem := t.stopItem
|
|
quitItem := t.quitItem
|
|
running := t.running
|
|
t.mu.Unlock()
|
|
if !running || showItem == nil || hideItem == nil || startItem == nil || stopItem == nil || quitItem == nil {
|
|
return
|
|
}
|
|
|
|
select {
|
|
case <-showItem.ClickedCh:
|
|
_ = t.withContext(showWindow)
|
|
case <-hideItem.ClickedCh:
|
|
_ = t.withContext(hideWindow)
|
|
case <-startItem.ClickedCh:
|
|
t.startProxy()
|
|
case <-stopItem.ClickedCh:
|
|
t.stopProxy()
|
|
case <-quitItem.ClickedCh:
|
|
t.requestQuit()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *Tray) startProxy() {
|
|
if t.service == nil {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
_ = t.service.StartProxy(ctx)
|
|
t.refresh()
|
|
}
|
|
|
|
func (t *Tray) stopProxy() {
|
|
if t.service == nil {
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
_ = t.service.StopProxy(ctx)
|
|
t.refresh()
|
|
}
|
|
|
|
func (t *Tray) requestQuit() {
|
|
t.mu.Lock()
|
|
t.quitting = true
|
|
ctx := t.ctx
|
|
t.mu.Unlock()
|
|
if ctx == nil {
|
|
return
|
|
}
|
|
_ = quitWindow(ctx)
|
|
}
|
|
|
|
func (t *Tray) withContext(fn func(context.Context) error) error {
|
|
t.mu.Lock()
|
|
ctx := t.ctx
|
|
t.mu.Unlock()
|
|
if ctx == nil {
|
|
return nil
|
|
}
|
|
return fn(ctx)
|
|
}
|
|
|
|
func (t *Tray) refresh() {
|
|
t.mu.Lock()
|
|
ready := t.ready
|
|
statusItem := t.statusItem
|
|
startItem := t.startItem
|
|
stopItem := t.stopItem
|
|
labels := t.trayLabelsLocked()
|
|
t.mu.Unlock()
|
|
if !ready || statusItem == nil || startItem == nil || stopItem == nil || t.service == nil {
|
|
return
|
|
}
|
|
|
|
status, err := t.service.GetProxyStatus(context.Background())
|
|
if err != nil {
|
|
statusItem.SetTitle(labels.proxyUnavailable)
|
|
startItem.Enable()
|
|
stopItem.Disable()
|
|
return
|
|
}
|
|
|
|
if status.Running {
|
|
statusItem.SetTitle(fmt.Sprintf(labels.proxyRunning, status.BindAddress))
|
|
startItem.Disable()
|
|
stopItem.Enable()
|
|
return
|
|
}
|
|
|
|
statusItem.SetTitle(fmt.Sprintf(labels.proxyStopped, status.BindAddress))
|
|
startItem.Enable()
|
|
stopItem.Disable()
|
|
}
|
|
|
|
func (t *Tray) labels() trayLabels {
|
|
t.mu.Lock()
|
|
defer t.mu.Unlock()
|
|
return t.trayLabelsLocked()
|
|
}
|
|
|
|
func (t *Tray) trayLabelsLocked() trayLabels {
|
|
return trayLabels{
|
|
appTitle: trayLocaleString(t.language, "tray", "appTitle", "ocswitch"),
|
|
appTooltip: trayLocaleString(t.language, "tray", "appTooltip", "ocswitch desktop"),
|
|
proxyChecking: trayLocaleString(t.language, "tray", "proxyChecking", "Proxy: checking..."),
|
|
proxyStatusHint: trayLocaleString(t.language, "tray", "proxyStatusHint", "Current proxy status"),
|
|
proxyUnavailable: trayLocaleString(t.language, "tray", "proxyUnavailable", "Proxy: unavailable"),
|
|
proxyRunning: trayLocaleString(t.language, "tray", "proxyRunning", "Proxy: running (%s)"),
|
|
proxyStopped: trayLocaleString(t.language, "tray", "proxyStopped", "Proxy: stopped (%s)"),
|
|
openWindow: trayLocaleString(t.language, "tray", "openWindow", "Open window"),
|
|
openWindowHint: trayLocaleString(t.language, "tray", "openWindowHint", "Show desktop window"),
|
|
hideWindow: trayLocaleString(t.language, "tray", "hideWindow", "Hide window"),
|
|
hideWindowHint: trayLocaleString(t.language, "tray", "hideWindowHint", "Hide desktop window"),
|
|
startProxy: trayLocaleString(t.language, "tray", "startProxy", "Start proxy"),
|
|
startProxyHint: trayLocaleString(t.language, "tray", "startProxyHint", "Start local proxy"),
|
|
stopProxy: trayLocaleString(t.language, "tray", "stopProxy", "Stop proxy"),
|
|
stopProxyHint: trayLocaleString(t.language, "tray", "stopProxyHint", "Stop local proxy"),
|
|
quit: trayLocaleString(t.language, "tray", "quit", "Quit"),
|
|
quitHint: trayLocaleString(t.language, "tray", "quitHint", "Exit application"),
|
|
}
|
|
}
|
|
|
|
func trayLocaleString(language string, key string, field string, fallback string) string {
|
|
value, ok, err := frontendassets.LocaleValue(language, key, field)
|
|
if err == nil && ok && strings.TrimSpace(value) != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
type trayLabels struct {
|
|
appTitle string
|
|
appTooltip string
|
|
proxyChecking string
|
|
proxyStatusHint string
|
|
proxyUnavailable string
|
|
proxyRunning string
|
|
proxyStopped string
|
|
openWindow string
|
|
openWindowHint string
|
|
hideWindow string
|
|
hideWindowHint string
|
|
startProxy string
|
|
startProxyHint string
|
|
stopProxy string
|
|
stopProxyHint string
|
|
quit string
|
|
quitHint string
|
|
}
|