init commit
This commit is contained in:
185
tray.go
Normal file
185
tray.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// Precomputed tray icons
|
||||
var (
|
||||
iconGray = makeVpnIcon(130, 130, 130)
|
||||
iconGreen = makeVpnIcon(76, 175, 80)
|
||||
iconYellow = makeVpnIcon(253, 216, 53)
|
||||
iconRed = makeVpnIcon(229, 57, 53)
|
||||
)
|
||||
|
||||
func makeVpnIcon(r, g, b uint8) []byte {
|
||||
const size = 16
|
||||
img := image.NewNRGBA(image.Rect(0, 0, size, size))
|
||||
bg := color.NRGBA{R: r, G: g, B: b, A: 255}
|
||||
|
||||
type pt struct{ x, y int }
|
||||
skip := make(map[pt]bool)
|
||||
|
||||
// Make a horizontal pill-shaped rectangle
|
||||
// Skip rows 0, 1, 14, 15
|
||||
for x := 0; x < size; x++ {
|
||||
skip[pt{x, 0}] = true; skip[pt{x, 1}] = true
|
||||
skip[pt{x, 14}] = true; skip[pt{x, 15}] = true
|
||||
}
|
||||
// Round corners of the remaining rectangle (rows 2-13)
|
||||
// y=2 and y=13 skip x=0,1, 14,15
|
||||
skip[pt{0, 2}] = true; skip[pt{1, 2}] = true; skip[pt{14, 2}] = true; skip[pt{15, 2}] = true
|
||||
skip[pt{0, 13}] = true; skip[pt{1, 13}] = true; skip[pt{14, 13}] = true; skip[pt{15, 13}] = true
|
||||
// y=3 and y=12 skip x=0, 15
|
||||
skip[pt{0, 3}] = true; skip[pt{15, 3}] = true
|
||||
skip[pt{0, 12}] = true; skip[pt{15, 12}] = true
|
||||
|
||||
// Draw rounded background
|
||||
for y := 0; y < size; y++ {
|
||||
for x := 0; x < size; x++ {
|
||||
if !skip[pt{x, y}] {
|
||||
img.SetNRGBA(x, y, bg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw "VPN" text in white using pixel font
|
||||
white := color.NRGBA{255, 255, 255, 255}
|
||||
// V (x: 2-4, y: 5-9)
|
||||
img.SetNRGBA(2, 5, white); img.SetNRGBA(4, 5, white)
|
||||
img.SetNRGBA(2, 6, white); img.SetNRGBA(4, 6, white)
|
||||
img.SetNRGBA(2, 7, white); img.SetNRGBA(4, 7, white)
|
||||
img.SetNRGBA(3, 8, white); img.SetNRGBA(3, 9, white)
|
||||
|
||||
// P (x: 6-8, y: 5-9)
|
||||
for y := 5; y <= 9; y++ { img.SetNRGBA(6, y, white) }
|
||||
img.SetNRGBA(7, 5, white); img.SetNRGBA(8, 5, white)
|
||||
img.SetNRGBA(8, 6, white)
|
||||
img.SetNRGBA(7, 7, white); img.SetNRGBA(8, 7, white)
|
||||
|
||||
// N (x: 10-13, y: 5-9)
|
||||
for y := 5; y <= 9; y++ { img.SetNRGBA(10, y, white); img.SetNRGBA(13, y, white) }
|
||||
img.SetNRGBA(11, 6, white)
|
||||
img.SetNRGBA(11, 7, white); img.SetNRGBA(12, 7, white)
|
||||
img.SetNRGBA(12, 8, white)
|
||||
|
||||
var buf bytes.Buffer
|
||||
_ = png.Encode(&buf, img)
|
||||
|
||||
// Wrap PNG in ICO format
|
||||
pngData := buf.Bytes()
|
||||
var ico bytes.Buffer
|
||||
ico.Write([]byte{0, 0, 1, 0, 1, 0}) // ICONDIR
|
||||
ico.Write([]byte{16, 16, 0, 0, 1, 0, 32, 0}) // ICONDIRENTRY (16x16, 32bpp)
|
||||
sizeBytes := len(pngData)
|
||||
ico.WriteByte(byte(sizeBytes & 0xFF))
|
||||
ico.WriteByte(byte((sizeBytes >> 8) & 0xFF))
|
||||
ico.WriteByte(byte((sizeBytes >> 16) & 0xFF))
|
||||
ico.WriteByte(byte((sizeBytes >> 24) & 0xFF))
|
||||
ico.Write([]byte{22, 0, 0, 0}) // Image offset
|
||||
ico.Write(pngData)
|
||||
|
||||
return ico.Bytes()
|
||||
}
|
||||
|
||||
func (a *App) setupTray() {
|
||||
go systray.Run(a.onTrayReady, func() {})
|
||||
}
|
||||
|
||||
func (a *App) onTrayReady() {
|
||||
systray.SetIcon(iconGray)
|
||||
systray.SetTooltip("kettuRay — Disconnected")
|
||||
|
||||
mToggle := systray.AddMenuItem("Connect", "Toggle VPN")
|
||||
systray.AddSeparator()
|
||||
mShow := systray.AddMenuItem("Show", "Show window")
|
||||
systray.AddSeparator()
|
||||
mQuit := systray.AddMenuItem("Exit", "Exit kettuRay")
|
||||
|
||||
// Store menu item reference for text updates
|
||||
a.mu.Lock()
|
||||
a.trayToggleItem = mToggle
|
||||
a.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-mToggle.ClickedCh:
|
||||
a.toggleVPN()
|
||||
case <-mShow.ClickedCh:
|
||||
runtime.WindowShow(a.ctx)
|
||||
case <-mQuit.ClickedCh:
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) updateTrayState(state string) {
|
||||
switch state {
|
||||
case "Connected":
|
||||
systray.SetIcon(iconGreen)
|
||||
systray.SetTooltip("kettuRay — Connected")
|
||||
a.mu.Lock()
|
||||
if a.trayToggleItem != nil {
|
||||
a.trayToggleItem.SetTitle("Disconnect")
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case "Connecting":
|
||||
systray.SetIcon(iconYellow)
|
||||
systray.SetTooltip("kettuRay — Connecting...")
|
||||
a.mu.Lock()
|
||||
if a.trayToggleItem != nil {
|
||||
a.trayToggleItem.SetTitle("Connecting...")
|
||||
}
|
||||
a.mu.Unlock()
|
||||
case "Disconnecting":
|
||||
systray.SetIcon(iconYellow)
|
||||
systray.SetTooltip("kettuRay — Disconnecting...")
|
||||
case "Error":
|
||||
systray.SetIcon(iconRed)
|
||||
systray.SetTooltip("kettuRay — Error")
|
||||
a.mu.Lock()
|
||||
if a.trayToggleItem != nil {
|
||||
a.trayToggleItem.SetTitle("Connect")
|
||||
}
|
||||
a.mu.Unlock()
|
||||
default: // Disconnected
|
||||
systray.SetIcon(iconGray)
|
||||
systray.SetTooltip("kettuRay — Disconnected")
|
||||
a.mu.Lock()
|
||||
if a.trayToggleItem != nil {
|
||||
a.trayToggleItem.SetTitle("Connect")
|
||||
}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) toggleVPN() {
|
||||
a.mu.Lock()
|
||||
state := a.manager.State
|
||||
id := a.selectedConfigID
|
||||
a.mu.Unlock()
|
||||
|
||||
switch state {
|
||||
case 2: // Connected
|
||||
go a.manager.Disconnect()
|
||||
case 1: // Connecting
|
||||
go a.manager.Disconnect()
|
||||
default:
|
||||
if id != "" {
|
||||
go a.connectByID(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HideWindow hides the app window (minimize to tray).
|
||||
func (a *App) HideWindow() {
|
||||
runtime.WindowHide(a.ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user