Refactor main UI and update dependencies. Removed unused Fyne packages and integrated systray for system tray functionality. Updated .gitignore and Makefile for improved build process. Cleaned up theme and UI components for better maintainability.

This commit is contained in:
kony
2026-08-17 18:07:23 +08:00
parent 2297efe7a1
commit 215b0e2067
18 changed files with 1292 additions and 1418 deletions
-2
View File
@@ -7,8 +7,6 @@ bin
upx
go2
*.exe
fyne_metadata_init.go
fyne.syso
goodlink.json
*.7z
*.zip
+12 -4
View File
@@ -12,6 +12,15 @@ GOBUILD=GO111MODULE=on \
-X "go2.GitStatus=$(GitStatus)" \
-X "go2.BuildTime=$(BuildTime)" \
-w -s -buildid='
GOBUILD_UI=CGO_ENABLED=0 GO111MODULE=on \
GOPROXY="https://goproxy.cn,direct" \
GOEXPERIMENT=jsonv2 \
GOEXPERIMENT=loopvar \
go build -trimpath -ldflags \
'-H=windowsgui -X "go2.GitCommitLog=$(GitCommitLog)" \
-X "go2.GitStatus=$(GitStatus)" \
-X "go2.BuildTime=$(BuildTime)" \
-w -s -buildid='
LINUX_PLATFORM_LIST = \
linux-386-cmd \
@@ -92,9 +101,8 @@ windows-arm64-cmd:
GOARCH=arm64 GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
windows-amd64-ui:
# CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 fyne package -os windows -icon theme/favicon.ico
# go build -ldflags -H=windowsgui
mkdir bin; fyne package -release; mv *.exe bin/
mkdir -p $(BINDIR)
GOARCH=amd64 GOOS=windows $(GOBUILD_UI) -o $(BINDIR)/$(NAME)-$@.exe
windows-386-cmd:
GOARCH=386 GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
@@ -102,7 +110,7 @@ windows-386-cmd:
windows-arm-cmd:
GOARCH=arm GOOS=windows $(GOBUILD) -tags "cmd" -o $(BINDIR)/$(NAME)-$@.exe
create_nac:
rsrc -manifest nac.manifest -o nac.syso
rsrc -manifest nac.manifest -ico assert/favicon.ico -o nac.syso
rm_nac:
rm -rf nac.syso
+9 -42
View File
@@ -3,15 +3,10 @@ module goodlink
go 1.25.5
require (
fyne.io/fyne v1.4.3
fyne.io/fyne/v2 v2.7.4
github.com/BurntSushi/toml v1.6.0
github.com/atotto/clipboard v0.1.4
github.com/getlantern/systray v1.2.2
github.com/go-redis/redis v6.15.9+incompatible
github.com/pion/stun/v2 v2.0.0
github.com/pkg/errors v0.9.1
github.com/quic-go/quic-go v0.59.0
github.com/sirupsen/logrus v1.9.3
go2 v0.0.0
golang.org/x/sys v0.40.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
@@ -22,51 +17,23 @@ require (
)
require (
fyne.io/systray v1.12.1 // indirect
github.com/akavel/rsrc v0.10.2 // indirect
github.com/antoinefink/golang-ico v0.0.0-20251207114801-fa953cf78f26 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
github.com/go-text/render v0.2.1 // indirect
github.com/go-text/typesetting v0.3.4 // indirect
github.com/godbus/dbus/v5 v5.2.2 // indirect
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mattn/go-runewidth v0.0.17 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.6.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/pion/dtls/v2 v2.2.12 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v2 v2.2.10 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/image v0.34.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/text v0.33.0 // indirect
golang.org/x/time v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace go2 => ../go2
+25 -209
View File
@@ -1,261 +1,77 @@
fyne.io/fyne v1.4.3 h1:356CnXCiYrrfaLGsB7qLK3c6ktzyh8WR05v/2RBu51I=
fyne.io/fyne v1.4.3/go.mod h1:8kiPBNSDmuplxs9WnKCkaWYqbcXFy0DeAzwa6PBO9Z8=
fyne.io/fyne/v2 v2.7.2 h1:XiNpWkn0PzX43ZCjbb0QYGg1RCxVbugwfVgikWZBCMw=
fyne.io/fyne/v2 v2.7.2/go.mod h1:PXbqY3mQmJV3J1NRUR2VbVgUUx3vgvhuFJxyjRK/4Ug=
fyne.io/fyne/v2 v2.7.4 h1:OVCI5mT+Onb2kA4wlmGA5pLCqKik9f4NDb5jiR1OMTc=
fyne.io/fyne/v2 v2.7.4/go.mod h1:ZD1mmhBY75mSa97IXl3MPlICd1uNHfCXYh5hKIlVOII=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/akavel/rsrc v0.10.2 h1:Zxm8V5eI1hW4gGaYsJQUhxpjkENuG91ki8B4zCrvEsw=
github.com/akavel/rsrc v0.10.2/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
github.com/antoinefink/golang-ico v0.0.0-20251207114801-fa953cf78f26 h1:aRJlisWHMprWOjJ/ow5ykKz5x8D6uPEU16x35S7rdRc=
github.com/antoinefink/golang-ico v0.0.0-20251207114801-fa953cf78f26/go.mod h1:kupAIPIZtaUZRWnkktmLPEJoaQSwZpkCqXQz2dorV6Y=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/mobile v0.1.2/go.mod h1:/kOrWrZB6sasLbEy2JIvr4arEzQTXBTZGb3Y96yWbHY=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200625191551-73d3c3675aa3/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 h1:RkGhqHxEVAvPM0/R+8g7XRwQnHatO0KAuVcwHo8q9W8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728/go.mod h1:SyRD8YfuKk+ZXlDqYiqe1qMSqjNgtHzBTG810KUagMc=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg=
github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
github.com/go-text/typesetting v0.3.2 h1:OUOFxp9Rx5PiO0/rh2IY+5gmyXjXsVG8+LfEyk9NMcE=
github.com/go-text/typesetting v0.3.2/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.1 h1:d5qPO0iQ7h2oVtpzGnLExE+Wn9AtytxIfltcS2b9KD8=
github.com/hack-pad/safejs v0.1.1/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lucor/goinfo v0.0.0-20200401173949-526b5363a13a/go.mod h1:ORP3/rB5IsulLEBwQZCJyyV6niqmI7P4EWSmkug+1Ng=
github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ=
github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ=
github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY=
github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY=
github.com/onsi/gomega v1.38.0/go.mod h1:OcXcwId0b9QsE7Y49u+BTrL4IdKOBOKnD6VQNTJEB6o=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0=
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss=
go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8=
golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200720211630-cb9d2d5c5666/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190808195139-e713427fea3f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200328031815-3db5fc6bac03/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gvisor.dev/gvisor v0.0.0-20260106215814-b2227fa9cfe0 h1:1TYEkeqPAyXFBl91xFBY6j0U53SuIA48dBLTqfW3Li4=
gvisor.dev/gvisor v0.0.0-20260106215814-b2227fa9cfe0/go.mod h1:W1ZgZ/Dh85TgSZWH67l2jKVpDE5bjIaut7rjwwOiHzQ=
gvisor.dev/gvisor v0.0.0-20260112223324-f23d52117c28 h1:1AgA0K1rSgr3pfSrXwcPoMDSNuUChVe++wFcE2KXWIk=
gvisor.dev/gvisor v0.0.0-20260112223324-f23d52117c28/go.mod h1:QkHjoMIBaYtpVufgwv3keYAbln78mBoCuShZrPrer1Q=
+61 -102
View File
@@ -1,102 +1,61 @@
//go:build windows && !cmd
package main
import (
"embed"
"goodlink/config"
_ "goodlink/pro"
"goodlink/theme"
"goodlink/ui2"
"goodlink/utils"
goodlink_config "goodlink3/config"
_ "net/http/pprof"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/driver/desktop"
)
//go:embed assert/tray_idle.ico assert/tray_warning.ico assert/tray_danger.ico assert/tray_success.ico
var trayIcons embed.FS
const (
M_APP_TITLE = "Goodlink"
)
func main() {
// 检查单实例,如果不是第一个实例则退出
// 必须在创建任何UI资源之前检查,避免影响已运行的实例
if !utils.CheckSingleInstance() {
// 已有实例运行,直接退出
return
}
// 启动前清理遗留的cmd进程
utils.CleanupOrphanedCmdProcesses()
goodlink_config.DeleteLocalConfig()
config.SetVersion(GetVersionFromAppConfig())
config.Help()
myApp := app.New()
myApp.Settings().SetTheme(&theme.MyTheme{})
myWindow := myApp.NewWindow(M_APP_TITLE + " v" + config.GetVersion())
// 监听显示窗口请求
// Fyne的Show()方法会自动处理线程安全,可以直接在goroutine中调用
go func() {
for range utils.GetShowWindowChan() {
// Fyne会自动处理线程安全,直接调用Show()
myWindow.Show()
myWindow.RequestFocus()
}
}()
idle, _ := trayIcons.ReadFile("assert/tray_idle.ico")
warning, _ := trayIcons.ReadFile("assert/tray_warning.ico")
danger, _ := trayIcons.ReadFile("assert/tray_danger.ico")
success, _ := trayIcons.ReadFile("assert/tray_success.ico")
if desk, ok := myApp.(desktop.App); ok {
// 创建菜单项
openItem := fyne.NewMenuItem("打开主程序", func() {
// 系统托盘菜单回调已经在主线程中执行,可以直接调用Show()
myWindow.Show()
myWindow.RequestFocus()
})
quitItem := fyne.NewMenuItem("退出", func() {
ui2.StopCmdProcess()
myApp.Quit()
})
// 创建菜单,确保只有一个退出选项
m := fyne.NewMenu("",
openItem,
fyne.NewMenuItemSeparator(),
quitItem)
desk.SetSystemTrayMenu(m)
ui2.InitTrayIcons(desk, idle, warning, danger, success)
}
myWindow.SetContent(ui2.GetMainUI(&myWindow))
// 设置窗口初始大小:宽度等于高度(正方形窗口)
minSize := myWindow.Content().MinSize()
myWindow.Resize(fyne.NewSize(minSize.Width*2, minSize.Height))
myWindow.SetCloseIntercept(func() {
myWindow.Hide()
})
// 程序退出时停止子进程
myApp.Lifecycle().SetOnStopped(func() {
ui2.StopCmdProcess()
})
myWindow.ShowAndRun()
}
//go:build windows && !cmd
package main
import (
"embed"
"goodlink/config"
"goodlink/ui2"
"goodlink/utils"
goodlink_config "goodlink3/config"
"github.com/getlantern/systray"
)
//go:embed assert/tray_idle.ico assert/tray_warning.ico assert/tray_danger.ico assert/tray_success.ico
var trayIcons embed.FS
func main() {
// 检查单实例,如果不是第一个实例则退出
// 必须在创建任何UI资源之前检查,避免影响已运行的实例
if !utils.CheckSingleInstance() {
// 已有实例运行,直接退出
return
}
// 启动前清理遗留的cmd进程
utils.CleanupOrphanedCmdProcesses()
goodlink_config.DeleteLocalConfig()
config.SetVersion(GetVersionFromAppConfig())
config.Help()
idle, _ := trayIcons.ReadFile("assert/tray_idle.ico")
warning, _ := trayIcons.ReadFile("assert/tray_warning.ico")
danger, _ := trayIcons.ReadFile("assert/tray_danger.ico")
success, _ := trayIcons.ReadFile("assert/tray_success.ico")
ui2.InitTrayIcons(idle, warning, danger, success)
ui2.Init()
uiURL, err := ui2.StartServer()
if err != nil {
return
}
// 监听显示窗口请求(二次启动时重新打开浏览器)
go func() {
for range utils.GetShowWindowChan() {
ui2.OpenBrowser(uiURL)
}
}()
ui2.OpenBrowser(uiURL)
// 主线程运行托盘消息循环;退出时停止子进程
systray.Run(func() {
ui2.SetupTray(uiURL)
}, ui2.OnTrayExit)
}
Binary file not shown.
-196
View File
@@ -1,196 +0,0 @@
package theme
import (
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type iconInfo struct {
name string
icon fyne.Resource
}
type browser struct {
current int
icons []iconInfo
name *widget.Select
icon *widget.Icon
}
func (b *browser) setIcon(index int) {
if index < 0 || index > len(b.icons)-1 {
return
}
b.current = index
b.name.SetSelected(b.icons[index].name)
b.icon.SetResource(b.icons[index].icon)
}
// iconScreen loads a panel that shows the various icons available in Fyne
func iconScreen(_ fyne.Window) fyne.CanvasObject {
b := &browser{}
b.icons = loadIcons()
prev := widget.NewButtonWithIcon("", theme.NavigateBackIcon(), func() {
b.setIcon(b.current - 1)
})
next := widget.NewButtonWithIcon("", theme.NavigateNextIcon(), func() {
b.setIcon(b.current + 1)
})
b.name = widget.NewSelect(iconList(b.icons), func(name string) {
for i, icon := range b.icons {
if icon.name == name {
if b.current != i {
b.setIcon(i)
}
break
}
}
})
b.name.SetSelected(b.icons[b.current].name)
buttons := container.NewHBox(prev, next)
bar := container.NewBorder(nil, nil, buttons, nil, b.name)
background := canvas.NewRasterWithPixels(checkerPattern)
background.SetMinSize(fyne.NewSize(280, 280))
b.icon = widget.NewIcon(b.icons[b.current].icon)
return container.NewBorder(bar, nil, nil, nil, background, b.icon)
}
func checkerPattern(x, y, _, _ int) color.Color {
x /= 20
y /= 20
if x%2 == y%2 {
return theme.Color(theme.ColorNameBackground)
}
return theme.Color(theme.ColorNameShadow)
}
func iconList(icons []iconInfo) []string {
ret := make([]string, len(icons))
for i, icon := range icons {
ret[i] = icon.name
}
return ret
}
func loadIcons() []iconInfo {
return []iconInfo{
{"CancelIcon", theme.CancelIcon()},
{"ConfirmIcon", theme.ConfirmIcon()},
{"DeleteIcon", theme.DeleteIcon()},
{"SearchIcon", theme.SearchIcon()},
{"SearchReplaceIcon", theme.SearchReplaceIcon()},
{"CheckButtonIcon", theme.CheckButtonIcon()},
{"CheckButtonFillIcon", theme.CheckButtonFillIcon()},
{"CheckButtonCheckedIcon", theme.CheckButtonCheckedIcon()},
{"RadioButtonIcon", theme.RadioButtonIcon()},
{"RadioButtonFillIcon", theme.RadioButtonFillIcon()},
{"RadioButtonCheckedIcon", theme.RadioButtonCheckedIcon()},
{"ColorAchromaticIcon", theme.ColorAchromaticIcon()},
{"ColorChromaticIcon", theme.ColorChromaticIcon()},
{"ColorPaletteIcon", theme.ColorPaletteIcon()},
{"ContentAddIcon", theme.ContentAddIcon()},
{"ContentRemoveIcon", theme.ContentRemoveIcon()},
{"ContentClearIcon", theme.ContentClearIcon()},
{"ContentCutIcon", theme.ContentCutIcon()},
{"ContentCopyIcon", theme.ContentCopyIcon()},
{"ContentPasteIcon", theme.ContentPasteIcon()},
{"ContentRedoIcon", theme.ContentRedoIcon()},
{"ContentUndoIcon", theme.ContentUndoIcon()},
{"InfoIcon", theme.InfoIcon()},
{"ErrorIcon", theme.ErrorIcon()},
{"QuestionIcon", theme.QuestionIcon()},
{"WarningIcon", theme.WarningIcon()},
{"BrokenImageIcon", theme.BrokenImageIcon()},
{"DocumentIcon", theme.DocumentIcon()},
{"DocumentCreateIcon", theme.DocumentCreateIcon()},
{"DocumentPrintIcon", theme.DocumentPrintIcon()},
{"DocumentSaveIcon", theme.DocumentSaveIcon()},
{"FileIcon", theme.FileIcon()},
{"FileApplicationIcon", theme.FileApplicationIcon()},
{"FileAudioIcon", theme.FileAudioIcon()},
{"FileImageIcon", theme.FileImageIcon()},
{"FileTextIcon", theme.FileTextIcon()},
{"FileVideoIcon", theme.FileVideoIcon()},
{"FolderIcon", theme.FolderIcon()},
{"FolderNewIcon", theme.FolderNewIcon()},
{"FolderOpenIcon", theme.FolderOpenIcon()},
{"ComputerIcon", theme.ComputerIcon()},
{"HomeIcon", theme.HomeIcon()},
{"HelpIcon", theme.HelpIcon()},
{"HistoryIcon", theme.HistoryIcon()},
{"SettingsIcon", theme.SettingsIcon()},
{"StorageIcon", theme.StorageIcon()},
{"DownloadIcon", theme.DownloadIcon()},
{"UploadIcon", theme.UploadIcon()},
{"ViewFullScreenIcon", theme.ViewFullScreenIcon()},
{"ViewRestoreIcon", theme.ViewRestoreIcon()},
{"ViewRefreshIcon", theme.ViewRefreshIcon()},
{"VisibilityIcon", theme.VisibilityIcon()},
{"VisibilityOffIcon", theme.VisibilityOffIcon()},
{"ViewZoomFitIcon", theme.ZoomFitIcon()},
{"ViewZoomInIcon", theme.ZoomInIcon()},
{"ViewZoomOutIcon", theme.ZoomOutIcon()},
{"MoreHorizontalIcon", theme.MoreHorizontalIcon()},
{"MoreVerticalIcon", theme.MoreVerticalIcon()},
{"MoveDownIcon", theme.MoveDownIcon()},
{"MoveUpIcon", theme.MoveUpIcon()},
{"NavigateBackIcon", theme.NavigateBackIcon()},
{"NavigateNextIcon", theme.NavigateNextIcon()},
{"Menu", theme.MenuIcon()},
{"MenuExpand", theme.MenuExpandIcon()},
{"MenuDropDown", theme.MenuDropDownIcon()},
{"MenuDropUp", theme.MenuDropUpIcon()},
{"MailAttachmentIcon", theme.MailAttachmentIcon()},
{"MailComposeIcon", theme.MailComposeIcon()},
{"MailForwardIcon", theme.MailForwardIcon()},
{"MailReplyIcon", theme.MailReplyIcon()},
{"MailReplyAllIcon", theme.MailReplyAllIcon()},
{"MailSendIcon", theme.MailSendIcon()},
{"MediaFastForward", theme.MediaFastForwardIcon()},
{"MediaFastRewind", theme.MediaFastRewindIcon()},
{"MediaPause", theme.MediaPauseIcon()},
{"MediaPlay", theme.MediaPlayIcon()},
{"MediaStop", theme.MediaStopIcon()},
{"MediaRecord", theme.MediaRecordIcon()},
{"MediaReplay", theme.MediaReplayIcon()},
{"MediaSkipNext", theme.MediaSkipNextIcon()},
{"MediaSkipPrevious", theme.MediaSkipPreviousIcon()},
{"VolumeDown", theme.VolumeDownIcon()},
{"VolumeMute", theme.VolumeMuteIcon()},
{"VolumeUp", theme.VolumeUpIcon()},
{"AccountIcon", theme.AccountIcon()},
{"LoginIcon", theme.LoginIcon()},
{"LogoutIcon", theme.LogoutIcon()},
{"ListIcon", theme.ListIcon()},
{"GridIcon", theme.GridIcon()},
}
}
-38
View File
@@ -1,38 +0,0 @@
package theme
import (
_ "embed"
"image/color"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
)
var (
//go:embed fonts/simhei.ttf
NotoSansSC []byte
)
type MyTheme struct{}
var _ fyne.Theme = (*MyTheme)(nil)
// StaticName 为 fonts 目录下的 ttf 类型的字体文件名
func (m MyTheme) Font(fyne.TextStyle) fyne.Resource {
return &fyne.StaticResource{
StaticName: "simhei.ttf",
StaticContent: NotoSansSC,
}
}
func (*MyTheme) Color(n fyne.ThemeColorName, v fyne.ThemeVariant) color.Color {
return theme.DefaultTheme().Color(n, theme.VariantDark)
}
func (*MyTheme) Icon(n fyne.ThemeIconName) fyne.Resource {
return theme.DefaultTheme().Icon(n)
}
func (*MyTheme) Size(n fyne.ThemeSizeName) float32 {
return theme.DefaultTheme().Size(n)
}
+69 -152
View File
@@ -1,152 +1,69 @@
//go:build windows
package ui2
import (
"crypto/tls"
"encoding/json"
"image/color"
"io"
"log"
"net/http"
"net/url"
"strings"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
type giteeRelease struct {
TagName string `json:"tag_name"`
}
// checkLatestVersion 请求 Gitee API 获取最新版本号,与当前版本比较。
// 返回 (需要升级, 最新版本号)。
func checkLatestVersion(currentVersion string) (bool, string) {
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Get("https://gitee.com/api/v5/repos/konyshe/goodlink/releases/latest")
if err != nil {
log.Println("检查版本失败:", err)
return false, ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("检查版本失败, HTTP状态码:", resp.StatusCode)
return false, ""
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("读取版本响应失败:", err)
return false, ""
}
var release giteeRelease
if err := json.Unmarshal(body, &release); err != nil {
log.Println("解析版本JSON失败:", err)
return false, ""
}
latestVersion := strings.TrimPrefix(release.TagName, "v")
if latestVersion != "" && latestVersion != currentVersion {
return true, latestVersion
}
return false, latestVersion
}
var (
upgradeHintColor = color.NRGBA{R: 255, G: 80, B: 60, A: 255}
nat4WarnColor = color.NRGBA{R: 240, G: 180, B: 40, A: 255}
natOkColor = color.NRGBA{R: 50, G: 220, B: 80, A: 255}
natDetectColor = color.NRGBA{R: 180, G: 180, B: 180, A: 255}
)
var (
m_nat_hint_box *fyne.Container
m_nat_hint_icon *widget.Icon
m_nat_hint_text *canvas.Text
)
// ShowNATHint 根据 STUN 检测结果显示 NAT 类型提示
func ShowNATHint(isNAT4 bool) {
if m_nat_hint_box == nil {
return
}
if isNAT4 {
m_nat_hint_icon.SetResource(theme.WarningIcon())
m_nat_hint_text.Text = "当前网络为NAT4"
m_nat_hint_text.Color = nat4WarnColor
} else {
m_nat_hint_icon.SetResource(theme.ConfirmIcon())
m_nat_hint_text.Text = "当前网络为NAT1-NAT3"
m_nat_hint_text.Color = natOkColor
}
m_nat_hint_icon.Refresh()
m_nat_hint_text.Refresh()
m_nat_hint_box.Show()
}
// NewFooter 创建底部归属信息和官网链接组件
func NewFooter(currentVersion string) fyne.CanvasObject {
separator := canvas.NewRectangle(separatorColor)
separator.SetMinSize(fyne.NewSize(0, 1))
//versionLabel := widget.NewRichTextFromMarkdown("**@2026 Goodlink**")
updateURL, _ := url.Parse("https://gitee.com/konyshe/goodlink/releases")
updateLink := widget.NewHyperlink("升级版本", updateURL)
updateIcon := widget.NewIcon(theme.DownloadIcon())
feedbackURL, _ := url.Parse("https://gitee.com/konyshe/goodlink/issues")
feedbackLink := widget.NewHyperlink("反馈问题", feedbackURL)
feedbackIcon := widget.NewIcon(theme.InfoIcon())
newBadge := canvas.NewText("", upgradeHintColor)
newBadge.TextSize = 14
newBadge.TextStyle = fyne.TextStyle{Bold: true}
upgradeBox := container.NewHBox(updateIcon, newBadge, updateLink)
upgradeBox.Hide()
m_nat_hint_icon = widget.NewIcon(theme.InfoIcon())
m_nat_hint_text = canvas.NewText("正在检测当前网络环境...", natDetectColor)
m_nat_hint_text.TextSize = 14
m_nat_hint_text.TextStyle = fyne.TextStyle{Bold: true}
m_nat_hint_box = container.NewHBox(m_nat_hint_icon, m_nat_hint_text)
footerContent := container.NewHBox(
m_nat_hint_box,
layout.NewSpacer(),
upgradeBox,
container.NewHBox(feedbackIcon, feedbackLink),
)
go func() {
needUpgrade, latestVer := checkLatestVersion(currentVersion)
if needUpgrade {
fyne.Do(func() {
updateLink.SetText("v" + latestVer)
newBadge.Text = "有新版本!"
newBadge.Refresh()
upgradeBox.Show()
})
}
}()
return container.NewVBox(
separator,
footerContent,
)
}
//go:build windows
package ui2
import (
"crypto/tls"
"encoding/json"
"io"
"log"
"net/http"
"strings"
"time"
"goodlink/config"
)
type giteeRelease struct {
TagName string `json:"tag_name"`
}
// checkLatestVersion 请求 Gitee API 获取最新版本号,与当前版本比较。
// 返回 (需要升级, 最新版本号)。
func checkLatestVersion(currentVersion string) (bool, string) {
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
resp, err := client.Get("https://gitee.com/api/v5/repos/konyshe/goodlink/releases/latest")
if err != nil {
log.Println("检查版本失败:", err)
return false, ""
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Println("检查版本失败, HTTP状态码:", resp.StatusCode)
return false, ""
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Println("读取版本响应失败:", err)
return false, ""
}
var release giteeRelease
if err := json.Unmarshal(body, &release); err != nil {
log.Println("解析版本JSON失败:", err)
return false, ""
}
latestVersion := strings.TrimPrefix(release.TagName, "v")
if latestVersion != "" && latestVersion != currentVersion {
return true, latestVersion
}
return false, latestVersion
}
func startUpgradeCheck() {
go func() {
needUpgrade, latestVer := checkLatestVersion(config.GetVersion())
if needUpgrade {
setUpgradeInfo(true, latestVer)
}
}()
}
+55 -114
View File
@@ -1,114 +1,55 @@
//go:build windows
package ui2
import (
"fmt"
"sync"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/widget"
)
const (
// 日志最大条目数,避免内存占用过大
maxLogEntries = 500
// 日志显示行高度
logRowHeight = 15
// 日志显示行数
logVisibleRows = 15
)
var (
m_log_list *widget.List
m_log_entries []string
m_log_mutex sync.RWMutex
)
// appendLogEntry 追加日志条目到列表
func appendLogEntry(content string) {
// 先添加日志条目
m_log_mutex.Lock()
m_log_entries = append(m_log_entries, content)
// 限制日志数量
if len(m_log_entries) > maxLogEntries {
m_log_entries = m_log_entries[len(m_log_entries)-maxLogEntries:]
}
m_log_mutex.Unlock()
// 在锁外、主线程上刷新列表(UILogPrintF 可能从 goroutine 调用)
if m_log_list != nil {
fyne.Do(func() {
m_log_list.Refresh()
m_log_list.ScrollToBottom()
})
}
}
func UILogPrintF(a ...any) {
var content string
switch len(a) {
case 1:
content = a[0].(string)
default:
content = fmt.Sprintf(a[0].(string), a[1:]...)
}
appendLogEntry(content)
}
// NewLogList 创建日志显示列表组件,带滚动条,20行高度
func NewLogList() fyne.CanvasObject {
// 初始化日志条目
m_log_entries = make([]string, 0, maxLogEntries)
// 创建 List 组件
m_log_list = widget.NewList(
// 返回条目数量
func() int {
m_log_mutex.RLock()
defer m_log_mutex.RUnlock()
return len(m_log_entries)
},
// 创建条目模板
func() fyne.CanvasObject {
label := widget.NewLabel("")
label.TextStyle = fyne.TextStyle{Monospace: true}
return label
},
// 更新条目内容
func(id widget.ListItemID, obj fyne.CanvasObject) {
m_log_mutex.RLock()
defer m_log_mutex.RUnlock()
if id < len(m_log_entries) {
content := m_log_entries[id]
if label, ok := obj.(*widget.Label); ok {
label.SetText(content)
}
}
},
)
// 创建可滚动的容器包装 List,自适应高度
logContainer := container.NewVScroll(m_log_list)
// 设置最小高度,但允许根据窗口大小自动扩展
minListHeight := float32(logVisibleRows * logRowHeight)
logContainer.SetMinSize(fyne.NewSize(0, minListHeight))
// 创建日志标题
logTitle := widget.NewRichTextFromMarkdown("**运行日志**: ")
// 组合标题和日志列表,使用 Border 布局让日志列表自适应
logContent := container.NewBorder(
logTitle, // 顶部标题
nil, // 底部
nil, // 左侧
nil, // 右侧
logContainer, // 中心(自适应区域)
)
return logContent
}
//go:build windows
package ui2
import (
"fmt"
"sync"
)
const (
// 日志最大条目数,避免内存占用过大
maxLogEntries = 500
)
var (
m_log_entries []string
m_log_mutex sync.RWMutex
)
// appendLogEntry 追加日志条目到列表
func appendLogEntry(content string) {
// 先添加日志条目
m_log_mutex.Lock()
m_log_entries = append(m_log_entries, content)
// 限制日志数量
if len(m_log_entries) > maxLogEntries {
m_log_entries = m_log_entries[len(m_log_entries)-maxLogEntries:]
}
m_log_mutex.Unlock()
// 通过 SSE 推送到浏览器(UILogPrintF 可能从 goroutine 调用)
broadcastLog(content)
}
func UILogPrintF(a ...any) {
var content string
switch len(a) {
case 1:
content = a[0].(string)
default:
content = fmt.Sprintf(a[0].(string), a[1:]...)
}
appendLogEntry(content)
}
func snapshotLogs() []string {
m_log_mutex.RLock()
defer m_log_mutex.RUnlock()
out := make([]string, len(m_log_entries))
copy(out, m_log_entries)
return out
}
+154
View File
@@ -0,0 +1,154 @@
//go:build windows
package ui2
import (
"embed"
"encoding/json"
"io/fs"
"net"
"net/http"
"os/exec"
"syscall"
"time"
)
//go:embed web
var webFS embed.FS
const defaultUIAddr = "127.0.0.1:16780"
func OpenBrowser(url string) {
cmd := exec.Command("cmd", "/c", "start", "", url)
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: 0x08000000, // CREATE_NO_WINDOW
}
_ = cmd.Start()
}
func StartServer() (string, error) {
ln, err := net.Listen("tcp", defaultUIAddr)
if err != nil {
// 固定端口占用时回退到随机端口
ln, err = net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", err
}
}
webContent, err := fs.Sub(webFS, "web")
if err != nil {
ln.Close()
return "", err
}
mux := http.NewServeMux()
mux.HandleFunc("/api/state", handleState)
mux.HandleFunc("/api/start", handleStart)
mux.HandleFunc("/api/stop", handleStop)
mux.HandleFunc("/api/key/generate", handleGenerateKey)
mux.HandleFunc("/api/events", handleEvents)
mux.Handle("/", http.FileServer(http.FS(webContent)))
go func() {
_ = http.Serve(ln, mux)
}()
return "http://" + ln.Addr().String() + "/", nil
}
func writeJSON(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(v)
}
func handleState(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
writeJSON(w, http.StatusOK, snapshot(true))
}
func handleStart(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var req struct {
WorkType string `json:"workType"`
TunKey string `json:"tunKey"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
if err := HandleStart(req.WorkType, req.TunKey); err != nil {
writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()})
return
}
writeJSON(w, http.StatusOK, snapshot(false))
}
func handleStop(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
HandleStop()
writeJSON(w, http.StatusOK, snapshot(false))
}
func handleGenerateKey(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !othersEnabled() {
writeJSON(w, http.StatusConflict, map[string]string{"error": "busy"})
return
}
writeJSON(w, http.StatusOK, map[string]string{"tunKey": GenerateKey()})
}
func handleEvents(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
ch := sseHubInst.subscribe()
defer sseHubInst.unsubscribe(ch)
if _, err := w.Write(sseMessage("snapshot", snapshot(true))); err != nil {
return
}
flusher.Flush()
heartbeat := time.NewTicker(15 * time.Second)
defer heartbeat.Stop()
for {
select {
case <-r.Context().Done():
return
case <-heartbeat.C:
if _, err := w.Write([]byte(": ping\n\n")); err != nil {
return
}
flusher.Flush()
case msg, ok := <-ch:
if !ok {
return
}
if _, err := w.Write(msg); err != nil {
return
}
flusher.Flush()
}
}
}
+75 -218
View File
@@ -7,7 +7,6 @@ import (
"encoding/json"
"fmt"
"go2"
"image/color"
"log"
"os"
"os/exec"
@@ -20,20 +19,11 @@ import (
"goodlink/pro"
"goodlink/utils"
goodlink_config "goodlink3/config"
_ "embed"
_ "net/http/pprof"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
var (
m_start_button_lock sync.Mutex
m_button_start *widget.Button
m_activity_start_button *widget.Activity
m_start_button_state int
m_start_button_lock sync.Mutex
m_start_button_state int
// 子进程管理
m_cmd_process *exec.Cmd
@@ -43,24 +33,6 @@ var (
m_auto_restart_enabled bool
)
// UI组件接口,用于统一管理启用/禁用
type uiComponent interface {
Enable()
Disable()
}
// 所有需要控制的UI组件列表
var uiComponents = []uiComponent{}
func init() {
// 延迟初始化,在 GetMainUI 中设置
}
// setUIComponents 设置需要控制的UI组件列表
func setUIComponents(components []uiComponent) {
uiComponents = components
}
// StopCmdProcess 停止子进程(供外部调用,如窗口关闭时)
func StopCmdProcess() {
m_cmd_mutex.Lock()
@@ -91,143 +63,18 @@ func parseStatusMessage(line string) (string, bool) {
return "", false
}
// 按钮状态类型
type buttonState struct {
text string
importance widget.Importance
icon fyne.Resource
dotColor color.NRGBA
enabled bool
activity bool
other_enabled bool
}
// 预定义的按钮状态
var (
buttonStateInitializing = buttonState{
text: "检测网络中...",
importance: widget.HighImportance,
icon: theme.MediaPlayIcon(),
enabled: false,
activity: false,
other_enabled: true,
}
buttonStateIdle = buttonState{
text: "点击启动",
importance: widget.HighImportance,
icon: theme.MediaPlayIcon(),
enabled: true,
activity: false,
other_enabled: true,
}
buttonStateStarting = buttonState{
text: "启动中...",
importance: widget.WarningImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnecting = buttonState{
text: "连接中...",
importance: widget.WarningImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnectingNat4 = buttonState{
text: "当前网络是NAT4, 连接中...",
importance: widget.WarningImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnectingNat4ToNat4 = buttonState{
text: "两端网络都是NAT4, 连接中...",
importance: widget.DangerImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnected = buttonState{
text: "连接成功, 点击停止",
importance: widget.SuccessImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: false,
other_enabled: false,
}
buttonStateRunning = buttonState{
text: "启动成功, 点击停止",
importance: widget.SuccessImportance,
icon: theme.MediaStopIcon(),
enabled: true,
activity: false,
other_enabled: false,
}
buttonStateStopping = buttonState{
text: "停止中...",
importance: widget.WarningImportance,
icon: theme.MediaStopIcon(),
enabled: false,
activity: false,
other_enabled: false,
}
)
// updateButtonState 更新启动按钮的状态,同时同步托盘图标小圆点颜色
func updateButtonState(state buttonState) {
if m_button_start == nil {
return
}
if state.enabled {
m_button_start.Enable()
} else {
m_button_start.Disable()
}
if state.activity {
m_activity_start_button.Start()
m_activity_start_button.Show()
} else {
m_activity_start_button.Stop()
m_activity_start_button.Hide()
}
if state.other_enabled {
for _, comp := range uiComponents {
comp.Enable()
}
} else {
for _, comp := range uiComponents {
comp.Disable()
}
}
m_button_start.Importance = state.importance
m_button_start.SetText(state.text)
m_button_start.SetIcon(state.icon)
m_button_start.Refresh()
UpdateTrayIcon(state)
}
// updateConnectionStatus 根据连接状态更新按钮(Local端直接映射,Remote端在连接成功后才切换为运行状态)
// 由 handleProcessOutput goroutine 调用,UI 更新通过 fyne.Do 调度到主线程
// 由 handleProcessOutput goroutine 调用,状态通过 SSE 推送到浏览器
func updateConnectionStatus(status string) {
switch GetWorkType() {
case workTypeLocal:
switch status {
case pro.TagStatusConnecting:
fyne.Do(func() { updateButtonState(buttonStateConnecting) })
updateButtonState(buttonStateConnecting)
case pro.TagStatusConnected:
fyne.Do(func() { updateButtonState(buttonStateConnected) })
updateButtonState(buttonStateConnected)
case pro.TagStatusConnectingNAT4:
fyne.Do(func() { updateButtonState(buttonStateConnectingNat4ToNat4) })
updateButtonState(buttonStateConnectingNat4ToNat4)
case pro.TagStatusVersionMismatch:
// 版本不一致,禁用自动重启并停止进程
m_start_button_lock.Lock()
@@ -246,7 +93,7 @@ func updateConnectionStatus(status string) {
case workTypeRemote:
switch status {
case pro.TagStatusRunning:
fyne.Do(func() { updateButtonState(buttonStateRunning) })
updateButtonState(buttonStateRunning)
}
}
}
@@ -263,7 +110,7 @@ func startCmdProcess() error {
// 构建命令行参数
workType := GetWorkType()
args := []string{"--fork", "--" + strings.ToLower(workType), "--key=" + m_validated_key.Text, "--local_config"}
args := []string{"--fork", "--" + strings.ToLower(workType), "--key=" + getTunKey(), "--local_config"}
// 创建子进程
m_cmd_mutex.Lock()
@@ -322,66 +169,80 @@ func saveConfig() {
//先对需要填写的数据进行校验
configByte, _ := json.Marshal(&goodlink_config.ConfigInfo{
WorkType: GetWorkType(),
TunKey: m_validated_key.Text,
TunKey: getTunKey(),
})
log.Println(string(configByte))
os.Remove(goodlinkFileName)
go2.FileAppend(goodlinkFileName, configByte)
}
func start_button_click() {
func HandleStart(workType, tunKey string) error {
m_start_button_lock.Lock()
defer m_start_button_lock.Unlock()
switch m_start_button_state {
case 0:
if GetWorkType() == workTypeLocal {
updateButtonState(buttonStateConnecting)
} else {
updateButtonState(buttonStateStarting)
}
saveConfig()
// 强制刷新工作端侧按钮高亮,确保选中项明显显示
updateWorkTypeButtons(GetWorkType())
m_start_button_state = 1
// 设置自动重启标志
m_auto_restart_enabled = true
// 启动进程
if err := startCmdProcess(); err != nil {
UILogPrintF("启动失败: %v", err)
m_start_button_state = 0
m_auto_restart_enabled = false
updateButtonState(buttonStateIdle)
return
}
// 更新按钮状态并等待进程结束
go waitForProcessAndHandleExit(false)
case 1:
updateButtonState(buttonStateStopping)
// 停止子进程(在 goroutine 中执行,避免阻塞 UI
go func() {
// 设置自动重启标志为false,防止误触发重启
m_auto_restart_enabled = false
StopCmdProcess()
fyne.Do(func() {
m_start_button_state = 0
updateButtonState(buttonStateIdle)
})
}()
if m_start_button_state != 0 {
return fmt.Errorf("already started")
}
stateMu.RLock()
busy := currentButton.kind == kindInitializing || currentButton.kind == kindStopping
stateMu.RUnlock()
if busy {
return fmt.Errorf("busy")
}
setWorkTypeAndKey(workType, tunKey)
if GetWorkType() == workTypeLocal {
updateButtonState(buttonStateConnecting)
} else {
updateButtonState(buttonStateStarting)
}
saveConfig()
m_start_button_state = 1
// 设置自动重启标志
m_auto_restart_enabled = true
// 启动进程
if err := startCmdProcess(); err != nil {
UILogPrintF("启动失败: %v", err)
m_start_button_state = 0
m_auto_restart_enabled = false
updateButtonState(buttonStateIdle)
return err
}
// 更新按钮状态并等待进程结束
go waitForProcessAndHandleExit()
return nil
}
// waitForProcessAndHandleExit 等待进程结束并处理退出逻辑(在 goroutine 中运行,UI 更新通过 fyne.Do 调度到主线程)
func waitForProcessAndHandleExit(isRestart bool) {
func HandleStop() {
m_start_button_lock.Lock()
defer m_start_button_lock.Unlock()
if m_start_button_state != 1 {
return
}
updateButtonState(buttonStateStopping)
// 设置自动重启标志为false,防止误触发重启
m_auto_restart_enabled = false
// 停止子进程(在 goroutine 中执行,避免阻塞 UI
go func() {
StopCmdProcess()
m_start_button_state = 0
updateButtonState(buttonStateIdle)
}()
}
// waitForProcessAndHandleExit 等待进程结束并处理退出逻辑(在 goroutine 中运行,状态通过 SSE 推送到浏览器)
func waitForProcessAndHandleExit() {
time.Sleep(time.Second * 1)
if m_start_button_state != 1 {
return
@@ -406,10 +267,8 @@ func waitForProcessAndHandleExit(isRestart bool) {
autoRestartProcess()
} else {
// 正常停止,恢复 UI
fyne.Do(func() {
m_start_button_state = 0
updateButtonState(buttonStateIdle)
})
m_start_button_state = 0
updateButtonState(buttonStateIdle)
}
}
@@ -431,13 +290,11 @@ func autoRestartProcess() {
// 重启进程
if err := startCmdProcess(); err != nil {
UILogPrintF("启动失败: %v", err)
fyne.Do(func() {
m_start_button_state = 0
updateButtonState(buttonStateIdle)
})
m_start_button_state = 0
updateButtonState(buttonStateIdle)
return
}
// 启动新的等待goroutine
go waitForProcessAndHandleExit(true)
go waitForProcessAndHandleExit()
}
+283
View File
@@ -0,0 +1,283 @@
//go:build windows
package ui2
import (
"encoding/json"
"sync"
"goodlink/config"
)
const (
kindInitializing = "initializing"
kindIdle = "idle"
kindStarting = "starting"
kindConnecting = "connecting"
kindConnectingNat4 = "connecting_nat4"
kindConnectingNat4ToNat4 = "connecting_nat4_to_nat4"
kindConnected = "connected"
kindRunning = "running"
kindStopping = "stopping"
)
// 按钮状态类型
type buttonState struct {
kind string
text string
importance string
enabled bool
activity bool
other_enabled bool
}
type ButtonInfo struct {
Kind string `json:"kind"`
Text string `json:"text"`
Importance string `json:"importance"`
Enabled bool `json:"enabled"`
Activity bool `json:"activity"`
OthersEnabled bool `json:"othersEnabled"`
}
type NATInfo struct {
Ready bool `json:"ready"`
IsNAT4 bool `json:"isNAT4"`
Text string `json:"text"`
}
type UpgradeInfo struct {
Need bool `json:"need"`
Latest string `json:"latest"`
}
type UIState struct {
Version string `json:"version"`
WorkType string `json:"workType"`
TunKey string `json:"tunKey"`
Button ButtonInfo `json:"button"`
NAT NATInfo `json:"nat"`
Upgrade UpgradeInfo `json:"upgrade"`
Logs []string `json:"logs,omitempty"`
}
// 预定义的按钮状态
var (
buttonStateInitializing = buttonState{
kind: kindInitializing,
text: "检测网络中...",
importance: "high",
enabled: false,
activity: false,
other_enabled: true,
}
buttonStateIdle = buttonState{
kind: kindIdle,
text: "点击启动",
importance: "high",
enabled: true,
activity: false,
other_enabled: true,
}
buttonStateStarting = buttonState{
kind: kindStarting,
text: "启动中...",
importance: "warning",
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnecting = buttonState{
kind: kindConnecting,
text: "连接中...",
importance: "warning",
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnectingNat4 = buttonState{
kind: kindConnectingNat4,
text: "当前网络是NAT4, 连接中...",
importance: "warning",
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnectingNat4ToNat4 = buttonState{
kind: kindConnectingNat4ToNat4,
text: "两端网络都是NAT4, 连接中...",
importance: "danger",
enabled: true,
activity: true,
other_enabled: false,
}
buttonStateConnected = buttonState{
kind: kindConnected,
text: "连接成功, 点击停止",
importance: "success",
enabled: true,
activity: false,
other_enabled: false,
}
buttonStateRunning = buttonState{
kind: kindRunning,
text: "启动成功, 点击停止",
importance: "success",
enabled: true,
activity: false,
other_enabled: false,
}
buttonStateStopping = buttonState{
kind: kindStopping,
text: "停止中...",
importance: "warning",
enabled: false,
activity: false,
other_enabled: false,
}
)
var (
stateMu sync.RWMutex
m_work_type string
m_tun_key string
currentButton buttonState
m_nat = NATInfo{Text: "正在检测当前网络环境..."}
m_upgrade UpgradeInfo
sseHubInst = newSSEHub()
)
type sseHub struct {
mu sync.Mutex
clients map[chan []byte]struct{}
}
func newSSEHub() *sseHub {
return &sseHub{clients: make(map[chan []byte]struct{})}
}
func (h *sseHub) subscribe() chan []byte {
ch := make(chan []byte, 64)
h.mu.Lock()
h.clients[ch] = struct{}{}
h.mu.Unlock()
return ch
}
func (h *sseHub) unsubscribe(ch chan []byte) {
h.mu.Lock()
delete(h.clients, ch)
h.mu.Unlock()
}
func (h *sseHub) broadcast(msg []byte) {
h.mu.Lock()
defer h.mu.Unlock()
for ch := range h.clients {
select {
case ch <- msg:
default:
}
}
}
func sseMessage(event string, v any) []byte {
b, _ := json.Marshal(v)
out := make([]byte, 0, 16+len(event)+len(b))
out = append(out, "event: "...)
out = append(out, event...)
out = append(out, "\ndata: "...)
out = append(out, b...)
out = append(out, "\n\n"...)
return out
}
func broadcastState() {
sseHubInst.broadcast(sseMessage("state", snapshot(false)))
}
func broadcastLog(line string) {
sseHubInst.broadcast(sseMessage("log", map[string]string{"line": line}))
}
func snapshot(includeLogs bool) UIState {
stateMu.RLock()
st := UIState{
Version: config.GetVersion(),
WorkType: m_work_type,
TunKey: m_tun_key,
Button: ButtonInfo{
Kind: currentButton.kind,
Text: currentButton.text,
Importance: currentButton.importance,
Enabled: currentButton.enabled,
Activity: currentButton.activity,
OthersEnabled: currentButton.other_enabled,
},
NAT: m_nat,
Upgrade: m_upgrade,
}
stateMu.RUnlock()
if includeLogs {
st.Logs = snapshotLogs()
}
return st
}
// updateButtonState 更新启动按钮的状态,同时同步托盘图标
func updateButtonState(state buttonState) {
stateMu.Lock()
currentButton = state
stateMu.Unlock()
UpdateTrayIcon(state)
broadcastState()
}
func othersEnabled() bool {
stateMu.RLock()
defer stateMu.RUnlock()
return currentButton.other_enabled
}
// 获取当前工作类型
func GetWorkType() string {
stateMu.RLock()
defer stateMu.RUnlock()
return m_work_type
}
func getTunKey() string {
stateMu.RLock()
defer stateMu.RUnlock()
return m_tun_key
}
func setWorkTypeAndKey(workType, tunKey string) {
stateMu.Lock()
if workType == workTypeLocal || workType == workTypeRemote {
m_work_type = workType
}
if tunKey != "" {
m_tun_key = tunKey
}
stateMu.Unlock()
}
// SetNATHint 根据 STUN 检测结果显示 NAT 类型提示
func SetNATHint(isNAT4 bool) {
stateMu.Lock()
if isNAT4 {
m_nat = NATInfo{Ready: true, IsNAT4: true, Text: "当前网络为NAT4"}
} else {
m_nat = NATInfo{Ready: true, IsNAT4: false, Text: "当前网络为NAT1-NAT3"}
}
stateMu.Unlock()
broadcastState()
}
func setUpgradeInfo(need bool, latest string) {
stateMu.Lock()
m_upgrade = UpgradeInfo{Need: need, Latest: latest}
stateMu.Unlock()
broadcastState()
}
+55 -26
View File
@@ -3,56 +3,85 @@
package ui2
import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/driver/desktop"
"sync/atomic"
"goodlink/config"
"github.com/getlantern/systray"
)
var (
trayApp desktop.App
// Pre-generated tray icon data (ICO bytes), set by InitTrayIcons.
trayIconIdle []byte
trayIconWarning []byte
trayIconDanger []byte
trayIconSuccess []byte
trayReady atomic.Bool
)
// InitTrayIcons sets the 4 pre-generated tray icon bytes (ICO format).
// Icons should be from assert/tray_idle.ico, tray_warning.ico, tray_danger.ico, tray_success.ico.
func InitTrayIcons(desk desktop.App, idle, warning, danger, success []byte) {
trayApp = desk
func InitTrayIcons(idle, warning, danger, success []byte) {
trayIconIdle = idle
trayIconWarning = warning
trayIconDanger = danger
trayIconSuccess = success
}
// iconForState returns the tray icon resource for the given button state.
func iconForState(state buttonState) fyne.Resource {
var data []byte
switch {
case state == buttonStateIdle, state == buttonStateInitializing:
data = trayIconIdle
case state == buttonStateStarting, state == buttonStateConnecting, state == buttonStateConnectingNat4, state == buttonStateStopping:
data = trayIconWarning
case state == buttonStateConnectingNat4ToNat4:
data = trayIconDanger
case state == buttonStateConnected, state == buttonStateRunning:
data = trayIconSuccess
// iconBytesForState returns the tray icon bytes for the given button state.
func iconBytesForState(state buttonState) []byte {
switch state.kind {
case kindIdle, kindInitializing:
return trayIconIdle
case kindStarting, kindConnecting, kindConnectingNat4, kindStopping:
return trayIconWarning
case kindConnectingNat4ToNat4:
return trayIconDanger
case kindConnected, kindRunning:
return trayIconSuccess
default:
data = trayIconIdle
return trayIconIdle
}
if len(data) == 0 {
return nil
}
return fyne.NewStaticResource("tray_icon.ico", data)
}
func UpdateTrayIcon(state buttonState) {
if trayApp == nil {
if !trayReady.Load() {
return
}
icon := iconForState(state)
if icon != nil {
trayApp.SetSystemTrayIcon(icon)
data := iconBytesForState(state)
if len(data) > 0 {
systray.SetIcon(data)
}
}
func SetupTray(uiURL string) {
stateMu.RLock()
btn := currentButton
stateMu.RUnlock()
trayReady.Store(true)
UpdateTrayIcon(btn)
systray.SetTooltip(M_APP_TITLE + " v" + config.GetVersion())
// 创建菜单,确保只有一个退出选项
mOpen := systray.AddMenuItem("打开主程序", "在浏览器中打开")
systray.AddSeparator()
mQuit := systray.AddMenuItem("退出", "退出")
go func() {
for {
select {
case <-mOpen.ClickedCh:
OpenBrowser(uiURL)
case <-mQuit.ClickedCh:
StopCmdProcess()
systray.Quit()
}
}
}()
}
// OnTrayExit 程序退出时停止子进程
func OnTrayExit() {
StopCmdProcess()
}
+83 -315
View File
@@ -1,315 +1,83 @@
//go:build windows
package ui2
import (
"encoding/json"
"fmt"
"go2"
"image/color"
"log"
"net"
"time"
"goodlink/config"
"goodlink/stun2"
goodlink_config "goodlink3/config"
_ "embed"
_ "net/http/pprof"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/atotto/clipboard"
)
const (
goodlinkFileName = "goodlink.json"
)
// entryWrapper 包装 Entry 以实现 uiComponent 接口
type entryWrapper struct {
entry *widget.Entry
}
func (e *entryWrapper) Enable() { e.entry.Enable() }
func (e *entryWrapper) Disable() { e.entry.Disable() }
// buttonWrapper 包装 Button 以实现 uiComponent 接口
type buttonWrapper struct {
btn *widget.Button
}
func (b *buttonWrapper) Enable() { b.btn.Enable() }
func (b *buttonWrapper) Disable() { b.btn.Disable() }
var (
m_work_type string
m_btn_local *widget.Button
m_btn_remote *widget.Button
m_validated_key *widget.Entry
m_button_key_create *widget.Button
m_button_key_paste *widget.Button
m_btn_local_bg *canvas.Rectangle // 本地端按钮高亮背景
m_btn_remote_bg *canvas.Rectangle // 远程端按钮高亮背景
m_btn_local_border *canvas.Rectangle // 本地端按钮边框容器
m_btn_remote_border *canvas.Rectangle // 远程端按钮边框容器
)
const (
M_APP_TITLE = "Goodlink"
workTypeLocal = "Local"
workTypeRemote = "Remote"
)
// UI样式常量
var (
// 统一的背景颜色
separatorColor = color.NRGBA{R: 100, G: 100, B: 100, A: 80} // 分隔线颜色
highlightColor = color.NRGBA{R: 0, G: 120, B: 255, A: 255} // 选中按钮高亮颜色(完全不透明,更亮)
highlightBorderColor = color.NRGBA{R: 0, G: 180, B: 255, A: 255} // 高亮边框颜色
// 统一的圆角半径
cornerRadius = float32(8)
// 统一的间距
paddingSize = float32(12)
)
// updateButtonHighlight 更新单个按钮的高亮状态
func updateButtonHighlight(btn *widget.Button, bg *canvas.Rectangle, border *canvas.Rectangle, isHighlighted bool) {
if isHighlighted {
btn.Importance = widget.HighImportance
bg.FillColor = highlightColor
border.StrokeColor = highlightBorderColor
border.StrokeWidth = 3
border.FillColor = color.Transparent
} else {
btn.Importance = widget.MediumImportance
bg.FillColor = color.Transparent
border.StrokeColor = color.Transparent
border.StrokeWidth = 0
border.FillColor = color.Transparent
}
btn.Refresh()
bg.Refresh()
border.Refresh()
}
// 更新工作模式按钮样式
func updateWorkTypeButtons(selected string) {
m_work_type = selected
isLocal := selected == workTypeLocal
updateButtonHighlight(m_btn_local, m_btn_local_bg, m_btn_local_border, isLocal)
updateButtonHighlight(m_btn_remote, m_btn_remote_bg, m_btn_remote_border, !isLocal)
}
// 获取当前工作类型
func GetWorkType() string {
return m_work_type
}
// createButtonWithHighlight 创建带高亮效果的按钮容器
func createButtonWithHighlight(btn *widget.Button, bg *canvas.Rectangle, border *canvas.Rectangle) *fyne.Container {
bg.CornerRadius = cornerRadius
border.CornerRadius = cornerRadius
buttonInner := container.NewStack(bg, btn)
buttonWithPadding := container.NewPadded(buttonInner)
return container.NewStack(border, buttonWithPadding)
}
// 创建工作模式选择器
func createWorkTypeSelector(configInfo *goodlink_config.ConfigInfo) fyne.CanvasObject {
// 创建本地端按钮
m_btn_local = widget.NewButtonWithIcon(" Local端 ", theme.ComputerIcon(), nil)
// 创建远程端按钮
m_btn_remote = widget.NewButtonWithIcon(" Remote端 ", theme.StorageIcon(), nil)
// 创建按钮高亮背景
m_btn_local_bg = canvas.NewRectangle(color.Transparent)
m_btn_remote_bg = canvas.NewRectangle(color.Transparent)
// 创建外层边框容器(用于更明显的高亮显示)
m_btn_local_border = canvas.NewRectangle(color.Transparent)
m_btn_remote_border = canvas.NewRectangle(color.Transparent)
// 创建带高亮的按钮容器
localButtonContainer := createButtonWithHighlight(m_btn_local, m_btn_local_bg, m_btn_local_border)
remoteButtonContainer := createButtonWithHighlight(m_btn_remote, m_btn_remote_bg, m_btn_remote_border)
// 根据配置设置初始状态
if configInfo.WorkType == "" {
configInfo.WorkType = workTypeLocal
}
updateWorkTypeButtons(configInfo.WorkType)
// 创建分隔线
separator := canvas.NewRectangle(separatorColor)
separator.SetMinSize(fyne.NewSize(2, 30))
// 创建标签
label := widget.NewRichTextFromMarkdown("**工作端侧**: ")
// 组合按钮和分隔线
buttonGroup := container.NewHBox(
localButtonContainer,
separator,
remoteButtonContainer,
)
return container.NewBorder(nil, nil, label, nil, buttonGroup)
}
// 创建连接密钥输入区域
func createKeyInputSection(configInfo *goodlink_config.ConfigInfo) fyne.CanvasObject {
m_validated_key = widget.NewEntry()
m_validated_key.SetPlaceHolder("16-64字节长度")
if len(configInfo.TunKey) > 0 {
m_validated_key.SetText(configInfo.TunKey)
}
// 创建密钥标签和图标
keyLabel := widget.NewRichTextFromMarkdown("**连接密钥**: ")
// 创建输入框容器,将标签和输入框放在同一行
return container.NewBorder(nil, nil, keyLabel, nil, m_validated_key)
}
// 创建密钥操作按钮组
func createKeyButtons() fyne.CanvasObject {
m_button_key_create = widget.NewButtonWithIcon("生成密钥", theme.ContentAddIcon(), func() {
m_validated_key.SetText(string(go2.RandomBytes(config.TunKeyByteLen)))
})
key_copy_button := widget.NewButtonWithIcon("复制密钥", theme.ContentCopyIcon(), func() {
clipboard.WriteAll(m_validated_key.Text)
})
m_button_key_paste = widget.NewButtonWithIcon("粘贴密钥", theme.ContentPasteIcon(), func() {
if s, err := clipboard.ReadAll(); err == nil {
m_validated_key.SetText(s)
}
})
// 统一按钮样式
m_button_key_create.Importance = widget.MediumImportance
key_copy_button.Importance = widget.MediumImportance
m_button_key_paste.Importance = widget.MediumImportance
// 创建按钮容器,使用网格布局
return container.NewGridWithColumns(3, m_button_key_create, key_copy_button, m_button_key_paste)
}
func GetMainUI(myWindow *fyne.Window) *fyne.Container {
var configInfo goodlink_config.ConfigInfo
json.Unmarshal(go2.FileReadAll(goodlinkFileName), &configInfo)
log.Println(configInfo)
// 如果密钥为空,自动生成密钥
if len(configInfo.TunKey) == 0 {
configInfo.TunKey = string(go2.RandomBytes(config.TunKeyByteLen))
log.Println("自动生成密钥:", configInfo.TunKey)
}
// 创建各个UI组件
workTypeSelector := createWorkTypeSelector(&configInfo)
keyInputSection := createKeyInputSection(&configInfo)
keyButtons := createKeyButtons()
// 设置需要控制的UI组件列表(必须在所有组件创建后设置)
setUIComponents([]uiComponent{
&entryWrapper{entry: m_validated_key},
&buttonWrapper{btn: m_button_key_create},
&buttonWrapper{btn: m_button_key_paste},
&buttonWrapper{btn: m_btn_local},
&buttonWrapper{btn: m_btn_remote},
})
m_start_button_state = 0
m_activity_start_button = widget.NewActivity()
m_button_start = widget.NewButtonWithIcon("点击启动", theme.MediaPlayIcon(), start_button_click)
m_button_start.Importance = widget.HighImportance
// 创建启动按钮容器
startButtonContainer := container.NewStack(m_button_start, m_activity_start_button)
// 创建配置区域容器(根据工作模式动态显示)
configContainer := container.NewVBox()
// 根据工作模式显示对应的配置
updateConfigDisplay := func() {
configContainer.RemoveAll()
configContainer.Refresh()
}
// 设置按钮点击事件
m_btn_local.OnTapped = func() {
updateWorkTypeButtons(workTypeLocal)
updateConfigDisplay()
}
m_btn_remote.OnTapped = func() {
updateWorkTypeButtons(workTypeRemote)
updateConfigDisplay()
}
// 初始显示
updateConfigDisplay()
// 创建顶部内容(工作模式选择器、密钥输入、配置区域)
topContent := container.NewVBox(
workTypeSelector,
keyInputSection,
keyButtons,
configContainer,
)
// 创建底部内容(启动按钮和页脚)
bottomContent := container.NewVBox(
startButtonContainer,
NewFooter(config.GetVersion()),
)
// 使用 Border 布局,让日志区域自适应占用剩余空间
mainContent := container.NewBorder(
topContent, // 顶部
bottomContent, // 底部
nil, // 左侧
nil, // 右侧
NewLogList(), // 中心(自适应区域)
)
updateButtonState(buttonStateInitializing)
// UI客户端自身的日志输出
stun2.SetExtraLogSink(func(s string) {
UILogPrintF(fmt.Sprintf("%s %s", time.Now().Format("2006/01/02 15:04:05"), s))
})
go func() {
for {
conn, err := net.ListenUDP("udp4", nil)
if err != nil {
UILogPrintF("NAT检测: UDP监听失败: " + err.Error())
time.Sleep(5 * time.Second)
continue
}
_, wanPort1, wanPort2, _ := stun2.GetStunIpPort(conn)
conn.Close()
isNAT4 := wanPort1 != wanPort2
fyne.Do(func() {
ShowNATHint(isNAT4)
updateButtonState(buttonStateIdle)
})
return
}
}()
// 添加最小外层padding,确保整体有合适的边距
return container.NewPadded(mainContent)
}
//go:build windows
package ui2
import (
"encoding/json"
"fmt"
"go2"
"log"
"net"
"time"
"goodlink/config"
"goodlink/stun2"
goodlink_config "goodlink3/config"
)
const (
goodlinkFileName = "goodlink.json"
M_APP_TITLE = "Goodlink"
workTypeLocal = "Local"
workTypeRemote = "Remote"
)
func Init() {
m_log_entries = make([]string, 0, maxLogEntries)
var configInfo goodlink_config.ConfigInfo
json.Unmarshal(go2.FileReadAll(goodlinkFileName), &configInfo)
log.Println(configInfo)
// 如果密钥为空,自动生成密钥
if len(configInfo.TunKey) == 0 {
configInfo.TunKey = string(go2.RandomBytes(config.TunKeyByteLen))
log.Println("自动生成密钥:", configInfo.TunKey)
}
if configInfo.WorkType == "" {
configInfo.WorkType = workTypeLocal
}
stateMu.Lock()
m_work_type = configInfo.WorkType
m_tun_key = configInfo.TunKey
stateMu.Unlock()
m_start_button_state = 0
updateButtonState(buttonStateInitializing)
// UI客户端自身的日志输出
stun2.SetExtraLogSink(func(s string) {
UILogPrintF(fmt.Sprintf("%s %s", time.Now().Format("2006/01/02 15:04:05"), s))
})
startUpgradeCheck()
go detectNAT()
}
func detectNAT() {
for {
conn, err := net.ListenUDP("udp4", nil)
if err != nil {
UILogPrintF("NAT检测: UDP监听失败: " + err.Error())
time.Sleep(5 * time.Second)
continue
}
_, wanPort1, wanPort2, _ := stun2.GetStunIpPort(conn)
conn.Close()
SetNATHint(wanPort1 != wanPort2)
updateButtonState(buttonStateIdle)
return
}
}
func GenerateKey() string {
key := string(go2.RandomBytes(config.TunKeyByteLen))
stateMu.Lock()
m_tun_key = key
stateMu.Unlock()
broadcastState()
return key
}
+225
View File
@@ -0,0 +1,225 @@
:root {
--bg: #2b2b2b;
--panel: #333333;
--fg: #eeeeee;
--muted: #b0b0b0;
--line: rgba(160, 160, 160, 0.35);
--primary: #0078ff;
--primary-hi: #00b4ff;
--warning: #e6a817;
--danger: #e5534b;
--success: #3dcc6d;
--btn: #4a4a4a;
--btn-hover: #5a5a5a;
--radius: 8px;
}
* {
box-sizing: border-box;
}
html, body {
margin: 0;
height: 100%;
background: var(--bg);
color: var(--fg);
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
}
.app {
min-height: 100%;
max-width: 860px;
margin: 0 auto;
padding: 16px 18px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.header h1 {
margin: 0 0 4px;
font-size: 20px;
font-weight: 700;
}
.row {
display: flex;
align-items: center;
gap: 10px;
}
.label {
font-weight: 700;
white-space: nowrap;
}
.work-type {
display: flex;
align-items: center;
gap: 10px;
}
.work-btn {
min-width: 120px;
padding: 8px 16px;
border: 3px solid transparent;
border-radius: var(--radius);
background: var(--btn);
color: var(--fg);
font-size: 14px;
cursor: pointer;
}
.work-btn.active {
background: var(--primary);
border-color: var(--primary-hi);
}
.work-btn:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.vsep {
width: 2px;
height: 28px;
background: var(--line);
}
.key-input {
flex: 1;
min-width: 0;
padding: 8px 10px;
border: 1px solid #555;
border-radius: 6px;
background: #1f1f1f;
color: var(--fg);
font-size: 14px;
}
.key-input:disabled {
opacity: 0.6;
}
.key-actions {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
}
.key-actions button {
padding: 8px 10px;
border: 0;
border-radius: 6px;
background: var(--btn);
color: var(--fg);
cursor: pointer;
}
.key-actions button:hover:not(:disabled) {
background: var(--btn-hover);
}
.key-actions button:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.start-btn {
width: 100%;
padding: 12px 16px;
border: 0;
border-radius: var(--radius);
color: #fff;
font-size: 16px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.start-btn.high { background: var(--primary); }
.start-btn.warning { background: var(--warning); color: #222; }
.start-btn.danger { background: var(--danger); }
.start-btn.success { background: var(--success); color: #123; }
.start-btn:disabled {
opacity: 0.65;
cursor: not-allowed;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid rgba(255, 255, 255, 0.35);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.hidden { display: none !important; }
@keyframes spin {
to { transform: rotate(360deg); }
}
.logs {
flex: 1;
min-height: 240px;
display: flex;
flex-direction: column;
gap: 6px;
}
#log-list {
flex: 1;
margin: 0;
min-height: 220px;
max-height: 48vh;
overflow: auto;
padding: 10px;
border-radius: 6px;
background: #1a1a1a;
color: #d4d4d4;
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-all;
}
.footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 10px;
border-top: 1px solid var(--line);
font-size: 14px;
}
.nat {
font-weight: 700;
}
.nat.detecting { color: #b4b4b4; }
.nat.ok { color: var(--success); }
.nat.warn { color: var(--warning); }
.footer-links {
display: flex;
align-items: center;
gap: 14px;
}
.footer a {
color: #7ec3ff;
text-decoration: none;
}
.upgrade {
color: #ff503c;
font-weight: 700;
}
+130
View File
@@ -0,0 +1,130 @@
const els = {
title: document.getElementById("app-title"),
local: document.getElementById("btn-local"),
remote: document.getElementById("btn-remote"),
key: document.getElementById("tun-key"),
generate: document.getElementById("btn-generate"),
copy: document.getElementById("btn-copy"),
paste: document.getElementById("btn-paste"),
start: document.getElementById("btn-start"),
startText: document.getElementById("start-text"),
spinner: document.getElementById("start-spinner"),
logs: document.getElementById("log-list"),
nat: document.getElementById("nat-hint"),
upgrade: document.getElementById("upgrade-box"),
upgradeVer: document.getElementById("upgrade-ver"),
};
let workType = "Local";
let initialized = false;
let currentKind = "initializing";
function setWorkType(type) {
workType = type;
els.local.classList.toggle("active", type === "Local");
els.remote.classList.toggle("active", type === "Remote");
}
function setControlsEnabled(enabled) {
els.local.disabled = !enabled;
els.remote.disabled = !enabled;
els.key.disabled = !enabled;
els.generate.disabled = !enabled;
els.paste.disabled = !enabled;
}
function applyState(s) {
if (!s) return;
els.title.textContent = "Goodlink v" + (s.version || "");
currentKind = s.button.kind;
if (!initialized || !s.button.othersEnabled) {
setWorkType(s.workType || "Local");
if (document.activeElement !== els.key) {
els.key.value = s.tunKey || "";
}
initialized = true;
}
setControlsEnabled(!!s.button.othersEnabled);
els.startText.textContent = s.button.text || "";
els.start.disabled = !s.button.enabled;
els.start.className = "start-btn " + (s.button.importance || "high");
els.spinner.classList.toggle("hidden", !s.button.activity);
if (s.nat) {
els.nat.textContent = s.nat.text || "";
els.nat.className = "nat " + (s.nat.ready ? (s.nat.isNAT4 ? "warn" : "ok") : "detecting");
}
if (s.upgrade && s.upgrade.need) {
els.upgrade.classList.remove("hidden");
els.upgradeVer.textContent = "v" + s.upgrade.latest;
} else {
els.upgrade.classList.add("hidden");
}
}
function renderLogs(lines) {
els.logs.textContent = (lines || []).join("\n");
els.logs.scrollTop = els.logs.scrollHeight;
}
function appendLog(line) {
if (els.logs.textContent) {
els.logs.textContent += "\n" + line;
} else {
els.logs.textContent = line;
}
els.logs.scrollTop = els.logs.scrollHeight;
}
els.local.addEventListener("click", () => setWorkType("Local"));
els.remote.addEventListener("click", () => setWorkType("Remote"));
els.generate.addEventListener("click", async () => {
const resp = await fetch("/api/key/generate", { method: "POST" });
if (!resp.ok) return;
const data = await resp.json();
if (data.tunKey) els.key.value = data.tunKey;
});
els.copy.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(els.key.value || "");
} catch (_) {}
});
els.paste.addEventListener("click", async () => {
try {
const text = await navigator.clipboard.readText();
if (text) els.key.value = text;
} catch (_) {}
});
els.start.addEventListener("click", async () => {
if (els.start.disabled) return;
if (currentKind === "idle") {
await fetch("/api/start", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ workType, tunKey: els.key.value }),
});
return;
}
await fetch("/api/stop", { method: "POST" });
});
function connectEvents() {
const es = new EventSource("/api/events");
es.addEventListener("snapshot", (e) => {
const s = JSON.parse(e.data);
applyState(s);
renderLogs(s.logs || []);
});
es.addEventListener("state", (e) => applyState(JSON.parse(e.data)));
es.addEventListener("log", (e) => appendLog(JSON.parse(e.data).line));
}
connectEvents();
+56
View File
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Goodlink</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='6' fill='%230078ff'/%3E%3Ctext x='16' y='22' text-anchor='middle' fill='white' font-size='18' font-family='sans-serif'%3EG%3C/text%3E%3C/svg%3E">
<link rel="stylesheet" href="/app.css">
</head>
<body>
<div class="app">
<header class="header">
<h1 id="app-title">Goodlink</h1>
</header>
<section class="row">
<span class="label">工作端侧:</span>
<div class="work-type" id="work-type">
<button type="button" class="work-btn" data-type="Local" id="btn-local">Local端</button>
<span class="vsep"></span>
<button type="button" class="work-btn" data-type="Remote" id="btn-remote">Remote端</button>
</div>
</section>
<section class="row">
<span class="label">连接密钥:</span>
<input id="tun-key" class="key-input" type="text" placeholder="16-64字节长度" autocomplete="off" spellcheck="false">
</section>
<section class="key-actions">
<button type="button" id="btn-generate">生成密钥</button>
<button type="button" id="btn-copy">复制密钥</button>
<button type="button" id="btn-paste">粘贴密钥</button>
</section>
<button type="button" id="btn-start" class="start-btn high" disabled>
<span class="spinner hidden" id="start-spinner"></span>
<span id="start-text">检测网络中...</span>
</button>
<section class="logs">
<div class="label">运行日志:</div>
<pre id="log-list"></pre>
</section>
<footer class="footer">
<div id="nat-hint" class="nat detecting">正在检测当前网络环境...</div>
<div class="footer-links">
<a id="upgrade-box" class="upgrade hidden" href="https://gitee.com/konyshe/goodlink/releases" target="_blank" rel="noopener">有新版本! <span id="upgrade-ver"></span></a>
<a href="https://gitee.com/konyshe/goodlink/issues" target="_blank" rel="noopener">反馈问题</a>
</div>
</footer>
</div>
<script src="/app.js"></script>
</body>
</html>