Release 3.8.2.7: Windows taskbar/tray sea-wave icon when connected.
Match macOS Dock behavior using the same idle/connected glyph assets. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -256,7 +256,8 @@ https://evilfox.win/
|
|||||||
- 3.8.2+3: poll без os.Stat ядер + кэш host; Darwin sysproxy batch; Connect без Config.Clone;
|
- 3.8.2+3: poll без os.Stat ядер + кэш host; Darwin sysproxy batch; Connect без Config.Clone;
|
||||||
- 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP `/api` (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (`Navis-x.y.z.b.exe`);
|
- 3.8.2+4: единый GUI Windows/macOS на glaze + HTTP `/api` (вместо WebView2 Bind); автоинкремент номера сборки при compile; артефакты Windows с версией в имени (`Navis-x.y.z.b.exe`);
|
||||||
- 3.8.2+5: delta-poll UI (`getState` по rev); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на `Engine.Running`; singleflight corebin; PingAll worker-pool;
|
- 3.8.2+5: delta-poll UI (`getState` по rev); пауза опроса при скрытом окне; cancel lifecycle dock/watchdog; invalidate/лимит hostCache; PollUI без lock на `Engine.Running`; singleflight corebin; PingAll worker-pool;
|
||||||
- 3.8.2+6: поиск по серверам и хоткеи; Windows close→tray (Открыть/Выход); фоновый прогрев активного core; SSE `/api/events` + редкий poll fallback.
|
- 3.8.2+6: поиск по серверам и хоткеи; Windows close→tray (Открыть/Выход); фоновый прогрев активного core; SSE `/api/events` + редкий poll fallback;
|
||||||
|
- 3.8.2+7: Windows — иконка в панели задач и трее цвета морской волны при подключении (как Dock на macOS).
|
||||||
|
|
||||||
В 3.8.1:
|
В 3.8.1:
|
||||||
- меньше нагрузка в простое: лёгкий опрос интерфейса без полного клонирования конфига, кэш путей к ядрам, лог с лимитом размера;
|
- меньше нагрузка в простое: лёгкий опрос интерфейса без полного клонирования конфига, кэш путей к ядрам, лог с лимитом размера;
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ android {
|
|||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
// versionCode = major*1_000_000 + minor*10_000 + patch*100 + build
|
||||||
versionCode = 3080206
|
versionCode = 3080207
|
||||||
versionName = "3.8.2+6"
|
versionName = "3.8.2+7"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64")
|
||||||
|
|||||||
+3
-3
@@ -42,11 +42,11 @@ if errorlevel 1 exit /b 1
|
|||||||
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
go build -o "tools\packmac\packmac.exe" .\tools\packmac
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.6 -arch arm64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-arm64\Navis" -out "dist\navis-release\darwin-arm64" -version 3.8.2 -build 3.8.2.7 -arch arm64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.6 -arch amd64
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-amd64\Navis" -out "dist\navis-release\darwin-amd64" -version 3.8.2 -build 3.8.2.7 -arch amd64
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.6 -arch universal
|
tools\packmac\packmac.exe -bin "dist\navis-release\darwin-universal\Navis" -out "dist\navis-release\darwin-universal" -version 3.8.2 -build 3.8.2.7 -arch universal
|
||||||
if errorlevel 1 exit /b 1
|
if errorlevel 1 exit /b 1
|
||||||
|
|
||||||
echo Built Mac GUI + CLI:
|
echo Built Mac GUI + CLI:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"vpnclient/internal/apphost"
|
"vpnclient/internal/apphost"
|
||||||
"vpnclient/internal/config"
|
"vpnclient/internal/config"
|
||||||
"vpnclient/internal/core"
|
"vpnclient/internal/core"
|
||||||
|
"vpnclient/internal/dockicon"
|
||||||
"vpnclient/internal/logbuf"
|
"vpnclient/internal/logbuf"
|
||||||
"vpnclient/internal/trayhost"
|
"vpnclient/internal/trayhost"
|
||||||
"vpnclient/internal/update"
|
"vpnclient/internal/update"
|
||||||
@@ -71,6 +72,7 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
a.StartBackground()
|
a.StartBackground()
|
||||||
|
dockicon.OnIconChange = trayhost.SetConnectedIcon
|
||||||
trayhost.Start("Navis", trayhost.Hooks{
|
trayhost.Start("Navis", trayhost.Hooks{
|
||||||
Connect: func() { _ = a.Connect() },
|
Connect: func() { _ = a.Connect() },
|
||||||
Disconnect: func() { _ = a.Disconnect() },
|
Disconnect: func() { _ = a.Disconnect() },
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import (
|
|||||||
|
|
||||||
"github.com/crgimenes/glaze"
|
"github.com/crgimenes/glaze"
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
|
|
||||||
|
"vpnclient/internal/dockicon"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -68,6 +70,7 @@ func decorateWindow(w glaze.WebView) {
|
|||||||
}
|
}
|
||||||
hwnd := w.Window()
|
hwnd := w.Window()
|
||||||
applyWindowIcon(hwnd)
|
applyWindowIcon(hwnd)
|
||||||
|
dockicon.BindWindow(hwnd)
|
||||||
installCloseToHide(hwnd)
|
installCloseToHide(hwnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+3
-3
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.2",
|
"version": "3.8.2",
|
||||||
"notes": "Navis 3.8.2+6: поиск/фильтр серверов и хоткеи (/, Ctrl+F, Space/Enter, Esc); Windows: закрытие окна = скрытие в трей, «Открыть»/Выход из трея; фоновый EnsureCore активного протокола; SSE /api/events для мгновенного статуса + poll 12 с как safety net.",
|
"notes": "Navis 3.8.2+7: Windows — иконка в панели задач и трее цвета морской волны при подключении (как Dock на macOS); при отключении возвращается обычная.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+3
-3
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.2",
|
"version": "3.8.2",
|
||||||
"notes": "Navis 3.8.2+6: поиск/фильтр серверов и хоткеи (/, Ctrl+F, Space/Enter, Esc); Windows: закрытие окна = скрытие в трей, «Открыть»/Выход из трея; фоновый EnsureCore активного протокола; SSE /api/events для мгновенного статуса + poll 12 с как safety net.",
|
"notes": "Navis 3.8.2+7: Windows — иконка в панели задач и трее цвета морской волны при подключении (как Dock на macOS); при отключении возвращается обычная.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import (
|
|||||||
"github.com/ebitengine/purego/objc"
|
"github.com/ebitengine/purego/objc"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BindWindow is a no-op on macOS (Dock uses NSApplication icon image).
|
||||||
|
func BindWindow(h unsafe.Pointer) { _ = h }
|
||||||
|
|
||||||
//go:embed icon_idle.png
|
//go:embed icon_idle.png
|
||||||
var iconIdlePNG []byte
|
var iconIdlePNG []byte
|
||||||
|
|
||||||
@@ -103,4 +106,7 @@ func SetConnected(connected bool) {
|
|||||||
}
|
}
|
||||||
// AppKit must run on the main thread (glaze owns it).
|
// AppKit must run on the main thread (glaze owns it).
|
||||||
app.Send(selPerf, selSet, img, false)
|
app.Send(selPerf, selSet, img, false)
|
||||||
|
if OnIconChange != nil {
|
||||||
|
OnIconChange(connected)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
//go:build !darwin
|
//go:build !darwin && !windows
|
||||||
|
|
||||||
package dockicon
|
package dockicon
|
||||||
|
|
||||||
// SetConnected is a no-op outside macOS (Dock icons are a Darwin feature).
|
import "unsafe"
|
||||||
|
|
||||||
|
// SetConnected is a no-op on platforms without a Dock/taskbar icon swap.
|
||||||
func SetConnected(connected bool) {}
|
func SetConnected(connected bool) {}
|
||||||
|
|
||||||
|
// BindWindow is a no-op outside Windows.
|
||||||
|
func BindWindow(h unsafe.Pointer) { _ = h }
|
||||||
|
|||||||
@@ -0,0 +1,295 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package dockicon
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
_ "embed"
|
||||||
|
"image"
|
||||||
|
"image/png"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"golang.org/x/sys/windows"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed icon_idle.png
|
||||||
|
var iconIdlePNG []byte
|
||||||
|
|
||||||
|
//go:embed icon_connected.png
|
||||||
|
var iconConnectedPNG []byte
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||||
|
gdi32 = windows.NewLazySystemDLL("gdi32.dll")
|
||||||
|
|
||||||
|
procSendMessage = user32.NewProc("SendMessageW")
|
||||||
|
procCreateIconFromResource = user32.NewProc("CreateIconFromResourceEx")
|
||||||
|
procCreateIconIndirect = user32.NewProc("CreateIconIndirect")
|
||||||
|
procDestroyIcon = user32.NewProc("DestroyIcon")
|
||||||
|
procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
|
||||||
|
procCreateBitmap = gdi32.NewProc("CreateBitmap")
|
||||||
|
procDeleteObject = gdi32.NewProc("DeleteObject")
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
hwnd uintptr
|
||||||
|
last *bool
|
||||||
|
|
||||||
|
idleSmall windows.Handle
|
||||||
|
idleBig windows.Handle
|
||||||
|
connSmall windows.Handle
|
||||||
|
connBig windows.Handle
|
||||||
|
iconsReady bool
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
wmSetIcon = 0x0080
|
||||||
|
iconSmall = 0
|
||||||
|
iconBig = 1
|
||||||
|
biRGB = 0
|
||||||
|
dibRGBColors = 0
|
||||||
|
)
|
||||||
|
|
||||||
|
type iconInfo struct {
|
||||||
|
FIcon int32
|
||||||
|
XHotspot uint32
|
||||||
|
YHotspot uint32
|
||||||
|
HbmMask windows.Handle
|
||||||
|
HbmColor windows.Handle
|
||||||
|
}
|
||||||
|
|
||||||
|
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 bitmapInfo struct {
|
||||||
|
Header bitmapInfoHeader
|
||||||
|
Colors [1]uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// BindWindow attaches the main HWND so SetConnected can update the taskbar icon.
|
||||||
|
func BindWindow(h unsafe.Pointer) {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
hwnd = uintptr(h)
|
||||||
|
if err := ensureIconsLocked(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
connected := last != nil && *last
|
||||||
|
applyLocked(connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConnected swaps the taskbar/window icon: sea-wave N when connected, original when idle.
|
||||||
|
func SetConnected(connected bool) {
|
||||||
|
mu.Lock()
|
||||||
|
if last != nil && *last == connected {
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v := connected
|
||||||
|
last = &v
|
||||||
|
if hwnd == 0 {
|
||||||
|
mu.Unlock()
|
||||||
|
if OnIconChange != nil {
|
||||||
|
OnIconChange(connected)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := ensureIconsLocked(); err != nil {
|
||||||
|
mu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applyLocked(connected)
|
||||||
|
mu.Unlock()
|
||||||
|
if OnIconChange != nil {
|
||||||
|
OnIconChange(connected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusIcon returns a shared HICON for the current connection state (do not DestroyIcon).
|
||||||
|
func StatusIcon(connected bool) windows.Handle {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if err := ensureIconsLocked(); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if connected {
|
||||||
|
return connBig
|
||||||
|
}
|
||||||
|
return idleBig
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyLocked(connected bool) {
|
||||||
|
if hwnd == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
small, big := idleSmall, idleBig
|
||||||
|
if connected {
|
||||||
|
small, big = connSmall, connBig
|
||||||
|
}
|
||||||
|
if small != 0 {
|
||||||
|
procSendMessage.Call(hwnd, wmSetIcon, iconSmall, uintptr(small))
|
||||||
|
}
|
||||||
|
if big != 0 {
|
||||||
|
procSendMessage.Call(hwnd, wmSetIcon, iconBig, uintptr(big))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureIconsLocked() error {
|
||||||
|
if iconsReady {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
idleSmall, err = hiconFromPNG(iconIdlePNG, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
idleBig, err = hiconFromPNG(iconIdlePNG, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connSmall, err = hiconFromPNG(iconConnectedPNG, 16)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
connBig, err = hiconFromPNG(iconConnectedPNG, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
iconsReady = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hiconFromPNG(src []byte, size int) (windows.Handle, error) {
|
||||||
|
resized, err := resizePNG(src, size)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if h := createIconFromPNGResource(resized, size); h != 0 {
|
||||||
|
return h, nil
|
||||||
|
}
|
||||||
|
return createIconFromImage(resized, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resizePNG(src []byte, size int) ([]byte, error) {
|
||||||
|
img, err := png.Decode(bytes.NewReader(src))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dst := image.NewRGBA(image.Rect(0, 0, size, size))
|
||||||
|
draw.CatmullRom.Scale(dst, dst.Bounds(), img, img.Bounds(), draw.Over, nil)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := png.Encode(&buf, dst); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIconFromPNGResource(pngBytes []byte, size int) windows.Handle {
|
||||||
|
if len(pngBytes) == 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
h, _, _ := procCreateIconFromResource.Call(
|
||||||
|
uintptr(unsafe.Pointer(&pngBytes[0])),
|
||||||
|
uintptr(len(pngBytes)),
|
||||||
|
1,
|
||||||
|
0x00030000,
|
||||||
|
uintptr(size),
|
||||||
|
uintptr(size),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
return windows.Handle(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
func createIconFromImage(pngBytes []byte, size int) (windows.Handle, error) {
|
||||||
|
img, err := png.Decode(bytes.NewReader(pngBytes))
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
rgba, ok := img.(*image.RGBA)
|
||||||
|
if !ok {
|
||||||
|
tmp := image.NewRGBA(img.Bounds())
|
||||||
|
draw.Draw(tmp, tmp.Bounds(), img, img.Bounds().Min, draw.Src)
|
||||||
|
rgba = tmp
|
||||||
|
}
|
||||||
|
|
||||||
|
hbmColor, err := dibFromRGBA(rgba)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
// Monochrome mask: all zeros => fully opaque where color has alpha handled by 32-bit DIB.
|
||||||
|
hbmMask, _, err2 := procCreateBitmap.Call(uintptr(size), uintptr(size), 1, 1, 0)
|
||||||
|
if hbmMask == 0 {
|
||||||
|
procDeleteObject.Call(uintptr(hbmColor))
|
||||||
|
if err2 != nil {
|
||||||
|
return 0, err2
|
||||||
|
}
|
||||||
|
return 0, windows.ERROR_INVALID_HANDLE
|
||||||
|
}
|
||||||
|
|
||||||
|
ii := iconInfo{
|
||||||
|
FIcon: 1,
|
||||||
|
XHotspot: 0,
|
||||||
|
YHotspot: 0,
|
||||||
|
HbmMask: windows.Handle(hbmMask),
|
||||||
|
HbmColor: hbmColor,
|
||||||
|
}
|
||||||
|
h, _, err3 := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&ii)))
|
||||||
|
procDeleteObject.Call(uintptr(hbmMask))
|
||||||
|
procDeleteObject.Call(uintptr(hbmColor))
|
||||||
|
if h == 0 {
|
||||||
|
if err3 != nil {
|
||||||
|
return 0, err3
|
||||||
|
}
|
||||||
|
return 0, windows.ERROR_INVALID_HANDLE
|
||||||
|
}
|
||||||
|
return windows.Handle(h), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dibFromRGBA(src *image.RGBA) (windows.Handle, error) {
|
||||||
|
w := src.Bounds().Dx()
|
||||||
|
h := src.Bounds().Dy()
|
||||||
|
bi := bitmapInfo{
|
||||||
|
Header: bitmapInfoHeader{
|
||||||
|
BiSize: uint32(unsafe.Sizeof(bitmapInfoHeader{})),
|
||||||
|
BiWidth: int32(w),
|
||||||
|
BiHeight: -int32(h), // top-down
|
||||||
|
BiPlanes: 1,
|
||||||
|
BiBitCount: 32,
|
||||||
|
BiCompression: biRGB,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var bits unsafe.Pointer
|
||||||
|
hbm, _, err := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bi)), dibRGBColors, uintptr(unsafe.Pointer(&bits)), 0, 0)
|
||||||
|
if hbm == 0 || bits == nil {
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return 0, windows.ERROR_INVALID_HANDLE
|
||||||
|
}
|
||||||
|
// Windows wants BGRA.
|
||||||
|
dst := unsafe.Slice((*byte)(bits), w*h*4)
|
||||||
|
for y := 0; y < h; y++ {
|
||||||
|
for x := 0; x < w; x++ {
|
||||||
|
i := src.PixOffset(x, y)
|
||||||
|
o := (y*w + x) * 4
|
||||||
|
dst[o+0] = src.Pix[i+2] // B
|
||||||
|
dst[o+1] = src.Pix[i+1] // G
|
||||||
|
dst[o+2] = src.Pix[i+0] // R
|
||||||
|
dst[o+3] = src.Pix[i+3] // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return windows.Handle(hbm), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package dockicon
|
||||||
|
|
||||||
|
// OnIconChange is invoked after SetConnected updates the platform icon.
|
||||||
|
// Used on Windows to keep the tray glyph in sync; unused on other platforms.
|
||||||
|
var OnIconChange func(connected bool)
|
||||||
@@ -8,3 +8,6 @@ func start(appName string, h Hooks) bool {
|
|||||||
_ = h
|
_ = h
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetConnectedIcon is a no-op when tray is unsupported.
|
||||||
|
func SetConnectedIcon(connected bool) { _ = connected }
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ package trayhost
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"unsafe"
|
"unsafe"
|
||||||
|
|
||||||
|
"vpnclient/internal/dockicon"
|
||||||
|
|
||||||
"golang.org/x/sys/windows"
|
"golang.org/x/sys/windows"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -90,6 +93,12 @@ type msg struct {
|
|||||||
Pt point
|
Pt point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
trayMu sync.Mutex
|
||||||
|
trayNID *notifyIconData
|
||||||
|
trayLastUp *bool
|
||||||
|
)
|
||||||
|
|
||||||
func start(appName string, h Hooks) bool {
|
func start(appName string, h Hooks) bool {
|
||||||
if h.Quit == nil {
|
if h.Quit == nil {
|
||||||
return false
|
return false
|
||||||
@@ -102,6 +111,35 @@ func start(appName string, h Hooks) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetConnectedIcon updates the tray glyph (sea-wave N when connected).
|
||||||
|
func SetConnectedIcon(connected bool) {
|
||||||
|
trayMu.Lock()
|
||||||
|
nid := trayNID
|
||||||
|
if trayLastUp != nil && *trayLastUp == connected {
|
||||||
|
trayMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
v := connected
|
||||||
|
trayLastUp = &v
|
||||||
|
trayMu.Unlock()
|
||||||
|
if nid == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
icon := dockicon.StatusIcon(connected)
|
||||||
|
if icon == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trayMu.Lock()
|
||||||
|
if trayNID == nil {
|
||||||
|
trayMu.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trayNID.HIcon = icon
|
||||||
|
trayNID.UFlags = nifMessage | nifIcon | nifTip
|
||||||
|
procShellNotify.Call(nimModify, uintptr(unsafe.Pointer(trayNID)))
|
||||||
|
trayMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func runTray(appName string, h Hooks) {
|
func runTray(appName string, h Hooks) {
|
||||||
className, _ := windows.UTF16PtrFromString("NavisTrayClass")
|
className, _ := windows.UTF16PtrFromString("NavisTrayClass")
|
||||||
title, _ := windows.UTF16PtrFromString(appName)
|
title, _ := windows.UTF16PtrFromString(appName)
|
||||||
@@ -155,17 +193,27 @@ func runTray(appName string, h Hooks) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
icon, _, _ := procLoadIcon.Call(0, uintptr(idiApplication))
|
icon := dockicon.StatusIcon(false)
|
||||||
|
if icon == 0 {
|
||||||
|
h, _, _ := procLoadIcon.Call(0, uintptr(idiApplication))
|
||||||
|
icon = windows.Handle(h)
|
||||||
|
}
|
||||||
var nid notifyIconData
|
var nid notifyIconData
|
||||||
nid.CbSize = uint32(unsafe.Sizeof(nid))
|
nid.CbSize = uint32(unsafe.Sizeof(nid))
|
||||||
nid.Hwnd = windows.Handle(hwnd)
|
nid.Hwnd = windows.Handle(hwnd)
|
||||||
nid.UID = 1
|
nid.UID = 1
|
||||||
nid.UFlags = nifMessage | nifIcon | nifTip
|
nid.UFlags = nifMessage | nifIcon | nifTip
|
||||||
nid.UCallbackMessage = wmTray
|
nid.UCallbackMessage = wmTray
|
||||||
nid.HIcon = windows.Handle(icon)
|
nid.HIcon = icon
|
||||||
tip, _ := windows.UTF16FromString(appName)
|
tip, _ := windows.UTF16FromString(appName)
|
||||||
copy(nid.SzTip[:], tip)
|
copy(nid.SzTip[:], tip)
|
||||||
procShellNotify.Call(nimAdd, uintptr(unsafe.Pointer(&nid)))
|
procShellNotify.Call(nimAdd, uintptr(unsafe.Pointer(&nid)))
|
||||||
|
trayMu.Lock()
|
||||||
|
trayNID = &nid
|
||||||
|
trayMu.Unlock()
|
||||||
|
if h.IsUp != nil {
|
||||||
|
SetConnectedIcon(h.IsUp())
|
||||||
|
}
|
||||||
|
|
||||||
var m msg
|
var m msg
|
||||||
for {
|
for {
|
||||||
@@ -176,6 +224,9 @@ func runTray(appName string, h Hooks) {
|
|||||||
procTranslate.Call(uintptr(unsafe.Pointer(&m)))
|
procTranslate.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
procDispatch.Call(uintptr(unsafe.Pointer(&m)))
|
procDispatch.Call(uintptr(unsafe.Pointer(&m)))
|
||||||
}
|
}
|
||||||
|
trayMu.Lock()
|
||||||
|
trayNID = nil
|
||||||
|
trayMu.Unlock()
|
||||||
procShellNotify.Call(nimDelete, uintptr(unsafe.Pointer(&nid)))
|
procShellNotify.Call(nimDelete, uintptr(unsafe.Pointer(&nid)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const CurrentVersion = "3.8.2"
|
|||||||
|
|
||||||
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
// BuildNumber is the monotonic build within CurrentVersion (Windows FileVersion 4th part,
|
||||||
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
// macOS CFBundleVersion suffix, Android versionCode low digits). Bump on every release build.
|
||||||
const BuildNumber = 6
|
const BuildNumber = 7
|
||||||
|
|
||||||
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
// DefaultManifestURL is the update feed (hosted in the project git repo).
|
||||||
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
const DefaultManifestURL = "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/update.json"
|
||||||
|
|||||||
+3
-3
@@ -1,16 +1,16 @@
|
|||||||
{
|
{
|
||||||
"version": "3.8.2",
|
"version": "3.8.2",
|
||||||
"notes": "Navis 3.8.2+6: поиск/фильтр серверов и хоткеи (/, Ctrl+F, Space/Enter, Esc); Windows: закрытие окна = скрытие в трей, «Открыть»/Выход из трея; фоновый EnsureCore активного протокола; SSE /api/events для мгновенного статуса + poll 12 с как safety net.",
|
"notes": "Navis 3.8.2+7: Windows — иконка в панели задач и трее цвета морской волны при подключении (как Dock на macOS); при отключении возвращается обычная.",
|
||||||
"platform": "windows-amd64",
|
"platform": "windows-amd64",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64",
|
"arch": "amd64",
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"mandatory": false,
|
"mandatory": false,
|
||||||
"platforms": {
|
"platforms": {
|
||||||
"windows-amd64": {
|
"windows-amd64": {
|
||||||
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
"url": "https://git.evilfox.cc/test2/navi/raw/branch/main/dist/navis-release/Navis.exe",
|
||||||
"sha256": "aab618d452f8eddd7cc4400770e53a6a9c456b2bc6093190dd21f1b3fbcd765e",
|
"sha256": "b7902251aeb671946dbfe7ab2f3470dea82cf29cc0cd68e42f6632653ea90605",
|
||||||
"os": "windows",
|
"os": "windows",
|
||||||
"arch": "amd64"
|
"arch": "amd64"
|
||||||
},
|
},
|
||||||
|
|||||||
+4
-4
@@ -4,13 +4,13 @@
|
|||||||
"Major": 3,
|
"Major": 3,
|
||||||
"Minor": 8,
|
"Minor": 8,
|
||||||
"Patch": 2,
|
"Patch": 2,
|
||||||
"Build": 6
|
"Build": 7
|
||||||
},
|
},
|
||||||
"ProductVersion": {
|
"ProductVersion": {
|
||||||
"Major": 3,
|
"Major": 3,
|
||||||
"Minor": 8,
|
"Minor": 8,
|
||||||
"Patch": 2,
|
"Patch": 2,
|
||||||
"Build": 6
|
"Build": 7
|
||||||
},
|
},
|
||||||
"FileFlagsMask": "3f",
|
"FileFlagsMask": "3f",
|
||||||
"FileFlags": "00",
|
"FileFlags": "00",
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
"StringFileInfo": {
|
"StringFileInfo": {
|
||||||
"CompanyName": "EvilFox",
|
"CompanyName": "EvilFox",
|
||||||
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
"FileDescription": "Navis — VPN client (Naive / Hy2 / AWG / VLESS / VMess / Trojan)",
|
||||||
"FileVersion": "3.8.2.6",
|
"FileVersion": "3.8.2.7",
|
||||||
"InternalName": "Navis",
|
"InternalName": "Navis",
|
||||||
"LegalCopyright": "Copyright (c) EvilFox",
|
"LegalCopyright": "Copyright (c) EvilFox",
|
||||||
"OriginalFilename": "Navis.exe",
|
"OriginalFilename": "Navis.exe",
|
||||||
"ProductName": "Navis",
|
"ProductName": "Navis",
|
||||||
"ProductVersion": "3.8.2.6",
|
"ProductVersion": "3.8.2.7",
|
||||||
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
"Comments": "Open-source VPN/proxy client. https://evilfox.win/"
|
||||||
},
|
},
|
||||||
"VarFileInfo": {
|
"VarFileInfo": {
|
||||||
|
|||||||
Reference in New Issue
Block a user