add theme

This commit is contained in:
ginuerzh
2024-03-16 18:19:00 +08:00
parent c2987df06d
commit ee37621f97
67 changed files with 5443 additions and 4059 deletions
+2 -1
View File
@@ -21,8 +21,9 @@
go.work
bin/
build/
*.syso
*.apk
*.idsig
gost-plus
gost.plus
+17 -7
View File
@@ -1,19 +1,24 @@
# https://gioui.org/doc/install
NAME=gost-plus
NAME=gost.plus
BINDIR=bin
VERSION=$(shell cat version/version.go | grep 'Version =' | sed 's/.*\"\(.*\)\".*/\1/g')
GOBUILD=CGO_ENABLED=0 go build --ldflags="-s -w" -v -x -a
GOFILES=*.go
PLATFORM_LIST = \
linux-amd64
linux-amd64 \
linux-arm64
WINDOWS_ARCH_LIST = \
windows-amd64
windows-amd64 \
windows-arm64
linux-amd64:
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
linux-arm64:
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
darwin-amd64:
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
@@ -22,13 +27,17 @@ darwin-arm64:
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
# https://github.com/tc-hib/go-winres
windows-amd64: winres
go-winres make
windows-amd64:
GOOS=windows GOARCH=amd64 go-winres make --in winres/winres.json --out winres/rsrc
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w -H windowsgui" -o $(BINDIR)/$(NAME)-$@.exe $(GOFILES)
windows-arm64:
GOOS=windows GOARCH=arm64 go-winres make --in winres/winres.json --out winres/rsrc
GOOS=windows GOARCH=arm64 CGO_ENABLED=0 go build -ldflags="-s -w -H windowsgui" -o $(BINDIR)/$(NAME)-$@.exe $(GOFILES)
# go install gioui.org/cmd/gogio@latest
android:
gogio -x -work -target android -minsdk 22 -version 1 -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).apk .
gogio -x -work -target android -minsdk 33 -version $(VERSION).2 -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).aab .
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
@@ -43,4 +52,5 @@ $(zip_releases): %.zip : %
releases: $(gz_releases) $(zip_releases)
clean:
rm $(BINDIR)/*
rm *.syso -f
rm $(BINDIR)/* -rf
-44
View File
@@ -1,44 +0,0 @@
# GOST-PLUS
A cross-platform GUI client for [GOST.PLUS](https://gost.plus) built with [gioui](https://gioui.org).
## Features
### File Tunnel
Expose local files to the public network.
<img src="assets/file-tunnel.gif">
### HTTP Tunnel
Expose local HTTP service to the public network.
<img src="assets/http-tunnel.gif">
### TCP Tunnel
Expose local TCP service to the public network.
<img src="assets/tcp-tunnel.gif">
### UDP Tunnel
Expose local UDP service to the public network.
<img src="assets/udp-tunnel.gif">
## Screenshot
### Desktop
<img src="assets/list.png" width="512" />
<img src="assets/menu.png" width="512" />
<img src="assets/add.png" width="512" />
<img src="assets/edit.png" width="512" />
### Mobile
<img src="assets/list-android.png" width="512" />
<img src="assets/add-android.png" width="512" />
<img src="assets/edit-android.png" width="512" />
Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 778 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 751 KiB

+38 -38
View File
@@ -2,14 +2,15 @@ package config
import (
"bytes"
"log"
"fmt"
"log/slog"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"gioui.org/app"
"github.com/go-gost/core/logger"
"github.com/go-gost/x/config"
xconfig "github.com/go-gost/x/config"
logger_parser "github.com/go-gost/x/config/parsing/logger"
"gopkg.in/yaml.v3"
@@ -23,12 +24,16 @@ var (
configDir string
)
func init() {
config.Store(&Config{})
}
func Init() {
log.SetFlags(log.Lshortfile | log.LstdFlags | log.Lmicroseconds)
slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{AddSource: true})))
dir, err := app.DataDir()
if err != nil {
log.Println(err)
slog.Error(fmt.Sprintf("appDir: %v", err))
}
if dir == "" {
dir, _ = os.Getwd()
@@ -36,55 +41,49 @@ func Init() {
configDir = filepath.Join(dir, "gost.plus")
os.MkdirAll(configDir, 0755)
log.Println("config dir:", configDir)
slog.Info(fmt.Sprintf("appDir: %s", configDir))
if err := global.Load(); err != nil {
log.Println(err)
cfg := Get()
if err := cfg.load(); err != nil {
slog.Error(fmt.Sprintf("load config: %v", err))
if _, ok := err.(*os.PathError); ok {
global.Write()
cfg.Write()
}
}
if global.Log == nil {
logDir := filepath.Join(configDir, "logs")
os.MkdirAll(logDir, 0755)
log.Println("log dir:", logDir)
Set(cfg)
global.Log = &xconfig.LogConfig{
Output: filepath.Join(logDir, "gost-plus.log"),
initLog()
}
func initLog() {
cfg := Get().Log
if cfg == nil {
cfg = &xconfig.LogConfig{
Output: "stdout",
Level: string(logger.InfoLevel),
Format: string(logger.JSONFormat),
Rotation: &xconfig.LogRotationConfig{
MaxSize: 10,
MaxAge: 7,
MaxBackups: 10,
LocalTime: true,
Compress: true,
},
}
}
logger.SetDefault(logger_parser.ParseLogger(&xconfig.LoggerConfig{Log: global.Log}))
logger.SetDefault(logger_parser.ParseLogger(&xconfig.LoggerConfig{Log: cfg}))
}
var (
global = &Config{}
globalMux sync.RWMutex
config atomic.Value
)
func Global() *Config {
globalMux.RLock()
defer globalMux.RUnlock()
func Get() *Config {
c := config.Load().(*Config)
cfg := &Config{}
*cfg = *global
*cfg = *c
return cfg
}
func Set(c *Config) {
globalMux.Lock()
defer globalMux.Unlock()
global = c
if c == nil {
c = &Config{}
}
config.Store(c)
}
type Settings struct {
@@ -104,18 +103,19 @@ type Tunnel struct {
Keepalive bool `yaml:",omitempty"`
TTL int `yaml:"ttl,omitempty"`
Favorite bool
Closed bool
Favorite bool
Closed bool
CreatedAt time.Time
}
type Config struct {
Settings *Settings
Tunnels []*Tunnel
EntryPoints []*Tunnel
Log *config.LogConfig
Log *xconfig.LogConfig
}
func (c *Config) Load() error {
func (c *Config) load() error {
f, err := os.Open(filepath.Join(configDir, configFile))
if err != nil {
return err
+10 -54
View File
@@ -1,65 +1,21 @@
module github.com/go-gost/gost-plus
module github.com/go-gost/gost.plus
go 1.21.1
go 1.22.0
require (
gioui.org v0.4.1
gioui.org/x v0.4.0
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7
github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98
github.com/google/uuid v1.4.0
github.com/spf13/viper v1.18.1
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb
gioui.org v0.5.0
gioui.org/x v0.5.0
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
)
require (
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
gioui.org/shader v1.0.8 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-gost/gosocks5 v0.4.0 // indirect
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b // indirect
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 // indirect
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 // indirect
github.com/go-redis/redis/v8 v8.11.5 // indirect
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/prometheus/client_golang v1.17.0 // indirect
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.11.1 // indirect
github.com/rs/xid v1.3.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/xtaci/smux v1.5.24 // indirect
github.com/yl2chen/cidranger v1.0.2 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect
golang.org/x/image v0.7.0 // indirect
golang.org/x/net v0.19.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/image v0.14.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
google.golang.org/grpc v1.59.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+13 -181
View File
@@ -1,199 +1,31 @@
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY=
eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA=
gioui.org v0.4.1 h1:sCTw5Fexg0xg9CxYmbrkKtHXcobf0JMbl5XpF2TC/zc=
gioui.org v0.4.1/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
gioui.org v0.5.0 h1:07g7/LY1MFuTncfO4A5DIKMMsQV6PkPHyx0MhDqgmYY=
gioui.org v0.5.0/go.mod h1:2atiYR4upH71/6ehnh6XsUELa7JZOrOHHNMDxGBZF0Q=
gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc=
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ=
gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA=
gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM=
gioui.org/x v0.4.0 h1:H6DofC86KoG51wgzeeA4ujZDDfcIa8vbL+jD9SpF/D8=
gioui.org/x v0.4.0/go.mod h1:YAoFl2lbeARk4LopDXHK1N7fBQJupPYDSm9maf6tFlM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7 h1:fxVUlZANqPApygO7lT8bYySyajiCFA62bDiNorral1w=
github.com/go-gost/core v0.0.0-20231119081403-abc73f2ca2b7/go.mod h1:ndkgWVYRLwupVaFFWv8ML1Nr8tD3xhHK245PLpUDg4E=
github.com/go-gost/gosocks5 v0.4.0 h1:EIrOEkpJez4gwHrMa33frA+hHXJyevjp47thpMQsJzI=
github.com/go-gost/gosocks5 v0.4.0/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b h1:ZmnYutflq+KOZK+Px5RDckorDSxTYlkT4aQbjTC8/C4=
github.com/go-gost/plugin v0.0.0-20231119084331-d49a1cb23b3b/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw=
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7 h1:qAG1OyjvdA5h221CfFSS3J359V3d2E7dJWyP29QoDSI=
github.com/go-gost/relay v0.4.1-0.20230916134211-828f314ddfe7/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451 h1:xj8gUZGYO3nb5+6Bjw9+tsFkA9sYynrOvDvvC4uDV2I=
github.com/go-gost/tls-dissector v0.0.2-0.20220408131628-aac992c27451/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8 h1:aM2tAfTg4+oBiB161GrYbRBhUHQM1CYNiDNYsqoVAlE=
github.com/go-gost/x v0.0.0-20231130113937-b1390dda1cc8/go.mod h1:YaeMQsu+I8Q3bxPFeo5MbnmLsAdbGUxxG3A4mvUt2YE=
github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98 h1:dFjwqp6SUJhDlFOBeBZXn5b2DqRjdbyBt+6w40+6cR8=
github.com/go-gost/x v0.0.0-20231216062858-f847fa533e98/go.mod h1:YaeMQsu+I8Q3bxPFeo5MbnmLsAdbGUxxG3A4mvUt2YE=
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
gioui.org/x v0.5.0 h1:NVKTn5AZuYhkAnF7MYcy1dIes36+U1N4gUTsgBhfr4A=
gioui.org/x v0.5.0/go.mod h1:X4UBhvanAN+8S16L3K6jDMrVo7Dii7NptgBpOLBD7E4=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
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.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
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/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.1 h1:rmuU42rScKWlhhJDyXZRKJQHXFX02chSVW1IvkPGiVM=
github.com/spf13/viper v1.18.1/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
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.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb h1:t3SA1mKG2eyhChDjOL0n0VLKZQmqU4J8UlCoQ0+nqSk=
golang.org/x/exp/shiny v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:UH99kUObWAZkDnWqppdQe5ZhPYESUw8I0zVV1uWBR+0=
golang.org/x/image v0.7.0 h1:gzS29xtG1J5ybQlv0PuyfE3nmc6R4qB73m6LUUmvFuw=
golang.org/x/image v0.7.0/go.mod h1:nd/q4ef1AKKYl/4kft7g+6UyGbdiqWqTP1ZAbRoV7Rg=
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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
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.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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-20191026070338-33540a1f6037/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
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/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc h1:OG+uKOKt/BW+ydf/M7gym7ONo8U+dyIlLazys3du298=
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
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=
+21 -13
View File
@@ -7,23 +7,21 @@ import (
"gioui.org/app"
_ "gioui.org/app/permission/storage"
"gioui.org/io/system"
"gioui.org/layout"
"gioui.org/io/key"
"gioui.org/op"
"github.com/go-gost/gost-plus/config"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/tunnel/entrypoint"
"github.com/go-gost/gost-plus/ui"
"github.com/go-gost/gost.plus/config"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/tunnel/entrypoint"
"github.com/go-gost/gost.plus/ui"
_ "github.com/go-gost/gost.plus/winres"
)
func main() {
config.Init()
tunnel.LoadConfig()
entrypoint.LoadConfig()
Init()
go func() {
w := app.NewWindow(
app.Title("GOST.PLUS"),
app.Title("GOST+"),
app.MinSize(800, 600),
)
err := run(w)
@@ -40,12 +38,22 @@ func run(w *app.Window) error {
var ops op.Ops
for {
switch e := w.NextEvent().(type) {
case system.DestroyEvent:
case app.DestroyEvent:
return e.Err
case system.FrameEvent:
gtx := layout.NewContext(&ops, e)
case app.FrameEvent:
gtx := app.NewContext(&ops, e)
ui.Layout(gtx)
e.Frame(gtx.Ops)
case key.Event:
if e.Name == key.NameBack {
return nil
}
}
}
}
func Init() {
config.Init()
tunnel.LoadConfig()
entrypoint.LoadConfig()
}
+4 -4
View File
@@ -5,8 +5,8 @@ import (
"sync"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost-plus/config"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost.plus/config"
"github.com/go-gost/gost.plus/tunnel"
)
const (
@@ -97,7 +97,7 @@ func Delete(id string) {
}
func LoadConfig() {
for _, cfg := range config.Global().EntryPoints {
for _, cfg := range config.Get().EntryPoints {
if cfg == nil {
continue
}
@@ -129,7 +129,7 @@ func LoadConfig() {
}
func SaveConfig() error {
cfg := config.Global()
cfg := config.Get()
cfg.EntryPoints = nil
for i := 0; i < Count(); i++ {
+8 -1
View File
@@ -12,7 +12,7 @@ import (
"github.com/go-gost/core/listener"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/x/config"
chain_parser "github.com/go-gost/x/config/parsing/chain"
"github.com/go-gost/x/handler/forward/local"
@@ -189,6 +189,13 @@ func (s *tcpEntryPoint) Run() (err error) {
return nil
}
func (s *tcpEntryPoint) Status() *xservice.Status {
if ss, _ := s.forward.(tunnel.ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *tcpEntryPoint) Close() error {
defer func() {
select {
+8 -1
View File
@@ -13,7 +13,7 @@ import (
"github.com/go-gost/core/listener"
"github.com/go-gost/core/logger"
"github.com/go-gost/core/service"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/x/config"
chain_parser "github.com/go-gost/x/config/parsing/chain"
"github.com/go-gost/x/handler/forward/local"
@@ -194,6 +194,13 @@ func (s *udpEntryPoint) Run() (err error) {
return nil
}
func (s *udpEntryPoint) Status() *xservice.Status {
if ss, _ := s.forward.(tunnel.ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *udpEntryPoint) Close() error {
defer func() {
select {
+7
View File
@@ -227,6 +227,13 @@ func (s *fileTunnel) Run() (err error) {
return nil
}
func (s *fileTunnel) Status() *xservice.Status {
if ss, _ := s.forward.(ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *fileTunnel) Close() error {
defer func() {
select {
+13 -6
View File
@@ -188,15 +188,15 @@ func (s *httpTunnel) Run() (err error) {
node := cfg.Forwarder.Nodes[0]
var nodeOpts []chain.NodeOption
if node.Auth != nil {
auther := xauth.NewAuthenticator(xauth.AuthsOption(map[string]string{node.Auth.Username: node.Auth.Password}))
nodeOpts = append(nodeOpts, chain.AutherNodeOption(auther))
}
if node.HTTP != nil {
nodeOpts = append(nodeOpts, chain.HTTPNodeOption(&chain.HTTPNodeSettings{
httpNodeSettings := &chain.HTTPNodeSettings{
Host: node.HTTP.Host,
Header: node.HTTP.Header,
}))
}
if node.HTTP.Auth != nil {
httpNodeSettings.Auther = xauth.NewAuthenticator(xauth.AuthsOption(map[string]string{node.HTTP.Auth.Username: node.HTTP.Auth.Password}))
}
nodeOpts = append(nodeOpts, chain.HTTPNodeOption(httpNodeSettings))
}
if node.TLS != nil {
nodeOpts = append(nodeOpts, chain.TLSNodeOption(&chain.TLSNodeSettings{
@@ -219,6 +219,13 @@ func (s *httpTunnel) Run() (err error) {
return nil
}
func (s *httpTunnel) Status() *xservice.Status {
if ss, _ := s.forward.(ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *httpTunnel) Close() error {
defer func() {
select {
+7
View File
@@ -185,6 +185,13 @@ func (s *tcpTunnel) Run() (err error) {
return nil
}
func (s *tcpTunnel) Status() *xservice.Status {
if ss, _ := s.forward.(ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *tcpTunnel) Close() error {
defer func() {
select {
+27 -21
View File
@@ -5,10 +5,11 @@ import (
"sync"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost-plus/config"
"github.com/go-gost/gost.plus/config"
xconfig "github.com/go-gost/x/config"
_ "github.com/go-gost/x/connector/tunnel"
_ "github.com/go-gost/x/dialer/ws"
xservice "github.com/go-gost/x/service"
)
const (
@@ -96,6 +97,10 @@ func TTLOption(ttl int) Option {
}
}
type ServiceStatus interface {
Status() *xservice.Status
}
type Tunnel interface {
ID() string
Type() string
@@ -104,6 +109,7 @@ type Tunnel interface {
Entrypoint() string
Options() Options
Run() error
Status() *xservice.Status
Favorite(b bool)
IsFavorite() bool
Close() error
@@ -215,37 +221,37 @@ func ChainConfig(id string, name string) *xconfig.ChainConfig {
}
func LoadConfig() {
for _, tun := range config.Global().Tunnels {
for _, cfg := range config.Get().Tunnels {
if cfg == nil {
continue
}
tun := createTunnel(cfg.Type, Options{
ID: cfg.ID,
Name: cfg.Name,
Endpoint: cfg.Endpoint,
Hostname: cfg.Hostname,
Username: cfg.Username,
Password: cfg.Password,
EnableTLS: cfg.EnableTLS,
})
if tun == nil {
continue
}
s := createTunnel(tun.Type, Options{
ID: tun.ID,
Name: tun.Name,
Endpoint: tun.Endpoint,
Hostname: tun.Hostname,
Username: tun.Username,
Password: tun.Password,
EnableTLS: tun.EnableTLS,
})
if s == nil {
continue
}
if tun.Closed {
s.Close()
if cfg.Closed {
tun.Close()
} else {
s.Run()
tun.Run()
}
s.Favorite(tun.Favorite)
Add(s)
tun.Favorite(cfg.Favorite)
Add(tun)
}
}
func SaveConfig() error {
cfg := config.Global()
cfg := config.Get()
cfg.Tunnels = nil
for i := 0; i < Count(); i++ {
+7
View File
@@ -185,6 +185,13 @@ func (s *udpTunnel) Run() (err error) {
return nil
}
func (s *udpTunnel) Status() *xservice.Status {
if ss, _ := s.forward.(ServiceStatus); ss != nil {
return ss.Status()
}
return nil
}
func (s *udpTunnel) Close() error {
defer func() {
select {
Binary file not shown.
Binary file not shown.
-36
View File
@@ -1,36 +0,0 @@
package fonts
import (
_ "embed"
"fmt"
"gioui.org/font"
"gioui.org/font/opentype"
)
//go:embed NotoSansSC-Regular.ttf
var fontNotoRegular []byte
//go:embed NotoSansSC-Bold.ttf
var fontNotoBold []byte
var (
collection []font.FontFace
)
func init() {
register(fontNotoRegular)
register(fontNotoBold)
}
func Collection() []font.FontFace {
return collection
}
func register(ttf []byte) {
faces, err := opentype.ParseCollection(ttf)
if err != nil {
panic(fmt.Errorf("failed to parse font: %v", err))
}
collection = append(collection, faces[0])
}
+28 -17
View File
@@ -25,29 +25,40 @@ func init() {
IconApp = &widget.Image{
Src: paint.NewImageOp(img),
Fit: widget.Unscaled,
Fit: widget.ScaleDown,
}
}
var (
IconApp *widget.Image
IconHome = mustIcon(icons.ActionHome)
IconFavorite = mustIcon(icons.ActionFavorite)
IconAdd = mustIcon(icons.ContentAdd)
IconSettings = mustIcon(icons.ActionSettings)
IconDone = mustIcon(icons.ActionDone)
IconTunnelState = mustIcon(icons.ToggleRadioButtonChecked)
IconForward = mustIcon(icons.NavigationChevronRight)
IconEdit = mustIcon(icons.EditorModeEdit)
IconDelete = mustIcon(icons.ActionDelete)
IconStart = mustIcon(icons.AVPlayArrow)
IconStop = mustIcon(icons.AVStop)
IconBack = mustIcon(icons.NavigationArrowBack)
IconClose = mustIcon(icons.ContentClear)
IconCopy = mustIcon(icons.ContentContentCopy)
IconVisibility = mustIcon(icons.ActionVisibility)
IconVisibilityOff = mustIcon(icons.ActionVisibilityOff)
IconHome = mustIcon(icons.ActionHome)
IconFavorite = mustIcon(icons.ActionFavorite)
IconAdd = mustIcon(icons.ContentAdd)
IconRemove = mustIcon(icons.ContentRemove)
IconSettings = mustIcon(icons.ActionSettings)
IconDone = mustIcon(icons.ActionDone)
IconRadioButtonUnchecked = mustIcon(icons.ToggleRadioButtonUnchecked)
IconRadioButtonChecked = mustIcon(icons.ToggleRadioButtonChecked)
IconForward = mustIcon(icons.NavigationChevronRight)
IconEdit = mustIcon(icons.EditorModeEdit)
IconDelete = mustIcon(icons.ActionDelete)
IconDeleteForever = mustIcon(icons.ActionDeleteForever)
IconStart = mustIcon(icons.AVPlayArrow)
IconStop = mustIcon(icons.AVStop)
IconBack = mustIcon(icons.NavigationArrowBack)
IconClose = mustIcon(icons.ContentClear)
IconCopy = mustIcon(icons.ContentContentCopy)
IconVisibility = mustIcon(icons.ActionVisibility)
IconVisibilityOff = mustIcon(icons.ActionVisibilityOff)
IconNavArrowForward = mustIcon(icons.NavigationArrowForward)
IconCircle = mustIcon(icons.ImageLens)
IconNavRight = mustIcon(icons.NavigationChevronRight)
IconNavExpandLess = mustIcon(icons.NavigationExpandLess)
IconNavExpandMore = mustIcon(icons.NavigationExpandMore)
IconActionCode = mustIcon(icons.ActionCode)
IconActionUpdate = mustIcon(icons.ActionUpdate)
IconActionHourGlassEmpty = mustIcon(icons.ActionHourglassEmpty)
)
func mustIcon(data []byte) *widget.Icon {
-57
View File
@@ -1,57 +0,0 @@
package page
import (
"gioui.org/font"
"gioui.org/layout"
"gioui.org/widget/material"
"github.com/go-gost/gost-plus/ui/icons"
"github.com/go-gost/gost-plus/version"
)
type aboutPage struct {
router *Router
list layout.List
}
func NewAboutPage(r *Router) Page {
return &aboutPage{
router: r,
}
}
func (p *aboutPage) Init(opts ...PageOption) {
p.router.bar.SetActions(nil, nil)
p.router.bar.Title = "About"
p.router.bar.NavigationIcon = icons.IconBack
}
func (p *aboutPage) Layout(gtx C, th *material.Theme) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
if icons.IconApp == nil {
return D{}
}
return icons.IconApp.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
label := material.H6(th, "GOST.PLUS")
label.Font.Weight = font.Bold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, version.Version).Layout(gtx)
}),
)
})
})
})
}
-179
View File
@@ -1,179 +0,0 @@
package page
import (
"fmt"
"image/color"
"strings"
"sync/atomic"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel/entrypoint"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type entryPointState struct {
editor widget.Clickable
}
type entryPointPage struct {
router *Router
list layout.List
wgFavorite widget.Clickable
wgAdd widget.Clickable
entryPoints map[int]*entryPointState
favorite atomic.Bool
}
func NewEntryPointPage(r *Router) Page {
return &entryPointPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
entryPoints: make(map[int]*entryPointState),
}
}
func (p *entryPointPage) Init(opts ...PageOption) {
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgFavorite.Clicked(gtx) {
p.favorite.Store(!p.favorite.Load())
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if p.favorite.Load() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Add",
Tag: &p.wgAdd,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgAdd.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageMenu})
}
return component.SimpleIconButton(bg, fg, &p.wgAdd, icons.IconAdd).Layout(gtx)
},
},
},
[]component.OverflowAction{
{
Name: "About",
Tag: OverflowActionAbout,
},
},
)
p.router.bar.Title = "EntryPoints"
p.router.bar.NavigationIcon = icons.IconHome
}
func (p *entryPointPage) Layout(gtx C, th *material.Theme) D {
favorite := p.favorite.Load()
// gtx.Constraints.Min.X = gtx.Constraints.Max.X
return p.list.Layout(gtx, entrypoint.Count(), func(gtx C, index int) D {
s := entrypoint.GetIndex(index)
if s == nil {
delete(p.entryPoints, index)
return D{}
}
if p.entryPoints[index] == nil {
p.entryPoints[index] = &entryPointState{}
}
if favorite && !s.IsFavorite() {
return D{}
}
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
state := p.entryPoints[index]
if state.editor.Clicked(gtx) {
switch s.Type() {
case entrypoint.TCPEntryPoint:
p.router.SwitchTo(Route{Path: PageEditTCPEntryPoint, ID: s.ID()})
case entrypoint.UDPEntryPoint:
p.router.SwitchTo(Route{Path: PageEditUDPEntryPoint, ID: s.ID()})
}
op.InvalidateOp{}.Add(gtx.Ops)
}
return material.Clickable(gtx, &state.editor, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th, s)
})
})
})
})
})
})
}
func (p *entryPointPage) layout(gtx C, th *material.Theme, ep entrypoint.EntryPoint) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, ep.ID())
label.Font.Weight = font.Bold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(ep.Type()))).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", ep.Name())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", ep.Endpoint())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", ep.Entrypoint())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(func(gtx C) D {
if err := ep.Err(); !ep.IsClosed() && err != nil {
label := material.Body2(th, err.Error())
label.Color = color.NRGBA(colornames.Red500)
return label.Layout(gtx)
}
return D{}
}),
)
}),
layout.Rigid(layout.Spacer{Width: 10}.Layout),
layout.Rigid(func(gtx C) D {
c := colornames.Green500
if ep.Err() != nil {
c = colornames.Red500
}
if ep.IsClosed() {
c = colornames.Grey500
}
return icons.IconTunnelState.Layout(gtx, color.NRGBA(c))
}),
)
}
+144
View File
@@ -0,0 +1,144 @@
package entrypoint
import (
"image/color"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type navButton struct {
btn widget.Clickable
name string
desc string
path page.PagePath
}
type entrypointPage struct {
router *page.Router
list widget.List
navs []navButton
btnBack widget.Clickable
}
func NewPage(r *page.Router) page.Page {
return &entrypointPage{
router: r,
list: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
navs: []navButton{
{
name: "TCP",
desc: "Create an entrypoint to the specified TCP tunnel",
path: page.PageEntrypointTCP,
},
{
name: "UDP",
desc: "Create an entrypoint to the specified UDP tunnel",
path: page.PageEntrypointUDP,
},
},
}
}
func (p *entrypointPage) Init(opts ...page.PageOption) {}
func (p *entrypointPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
th := p.router.Theme
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
title := material.H6(th, "Entrypoint")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, len(p.navs), func(gtx C, index int) D {
if p.navs[index].btn.Clicked(gtx) {
p.router.Goto(page.Route{
Path: p.navs[index].path,
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.ButtonLayoutStyle{
Background: theme.Current().ListBg,
CornerRadius: 12,
Button: &p.navs[index].btn,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, p.navs[index].name).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, p.navs[index].desc).Layout),
)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
}),
)
})
})
})
})
}),
)
}
+407
View File
@@ -0,0 +1,407 @@
package tcp
import (
"fmt"
"image/color"
"net"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/tunnel/entrypoint"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"github.com/google/uuid"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type tcpPage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
tunnelID component.TextField
name component.TextField
entrypoint component.TextField
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &tcpPage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
entrypoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
delDialog: ui_widget.Dialog{
Title: "Delete entrypoint?",
},
}
}
func (p *tcpPage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.entrypoint.Clear()
s := entrypoint.Get(p.id)
if s != nil {
sopts := s.Options()
p.tunnelID.SetText(sopts.ID)
p.name.SetText(sopts.Name)
p.entrypoint.SetText(sopts.Endpoint)
}
}
func (p *tcpPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "TCP")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
ep := entrypoint.Get(p.id)
if ep == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
ep.Favorite(!ep.IsFavorite())
entrypoint.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if ep.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
ep := entrypoint.Get(p.id)
if ep == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !ep.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *tcpPage) layout(gtx C, th *material.Theme) D {
if !p.edit {
gtx = gtx.Disabled()
}
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.TrimSpace(p.tunnelID.Text())
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
if p.id != "" {
gtx = gtx.Disabled()
}
return p.tunnelID.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.entrypoint.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.entrypoint.SetError(err.Error())
} else {
p.entrypoint.ClearError()
}
return p.entrypoint.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *tcpPage) create() error {
defer entrypoint.SaveConfig()
ep := entrypoint.NewTCPEntryPoint(
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.entrypoint.Text())),
)
entrypoint.Add(ep)
if err := ep.Run(); err != nil {
ep.Close()
return err
}
return nil
}
func (p *tcpPage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer entrypoint.SaveConfig()
if t := entrypoint.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
opts = []tunnel.Option{
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.entrypoint.Text())),
}
}
ep := entrypoint.NewTCPEntryPoint(opts...)
entrypoint.Set(ep)
if err := ep.Run(); err != nil {
ep.Close()
logger.Default().Error(err)
}
return ep
}
func (p *tcpPage) onoff() {
ep := entrypoint.Get(p.id)
if ep == nil {
return
}
if ep.IsClosed() {
opts := ep.Options()
p.update(
tunnel.IDOption(opts.ID),
tunnel.NameOption(opts.Name),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
ep.Close()
}
entrypoint.SaveConfig()
}
func (p *tcpPage) delete() {
entrypoint.Delete(p.id)
entrypoint.SaveConfig()
}
+470
View File
@@ -0,0 +1,470 @@
package udp
import (
"fmt"
"image/color"
"net"
"strconv"
"strings"
"unicode"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/tunnel/entrypoint"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"github.com/google/uuid"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type udpPage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
tunnelID component.TextField
name component.TextField
entrypoint component.TextField
keepalive widget.Bool
ttl component.TextField
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &udpPage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
entrypoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
ttl: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
Suffix: func(gtx C) D {
return material.Label(r.Theme, r.Theme.TextSize, "s").Layout(gtx)
},
},
delDialog: ui_widget.Dialog{
Title: "Delete entrypoint?",
},
}
}
func (p *udpPage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.entrypoint.Clear()
p.keepalive.Value = false
p.ttl.Clear()
s := entrypoint.Get(p.id)
if s != nil {
sopts := s.Options()
p.tunnelID.SetText(sopts.ID)
p.name.SetText(sopts.Name)
p.entrypoint.SetText(sopts.Endpoint)
p.keepalive.Value = sopts.Keepalive
p.ttl.SetText(strconv.Itoa(sopts.TTL))
}
}
func (p *udpPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "UDP")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
ep := entrypoint.Get(p.id)
if ep == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
ep.Favorite(!ep.IsFavorite())
entrypoint.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if ep.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
ep := entrypoint.Get(p.id)
if ep == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !ep.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *udpPage) layout(gtx C, th *material.Theme) D {
if !p.edit {
gtx = gtx.Disabled()
}
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.TrimSpace(p.tunnelID.Text())
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
if p.id != "" {
gtx = gtx.Disabled()
}
return p.tunnelID.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.entrypoint.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.entrypoint.SetError(err.Error())
} else {
p.entrypoint.ClearError()
}
return p.entrypoint.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Keepalive").Layout),
layout.Rigid(material.Switch(th, &p.keepalive, "keepalive").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.keepalive.Value {
p.ttl.Clear()
return layout.Dimensions{}
}
if err := func() string {
for _, r := range p.ttl.Text() {
if !unicode.IsDigit(r) {
return "Must contain only digits"
}
}
return ""
}(); err != "" {
p.ttl.SetError(err)
} else {
p.ttl.ClearError()
}
return p.ttl.Layout(gtx, th, "TTL")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *udpPage) create() error {
defer entrypoint.SaveConfig()
ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
ep := entrypoint.NewUDPEntryPoint(
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.entrypoint.Text())),
tunnel.KeepaliveOption(p.keepalive.Value),
tunnel.TTLOption(ttl),
)
entrypoint.Add(ep)
if err := ep.Run(); err != nil {
ep.Close()
return err
}
return nil
}
func (p *udpPage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer entrypoint.SaveConfig()
if t := entrypoint.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
opts = []tunnel.Option{
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.entrypoint.Text())),
tunnel.KeepaliveOption(p.keepalive.Value),
tunnel.TTLOption(ttl),
}
}
ep := entrypoint.NewUDPEntryPoint(opts...)
entrypoint.Set(ep)
if err := ep.Run(); err != nil {
ep.Close()
logger.Default().Error(err)
}
return ep
}
func (p *udpPage) onoff() {
ep := entrypoint.Get(p.id)
if ep == nil {
return
}
if ep.IsClosed() {
opts := ep.Options()
p.update(
tunnel.IDOption(opts.ID),
tunnel.NameOption(opts.Name),
tunnel.EndpointOption(opts.Endpoint),
tunnel.KeepaliveOption(opts.Keepalive),
tunnel.TTLOption(opts.TTL),
)
} else {
ep.Close()
}
entrypoint.SaveConfig()
}
func (p *udpPage) delete() {
entrypoint.Delete(p.id)
entrypoint.SaveConfig()
}
-438
View File
@@ -1,438 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"net"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/tunnel/entrypoint"
"github.com/go-gost/gost-plus/ui/icons"
"github.com/google/uuid"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type tcpEntryPointAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
tunnelID component.TextField
addr component.TextField
}
func NewTCPEntryPointAddPage(r *Router) Page {
return &tcpEntryPointAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *tcpEntryPointAddPage) Init(opts ...PageOption) {
p.name.Clear()
p.tunnelID.Clear()
p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if !p.isValid() {
gtx = gtx.Disabled()
}
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
p.createEntryPoint()
entrypoint.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "TCP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *tcpEntryPointAddPage) isValid() bool {
if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
p.addr.Text() == "" || p.addr.IsErrored() {
return false
}
return true
}
func (p *tcpEntryPointAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *tcpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Create an entrypoint to the specified TCP tunnel").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
if ep := entrypoint.Get(tid); ep != nil {
return fmt.Errorf("the entrypoint for this tunnel exists")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
return p.tunnelID.Layout(gtx, th, "ID")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
func (p *tcpEntryPointAddPage) createEntryPoint() error {
ep := entrypoint.NewTCPEntryPoint(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
)
entrypoint.Add(ep)
if err := ep.Run(); err != nil {
return err
}
return nil
}
type tcpEntryPointEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
name component.TextField
tunnelID component.TextField
addr component.TextField
}
func NewTCPEntryPointEditPage(r *Router) Page {
return &tcpEntryPointEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
ReadOnly: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *tcpEntryPointEditPage) Init(opts ...PageOption) {
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := entrypoint.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.tunnelID.SetText(sopts.ID)
p.addr.SetText(sopts.Endpoint)
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := entrypoint.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
entrypoint.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := entrypoint.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
opts := s.Options()
s = p.createEntryPoint(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
s.Close()
}
entrypoint.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
entrypoint.Delete(p.id)
entrypoint.SaveConfig()
p.router.SwitchTo(Route{Path: PageEntryPoint})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if !p.isValid() {
gtx = gtx.Disabled()
}
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
if s := entrypoint.Get(p.id); s != nil {
s.Close()
p.createEntryPoint()
entrypoint.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "TCP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *tcpEntryPointEditPage) isValid() bool {
if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
p.addr.Text() == "" || p.addr.IsErrored() {
return false
}
return true
}
func (p *tcpEntryPointEditPage) createEntryPoint(opts ...tunnel.Option) entrypoint.EntryPoint {
if opts == nil {
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
}
}
ep := entrypoint.NewTCPEntryPoint(opts...)
entrypoint.Set(ep)
if err := ep.Run(); err != nil {
log.Println(err)
}
return ep
}
func (p *tcpEntryPointEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *tcpEntryPointEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.TrimSpace(p.tunnelID.Text())
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
return p.tunnelID.Layout(gtx, th, "ID")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
-535
View File
@@ -1,535 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"net"
"strconv"
"strings"
"unicode"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/tunnel/entrypoint"
"github.com/go-gost/gost-plus/ui/icons"
"github.com/google/uuid"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type udpEntryPointAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
tunnelID component.TextField
addr component.TextField
bKeepalive widget.Bool
ttl component.TextField
}
func NewUDPEntryPointAddPage(r *Router) Page {
return &udpEntryPointAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
ttl: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *udpEntryPointAddPage) Init(opts ...PageOption) {
p.name.SetText("")
p.tunnelID.SetText("")
p.addr.SetText("")
p.bKeepalive.Value = false
p.ttl.SetText("")
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if !p.isValid() {
gtx = gtx.Disabled()
}
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
p.createEntryPoint()
entrypoint.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "UDP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *udpEntryPointAddPage) isValid() bool {
if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
p.addr.Text() == "" || p.addr.IsErrored() {
return false
}
return true
}
func (p *udpEntryPointAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *udpEntryPointAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Create an entrypoint to the specified UDP tunnel").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
if ep := entrypoint.Get(tid); ep != nil {
return fmt.Errorf("the entrypoint for this tunnel exists")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
return p.tunnelID.Layout(gtx, th, "ID")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Enable keepalive").Layout),
layout.Rigid(material.Switch(th, &p.bKeepalive, "keepalive").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.bKeepalive.Value {
p.ttl.Clear()
return layout.Dimensions{}
}
p.ttl.Suffix = func(gtx C) D {
return material.Label(th, th.TextSize, "s").Layout(gtx)
}
if err := func() string {
for _, r := range p.ttl.Text() {
if !unicode.IsDigit(r) {
return "Must contain only digits"
}
}
return ""
}(); err != "" {
p.ttl.SetError(err)
} else {
p.ttl.ClearError()
}
return p.ttl.Layout(gtx, th, "TTL")
}),
)
}
func (p *udpEntryPointAddPage) createEntryPoint() error {
ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
ep := entrypoint.NewUDPEntryPoint(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(strings.ToLower(strings.TrimSpace(p.tunnelID.Text()))),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
tunnel.KeepaliveOption(p.bKeepalive.Value),
tunnel.TTLOption(ttl),
)
entrypoint.Add(ep)
if err := ep.Run(); err != nil {
return err
}
return nil
}
type udpEntryPointEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
name component.TextField
tunnelID component.TextField
addr component.TextField
bKeepalive widget.Bool
ttl component.TextField
}
func NewUDPEntryPointEditPage(r *Router) Page {
return &udpEntryPointEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
tunnelID: component.TextField{
Editor: widget.Editor{
SingleLine: true,
ReadOnly: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
ttl: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *udpEntryPointEditPage) Init(opts ...PageOption) {
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := entrypoint.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.tunnelID.SetText(sopts.ID)
p.addr.SetText(sopts.Endpoint)
p.bKeepalive.Value = sopts.Keepalive
p.ttl.SetText(strconv.Itoa(sopts.TTL))
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := entrypoint.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
entrypoint.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := entrypoint.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
opts := s.Options()
s = p.createEntryPoint(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
tunnel.KeepaliveOption(opts.Keepalive),
tunnel.TTLOption(opts.TTL),
)
} else {
s.Close()
}
entrypoint.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
entrypoint.Delete(p.id)
entrypoint.SaveConfig()
p.router.SwitchTo(Route{Path: PageEntryPoint})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if !p.isValid() {
gtx = gtx.Disabled()
}
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageEntryPoint})
if s := entrypoint.Get(p.id); s != nil {
s.Close()
p.createEntryPoint()
entrypoint.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "UDP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *udpEntryPointEditPage) isValid() bool {
if p.tunnelID.Text() == "" || p.tunnelID.IsErrored() ||
p.addr.Text() == "" || p.addr.IsErrored() {
return false
}
return true
}
func (p *udpEntryPointEditPage) createEntryPoint(opts ...tunnel.Option) entrypoint.EntryPoint {
if opts == nil {
ttl, _ := strconv.Atoi(strings.TrimSpace(p.ttl.Text()))
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
tunnel.KeepaliveOption(p.bKeepalive.Value),
tunnel.TTLOption(ttl),
}
}
ep := entrypoint.NewUDPEntryPoint(opts...)
entrypoint.Set(ep)
if err := ep.Run(); err != nil {
log.Println(err)
}
return ep
}
func (p *udpEntryPointEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *udpEntryPointEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel ID").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
tid := strings.TrimSpace(p.tunnelID.Text())
if tid == "" {
return nil
}
if _, err := uuid.Parse(tid); err != nil {
return fmt.Errorf("invalid tunnel ID, should be a valid UUID")
}
return nil
}(); err != nil {
p.tunnelID.SetError(err.Error())
} else {
p.tunnelID.ClearError()
}
return p.tunnelID.Layout(gtx, th, "ID")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Entrypoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Enable keepalive").Layout),
layout.Rigid(material.Switch(th, &p.bKeepalive, "keepalive").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.bKeepalive.Value {
p.ttl.Clear()
return layout.Dimensions{}
}
p.ttl.Suffix = func(gtx C) D {
return material.Label(th, th.TextSize, "s").Layout(gtx)
}
if err := func() string {
for _, r := range p.ttl.Text() {
if !unicode.IsDigit(r) {
return "Must contain only digits"
}
}
return ""
}(); err != "" {
p.ttl.SetError(err)
} else {
p.ttl.ClearError()
}
return p.ttl.Layout(gtx, th, "TTL")
}),
)
}
-531
View File
@@ -1,531 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"os"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type fileAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
path component.TextField
cbBasicAuth widget.Bool
username component.TextField
password component.TextField
wgPassword widget.Clickable
passwordVisible bool
}
func NewFileAddPage(r *Router) Page {
return &fileAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
path: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *fileAddPage) Init(opts ...PageOption) {
p.name.Clear()
p.path.Clear()
p.cbBasicAuth.Value = false
p.username.Clear()
p.password.Clear()
p.passwordVisible = false
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
p.createTunnel()
tunnel.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "File"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *fileAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *fileAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Expose local files to public network.").Layout),
layout.Rigid(layout.Spacer{Height: 15}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Service name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Root directory, default to the current working directory").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
dir := strings.TrimSpace(p.path.Text())
if dir == "" {
return nil
}
f, err := os.Open(dir)
if err != nil {
return err
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return err
}
if !fs.IsDir() {
return fmt.Errorf("%s is not a directory", dir)
}
return nil
}(); err != nil {
p.path.SetError(err.Error())
} else {
p.path.ClearError()
}
return p.path.Layout(gtx, th, "Path")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.username.Clear()
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.password.Clear()
return layout.Dimensions{}
}
if p.wgPassword.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
return p.password.Layout(gtx, th, "Password")
}),
)
}
func (p *fileAddPage) createTunnel() error {
var username, password string
if p.cbBasicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
tun := tunnel.NewFileTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
type fileEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
wgID widget.Clickable
wgEntrypoint widget.Clickable
name component.TextField
path component.TextField
cbBasicAuth widget.Bool
username component.TextField
password component.TextField
wgPassword widget.Clickable
passwordVisible bool
}
func NewFileEditPage(r *Router) Page {
return &fileEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
path: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *fileEditPage) Init(opts ...PageOption) {
p.passwordVisible = false
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.path.SetText(sopts.Endpoint)
if sopts.Username != "" {
p.cbBasicAuth.Value = true
p.username.SetText(sopts.Username)
p.password.SetText(sopts.Password)
}
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
tunnel.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if s == nil {
return D{}
}
if p.wgState.Clicked(gtx) {
if s.IsClosed() {
opts := s.Options()
s = p.createTunnel(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
tunnel.UsernameOption(opts.Username),
tunnel.PasswordOption(opts.Password),
)
} else {
s.Close()
}
tunnel.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
tunnel.Delete(p.id)
tunnel.SaveConfig()
p.router.SwitchTo(Route{Path: PageTunnel})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
if s := tunnel.Get(p.id); s != nil {
s.Close()
p.createTunnel()
tunnel.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "File"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *fileEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
if opts == nil {
var username, password string
if p.cbBasicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.path.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
}
}
tun := tunnel.NewFileTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
log.Println(err)
}
return tun
}
func (p *fileEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *fileEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint)
}),
layout.Rigid(func(gtx C) D {
div := component.Divider(th)
return div.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Root directory, default to the current working directory").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
dir := strings.TrimSpace(p.path.Text())
if dir == "" {
return nil
}
f, err := os.Open(dir)
if err != nil {
return err
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return err
}
if !fs.IsDir() {
return fmt.Errorf("%s is not a directory", dir)
}
return nil
}(); err != nil {
p.path.SetError(err.Error())
} else {
p.path.ClearError()
}
return p.path.Layout(gtx, th, "Path")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
)
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.username.Clear()
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.password.Clear()
return layout.Dimensions{}
}
if p.wgPassword.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
return p.password.Layout(gtx, th, "Password")
}),
)
}
+134
View File
@@ -0,0 +1,134 @@
package list
import (
"fmt"
"image/color"
"strings"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/tunnel/entrypoint"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type entrypointState struct {
btn widget.Clickable
}
type entrypointList struct {
router *page.Router
list layout.List
states []entrypointState
filter Filter
}
func (l *entrypointList) Filter(f Filter) {
l.filter = f
}
func Entrypoint(r *page.Router) List {
return &entrypointList{
router: r,
list: layout.List{
Axis: layout.Vertical,
},
states: make([]entrypointState, 16),
}
}
func (l *entrypointList) Layout(gtx C, th *material.Theme) D {
tn := entrypoint.Count()
if tn > len(l.states) {
states := l.states
l.states = make([]entrypointState, tn)
copy(l.states, states)
}
return l.list.Layout(gtx, tn, func(gtx C, index int) D {
t := entrypoint.GetIndex(index)
if t == nil {
return D{}
}
if l.filter.Favorite && !t.IsFavorite() {
return D{}
}
if l.states[index].btn.Clicked(gtx) {
var path page.PagePath
switch t.Type() {
case entrypoint.TCPEntryPoint:
path = page.PageEntrypointTCP
case entrypoint.UDPEntryPoint:
path = page.PageEntrypointUDP
}
l.router.Goto(page.Route{
Path: path,
ID: t.ID(),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return material.ButtonLayoutStyle{
Background: theme.Current().ListBg,
CornerRadius: 12,
Button: &l.states[index].btn,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, t.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(t.Type()))).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", t.Name())).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", t.Endpoint())).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", t.Entrypoint())).Layout),
)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(12)
c := colornames.GreenA700
if t.Err() != nil {
c = colornames.Red600
}
if t.IsClosed() {
c = colornames.Grey600
}
return icons.IconCircle.Layout(gtx, color.NRGBA(c))
}),
)
})
})
})
})
}
+19
View File
@@ -0,0 +1,19 @@
package list
import (
"gioui.org/layout"
"gioui.org/widget/material"
)
type C = layout.Context
type D = layout.Dimensions
type Filter struct {
Favorite bool
Name string
}
type List interface {
Layout(gtx C, th *material.Theme) D
Filter(f Filter)
}
+138
View File
@@ -0,0 +1,138 @@
package list
import (
"fmt"
"image/color"
"strings"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type tunnelState struct {
btn widget.Clickable
}
type tunnelList struct {
router *page.Router
list layout.List
states []tunnelState
filter Filter
}
func Tunnel(r *page.Router) List {
return &tunnelList{
router: r,
list: layout.List{
Axis: layout.Vertical,
},
states: make([]tunnelState, 16),
}
}
func (l *tunnelList) Filter(f Filter) {
l.filter = f
}
func (l *tunnelList) Layout(gtx C, th *material.Theme) D {
tn := tunnel.Count()
if tn > len(l.states) {
states := l.states
l.states = make([]tunnelState, tn)
copy(l.states, states)
}
return l.list.Layout(gtx, tn, func(gtx C, index int) D {
t := tunnel.GetIndex(index)
if t == nil {
return D{}
}
if l.filter.Favorite && !t.IsFavorite() {
return D{}
}
if l.states[index].btn.Clicked(gtx) {
var path page.PagePath
switch t.Type() {
case tunnel.FileTunnel:
path = page.PageTunnelFile
case tunnel.HTTPTunnel:
path = page.PageTunnelHTTP
case tunnel.TCPTunnel:
path = page.PageTunnelTCP
case tunnel.UDPTunnel:
path = page.PageTunnelUDP
}
l.router.Goto(page.Route{
Path: path,
ID: t.ID(),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return material.ButtonLayoutStyle{
Background: theme.Current().ListBg,
CornerRadius: 12,
Button: &l.states[index].btn,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, t.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(t.Type()))).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", t.Name())).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", t.Endpoint())).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", t.Entrypoint())).Layout),
)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(12)
c := colornames.GreenA700
if t.Err() != nil {
c = colornames.Red600
}
if t.IsClosed() {
c = colornames.Grey600
}
return icons.IconCircle.Layout(gtx, color.NRGBA(c))
}),
)
})
})
})
})
}
+175
View File
@@ -0,0 +1,175 @@
package home
import (
"image/color"
"sync/atomic"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/page/home/list"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type navPage struct {
list list.List
path page.PagePath
}
type homePage struct {
router *page.Router
btnFavorite widget.Clickable
favorite atomic.Bool
nav *ui_widget.Nav
pages []navPage
btnAdd widget.Clickable
btnSettings widget.Clickable
}
func NewPage(r *page.Router) page.Page {
return &homePage{
router: r,
nav: ui_widget.NewNav(
ui_widget.NewNavButton("Tunnel"),
ui_widget.NewNavButton("Entrypoint"),
),
pages: []navPage{
{
list: list.Tunnel(r),
path: page.PageTunnel,
},
{
list: list.Entrypoint(r),
path: page.PageEntrypoint,
},
},
}
}
func (p *homePage) Init(opts ...page.PageOption) {}
func (p *homePage) Layout(gtx C) D {
if p.btnAdd.Clicked(gtx) {
p.router.Goto(page.Route{Path: p.pages[p.nav.Current()].path})
}
th := p.router.Theme
return layout.Stack{
Alignment: layout.SE,
}.Layout(gtx,
layout.Expanded(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Max.X = gtx.Dp(50)
return icons.IconApp.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
label := material.H6(th, "GOST+")
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.btnFavorite.Clicked(gtx) {
p.favorite.Store(!p.favorite.Load())
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if p.favorite.Load() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.btnSettings.Clicked(gtx) {
p.router.Goto(page.Route{
Path: page.PageSettings,
})
}
btn := material.IconButton(th, &p.btnSettings, icons.IconSettings, "Settings")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
)
})
}),
// nav
layout.Rigid(func(gtx C) D {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 4,
Bottom: 4,
}.Layout(gtx, func(gtx C) D {
return p.nav.Layout(gtx, th)
})
})
}),
// list
layout.Flexed(1, func(gtx C) D {
current := p.nav.Current()
if current < 0 || current >= len(p.pages) {
current = 0
}
pg := p.pages[current]
if pg.list == nil {
return D{
Size: gtx.Constraints.Max,
}
}
pg.list.Filter(list.Filter{
Favorite: p.favorite.Load(),
})
return pg.list.Layout(gtx, th)
}),
)
}),
layout.Stacked(func(gtx C) D {
return layout.Inset{
Top: 16,
Bottom: 16,
Left: 16,
Right: 16,
}.Layout(gtx, func(gtx C) D {
btn := material.IconButton(th, &p.btnAdd, icons.IconAdd, "Add")
btn.Inset = layout.UniformInset(16)
return btn.Layout(gtx)
})
}),
)
}
+83
View File
@@ -0,0 +1,83 @@
package home
import (
"fmt"
"image/color"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost.plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type Widget interface {
Layout(gtx C, th *material.Theme) D
}
type serviceWidget struct {
list layout.List
nav widget.Clickable
}
func (p *serviceWidget) Layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Inset{
Left: 10,
Right: 10,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return material.H6(th, "Services").Layout(gtx)
}),
layout.Flexed(1, layout.Spacer{}.Layout),
layout.Rigid(func(gtx C) D {
btn := material.IconButtonStyle{
Color: color.NRGBA(colornames.Grey800),
Icon: icons.IconNavArrowForward,
Button: &p.nav,
Size: 24,
Inset: layout.UniformInset(5),
Description: "forward",
}
return btn.Layout(gtx)
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return p.list.Layout(gtx, 10, func(gtx C, index int) D {
return layout.Inset{
Top: 5,
Bottom: 5,
Right: 10,
Left: 10,
}.Layout(gtx, func(gtx C) D {
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 15,
},
Fill: color.NRGBA(colornames.BlueGrey50),
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 10,
Bottom: 10,
Left: 10,
Right: 10,
}.Layout(gtx, material.H4(th, fmt.Sprintf("Service-%d\n\n", index)).Layout)
})
})
})
}),
)
}
-602
View File
@@ -1,602 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"net"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type httpAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
addr component.TextField
bHost widget.Bool
hostname component.TextField
bBasicAuth widget.Bool
username component.TextField
password component.TextField
bTLS widget.Bool
wgPassword widget.Clickable
passwordVisible bool
}
func NewHTTPAddPage(r *Router) Page {
return &httpAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
hostname: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *httpAddPage) Init(opts ...PageOption) {
p.name.Clear()
p.addr.Clear()
p.hostname.Clear()
p.bBasicAuth.Value = false
p.username.Clear()
p.password.Clear()
p.bTLS.Value = false
p.passwordVisible = false
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
p.createTunnel()
tunnel.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "HTTP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *httpAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *httpAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Expose local http tunnel to public network.").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, _, err := net.SplitHostPort(addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use custom hostname (rewrite HTTP Host header)").Layout),
layout.Rigid(material.Switch(th, &p.bHost, "custom hostname").Layout),
)
}),
layout.Rigid(func(gtx C) D {
if !p.bHost.Value {
p.hostname.SetText("")
return layout.Dimensions{}
}
return p.hostname.Layout(gtx, th, "Hostname")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
layout.Rigid(material.Switch(th, &p.bBasicAuth, "use basic auth").Layout),
)
}),
layout.Rigid(func(gtx C) D {
if !p.bBasicAuth.Value {
p.username.SetText("")
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.bBasicAuth.Value {
p.password.SetText("")
return layout.Dimensions{}
}
if p.wgPassword.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
return p.password.Layout(gtx, th, "Password")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Enable TLS").Layout),
layout.Rigid(material.Switch(th, &p.bTLS, "enable TLS").Layout),
)
}),
)
}
func (p *httpAddPage) createTunnel() error {
var username, password string
if p.bBasicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
var hostname string
if p.bHost.Value {
hostname = strings.TrimSpace(p.hostname.Text())
}
tun := tunnel.NewHTTPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
tunnel.HostnameOption(hostname),
tunnel.EnableTLSOption(p.bTLS.Value),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
type httpEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
name component.TextField
addr component.TextField
bHost widget.Bool
hostname component.TextField
cbBasicAuth widget.Bool
username component.TextField
password component.TextField
bTLS widget.Bool
wgID widget.Clickable
wgEntrypoint widget.Clickable
wgPassword widget.Clickable
passwordVisible bool
}
func NewHTTPEditPage(r *Router) Page {
return &httpEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
hostname: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *httpEditPage) Init(opts ...PageOption) {
p.passwordVisible = false
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.addr.SetText(sopts.Endpoint)
if sopts.Hostname != "" {
p.bHost.Value = true
p.hostname.SetText(sopts.Hostname)
}
if sopts.Username != "" {
p.cbBasicAuth.Value = true
p.username.SetText(sopts.Username)
p.password.SetText(sopts.Password)
}
p.bTLS.Value = sopts.EnableTLS
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
tunnel.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
opts := s.Options()
s = p.createTunnel(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
tunnel.HostnameOption(opts.Hostname),
tunnel.UsernameOption(opts.Username),
tunnel.PasswordOption(opts.Password),
tunnel.EnableTLSOption(opts.EnableTLS),
)
} else {
s.Close()
}
tunnel.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
tunnel.Delete(p.id)
tunnel.SaveConfig()
p.router.SwitchTo(Route{Path: PageTunnel})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
if s := tunnel.Get(p.id); s != nil {
s.Close()
p.createTunnel()
tunnel.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "HTTP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *httpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
if opts == nil {
var username, password string
if p.cbBasicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
var hostname string
if p.bHost.Value {
hostname = strings.TrimSpace(p.hostname.Text())
}
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
tunnel.HostnameOption(hostname),
tunnel.EnableTLSOption(p.bTLS.Value),
}
}
tun := tunnel.NewHTTPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
log.Println(err)
}
return tun
}
func (p *httpEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *httpEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use custom hostname (rewrite HTTP Host header)").Layout),
layout.Rigid(material.Switch(th, &p.bHost, "custom hostname").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.bHost.Value {
p.hostname.SetText("")
return layout.Dimensions{}
}
return p.hostname.Layout(gtx, th, "Hostname")
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Use basic auth").Layout),
layout.Rigid(material.Switch(th, &p.cbBasicAuth, "use basic auth").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.username.SetText("")
return layout.Dimensions{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.cbBasicAuth.Value {
p.password.SetText("")
return layout.Dimensions{}
}
if p.wgPassword.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx layout.Context) layout.Dimensions {
return p.wgPassword.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
return p.password.Layout(gtx, th, "Password")
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 10, Bottom: 10}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Enable TLS").Layout),
layout.Rigid(material.Switch(th, &p.bTLS, "enable TLS").Layout),
)
})
}),
)
}
-195
View File
@@ -1,195 +0,0 @@
package page
import (
"image/color"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type menuPage struct {
router *Router
list widget.List
wgFile widget.Clickable
wgHTTP widget.Clickable
wgTCP widget.Clickable
wgUDP widget.Clickable
wgEntryPointTCP widget.Clickable
wgEntryPointUDP widget.Clickable
}
func NewMenuPage(r *Router) Page {
return &menuPage{
router: r,
list: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
}
}
func (p *menuPage) Init(opts ...PageOption) {
p.router.bar.SetActions(nil, nil)
p.router.bar.Title = "Add"
p.router.bar.NavigationIcon = icons.IconBack
}
func (p *menuPage) Layout(gtx C, th *material.Theme) D {
if clicked := func() bool {
if p.wgFile.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewFile})
return true
}
if p.wgHTTP.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewHTTP})
return true
}
if p.wgTCP.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewTCP})
return true
}
if p.wgUDP.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewUDP})
return true
}
if p.wgEntryPointTCP.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewTCPEntryPoint})
return true
}
if p.wgEntryPointUDP.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageNewUDPEntryPoint})
return true
}
return false
}(); clicked {
op.InvalidateOp{}.Add(gtx.Ops)
}
return p.list.List.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.H6(th, "Tunnels")
label.Font.Weight = font.Bold
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, label.Layout)
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgFile, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "File", "Expose local files to public network")
})
})
})
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgHTTP, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "HTTP", "Expose local HTTP service to public network")
})
})
})
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgTCP, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "TCP", "Expose local TCP service to public network")
})
})
})
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgUDP, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "UDP", "Expose local UDP service to public network")
})
})
})
})
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
label := material.H6(th, "EntryPoints")
label.Font.Weight = font.Bold
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, label.Layout)
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgEntryPointTCP, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "TCP", "Create an entrypoint to the specified TCP tunnel")
})
})
})
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, &p.wgEntryPointUDP, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layoutCard(gtx, th, "UDP", "Create an entrypoint to the specified UDP tunnel")
})
})
})
})
}),
)
})
})
})
}
func (p *menuPage) layoutCard(gtx C, th *material.Theme, name, desc string) D {
return layout.Flex{
Axis: layout.Horizontal,
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
title := material.Body1(th, name)
title.Font.Weight = font.Bold
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(func(gtx C) D {
title := material.Body1(th, desc)
return title.Layout(gtx)
}),
)
}),
layout.Rigid(layout.Spacer{Width: 5}.Layout),
layout.Rigid(func(gtx C) D {
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
}),
)
}
+16 -105
View File
@@ -1,52 +1,33 @@
package page
import (
"image/color"
"gioui.org/font"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
const (
PageMenu = "/menu"
PageTunnel = "/tunnel"
PageNewFile = "/tunnel/file/create"
PageNewHTTP = "/tunnel/http/create"
PageNewTCP = "/tunnel/tcp/create"
PageNewUDP = "/tunnel/udp/create"
PageEditFile = "/tunnel/file/edit"
PageEditHTTP = "/tunnel/http/edit"
PageEditTCP = "/tunnel/tcp/edit"
PageEditUDP = "/tunnel/udp/edit"
PageEntryPoint = "/entrypoint"
PageNewTCPEntryPoint = "/entrypoint/tcp/create"
PageEditTCPEntryPoint = "/entrypoint/tcp/edit"
PageNewUDPEntryPoint = "/entrypoint/udp/create"
PageEditUDPEntryPoint = "/entrypoint/udp/edit"
PageAbout = "/about"
)
type OverflowAction string
type PagePath string
const (
OverflowActionAbout OverflowAction = "about"
PageHome PagePath = "/"
PageSettings PagePath = "/settings"
PageTunnel PagePath = "/tunnel"
PageTunnelFile PagePath = "/tunnel/file"
PageTunnelHTTP PagePath = "/tunnel/http"
PageTunnelTCP PagePath = "/tunnel/tcp"
PageTunnelUDP PagePath = "/tunnel/udp"
PageEntrypoint PagePath = "/entrypoint"
PageEntrypointTCP PagePath = "/entrypoint/tcp"
PageEntrypointUDP PagePath = "/entrypoint/udp"
)
type PageOptions struct {
ID string
}
type PageOption func(*PageOptions)
type PageOption func(opts *PageOptions)
func IDPageOption(id string) PageOption {
func WithPageID(id string) PageOption {
return func(opts *PageOptions) {
opts.ID = id
}
@@ -54,75 +35,5 @@ func IDPageOption(id string) PageOption {
type Page interface {
Init(opts ...PageOption)
Layout(gtx layout.Context, th *material.Theme) layout.Dimensions
}
func layoutHeader(gtx C, th *material.Theme, tun tunnel.Tunnel, wgID, wgEntrypoint *widget.Clickable) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
if tun == nil {
return D{}
}
copied := false
if wgID.Clicked(gtx) {
copied = true
clipboard.WriteOp{
Text: tun.ID(),
}.Add(gtx.Ops)
}
return wgID.Layout(gtx, func(gtx C) D {
return layout.Flex{}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, tun.ID())
label.Font.Weight = font.Bold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 5}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
}),
layout.Rigid(func(gtx C) D {
if tun == nil {
return D{}
}
copied := false
if wgEntrypoint.Clicked(gtx) {
copied = true
clipboard.WriteOp{
Text: tun.Entrypoint(),
}.Add(gtx.Ops)
}
return wgEntrypoint.Layout(gtx, func(gtx C) D {
return layout.Inset{Top: 5, Bottom: 5}.Layout(gtx, func(gtx C) D {
return layout.Flex{}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, tun.Entrypoint())
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 5}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
)
Layout(gtx layout.Context) layout.Dimensions
}
+97 -72
View File
@@ -1,109 +1,134 @@
package page
import (
"fmt"
"log/slog"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/unit"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost.plus/ui/theme"
)
type C = layout.Context
type D = layout.Dimensions
type Route struct {
Path string
Path PagePath
ID string
}
type Router struct {
modal *component.ModalLayer
bar *component.AppBar
pages map[string]Page
pages map[PagePath]Page
stack routeStack
current Route
*material.Theme
}
func NewRouter() *Router {
modal := component.NewModal()
bar := component.NewAppBar(modal)
func NewRouter(th *material.Theme) *Router {
r := &Router{
modal: modal,
bar: bar,
pages: make(map[string]Page),
pages: make(map[PagePath]Page),
Theme: th,
}
r.Register(PageTunnel, NewTunnelPage(r))
r.Register(PageMenu, NewMenuPage(r))
r.Register(PageNewFile, NewFileAddPage(r))
r.Register(PageEditFile, NewFileEditPage(r))
r.Register(PageNewHTTP, NewHTTPAddPage(r))
r.Register(PageEditHTTP, NewHTTPEditPage(r))
r.Register(PageNewTCP, NewTCPAddPage(r))
r.Register(PageEditTCP, NewTCPEditPage(r))
r.Register(PageNewUDP, NewUDPAddPage(r))
r.Register(PageEditUDP, NewUDPEditPage(r))
r.Register(PageEntryPoint, NewEntryPointPage(r))
r.Register(PageNewTCPEntryPoint, NewTCPEntryPointAddPage(r))
r.Register(PageEditTCPEntryPoint, NewTCPEntryPointEditPage(r))
r.Register(PageNewUDPEntryPoint, NewUDPEntryPointAddPage(r))
r.Register(PageEditUDPEntryPoint, NewUDPEntryPointEditPage(r))
r.Register(PageAbout, NewAboutPage(r))
r.SwitchTo(Route{Path: PageTunnel})
return r
}
func (r *Router) Register(path string, page Page) {
if page != nil {
r.pages[path] = page
}
func (r *Router) Register(path PagePath, page Page) {
r.pages[path] = page
}
func (r *Router) SwitchTo(route Route) {
p := r.pages[route.Path]
if p == nil {
func (r *Router) Goto(route Route) {
page := r.pages[route.Path]
if page == nil {
return
}
p.Init(IDPageOption(route.ID))
r.current = route
r.stack.Push(route)
page.Init(WithPageID(route.ID))
slog.Debug(fmt.Sprintf("go to %s", route.Path), "kind", "router", "route.id", route.ID)
}
func (r *Router) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
for _, event := range r.bar.Events(gtx) {
switch event := event.(type) {
case component.AppBarNavigationClicked:
path := r.current.Path
// log.Printf("navigation clicked: %+v", event)
if path == PageTunnel ||
path == PageNewTCPEntryPoint || path == PageEditTCPEntryPoint ||
path == PageNewUDPEntryPoint || path == PageEditUDPEntryPoint {
r.SwitchTo(Route{Path: PageEntryPoint})
} else {
r.SwitchTo(Route{Path: PageTunnel})
}
case component.AppBarContextMenuDismissed:
// log.Printf("Context menu dismissed: %+v", event)
r.SwitchTo(Route{Path: PageTunnel})
case component.AppBarOverflowActionClicked:
if event.Tag == OverflowActionAbout {
r.SwitchTo(Route{Path: PageAbout})
}
}
func (r *Router) Back() {
r.stack.Pop()
route := r.stack.Peek()
page := r.pages[route.Path]
if page == nil {
return
}
r.current = route
defer r.modal.Layout(gtx, th)
page.Init(WithPageID(route.ID))
slog.Debug(fmt.Sprintf("back to %s", route.Path), "kind", "router", "route.id", route.ID)
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return r.bar.Layout(gtx, th, "Menu", "Actions")
}),
layout.Flexed(1, func(gtx C) D {
return r.pages[r.current.Path].Layout(gtx, th)
}),
func (r *Router) Layout(gtx C) D {
r.Theme.Palette = theme.Current().Material
return layout.Background{}.Layout(gtx,
func(gtx C) D {
defer clip.Rect{
Max: gtx.Constraints.Max,
}.Op().Push(gtx.Ops).Pop()
paint.ColorOp{
Color: r.Theme.Bg,
}.Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
return layout.Dimensions{
Size: gtx.Constraints.Max,
}
},
func(gtx C) D {
page := r.pages[r.current.Path]
if page == nil {
page = r.pages[PageHome]
}
inset := layout.Inset{}
width := unit.Dp(800)
if x := gtx.Metric.PxToDp(gtx.Constraints.Max.X); x > width {
inset.Left = (x - width) / 2
inset.Right = inset.Left
}
return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return page.Layout(gtx)
})
},
)
}
type routeStack struct {
routes []Route
}
func (p *routeStack) Push(route Route) {
p.routes = append(p.routes, route)
}
func (p *routeStack) Pop() (route Route) {
if len(p.routes) == 0 {
return
}
n := len(p.routes) - 1
route = p.routes[n]
p.routes = p.routes[:n]
return
}
func (p *routeStack) Peek() (route Route) {
if len(p.routes) == 0 {
return
}
return p.routes[len(p.routes)-1]
}
+257
View File
@@ -0,0 +1,257 @@
package settings
import (
"gioui.org/font"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost.plus/config"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"github.com/go-gost/gost.plus/version"
)
type settingsPage struct {
router *page.Router
modal *component.ModalLayer
menu ui_widget.Menu
list widget.List
btnBack widget.Clickable
lang ui_widget.Selector
theme ui_widget.Selector
}
func NewPage(r *page.Router) page.Page {
return &settingsPage{
router: r,
modal: component.NewModal(),
menu: ui_widget.Menu{
List: layout.List{
Axis: layout.Vertical,
},
},
list: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
lang: ui_widget.Selector{Title: "Language"},
theme: ui_widget.Selector{Title: "Theme"},
}
}
func (p *settingsPage) Init(opts ...page.PageOption) {
settings := config.Get().Settings
if settings == nil {
settings = &config.Settings{}
}
if settings.Lang == "" {
settings.Lang = "en_US"
}
if settings.Theme == "" {
settings.Theme = theme.Light
}
p.lang.Clear()
p.lang.Select(settings.Lang)
p.theme.Clear()
p.theme.Select(settings.Theme)
}
func (p *settingsPage) Layout(gtx layout.Context) layout.Dimensions {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
title := material.H6(th, "Settings")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
)
})
}),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return p.list.Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *settingsPage) layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Vertical,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return icons.IconApp.Layout(gtx)
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.H6(th, "GOST+")
label.Font.Weight = font.Bold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return material.Body1(th, version.Version).Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.lang.Clicked(gtx) {
p.showLangMenu(gtx)
}
return p.lang.Layout(gtx, th)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.theme.Clicked(gtx) {
p.showThemeMenu(gtx)
}
return p.theme.Layout(gtx, th)
}),
)
})
})
}
func (p *settingsPage) showLangMenu(gtx layout.Context) {
items := []ui_widget.MenuItem{
{Key: "en_US", Value: "en_US"},
{Key: "zh_CN", Value: "zh_CN"},
}
var found bool
for i := range items {
if found = p.lang.Any(items[i].Value); found {
items[i].Selected = found
break
}
}
if !found {
items[0].Selected = true
}
p.menu.Title = "Language"
p.menu.Items = items
p.menu.Selected = func(index int) {
p.lang.Clear()
p.lang.Select(p.menu.Items[index].Value)
p.modal.Disappear(gtx.Now)
cfg := config.Get()
if cfg.Settings == nil {
cfg.Settings = &config.Settings{}
}
cfg.Settings.Lang = p.lang.Value()
config.Set(cfg)
cfg.Write()
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.menu.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
func (p *settingsPage) showThemeMenu(gtx layout.Context) {
items := []ui_widget.MenuItem{
{Key: "light", Value: theme.Light},
{Key: "dark", Value: theme.Dark},
}
var found bool
for i := range items {
if found = p.theme.Any(items[i].Value); found {
items[i].Selected = found
break
}
}
if !found {
items[0].Selected = true
}
p.menu.Title = "Theme"
p.menu.Items = items
p.menu.Selected = func(index int) {
p.theme.Clear()
p.theme.Select(p.menu.Items[index].Value)
p.modal.Disappear(gtx.Now)
cfg := config.Get()
if cfg.Settings == nil {
cfg.Settings = &config.Settings{}
}
cfg.Settings.Theme = p.theme.Value()
config.Set(cfg)
cfg.Write()
switch cfg.Settings.Theme {
case theme.Dark:
theme.UseDark()
default:
theme.UseLight()
}
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.menu.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
-357
View File
@@ -1,357 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"net"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type tcpAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
addr component.TextField
}
func NewTCPAddPage(r *Router) Page {
return &tcpAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *tcpAddPage) Init(opts ...PageOption) {
p.name.Clear()
p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
p.createTunnel()
tunnel.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "TCP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *tcpAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *tcpAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Expose local TCP tunnel to public network.").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
func (p *tcpAddPage) createTunnel() error {
tun := tunnel.NewTCPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
type tcpEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
name component.TextField
addr component.TextField
wgID widget.Clickable
wgEntrypoint widget.Clickable
}
func NewTCPEditPage(r *Router) Page {
return &tcpEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *tcpEditPage) Init(opts ...PageOption) {
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.addr.SetText(sopts.Endpoint)
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
tunnel.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
opts := s.Options()
s = p.createTunnel(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
s.Close()
}
tunnel.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
tunnel.Delete(p.id)
tunnel.SaveConfig()
p.router.SwitchTo(Route{Path: PageTunnel})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
if s := tunnel.Get(p.id); s != nil {
s.Close()
p.createTunnel()
tunnel.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "TCP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *tcpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
if opts == nil {
opts = []tunnel.Option{
tunnel.IDOption(p.id),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
}
}
tun := tunnel.NewTCPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
log.Println(err)
}
return tun
}
func (p *tcpEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *tcpEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, _, err := net.SplitHostPort(addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
-184
View File
@@ -1,184 +0,0 @@
package page
import (
"fmt"
"image/color"
"strings"
"sync/atomic"
"gioui.org/font"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type tunnelState struct {
editor widget.Clickable
}
type tunnelPage struct {
router *Router
list layout.List
wgFavorite widget.Clickable
wgAdd widget.Clickable
tunnels map[int]*tunnelState
favorite atomic.Bool
}
func NewTunnelPage(r *Router) Page {
return &tunnelPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
tunnels: make(map[int]*tunnelState),
}
}
func (p *tunnelPage) Init(opts ...PageOption) {
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgFavorite.Clicked(gtx) {
p.favorite.Store(!p.favorite.Load())
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if p.favorite.Load() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Add",
Tag: &p.wgAdd,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgAdd.Clicked(gtx) {
p.router.SwitchTo(Route{Path: PageMenu})
}
return component.SimpleIconButton(bg, fg, &p.wgAdd, icons.IconAdd).Layout(gtx)
},
},
},
[]component.OverflowAction{
{
Name: "About",
Tag: OverflowActionAbout,
},
},
)
p.router.bar.Title = "Tunnels"
p.router.bar.NavigationIcon = icons.IconHome
}
func (p *tunnelPage) Layout(gtx C, th *material.Theme) D {
favorite := p.favorite.Load()
// gtx.Constraints.Min.X = gtx.Constraints.Max.X
return p.list.Layout(gtx, tunnel.Count(), func(gtx C, index int) D {
s := tunnel.GetIndex(index)
if s == nil {
delete(p.tunnels, index)
return D{}
}
if p.tunnels[index] == nil {
p.tunnels[index] = &tunnelState{}
}
if favorite && !s.IsFavorite() {
return D{}
}
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
surface := component.Surface(th)
return surface.Layout(gtx, func(gtx C) D {
state := p.tunnels[index]
if state.editor.Clicked(gtx) {
switch s.Type() {
case tunnel.FileTunnel:
p.router.SwitchTo(Route{Path: PageEditFile, ID: s.ID()})
case tunnel.HTTPTunnel:
p.router.SwitchTo(Route{Path: PageEditHTTP, ID: s.ID()})
case tunnel.TCPTunnel:
p.router.SwitchTo(Route{Path: PageEditTCP, ID: s.ID()})
case tunnel.UDPTunnel:
p.router.SwitchTo(Route{Path: PageEditUDP, ID: s.ID()})
}
op.InvalidateOp{}.Add(gtx.Ops)
}
return material.Clickable(gtx, &state.editor, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th, s)
})
})
})
})
})
})
}
func (p *tunnelPage) layout(gtx C, th *material.Theme, s tunnel.Tunnel) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
label := material.Body1(th, s.ID())
label.Font.Weight = font.Bold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Type: %s", strings.ToUpper(s.Type()))).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Name: %s", s.Name())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Endpoint: %s", s.Endpoint())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(material.Body2(th, fmt.Sprintf("Entrypoint: %s", s.Entrypoint())).Layout),
layout.Rigid(layout.Spacer{Height: 5}.Layout),
layout.Rigid(func(gtx C) D {
if err := s.Err(); !s.IsClosed() && err != nil {
label := material.Body2(th, err.Error())
label.Color = color.NRGBA(colornames.Red500)
return label.Layout(gtx)
}
return D{}
}),
)
}),
layout.Rigid(layout.Spacer{Width: 10}.Layout),
layout.Rigid(func(gtx C) D {
c := colornames.Green500
if s.Err() != nil {
c = colornames.Red500
}
if s.IsClosed() {
c = colornames.Grey500
}
return icons.IconTunnelState.Layout(gtx, color.NRGBA(c))
}),
)
}
+570
View File
@@ -0,0 +1,570 @@
package file
import (
"bytes"
"fmt"
"image/color"
"io"
"os"
"strings"
"gioui.org/font"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type filePage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
wgID widget.Clickable
wgEntrypoint widget.Clickable
name component.TextField
endpoint component.TextField
basicAuth widget.Bool
username component.TextField
password component.TextField
btnPasswordVisible widget.Clickable
passwordVisible bool
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &filePage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
endpoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
delDialog: ui_widget.Dialog{
Title: "Delete tunnel?",
},
}
}
func (p *filePage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.endpoint.Clear()
p.basicAuth.Value = false
p.username.Clear()
p.password.Clear()
p.passwordVisible = false
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.endpoint.SetText(sopts.Endpoint)
if sopts.Username != "" {
p.basicAuth.Value = true
p.username.SetText(sopts.Username)
p.password.SetText(sopts.Password)
}
}
}
func (p *filePage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "File")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
tun.Favorite(!tun.IsFavorite())
tunnel.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if tun.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !tun.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *filePage) layout(gtx C, th *material.Theme) D {
src := gtx.Source
if !p.edit {
gtx = gtx.Disabled()
}
tun := tunnel.Get(p.id)
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgID.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgID.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgEntrypoint.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgEntrypoint.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.Entrypoint())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
dir := strings.TrimSpace(p.endpoint.Text())
if dir == "" {
return nil
}
f, err := os.Open(dir)
if err != nil {
return err
}
defer f.Close()
fs, err := f.Stat()
if err != nil {
return err
}
if !fs.IsDir() {
return fmt.Errorf("%s is not a directory", dir)
}
return nil
}(); err != nil {
p.endpoint.SetError(err.Error())
} else {
p.endpoint.ClearError()
}
return p.endpoint.Layout(gtx, th, "Directory path")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Basic auth").Layout),
layout.Rigid(material.Switch(th, &p.basicAuth, "basic auth").Layout),
)
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
if !p.basicAuth.Value {
p.username.Clear()
return D{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.basicAuth.Value {
p.password.Clear()
return D{}
}
{
gtx := gtx
gtx.Source = src
if p.btnPasswordVisible.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx C) D {
return p.btnPasswordVisible.Layout(gtx, func(gtx C) D {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx C) D {
return p.btnPasswordVisible.Layout(gtx, func(gtx C) D {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
}
return layout.Inset{
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.password.Layout(gtx, th, "Password")
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *filePage) create() error {
defer tunnel.SaveConfig()
var username, password string
if p.basicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
tun := tunnel.NewFileTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
func (p *filePage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer tunnel.SaveConfig()
if t := tunnel.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
var username, password string
if p.basicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
}
}
tun := tunnel.NewFileTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
logger.Default().Error(err)
}
return tun
}
func (p *filePage) onoff() {
tun := tunnel.Get(p.id)
if tun == nil {
return
}
if tun.IsClosed() {
opts := tun.Options()
p.update(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
tunnel.UsernameOption(opts.Username),
tunnel.PasswordOption(opts.Password),
)
} else {
tun.Close()
}
tunnel.SaveConfig()
}
func (p *filePage) delete() {
tunnel.Delete(p.id)
tunnel.SaveConfig()
}
+635
View File
@@ -0,0 +1,635 @@
package http
import (
"bytes"
"fmt"
"image/color"
"io"
"net"
"strings"
"gioui.org/font"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type httpPage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
wgID widget.Clickable
wgEntrypoint widget.Clickable
name component.TextField
endpoint component.TextField
rewriteHost widget.Bool
hostname component.TextField
basicAuth widget.Bool
username component.TextField
password component.TextField
enableTLS widget.Bool
btnPasswordVisible widget.Clickable
passwordVisible bool
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &httpPage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
endpoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
hostname: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
username: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
password: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
delDialog: ui_widget.Dialog{
Title: "Delete tunnel?",
},
}
}
func (p *httpPage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.endpoint.Clear()
p.rewriteHost.Value = false
p.hostname.Clear()
p.basicAuth.Value = false
p.username.Clear()
p.password.Clear()
p.passwordVisible = false
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.endpoint.SetText(sopts.Endpoint)
if sopts.Hostname != "" {
p.rewriteHost.Value = true
p.hostname.SetText(sopts.Hostname)
}
if sopts.Username != "" {
p.basicAuth.Value = true
p.username.SetText(sopts.Username)
p.password.SetText(sopts.Password)
}
p.enableTLS.Value = sopts.EnableTLS
}
}
func (p *httpPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "HTTP")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
tun.Favorite(!tun.IsFavorite())
tunnel.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if tun.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !tun.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *httpPage) layout(gtx C, th *material.Theme) D {
src := gtx.Source
if !p.edit {
gtx = gtx.Disabled()
}
tun := tunnel.Get(p.id)
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgID.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgID.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgEntrypoint.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgEntrypoint.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.Entrypoint())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.endpoint.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.endpoint.SetError(err.Error())
} else {
p.endpoint.ClearError()
}
return p.endpoint.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Basic auth").Layout),
layout.Rigid(material.Switch(th, &p.basicAuth, "Basic auth").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.basicAuth.Value {
p.username.Clear()
return D{}
}
return p.username.Layout(gtx, th, "Username")
}),
layout.Rigid(func(gtx C) D {
if !p.basicAuth.Value {
p.password.Clear()
return D{}
}
{
gtx := gtx
gtx.Source = src
if p.btnPasswordVisible.Clicked(gtx) {
p.passwordVisible = !p.passwordVisible
}
if p.passwordVisible {
p.password.Suffix = func(gtx C) D {
return p.btnPasswordVisible.Layout(gtx, func(gtx C) D {
return icons.IconVisibility.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = 0
} else {
p.password.Suffix = func(gtx C) D {
return p.btnPasswordVisible.Layout(gtx, func(gtx C) D {
return icons.IconVisibilityOff.Layout(gtx, color.NRGBA(colornames.Grey500))
})
}
p.password.Mask = '*'
}
}
return layout.Inset{
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.password.Layout(gtx, th, "Password")
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Custom hostname (rewrite HTTP Host header)").Layout),
layout.Rigid(material.Switch(th, &p.rewriteHost, "Hostname").Layout),
)
})
}),
layout.Rigid(func(gtx C) D {
if !p.rewriteHost.Value {
p.hostname.SetText("")
return layout.Dimensions{}
}
return layout.Inset{
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.hostname.Layout(gtx, th, "Hostname")
})
}),
layout.Rigid(func(gtx C) D {
return layout.Inset{Top: 8, Bottom: 8}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, "Enable TLS").Layout),
layout.Rigid(material.Switch(th, &p.enableTLS, "enable TLS").Layout),
)
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *httpPage) create() error {
defer tunnel.SaveConfig()
var username, password string
if p.basicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
var hostname string
if p.rewriteHost.Value {
hostname = strings.TrimSpace(p.hostname.Text())
}
tun := tunnel.NewHTTPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
tunnel.HostnameOption(hostname),
tunnel.EnableTLSOption(p.enableTLS.Value),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
func (p *httpPage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer tunnel.SaveConfig()
if t := tunnel.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
var username, password string
if p.basicAuth.Value {
username = strings.TrimSpace(p.username.Text())
password = strings.TrimSpace(p.password.Text())
}
var hostname string
if p.rewriteHost.Value {
hostname = strings.TrimSpace(p.hostname.Text())
}
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
tunnel.UsernameOption(username),
tunnel.PasswordOption(password),
tunnel.HostnameOption(hostname),
tunnel.EnableTLSOption(p.enableTLS.Value),
}
}
tun := tunnel.NewHTTPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
logger.Default().Error(err)
}
return tun
}
func (p *httpPage) onoff() {
tun := tunnel.Get(p.id)
if tun == nil {
return
}
if tun.IsClosed() {
opts := tun.Options()
p.update(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
tunnel.UsernameOption(opts.Username),
tunnel.PasswordOption(opts.Password),
tunnel.HostnameOption(opts.Hostname),
tunnel.EnableTLSOption(opts.EnableTLS),
)
} else {
tun.Close()
}
tunnel.SaveConfig()
}
func (p *httpPage) delete() {
tunnel.Delete(p.id)
tunnel.SaveConfig()
}
+155
View File
@@ -0,0 +1,155 @@
package tunnel
import (
"image/color"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type navButton struct {
btn widget.Clickable
name string
desc string
path page.PagePath
}
type tunnelPage struct {
router *page.Router
list widget.List
navs []navButton
btnBack widget.Clickable
}
func NewPage(r *page.Router) page.Page {
return &tunnelPage{
router: r,
list: widget.List{
List: layout.List{
Axis: layout.Vertical,
},
},
navs: []navButton{
{
name: "File",
desc: "Expose local files to public network",
path: page.PageTunnelFile,
},
{
name: "HTTP",
desc: "Expose local HTTP service to public network",
path: page.PageTunnelHTTP,
},
{
name: "TCP",
desc: "Expose local TCP service to public network",
path: page.PageTunnelTCP,
},
{
name: "UDP",
desc: "Expose local UDP service to public network",
path: page.PageTunnelUDP,
},
},
}
}
func (p *tunnelPage) Init(opts ...page.PageOption) {}
func (p *tunnelPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
th := p.router.Theme
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
title := material.H6(th, "Tunnel")
// title.Font.Weight = font.Bold
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, len(p.navs), func(gtx C, index int) D {
if p.navs[index].btn.Clicked(gtx) {
p.router.Goto(page.Route{
Path: p.navs[index].path,
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.ButtonLayoutStyle{
Background: theme.Current().ListBg,
CornerRadius: 12,
Button: &p.navs[index].btn,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Alignment: layout.Middle,
Spacing: layout.SpaceBetween,
}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, p.navs[index].name).Layout),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(material.Body2(th, p.navs[index].desc).Layout),
)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
}),
)
})
})
})
})
}),
)
}
+463
View File
@@ -0,0 +1,463 @@
package tcp
import (
"bytes"
"fmt"
"image/color"
"io"
"net"
"strings"
"gioui.org/font"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type tcpPage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
wgID widget.Clickable
wgEntrypoint widget.Clickable
name component.TextField
endpoint component.TextField
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &tcpPage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
endpoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
delDialog: ui_widget.Dialog{
Title: "Delete tunnel?",
},
}
}
func (p *tcpPage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.endpoint.Clear()
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.endpoint.SetText(sopts.Endpoint)
}
}
func (p *tcpPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "TCP")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
tun.Favorite(!tun.IsFavorite())
tunnel.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if tun.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !tun.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *tcpPage) layout(gtx C, th *material.Theme) D {
src := gtx.Source
if !p.edit {
gtx = gtx.Disabled()
}
tun := tunnel.Get(p.id)
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgID.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgID.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgEntrypoint.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgEntrypoint.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.Entrypoint())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.endpoint.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.endpoint.SetError(err.Error())
} else {
p.endpoint.ClearError()
}
return p.endpoint.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *tcpPage) create() error {
defer tunnel.SaveConfig()
tun := tunnel.NewTCPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
func (p *tcpPage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer tunnel.SaveConfig()
if t := tunnel.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
}
}
tun := tunnel.NewTCPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
logger.Default().Error(err)
}
return tun
}
func (p *tcpPage) onoff() {
tun := tunnel.Get(p.id)
if tun == nil {
return
}
if tun.IsClosed() {
opts := tun.Options()
p.update(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
tun.Close()
}
tunnel.SaveConfig()
}
func (p *tcpPage) delete() {
tunnel.Delete(p.id)
tunnel.SaveConfig()
}
+463
View File
@@ -0,0 +1,463 @@
package udp
import (
"bytes"
"fmt"
"image/color"
"io"
"net"
"strings"
"gioui.org/font"
"gioui.org/io/clipboard"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/core/logger"
"github.com/go-gost/gost.plus/tunnel"
"github.com/go-gost/gost.plus/ui/icons"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/theme"
ui_widget "github.com/go-gost/gost.plus/ui/widget"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type C = layout.Context
type D = layout.Dimensions
type udpPage struct {
router *page.Router
modal *component.ModalLayer
btnBack widget.Clickable
btnState widget.Clickable
btnDelete widget.Clickable
btnEdit widget.Clickable
btnSave widget.Clickable
btnFavorite widget.Clickable
list layout.List
wgID widget.Clickable
wgEntrypoint widget.Clickable
name component.TextField
endpoint component.TextField
id string
edit bool
delDialog ui_widget.Dialog
}
func NewPage(r *page.Router) page.Page {
return &udpPage{
router: r,
modal: component.NewModal(),
list: layout.List{
// NOTE: the list must be vertical
Axis: layout.Vertical,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
endpoint: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
delDialog: ui_widget.Dialog{
Title: "Delete tunnel?",
},
}
}
func (p *udpPage) Init(opts ...page.PageOption) {
var options page.PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
if p.id != "" {
p.edit = false
} else {
p.edit = true
}
p.name.Clear()
p.endpoint.Clear()
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.endpoint.SetText(sopts.Endpoint)
}
}
func (p *udpPage) Layout(gtx C) D {
if p.btnBack.Clicked(gtx) {
p.router.Back()
}
if p.btnEdit.Clicked(gtx) {
p.edit = true
}
if p.btnSave.Clicked(gtx) {
if p.id == "" {
p.create()
} else {
p.update()
}
p.router.Back()
}
if p.btnDelete.Clicked(gtx) {
p.delDialog.Clicked = func(ok bool) {
if ok {
p.delete()
p.router.Back()
}
p.modal.Disappear(gtx.Now)
}
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
return p.delDialog.Layout(gtx, th)
}
p.modal.Appear(gtx.Now)
}
th := p.router.Theme
defer p.modal.Layout(gtx, th)
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
// header
layout.Rigid(func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
btn := material.IconButton(th, &p.btnBack, icons.IconBack, "Back")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx C) D {
title := material.H6(th, "UDP")
return title.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnFavorite.Clicked(gtx) {
tun.Favorite(!tun.IsFavorite())
tunnel.SaveConfig()
}
btn := material.IconButton(th, &p.btnFavorite, icons.IconFavorite, "Favorite")
if tun.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = th.Fg
}
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
tun := tunnel.Get(p.id)
if tun == nil {
return D{}
}
if p.btnState.Clicked(gtx) {
p.onoff()
}
if !tun.IsClosed() {
btn := material.IconButton(th, &p.btnState, icons.IconStop, "Stop")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
btn := material.IconButton(th, &p.btnState, icons.IconStart, "Start")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.id == "" {
return D{}
}
btn := material.IconButton(th, &p.btnDelete, icons.IconDelete, "Delete")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
if p.edit {
btn := material.IconButton(th, &p.btnSave, icons.IconDone, "Done")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
} else {
btn := material.IconButton(th, &p.btnEdit, icons.IconEdit, "Edit")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}
}),
)
})
}),
layout.Flexed(1, func(gtx C) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.layout(gtx, th)
})
})
}),
)
}
func (p *udpPage) layout(gtx C, th *material.Theme) D {
src := gtx.Source
if !p.edit {
gtx = gtx.Disabled()
}
tun := tunnel.Get(p.id)
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 12,
},
Fill: theme.Current().ContentSurfaceBg,
}.Layout(gtx, func(gtx C) D {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 8,
Right: 8,
}.Layout(gtx, func(gtx C) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgID.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgID.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.ID())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if tun == nil {
return layout.Dimensions{}
}
gtx.Source = src
copied := false
if p.wgEntrypoint.Clicked(gtx) {
copied = true
gtx.Execute(clipboard.WriteCmd{
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
})
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.wgEntrypoint.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, tun.Entrypoint())
label.Font.Weight = font.SemiBold
return label.Layout(gtx)
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx C) D {
c := color.NRGBA(colornames.Blue500)
if copied {
c = color.NRGBA(colornames.Green500)
}
return icons.IconCopy.Layout(gtx, c)
}),
)
})
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "")
}),
layout.Rigid(layout.Spacer{Height: 16}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.endpoint.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.endpoint.SetError(err.Error())
} else {
p.endpoint.ClearError()
}
return p.endpoint.Layout(gtx, th, "Address")
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
)
})
})
}
func (p *udpPage) create() error {
defer tunnel.SaveConfig()
tun := tunnel.NewUDPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
func (p *udpPage) update(opts ...tunnel.Option) tunnel.Tunnel {
defer tunnel.SaveConfig()
if t := tunnel.Get(p.id); t != nil {
t.Close()
}
if opts == nil {
opts = []tunnel.Option{
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.IDOption(p.id),
tunnel.EndpointOption(strings.TrimSpace(p.endpoint.Text())),
}
}
tun := tunnel.NewUDPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
logger.Default().Error(err)
}
return tun
}
func (p *udpPage) onoff() {
tun := tunnel.Get(p.id)
if tun == nil {
return
}
if tun.IsClosed() {
opts := tun.Options()
p.update(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
tun.Close()
}
tunnel.SaveConfig()
}
func (p *udpPage) delete() {
tunnel.Delete(p.id)
tunnel.SaveConfig()
}
-358
View File
@@ -1,358 +0,0 @@
package page
import (
"fmt"
"image/color"
"log"
"net"
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost-plus/tunnel"
"github.com/go-gost/gost-plus/ui/icons"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type udpAddPage struct {
router *Router
list layout.List
wgDone widget.Clickable
name component.TextField
addr component.TextField
}
func NewUDPAddPage(r *Router) Page {
return &udpAddPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *udpAddPage) Init(opts ...PageOption) {
p.name.Clear()
p.addr.Clear()
p.router.bar.SetActions(
[]component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Create",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
p.createTunnel()
tunnel.SaveConfig()
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}, nil)
p.router.bar.Title = "UDP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *udpAddPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *udpAddPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(material.Body1(th, "Expose local UDP tunnel to public network.").Layout),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
func (p *udpAddPage) createTunnel() error {
tun := tunnel.NewUDPTunnel(
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
)
tunnel.Add(tun)
if err := tun.Run(); err != nil {
tun.Close()
return err
}
return nil
}
type udpEditPage struct {
router *Router
id string
list layout.List
wgFavorite widget.Clickable
wgState widget.Clickable
wgDelete widget.Clickable
wgDone widget.Clickable
name component.TextField
addr component.TextField
wgID widget.Clickable
wgEntrypoint widget.Clickable
}
func NewUDPEditPage(r *Router) Page {
return &udpEditPage{
router: r,
list: layout.List{
Axis: layout.Vertical,
Alignment: layout.Middle,
},
name: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
addr: component.TextField{
Editor: widget.Editor{
SingleLine: true,
},
},
}
}
func (p *udpEditPage) Init(opts ...PageOption) {
var options PageOptions
for _, opt := range opts {
opt(&options)
}
p.id = options.ID
s := tunnel.Get(p.id)
if s != nil {
sopts := s.Options()
p.name.SetText(sopts.Name)
p.addr.SetText(sopts.Endpoint)
}
actions := []component.AppBarAction{
{
OverflowAction: component.OverflowAction{
Name: "Favorite",
Tag: &p.wgFavorite,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if s == nil {
return D{}
}
if p.wgFavorite.Clicked(gtx) {
s.Favorite(!s.IsFavorite())
tunnel.SaveConfig()
}
btn := component.SimpleIconButton(bg, fg, &p.wgFavorite, icons.IconFavorite)
if s.IsFavorite() {
btn.Color = color.NRGBA(colornames.Red500)
} else {
btn.Color = fg
}
return btn.Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Start/Stop",
Tag: &p.wgState,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
s := tunnel.Get(p.id)
if p.wgState.Clicked(gtx) && s != nil {
if s.IsClosed() {
opts := s.Options()
s = p.createTunnel(
tunnel.NameOption(opts.Name),
tunnel.IDOption(opts.ID),
tunnel.EndpointOption(opts.Endpoint),
)
} else {
s.Close()
}
tunnel.SaveConfig()
}
if s != nil && !s.IsClosed() {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStop).Layout(gtx)
} else {
return component.SimpleIconButton(bg, fg, &p.wgState, icons.IconStart).Layout(gtx)
}
},
},
{
OverflowAction: component.OverflowAction{
Name: "Delete",
Tag: &p.wgDelete,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDelete.Clicked(gtx) {
tunnel.Delete(p.id)
tunnel.SaveConfig()
p.router.SwitchTo(Route{Path: PageTunnel})
}
return component.SimpleIconButton(bg, fg, &p.wgDelete, icons.IconDelete).Layout(gtx)
},
},
{
OverflowAction: component.OverflowAction{
Name: "Save",
Tag: &p.wgDone,
},
Layout: func(gtx C, bg, fg color.NRGBA) D {
if p.wgDone.Clicked(gtx) {
defer p.router.SwitchTo(Route{Path: PageTunnel})
p.createTunnel()
if s := tunnel.Get(p.id); s != nil {
s.Close()
p.createTunnel()
tunnel.SaveConfig()
}
}
return component.SimpleIconButton(bg, fg, &p.wgDone, icons.IconDone).Layout(gtx)
},
},
}
p.router.bar.SetActions(actions, nil)
p.router.bar.Title = "UDP"
p.router.bar.NavigationIcon = icons.IconClose
}
func (p *udpEditPage) createTunnel(opts ...tunnel.Option) tunnel.Tunnel {
if opts == nil {
opts = []tunnel.Option{
tunnel.IDOption(p.id),
tunnel.NameOption(strings.TrimSpace(p.name.Text())),
tunnel.EndpointOption(strings.TrimSpace(p.addr.Text())),
}
}
tun := tunnel.NewUDPTunnel(opts...)
tunnel.Set(tun)
if err := tun.Run(); err != nil {
tun.Close()
log.Println(err)
}
return tun
}
func (p *udpEditPage) Layout(gtx C, th *material.Theme) D {
return p.list.Layout(gtx, 1, func(gtx C, _ int) D {
return layout.Center.Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return component.Surface(th).Layout(gtx, func(gtx C) D {
return layout.UniformInset(10).Layout(gtx, func(gtx C) D {
return p.layout(gtx, th)
})
})
})
})
})
}
func (p *udpEditPage) layout(gtx C, th *material.Theme) D {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layoutHeader(gtx, th, tunnel.Get(p.id), &p.wgID, &p.wgEntrypoint)
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Tunnel name").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
return p.name.Layout(gtx, th, "Name")
}),
layout.Rigid(layout.Spacer{Height: 10}.Layout),
layout.Rigid(func(gtx C) D {
return material.Body1(th, "Endpoint address").Layout(gtx)
}),
layout.Rigid(func(gtx C) D {
if err := func() error {
addr := strings.TrimSpace(p.addr.Text())
if addr == "" {
return nil
}
if _, _, err := net.SplitHostPort(addr); err != nil {
return fmt.Errorf("invalid address format, should be [IP]:PORT or [HOST]:PORT")
}
return nil
}(); err != nil {
p.addr.SetError(err.Error())
} else {
p.addr.ClearError()
}
return p.addr.Layout(gtx, th, "Address")
}),
)
}
+89
View File
@@ -0,0 +1,89 @@
package theme
import (
"image/color"
"sync"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
const (
Light string = "light"
Dark string = "dark"
)
type Palette struct {
Material material.Palette
ContentSurfaceBg color.NRGBA
ListBg color.NRGBA
NavButtonBg color.NRGBA
NavButtonContrastBg color.NRGBA
}
type Theme struct {
Palette
}
var (
light = Theme{
Palette: Palette{
Material: material.Palette{
Fg: color.NRGBA(colornames.Black),
Bg: color.NRGBA(colornames.White),
ContrastBg: color.NRGBA{
R: 0x3f,
G: 0x51,
B: 0xb5,
A: 0xff,
},
ContrastFg: color.NRGBA(colornames.White),
},
ContentSurfaceBg: color.NRGBA(colornames.Grey50),
ListBg: color.NRGBA(colornames.BlueGrey50),
NavButtonBg: color.NRGBA(colornames.White),
NavButtonContrastBg: color.NRGBA(colornames.BlueGrey100),
},
}
dark = Theme{
Palette: Palette{
Material: material.Palette{
Fg: color.NRGBA(colornames.White),
Bg: color.NRGBA(colornames.Grey900),
ContrastBg: color.NRGBA(colornames.LightBlueA400),
ContrastFg: color.NRGBA(colornames.White),
},
ContentSurfaceBg: color.NRGBA(colornames.Grey800),
ListBg: color.NRGBA(colornames.Grey700),
NavButtonBg: color.NRGBA(colornames.Grey800),
NavButtonContrastBg: color.NRGBA(colornames.Grey600),
},
}
)
var (
theme Theme = light
mux sync.RWMutex
)
func Current() Theme {
mux.RLock()
defer mux.RUnlock()
return theme
}
func UseLight() {
mux.Lock()
defer mux.Unlock()
theme = light
}
func UseDark() {
mux.Lock()
defer mux.Unlock()
theme = dark
}
+45 -14
View File
@@ -1,36 +1,67 @@
package ui
import (
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/text"
"gioui.org/widget/material"
"github.com/go-gost/gost-plus/ui/fonts"
"github.com/go-gost/gost-plus/ui/page"
"github.com/go-gost/gost.plus/config"
"github.com/go-gost/gost.plus/ui/page"
"github.com/go-gost/gost.plus/ui/page/entrypoint"
tcp_ep "github.com/go-gost/gost.plus/ui/page/entrypoint/tcp"
udp_ep "github.com/go-gost/gost.plus/ui/page/entrypoint/udp"
"github.com/go-gost/gost.plus/ui/page/home"
"github.com/go-gost/gost.plus/ui/page/settings"
"github.com/go-gost/gost.plus/ui/page/tunnel"
"github.com/go-gost/gost.plus/ui/page/tunnel/file"
"github.com/go-gost/gost.plus/ui/page/tunnel/http"
"github.com/go-gost/gost.plus/ui/page/tunnel/tcp"
"github.com/go-gost/gost.plus/ui/page/tunnel/udp"
"github.com/go-gost/gost.plus/ui/theme"
)
type C = layout.Context
type D = layout.Dimensions
type UI struct {
th *material.Theme
router *page.Router
}
func NewUI() *UI {
th := material.NewTheme()
// th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
th.Shaper = text.NewShaper(text.WithCollection(fonts.Collection()))
// th.Bg = color.NRGBA(colornames.Brown800)
// th.Fg = color.NRGBA(colornames.Grey50)
ui := &UI{
th: th,
router: page.NewRouter(),
if settings := config.Get().Settings; settings != nil {
switch settings.Theme {
case theme.Dark:
theme.UseDark()
default:
theme.UseLight()
}
}
return ui
th := material.NewTheme()
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
th.Palette = theme.Current().Material
router := page.NewRouter(th)
router.Register(page.PageHome, home.NewPage(router))
router.Register(page.PageTunnel, tunnel.NewPage(router))
router.Register(page.PageTunnelFile, file.NewPage(router))
router.Register(page.PageTunnelHTTP, http.NewPage(router))
router.Register(page.PageTunnelTCP, tcp.NewPage(router))
router.Register(page.PageTunnelUDP, udp.NewPage(router))
router.Register(page.PageEntrypoint, entrypoint.NewPage(router))
router.Register(page.PageEntrypointTCP, tcp_ep.NewPage(router))
router.Register(page.PageEntrypointUDP, udp_ep.NewPage(router))
router.Register(page.PageSettings, settings.NewPage(router))
router.Goto(page.Route{
Path: page.PageHome,
})
return &UI{
router: router,
}
}
func (ui *UI) Layout(gtx C) D {
return ui.router.Layout(gtx, ui.th)
return ui.router.Layout(gtx)
}
+280
View File
@@ -0,0 +1,280 @@
package widget
import (
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
)
type Dialog struct {
Title string
Body string
Widget func(gtx layout.Context, th *material.Theme) layout.Dimensions
Clicked func(ok bool)
btnCancel widget.Clickable
btnOK widget.Clickable
}
func (p *Dialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
if gtx.Constraints.Max.X > gtx.Dp(800) {
gtx.Constraints.Max.X = gtx.Dp(800)
}
gtx.Constraints.Max.X = gtx.Constraints.Max.X * 2 / 3
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 28,
},
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Title == "" {
return layout.Dimensions{}
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.H6(th, p.Title).Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Body == "" {
return layout.Dimensions{}
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.Body1(th, p.Body).Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Widget == nil {
return layout.Dimensions{}
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.Widget(gtx, th)
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Width: 8}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnCancel.Clicked(gtx) && p.Clicked != nil {
p.Clicked(false)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnCancel,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "Cancel")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Width: 8}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnOK.Clicked(gtx) && p.Clicked != nil {
p.Clicked(true)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnOK,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "OK")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
)
}),
)
})
})
})
})
}
type InputDialog struct {
Surface component.SurfaceStyle
Title string
Body string
Hint string
Input component.TextField
Clicked func(ok bool)
btnCancel widget.Clickable
btnOK widget.Clickable
}
func (p *InputDialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
if gtx.Constraints.Max.X > gtx.Dp(800) {
gtx.Constraints.Max.X = gtx.Dp(800)
}
gtx.Constraints.Max.X = gtx.Constraints.Max.X * 2 / 3
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.Surface.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Title == "" {
return layout.Dimensions{}
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.H6(th, p.Title).Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Body == "" {
return layout.Dimensions{}
}
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.Body1(th, p.Body).Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.Input.Layout(gtx, th, p.Hint)
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.End,
}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Width: 0}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnCancel.Clicked(gtx) && p.Clicked != nil {
p.Clicked(false)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnCancel,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "Cancel")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnOK.Clicked(gtx) && p.Clicked != nil {
p.Clicked(true)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnOK,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "OK")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
)
}),
)
})
})
})
})
}
+132
View File
@@ -0,0 +1,132 @@
package widget
import (
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost.plus/ui/icons"
)
type Menu struct {
List layout.List
Items []MenuItem
Title string
Selected func(index int)
btnAdd widget.Clickable
ShowAdd bool
Multiple bool
}
func (p *Menu) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
if gtx.Constraints.Max.X > gtx.Dp(800) {
gtx.Constraints.Max.X = gtx.Dp(800)
}
gtx.Constraints.Max.X = gtx.Constraints.Max.X * 2 / 3
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return component.SurfaceStyle{
Theme: th,
ShadowStyle: component.ShadowStyle{
CornerRadius: 28,
},
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, material.H6(th, p.Title).Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if !p.ShowAdd {
return layout.Dimensions{}
}
btn := material.IconButton(th, &p.btnAdd, icons.IconAdd, "Add")
btn.Background = th.Bg
btn.Color = th.Fg
btn.Inset = layout.UniformInset(0)
return btn.Layout(gtx)
}),
)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.List.Layout(gtx, len(p.Items), func(gtx layout.Context, index int) layout.Dimensions {
if p.Items[index].state.Clicked(gtx) {
if p.Multiple {
p.Items[index].Selected = !p.Items[index].Selected
} else {
for i := range p.Items {
p.Items[i].Selected = false
}
p.Items[index].Selected = true
}
if p.Selected != nil {
p.Selected(index)
}
}
return p.Items[index].Layout(gtx, th)
})
})
}),
)
})
})
})
})
}
type MenuItem struct {
state widget.Clickable
Key string
Value string
Selected bool
}
func (p *MenuItem) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return material.ButtonLayoutStyle{
Background: th.Bg,
Button: &p.state,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 16,
Right: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, material.Body2(th, p.Key).Layout),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.Selected {
gtx.Constraints.Min.X = gtx.Dp(16)
return icons.IconDone.Layout(gtx, th.Fg)
}
return layout.Dimensions{}
}),
)
})
})
}
+208
View File
@@ -0,0 +1,208 @@
package widget
import (
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"gioui.org/x/component"
"github.com/go-gost/gost.plus/ui/icons"
)
type MetadataDialog struct {
Surface component.SurfaceStyle
List material.ListStyle
Clicked func(ok bool)
btnAdd widget.Clickable
btnCancel widget.Clickable
btnOK widget.Clickable
metadata []*kv
}
func (p *MetadataDialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
if p.btnAdd.Clicked(gtx) {
p.metadata = append(p.metadata, &kv{})
}
if gtx.Constraints.Max.X > gtx.Dp(800) {
gtx.Constraints.Max.X = gtx.Dp(800)
}
gtx.Constraints.Max.X = gtx.Constraints.Max.X * 2 / 3
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.Surface.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 16,
Bottom: 16,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Axis: layout.Vertical,
}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, material.H6(th, "Metadata").Layout),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(th, &p.btnAdd, icons.IconAdd, "Add")
btn.Background = th.Bg
btn.Color = th.Fg
return btn.Layout(gtx)
}),
)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
for i := range p.metadata {
if p.metadata[i].remove.Clicked(gtx) {
p.metadata = append(p.metadata[:i], p.metadata[i+1:]...)
break
}
}
gtx.Constraints.Max.Y -= gtx.Dp(80)
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return p.List.Layout(gtx, len(p.metadata), func(gtx layout.Context, index int) layout.Dimensions {
return p.metadata[index].Layout(gtx, th)
})
})
}),
layout.Rigid(layout.Spacer{Height: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Spacing: layout.SpaceBetween,
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Width: 8}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnCancel.Clicked(gtx) && p.Clicked != nil {
p.Clicked(false)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnCancel,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "Cancel")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Spacer{Width: 8}.Layout(gtx)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if p.btnOK.Clicked(gtx) && p.Clicked != nil {
p.Clicked(true)
}
return material.ButtonLayoutStyle{
Background: th.Bg,
CornerRadius: 20,
Button: &p.btnOK,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 24,
Right: 24,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, "OK")
label.Color = th.Fg
return label.Layout(gtx)
})
})
}),
)
}),
)
})
})
})
})
}
func (p *MetadataDialog) Add(k, v string) {
kv := &kv{}
kv.k.SetText(k)
kv.v.SetText(v)
p.metadata = append(p.metadata, kv)
}
func (p *MetadataDialog) Clear() {
p.metadata = nil
}
func (p *MetadataDialog) Metadata() []*kv {
return p.metadata
}
type kv struct {
k component.TextField
v component.TextField
remove widget.Clickable
}
func (p *kv) Get() (string, string) {
return strings.TrimSpace(p.k.Text()), strings.TrimSpace(p.v.Text())
}
func (p *kv) Set(k, v string) {
p.k.SetText(k)
p.v.SetText(v)
}
func (p *kv) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return layout.Inset{
Top: 4,
Bottom: 4,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.End,
}.Layout(gtx,
layout.Flexed(0.4, func(gtx layout.Context) layout.Dimensions {
return p.k.Layout(gtx, th, "Key")
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(0.4, func(gtx layout.Context) layout.Dimensions {
return p.v.Layout(gtx, th, "Value")
}),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
btn := material.IconButton(th, &p.remove, icons.IconDelete, "Remove")
btn.Color = th.Fg
btn.Background = th.Bg
return btn.Layout(gtx)
}),
)
})
}
+104
View File
@@ -0,0 +1,104 @@
package widget
import (
"image/color"
"gioui.org/layout"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/ui/theme"
"golang.org/x/exp/shiny/materialdesign/colornames"
)
type Nav struct {
list layout.List
btns []*NavButton
current int
}
func NewNav(btns ...*NavButton) *Nav {
return &Nav{
btns: btns,
}
}
func (p *Nav) Current() int {
return p.current
}
func (p *Nav) SetCurrent(n int) {
p.current = n
}
func (p *Nav) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
for i, btn := range p.btns {
if btn.btn.Clicked(gtx) {
p.current = i
break
}
}
return p.list.Layout(gtx, len(p.btns), func(gtx layout.Context, index int) layout.Dimensions {
btn := p.btns[index]
if p.current == index {
btn.background = theme.Current().NavButtonContrastBg
btn.borderWidth = 0
} else {
btn.background = theme.Current().NavButtonBg
btn.borderWidth = 1
}
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 12,
Right: 12,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return btn.Layout(gtx, th)
})
})
}
type NavButton struct {
btn widget.Clickable
cornerRadius unit.Dp
borderWidth unit.Dp
borderColor color.NRGBA
background color.NRGBA
text string
}
func NewNavButton(text string) *NavButton {
return &NavButton{
cornerRadius: 18,
borderWidth: 1,
borderColor: color.NRGBA(colornames.Grey200),
text: text,
}
}
func (btn *NavButton) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return material.ButtonLayoutStyle{
Background: btn.background,
CornerRadius: btn.cornerRadius,
Button: &btn.btn,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return widget.Border{
Color: btn.borderColor,
Width: btn.borderWidth,
CornerRadius: btn.cornerRadius,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
Left: 20,
Right: 20,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
label := material.Body1(th, btn.text)
return label.Layout(gtx)
})
})
})
}
+99
View File
@@ -0,0 +1,99 @@
package widget
import (
"strings"
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
"github.com/go-gost/gost.plus/ui/icons"
)
type Selector struct {
Title string
items []string
clickable widget.Clickable
}
func (p *Selector) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
return material.Clickable(gtx, &p.clickable, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Rigid(material.Body1(th, p.Title).Layout),
layout.Rigid(layout.Spacer{Width: 8}.Layout),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.Body2(th, strings.Join(p.items, ",")).Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return icons.IconNavRight.Layout(gtx, th.Fg)
}),
)
})
})
}
func (p *Selector) Clicked(gtx layout.Context) bool {
return p.clickable.Clicked(gtx)
}
func (p *Selector) Select(items ...string) {
for _, item := range items {
if item == "" {
continue
}
p.items = append(p.items, item)
}
}
func (p *Selector) Unselect(item string) {
for i := range p.items {
if p.items[i] == item {
p.items = append(p.items[:i], p.items[i+1:]...)
return
}
}
}
func (p *Selector) Any(items ...string) bool {
for _, item := range items {
if p.contains(item) {
return true
}
}
return false
}
func (p *Selector) Value() string {
if len(p.items) == 0 {
return ""
}
return p.items[0]
}
func (p *Selector) Values() []string {
return p.items
}
func (p *Selector) contains(item string) bool {
if item == "" && len(p.items) == 0 {
return true
}
for i := range p.items {
if p.items[i] == item {
return true
}
}
return false
}
func (p *Selector) Clear() {
p.items = nil
}
+41
View File
@@ -0,0 +1,41 @@
package widget
import (
"gioui.org/layout"
"gioui.org/widget"
"gioui.org/widget/material"
)
type Switcher struct {
clickable widget.Clickable
b widget.Bool
Title string
}
func (p *Switcher) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
if p.clickable.Clicked(gtx) {
p.b.Value = !p.b.Value
}
return material.Clickable(gtx, &p.clickable, func(gtx layout.Context) layout.Dimensions {
return layout.Inset{
Top: 8,
Bottom: 8,
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{
Alignment: layout.Middle,
}.Layout(gtx,
layout.Flexed(1, material.Body1(th, p.Title).Layout),
layout.Rigid(material.Switch(th, &p.b, "").Layout),
)
})
})
}
func (p *Switcher) Value() bool {
return p.b.Value
}
func (p *Switcher) SetValue(b bool) {
p.b.Value = b
}
+1 -1
View File
@@ -1,3 +1,3 @@
package version
var Version = "0.2.0"
var Version = "0.3.0"
+1
View File
@@ -0,0 +1 @@
package winres
+7 -7
View File
@@ -13,7 +13,7 @@
"name": "",
"version": ""
},
"description": "Simple GUI client for GOST.PLUS",
"description": "GUI client for GOST.PLUS",
"minimum-os": "win7",
"execution-level": "as invoker",
"ui-access": false,
@@ -35,22 +35,22 @@
"#1": {
"0000": {
"fixed": {
"file_version": "0.2.0.0",
"product_version": "0.2.0.0"
"file_version": "0.3.0.0",
"product_version": "0.3.0.0"
},
"info": {
"0409": {
"Comments": "",
"CompanyName": "GOST.PLUS",
"FileDescription": "A simple GUI client for GOST.PLUS",
"FileVersion": "0.2.0",
"FileDescription": "GUI client for GOST.PLUS",
"FileVersion": "0.3.0",
"InternalName": "gost.plus",
"LegalCopyright": "Copyright © 2024 GOST.PLUS",
"LegalTrademarks": "GOST.PLUS",
"OriginalFilename": "gost-plus",
"OriginalFilename": "gost.plus",
"PrivateBuild": "",
"ProductName": "GOST.PLUS",
"ProductVersion": "0.2.0",
"ProductVersion": "0.3.0",
"SpecialBuild": ""
}
}