Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42846b8e7b | ||
|
|
64838e272f | ||
|
|
4cb9abe5dd | ||
|
|
6360ba3875 | ||
|
|
02e071bbbc | ||
|
|
ee37621f97 | ||
|
|
c2987df06d |
@@ -21,8 +21,9 @@
|
||||
go.work
|
||||
|
||||
bin/
|
||||
build/
|
||||
|
||||
*.syso
|
||||
*.apk
|
||||
*.idsig
|
||||
gost-plus
|
||||
gost.plus
|
||||
@@ -1,46 +1,57 @@
|
||||
# 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)
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a -o $(BINDIR)/$(NAME)-$(VERSION)-$@ $(GOFILES)
|
||||
|
||||
linux-arm64:
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build --ldflags="-s -w" -v -x -a -o $(BINDIR)/$(NAME)-$(VERSION)-$@ $(GOFILES)
|
||||
|
||||
darwin-amd64:
|
||||
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
|
||||
GOOS=darwin GOARCH=amd64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$(VERSION)-$@ $(GOFILES)
|
||||
|
||||
darwin-arm64:
|
||||
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$@ $(GOFILES)
|
||||
GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINDIR)/$(NAME)-$(VERSION)-$@ $(GOFILES)
|
||||
|
||||
# https://github.com/tc-hib/go-winres
|
||||
windows-amd64: winres
|
||||
go-winres make
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -ldflags="-s -w -H windowsgui" -o $(BINDIR)/$(NAME)-$@.exe $(GOFILES)
|
||||
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)-$(VERSION)-$@.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)-$(VERSION)-$@.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 22 -version $(VERSION).8 -name GOST+ -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).aab .
|
||||
gogio -x -work -target android -minsdk 22 -version $(VERSION).8 -name GOST+ -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).apk .
|
||||
|
||||
gz_releases=$(addsuffix .gz, $(PLATFORM_LIST))
|
||||
zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST))
|
||||
|
||||
$(gz_releases): %.gz : %
|
||||
chmod +x $(BINDIR)/$(NAME)-$(basename $@)
|
||||
gzip -f -S -$(VERSION).gz $(BINDIR)/$(NAME)-$(basename $@)
|
||||
chmod +x $(BINDIR)/$(NAME)-$(VERSION)-$(basename $@)
|
||||
gzip -f -S .gz $(BINDIR)/$(NAME)-$(VERSION)-$(basename $@)
|
||||
|
||||
$(zip_releases): %.zip : %
|
||||
zip -m -j $(BINDIR)/$(NAME)-$(basename $@)-$(VERSION).zip $(BINDIR)/$(NAME)-$(basename $@).exe
|
||||
zip -m -j $(BINDIR)/$(NAME)-$(VERSION)-$(basename $@).zip $(BINDIR)/$(NAME)-$(VERSION)-$(basename $@).exe
|
||||
|
||||
releases: $(gz_releases) $(zip_releases)
|
||||
releases: $(gz_releases) $(zip_releases) android
|
||||
|
||||
clean:
|
||||
rm $(BINDIR)/*
|
||||
rm *.syso -f
|
||||
rm $(BINDIR)/* -rf
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# GOST-PLUS
|
||||
|
||||
A cross-platform GUI client for GOST.PLUS built with [gioui](https://gioui.org).
|
||||
A cross-platform GUI client for [GOST.PLUS](https://gost.plus) built with [gioui](https://gioui.org).
|
||||
|
||||
## Features
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 116 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 778 KiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 391 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.7 MiB |
|
Before Width: | Height: | Size: 751 KiB After Width: | Height: | Size: 1.1 MiB |
@@ -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,20 @@ type Tunnel struct {
|
||||
Keepalive bool `yaml:",omitempty"`
|
||||
TTL int `yaml:"ttl,omitempty"`
|
||||
|
||||
Favorite bool
|
||||
Closed bool
|
||||
Stats ServiceStats
|
||||
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
|
||||
@@ -137,3 +138,15 @@ func (c *Config) Write() error {
|
||||
|
||||
return os.WriteFile(filepath.Join(configDir, configFile), buf.Bytes(), 0644)
|
||||
}
|
||||
|
||||
type ServiceStats struct {
|
||||
Time time.Time
|
||||
TotalConns uint64
|
||||
RequestRate float64
|
||||
CurrentConns uint64
|
||||
TotalErrs uint64
|
||||
InputBytes uint64
|
||||
InputRateBytes uint64
|
||||
OutputBytes uint64
|
||||
OutputRateBytes uint64
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
module github.com/go-gost/gost-plus
|
||||
module github.com/go-gost/gost.plus
|
||||
|
||||
go 1.21.1
|
||||
go 1.22
|
||||
|
||||
toolchain go1.22.2
|
||||
|
||||
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.6.0
|
||||
gioui.org/x v0.6.1
|
||||
github.com/go-gost/core v0.0.0-20240309124515-fea73cf68235
|
||||
github.com/go-gost/x v0.0.0-20240131151842-25dcf536c6f5
|
||||
github.com/google/uuid v1.6.0
|
||||
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -20,13 +22,13 @@ require (
|
||||
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/plugin v0.0.0-20240103125338-9c84e29cb81a // indirect
|
||||
github.com/go-gost/relay v0.5.0 // 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/go-text/typesetting v0.1.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // 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
|
||||
@@ -36,7 +38,7 @@ require (
|
||||
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/client_model v0.5.0 // 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
|
||||
@@ -47,19 +49,19 @@ require (
|
||||
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/spf13/viper v1.18.2 // 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-20240314144324-c7f7c6466f7f // indirect
|
||||
golang.org/x/image v0.14.0 // indirect
|
||||
golang.org/x/net v0.24.0 // indirect
|
||||
golang.org/x/sys v0.19.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
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect
|
||||
google.golang.org/grpc v1.63.2 // indirect
|
||||
google.golang.org/protobuf v1.33.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
|
||||
)
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
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.6.0 h1:ZSXO/AbpFZJ2L9NU69uFQfDI3BKIH+YEJElrn0B+aZI=
|
||||
gioui.org v0.6.0/go.mod h1:eUvGo6FAzA7jUqeSu5a+M1W03yc9r1nanIBS8A5+Nng=
|
||||
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=
|
||||
gioui.org/x v0.6.1 h1:cHxT3CA07YYm6yNv6FrHRUqbNH2KId8vli3uIM8BF5A=
|
||||
gioui.org/x v0.6.1/go.mod h1:18BNyA2swIgDWV3/Nf7cQh9TaCTQQGbdLUg+pmURS3o=
|
||||
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=
|
||||
@@ -23,37 +23,33 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
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/core v0.0.0-20240309124515-fea73cf68235 h1:2iju5ndwCnazCaOcacxJaVFf3J/yb0L0ebXYh2J4rik=
|
||||
github.com/go-gost/core v0.0.0-20240309124515-fea73cf68235/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/plugin v0.0.0-20240103125338-9c84e29cb81a h1:ME7P1Brcg4C640DSPqlvQr7JuvvQfJ8QpmS3yCFlK3A=
|
||||
github.com/go-gost/plugin v0.0.0-20240103125338-9c84e29cb81a/go.mod h1:qXr2Zm9Ex2ATqnWuNUzVZqySPMnuIihvblYZt4MlZLw=
|
||||
github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
|
||||
github.com/go-gost/relay v0.5.0/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-gost/x v0.0.0-20240131151842-25dcf536c6f5 h1:IiZLdqGMx0lGVbDBy/N9LPu10qSlxm939EBvZ77qJNI=
|
||||
github.com/go-gost/x v0.0.0-20240131151842-25dcf536c6f5/go.mod h1:FDqjiiPbCqJLU/wY+q2IZCBVcYnfTJTw+SJLrspLQms=
|
||||
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=
|
||||
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/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo=
|
||||
github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.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=
|
||||
@@ -85,8 +81,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
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/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
|
||||
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
|
||||
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=
|
||||
@@ -109,8 +105,8 @@ 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/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
github.com/spf13/viper v1.18.2/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=
|
||||
@@ -126,62 +122,28 @@ 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/exp v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw=
|
||||
golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/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/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.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=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
|
||||
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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=
|
||||
|
||||
@@ -1,51 +1,98 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
_ "net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
_ "gioui.org/app/permission/storage"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"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/core/logger"
|
||||
"github.com/go-gost/gost.plus/config"
|
||||
"github.com/go-gost/gost.plus/runner"
|
||||
"github.com/go-gost/gost.plus/runner/task"
|
||||
"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/ui/page"
|
||||
"github.com/go-gost/gost.plus/ui/theme"
|
||||
"github.com/go-gost/gost.plus/ui/widget"
|
||||
_ "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.MinSize(800, 600),
|
||||
)
|
||||
err := run(w)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
if err := run(); err != nil {
|
||||
logger.Default().Fatal(err)
|
||||
}
|
||||
os.Exit(0)
|
||||
}()
|
||||
app.Main()
|
||||
}
|
||||
|
||||
func run(w *app.Window) error {
|
||||
func run() error {
|
||||
ui := ui.NewUI()
|
||||
|
||||
go handleEvent(ui)
|
||||
|
||||
w := ui.Window()
|
||||
var ops op.Ops
|
||||
for {
|
||||
switch e := w.NextEvent().(type) {
|
||||
case system.DestroyEvent:
|
||||
switch e := w.Event().(type) {
|
||||
case app.DestroyEvent:
|
||||
tunnel.SaveConfig()
|
||||
entrypoint.SaveConfig()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleEvent(ui *ui.UI) {
|
||||
for {
|
||||
select {
|
||||
case e := <-ui.Router().Event():
|
||||
switch e.ID {
|
||||
case page.EventThemeChanged:
|
||||
slog.Debug("theme changed", "event", e.ID)
|
||||
ui.Window().Option(app.StatusColor(theme.Current().Material.Bg))
|
||||
}
|
||||
|
||||
case e := <-runner.Event():
|
||||
switch e.TaskID {
|
||||
case runner.TaskUpdateStats:
|
||||
ui.Window().Invalidate()
|
||||
|
||||
default:
|
||||
if e.Err != nil {
|
||||
slog.Error(fmt.Sprintf("task: %s", e.Err), "task", e.TaskID)
|
||||
ui.Router().Notify(widget.Message{
|
||||
Type: widget.Error,
|
||||
Content: e.Err.Error(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Init() {
|
||||
config.Init()
|
||||
tunnel.LoadConfig()
|
||||
entrypoint.LoadConfig()
|
||||
|
||||
runner.Exec(context.Background(), task.UpdateStats(),
|
||||
runner.WithAync(true),
|
||||
runner.WithInterval(time.Second),
|
||||
runner.WithCancel(true),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
runner = NewRunner()
|
||||
)
|
||||
|
||||
type TaskEvent struct {
|
||||
TaskID TaskID
|
||||
Err error
|
||||
}
|
||||
|
||||
type taskState struct {
|
||||
task Task
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Async bool
|
||||
Interval time.Duration
|
||||
Cancel bool
|
||||
}
|
||||
|
||||
type Option func(opts *Options)
|
||||
|
||||
func WithAync(aync bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Async = aync
|
||||
}
|
||||
}
|
||||
|
||||
func WithInterval(interval time.Duration) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Interval = interval
|
||||
}
|
||||
}
|
||||
|
||||
func WithCancel(cancel bool) Option {
|
||||
return func(opts *Options) {
|
||||
opts.Cancel = cancel
|
||||
}
|
||||
}
|
||||
|
||||
func Event() <-chan *TaskEvent {
|
||||
return runner.Event()
|
||||
}
|
||||
|
||||
func Exec(ctx context.Context, task Task, opts ...Option) error {
|
||||
return runner.Exec(ctx, task, opts...)
|
||||
}
|
||||
|
||||
func Cancel(id TaskID) {
|
||||
runner.Cancel(id)
|
||||
}
|
||||
|
||||
type Runner struct {
|
||||
events chan *TaskEvent
|
||||
states map[TaskID]taskState
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewRunner() *Runner {
|
||||
return &Runner{
|
||||
events: make(chan *TaskEvent, 16),
|
||||
states: make(map[TaskID]taskState),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) Event() <-chan *TaskEvent {
|
||||
return r.events
|
||||
}
|
||||
|
||||
func (r *Runner) Exec(ctx context.Context, task Task, opts ...Option) error {
|
||||
if task == nil || task.ID() == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var options Options
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.Cancel {
|
||||
r.Cancel(task.ID())
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
r.setState(taskState{
|
||||
task: task,
|
||||
cancel: cancel,
|
||||
})
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{
|
||||
"kind": "runner",
|
||||
"async": options.Async,
|
||||
})
|
||||
log.Debugf("task %s started", task.ID())
|
||||
|
||||
if !options.Async {
|
||||
t := time.Now()
|
||||
err := task.Run(ctx)
|
||||
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Debugf("task %s done: %v", task.ID(), err)
|
||||
|
||||
select {
|
||||
case r.events <- &TaskEvent{
|
||||
TaskID: task.ID(),
|
||||
Err: err,
|
||||
}:
|
||||
default:
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer cancel()
|
||||
|
||||
t := time.Now()
|
||||
defer func() {
|
||||
log.WithFields(map[string]any{
|
||||
"duration": time.Since(t),
|
||||
}).Debugf("task %s done", task.ID())
|
||||
}()
|
||||
|
||||
run := func() {
|
||||
select {
|
||||
case r.events <- &TaskEvent{
|
||||
TaskID: task.ID(),
|
||||
Err: task.Run(ctx),
|
||||
}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
run()
|
||||
|
||||
interval := options.Interval
|
||||
if interval <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
run()
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) Cancel(id TaskID) {
|
||||
r.delState(id)
|
||||
}
|
||||
|
||||
func (r *Runner) setState(state taskState) {
|
||||
if state.task == nil {
|
||||
return
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
r.states[state.task.ID()] = state
|
||||
}
|
||||
|
||||
func (r *Runner) delState(id TaskID) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
state := r.states[id]
|
||||
if state.cancel != nil {
|
||||
state.cancel()
|
||||
}
|
||||
|
||||
delete(r.states, id)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
type TaskID string
|
||||
|
||||
const (
|
||||
TaskUpdateStats TaskID = "service.stats.update"
|
||||
)
|
||||
|
||||
type Task interface {
|
||||
ID() TaskID
|
||||
Run(ctx context.Context) error
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/gost.plus/config"
|
||||
"github.com/go-gost/gost.plus/runner"
|
||||
"github.com/go-gost/gost.plus/tunnel"
|
||||
"github.com/go-gost/gost.plus/tunnel/entrypoint"
|
||||
stats_pkg "github.com/go-gost/x/stats"
|
||||
)
|
||||
|
||||
type updateStatsTask struct{}
|
||||
|
||||
func UpdateStats() runner.Task {
|
||||
return &updateStatsTask{}
|
||||
}
|
||||
|
||||
func (t *updateStatsTask) ID() runner.TaskID {
|
||||
return runner.TaskUpdateStats
|
||||
}
|
||||
|
||||
func (t *updateStatsTask) Run(context.Context) error {
|
||||
t.updateTunnel()
|
||||
t.updateEntrypoint()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *updateStatsTask) updateTunnel() error {
|
||||
for i := 0; i < tunnel.Count(); i++ {
|
||||
tun := tunnel.GetIndex(i)
|
||||
if tun == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
status := tun.Status()
|
||||
if status == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
oldStats := tun.Stats()
|
||||
|
||||
d := time.Since(oldStats.Time)
|
||||
if d <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
stats := config.ServiceStats{}
|
||||
|
||||
if s := status.Stats(); s != nil {
|
||||
stats.CurrentConns = s.Get(stats_pkg.KindCurrentConns)
|
||||
stats.InputBytes = s.Get(stats_pkg.KindInputBytes)
|
||||
stats.OutputBytes = s.Get(stats_pkg.KindOutputBytes)
|
||||
stats.TotalConns = s.Get(stats_pkg.KindTotalConns)
|
||||
stats.TotalErrs = s.Get(stats_pkg.KindTotalErrs)
|
||||
stats.Time = time.Now()
|
||||
}
|
||||
|
||||
inputRateBytes := int64(stats.InputBytes) - int64(oldStats.InputBytes)
|
||||
if inputRateBytes < 0 {
|
||||
inputRateBytes = 0
|
||||
}
|
||||
stats.InputRateBytes = uint64(float64(inputRateBytes) / d.Seconds())
|
||||
|
||||
outputRateBytes := int64(stats.OutputBytes) - int64(oldStats.OutputBytes)
|
||||
if outputRateBytes < 0 {
|
||||
outputRateBytes = 0
|
||||
}
|
||||
stats.OutputRateBytes = uint64(float64(outputRateBytes) / d.Seconds())
|
||||
|
||||
reqRate := int64(stats.TotalConns) - int64(oldStats.TotalConns)
|
||||
if reqRate < 0 {
|
||||
reqRate = 0
|
||||
}
|
||||
stats.RequestRate = float64(reqRate) / d.Seconds()
|
||||
|
||||
tun.SetStats(stats)
|
||||
}
|
||||
|
||||
return tunnel.SaveConfig()
|
||||
}
|
||||
|
||||
func (t *updateStatsTask) updateEntrypoint() error {
|
||||
for i := 0; i < entrypoint.Count(); i++ {
|
||||
ep := entrypoint.GetIndex(i)
|
||||
if ep == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
status := ep.Status()
|
||||
if status == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
oldStats := ep.Stats()
|
||||
|
||||
d := time.Since(oldStats.Time)
|
||||
if d <= 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
stats := config.ServiceStats{}
|
||||
|
||||
if s := status.Stats(); s != nil {
|
||||
stats.CurrentConns = s.Get(stats_pkg.KindCurrentConns)
|
||||
stats.InputBytes = s.Get(stats_pkg.KindInputBytes)
|
||||
stats.OutputBytes = s.Get(stats_pkg.KindOutputBytes)
|
||||
stats.TotalConns = s.Get(stats_pkg.KindTotalConns)
|
||||
stats.TotalErrs = s.Get(stats_pkg.KindTotalErrs)
|
||||
stats.Time = time.Now()
|
||||
}
|
||||
|
||||
inputRateBytes := int64(stats.InputBytes) - int64(oldStats.InputBytes)
|
||||
if inputRateBytes < 0 {
|
||||
inputRateBytes = 0
|
||||
}
|
||||
stats.InputRateBytes = uint64(float64(inputRateBytes) / d.Seconds())
|
||||
|
||||
outputRateBytes := int64(stats.OutputBytes) - int64(oldStats.OutputBytes)
|
||||
if outputRateBytes < 0 {
|
||||
outputRateBytes = 0
|
||||
}
|
||||
stats.OutputRateBytes = uint64(float64(outputRateBytes) / d.Seconds())
|
||||
|
||||
reqRate := int64(stats.TotalConns) - int64(oldStats.TotalConns)
|
||||
if reqRate < 0 {
|
||||
reqRate = 0
|
||||
}
|
||||
stats.RequestRate = float64(reqRate) / d.Seconds()
|
||||
|
||||
ep.SetStats(stats)
|
||||
}
|
||||
|
||||
return entrypoint.SaveConfig()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -112,6 +112,8 @@ func LoadConfig() {
|
||||
EnableTLS: cfg.EnableTLS,
|
||||
Keepalive: cfg.Keepalive,
|
||||
TTL: cfg.TTL,
|
||||
CreatedAt: cfg.CreatedAt,
|
||||
Stats: cfg.Stats,
|
||||
})
|
||||
if ep == nil {
|
||||
continue
|
||||
@@ -129,7 +131,7 @@ func LoadConfig() {
|
||||
}
|
||||
|
||||
func SaveConfig() error {
|
||||
cfg := config.Global()
|
||||
cfg := config.Get()
|
||||
cfg.EntryPoints = nil
|
||||
|
||||
for i := 0; i < Count(); i++ {
|
||||
@@ -151,6 +153,8 @@ func SaveConfig() error {
|
||||
EnableTLS: opts.EnableTLS,
|
||||
Favorite: ep.IsFavorite(),
|
||||
Closed: ep.IsClosed(),
|
||||
CreatedAt: opts.CreatedAt,
|
||||
Stats: ep.Stats(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -163,7 +167,7 @@ func SaveConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEntryPoint(st string, opts tunnel.Options) EntryPoint {
|
||||
func createEntryPoint(st string, opts tunnel.Options) (ep EntryPoint) {
|
||||
options := []tunnel.Option{
|
||||
tunnel.IDOption(opts.ID),
|
||||
tunnel.NameOption(opts.Name),
|
||||
@@ -172,13 +176,17 @@ func createEntryPoint(st string, opts tunnel.Options) EntryPoint {
|
||||
tunnel.UsernameOption(opts.Username),
|
||||
tunnel.PasswordOption(opts.Password),
|
||||
tunnel.EnableTLSOption(opts.EnableTLS),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
}
|
||||
switch st {
|
||||
case TCPEntryPoint:
|
||||
return NewTCPEntryPoint(options...)
|
||||
ep = NewTCPEntryPoint(options...)
|
||||
case UDPEntryPoint:
|
||||
return NewUDPEntryPoint(options...)
|
||||
ep = NewUDPEntryPoint(options...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
ep.SetStats(opts.Stats)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,13 +6,15 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"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"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
"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"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
"github.com/go-gost/x/listener/tcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -29,6 +32,7 @@ type tcpEntryPoint struct {
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -56,6 +60,9 @@ func NewTCPEntryPoint(opts ...tunnel.Option) EntryPoint {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &tcpEntryPoint{
|
||||
endpoint: endpoint,
|
||||
@@ -152,10 +159,13 @@ func (s *tcpEntryPoint) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
stats := &stats.Stats{}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := tcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "tcp"})),
|
||||
listener.StatsOption(stats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -179,7 +189,10 @@ func (s *tcpEntryPoint) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(stats),
|
||||
)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -189,6 +202,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *tcpEntryPoint) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *tcpEntryPoint) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -13,7 +13,8 @@ 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"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
"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"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"github.com/go-gost/x/listener/udp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -30,6 +32,7 @@ type udpEntryPoint struct {
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -57,6 +60,9 @@ func NewUDPEntryPoint(opts ...tunnel.Option) EntryPoint {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &udpEntryPoint{
|
||||
endpoint: endpoint,
|
||||
@@ -157,10 +163,13 @@ func (s *udpEntryPoint) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
stats := &stats.Stats{}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := udp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "udp"})),
|
||||
listener.StatsOption(stats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -184,7 +193,10 @@ func (s *udpEntryPoint) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(stats),
|
||||
)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -194,6 +206,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *udpEntryPoint) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *udpEntryPoint) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/chain"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
xauth "github.com/go-gost/x/auth"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
@@ -24,6 +26,7 @@ import (
|
||||
"github.com/go-gost/x/listener/tcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -34,6 +37,7 @@ type fileTunnel struct {
|
||||
file service.Service
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -46,7 +50,6 @@ func NewFileTunnel(opts ...Option) Tunnel {
|
||||
for _, opt := range opts {
|
||||
opt(&options)
|
||||
}
|
||||
|
||||
if options.ID == "" {
|
||||
options.ID = uuid.NewString()
|
||||
}
|
||||
@@ -61,6 +64,9 @@ func NewFileTunnel(opts ...Option) Tunnel {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &fileTunnel{
|
||||
endpoint: endpoint,
|
||||
@@ -192,11 +198,21 @@ func (s *fileTunnel) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
pStats := &stats.Stats{}
|
||||
{
|
||||
pStats.Add(stats.KindCurrentConns, int64(s.stats.CurrentConns))
|
||||
pStats.Add(stats.KindInputBytes, int64(s.stats.InputBytes))
|
||||
pStats.Add(stats.KindOutputBytes, int64(s.stats.OutputBytes))
|
||||
pStats.Add(stats.KindTotalConns, int64(s.stats.TotalConns))
|
||||
pStats.Add(stats.KindTotalErrs, int64(s.stats.TotalErrs))
|
||||
}
|
||||
|
||||
cfg := s.config.Services[1]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
listener.StatsOption(pStats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -214,7 +230,10 @@ func (s *fileTunnel) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(pStats),
|
||||
)
|
||||
log.Infof("service listen on %s", s.file.Addr())
|
||||
}
|
||||
|
||||
@@ -227,6 +246,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *fileTunnel) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *fileTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
xauth "github.com/go-gost/x/auth"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
@@ -20,6 +22,7 @@ import (
|
||||
"github.com/go-gost/x/listener/rtcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -29,6 +32,7 @@ type httpTunnel struct {
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -56,6 +60,9 @@ func NewHTTPTunnel(opts ...Option) Tunnel {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &httpTunnel{
|
||||
endpoint: endpoint,
|
||||
@@ -102,17 +109,16 @@ func (s *httpTunnel) init() error {
|
||||
node := &config.ForwardNodeConfig{
|
||||
Name: s.opts.Name,
|
||||
Addr: s.opts.Endpoint,
|
||||
HTTP: &config.HTTPNodeConfig{},
|
||||
}
|
||||
if s.opts.Username != "" {
|
||||
node.Auth = &config.AuthConfig{
|
||||
node.HTTP.Auth = &config.AuthConfig{
|
||||
Username: s.opts.Username,
|
||||
Password: s.opts.Password,
|
||||
}
|
||||
}
|
||||
if s.opts.Hostname != "" {
|
||||
node.HTTP = &config.HTTPNodeConfig{
|
||||
Host: s.opts.Hostname,
|
||||
}
|
||||
node.HTTP.Host = s.opts.Hostname
|
||||
}
|
||||
if s.opts.EnableTLS {
|
||||
node.TLS = &config.TLSNodeConfig{}
|
||||
@@ -169,11 +175,14 @@ func (s *httpTunnel) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
stats := &stats.Stats{}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
listener.StatsOption(stats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -188,15 +197,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{
|
||||
@@ -209,7 +218,10 @@ func (s *httpTunnel) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(stats),
|
||||
)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -219,6 +231,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *httpTunnel) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *httpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
"github.com/go-gost/x/listener/rtcp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -28,6 +31,7 @@ type tcpTunnel struct {
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -55,6 +59,9 @@ func NewTCPTunnel(opts ...Option) Tunnel {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &tcpTunnel{
|
||||
endpoint: endpoint,
|
||||
@@ -151,11 +158,14 @@ func (s *tcpTunnel) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
stats := &stats.Stats{}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rtcp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rtcp"})),
|
||||
listener.StatsOption(stats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -175,7 +185,10 @@ func (s *tcpTunnel) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(stats),
|
||||
)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -185,6 +198,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *tcpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -3,12 +3,14 @@ package tunnel
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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 (
|
||||
@@ -38,6 +40,8 @@ type Options struct {
|
||||
EnableTLS bool
|
||||
Keepalive bool
|
||||
TTL int
|
||||
CreatedAt time.Time
|
||||
Stats config.ServiceStats
|
||||
}
|
||||
|
||||
type Option func(opts *Options)
|
||||
@@ -96,6 +100,16 @@ func TTLOption(ttl int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func CreatedAtOption(createdAt time.Time) Option {
|
||||
return func(opts *Options) {
|
||||
opts.CreatedAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceStatus interface {
|
||||
Status() *xservice.Status
|
||||
}
|
||||
|
||||
type Tunnel interface {
|
||||
ID() string
|
||||
Type() string
|
||||
@@ -104,6 +118,9 @@ type Tunnel interface {
|
||||
Entrypoint() string
|
||||
Options() Options
|
||||
Run() error
|
||||
Status() *xservice.Status
|
||||
Stats() config.ServiceStats
|
||||
SetStats(stats config.ServiceStats)
|
||||
Favorite(b bool)
|
||||
IsFavorite() bool
|
||||
Close() error
|
||||
@@ -215,37 +232,39 @@ 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,
|
||||
CreatedAt: cfg.CreatedAt,
|
||||
Stats: cfg.Stats,
|
||||
})
|
||||
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++ {
|
||||
@@ -267,6 +286,8 @@ func SaveConfig() error {
|
||||
EnableTLS: opts.EnableTLS,
|
||||
Favorite: tun.IsFavorite(),
|
||||
Closed: tun.IsClosed(),
|
||||
CreatedAt: opts.CreatedAt,
|
||||
Stats: tun.Stats(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -279,7 +300,7 @@ func SaveConfig() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createTunnel(st string, opts Options) Tunnel {
|
||||
func createTunnel(st string, opts Options) (t Tunnel) {
|
||||
options := []Option{
|
||||
IDOption(opts.ID),
|
||||
NameOption(opts.Name),
|
||||
@@ -288,17 +309,22 @@ func createTunnel(st string, opts Options) Tunnel {
|
||||
UsernameOption(opts.Username),
|
||||
PasswordOption(opts.Password),
|
||||
EnableTLSOption(opts.EnableTLS),
|
||||
CreatedAtOption(opts.CreatedAt),
|
||||
}
|
||||
|
||||
switch st {
|
||||
case FileTunnel:
|
||||
return NewFileTunnel(options...)
|
||||
t = NewFileTunnel(options...)
|
||||
case HTTPTunnel:
|
||||
return NewHTTPTunnel(options...)
|
||||
t = NewHTTPTunnel(options...)
|
||||
case TCPTunnel:
|
||||
return NewTCPTunnel(options...)
|
||||
t = NewTCPTunnel(options...)
|
||||
case UDPTunnel:
|
||||
return NewUDPTunnel(options...)
|
||||
t = NewUDPTunnel(options...)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
t.SetStats(opts.Stats)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/chain"
|
||||
"github.com/go-gost/core/handler"
|
||||
"github.com/go-gost/core/listener"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
cfg "github.com/go-gost/gost.plus/config"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
"github.com/go-gost/x/handler/forward/remote"
|
||||
@@ -19,6 +21,7 @@ import (
|
||||
"github.com/go-gost/x/listener/rudp"
|
||||
mdx "github.com/go-gost/x/metadata"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/stats"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
@@ -28,6 +31,7 @@ type udpTunnel struct {
|
||||
config *config.Config
|
||||
forward service.Service
|
||||
favorite atomic.Bool
|
||||
stats cfg.ServiceStats
|
||||
|
||||
cclose chan struct{}
|
||||
|
||||
@@ -55,6 +59,9 @@ func NewUDPTunnel(opts ...Option) Tunnel {
|
||||
if options.Name == "" {
|
||||
options.Name = endpoint
|
||||
}
|
||||
if options.CreatedAt.IsZero() {
|
||||
options.CreatedAt = time.Now()
|
||||
}
|
||||
|
||||
s := &udpTunnel{
|
||||
endpoint: endpoint,
|
||||
@@ -151,11 +158,14 @@ func (s *udpTunnel) Run() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
stats := &stats.Stats{}
|
||||
|
||||
cfg := s.config.Services[0]
|
||||
ln := rudp.NewListener(
|
||||
listener.AddrOption(cfg.Addr),
|
||||
listener.ChainOption(ch),
|
||||
listener.LoggerOption(log.WithFields(map[string]any{"kind": "listener", "listener": "rudp"})),
|
||||
listener.StatsOption(stats),
|
||||
)
|
||||
if err = ln.Init(mdx.NewMetadata(cfg.Listener.Metadata)); err != nil {
|
||||
return
|
||||
@@ -175,7 +185,10 @@ func (s *udpTunnel) Run() (err error) {
|
||||
hop.LoggerOption(log.WithFields(map[string]any{"kind": "hop"})),
|
||||
))
|
||||
}
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h, xservice.LoggerOption(log))
|
||||
s.forward = xservice.NewService(s.opts.Name, ln, h,
|
||||
xservice.LoggerOption(log),
|
||||
xservice.StatsOption(stats),
|
||||
)
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -185,6 +198,25 @@ 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) Stats() cfg.ServiceStats {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.stats
|
||||
}
|
||||
|
||||
func (s *udpTunnel) SetStats(stats cfg.ServiceStats) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.stats = stats
|
||||
}
|
||||
|
||||
func (s *udpTunnel) Close() error {
|
||||
defer func() {
|
||||
select {
|
||||
|
||||
@@ -8,19 +8,28 @@ import (
|
||||
"gioui.org/font/opentype"
|
||||
)
|
||||
|
||||
//go:embed NotoSansSC-Regular.ttf
|
||||
var fontNotoRegular []byte
|
||||
var (
|
||||
|
||||
//go:embed NotoSansSC-Bold.ttf
|
||||
var fontNotoBold []byte
|
||||
//go:embed NotoSans-Regular.ttf
|
||||
notoSansRegular []byte
|
||||
//go:embed NotoSans-SemiBold.ttf
|
||||
notoSansSemiBold []byte
|
||||
|
||||
//go:embed NotoSansSC-Regular.ttf
|
||||
notoSansSCRegular []byte
|
||||
//go:embed NotoSansSC-SemiBold.ttf
|
||||
notoSansSCSemiBold []byte
|
||||
)
|
||||
|
||||
var (
|
||||
collection []font.FontFace
|
||||
)
|
||||
|
||||
func init() {
|
||||
register(fontNotoRegular)
|
||||
register(fontNotoBold)
|
||||
register(notoSansRegular)
|
||||
register(notoSansSemiBold)
|
||||
register(notoSansSCRegular)
|
||||
register(notoSansSCSemiBold)
|
||||
}
|
||||
|
||||
func Collection() []font.FontFace {
|
||||
@@ -32,5 +41,5 @@ func register(ttf []byte) {
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to parse font: %v", err))
|
||||
}
|
||||
collection = append(collection, faces[0])
|
||||
collection = append(collection, faces...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package i18n
|
||||
|
||||
var en_US = map[Key]string{
|
||||
Tunnel: "Tunnel",
|
||||
Entrypoint: "Entrypoint",
|
||||
Type: "Type",
|
||||
Name: "Name",
|
||||
Address: "Address",
|
||||
Endpoint: "Endpoint",
|
||||
BasicAuth: "Basic auth",
|
||||
Username: "Username",
|
||||
Password: "Password",
|
||||
DirectoryPath: "Directory path",
|
||||
CustomHostname: "Custom hostname (rewrite HTTP Host header)",
|
||||
Hostname: "Hostname",
|
||||
EnableTLS: "Enalbe TLS",
|
||||
TunnelID: "Tunnel ID",
|
||||
Keepalive: "Keepalive",
|
||||
FileTunnelDesc: "Expose local files to public network",
|
||||
HTTPTunnelDesc: "Expose local HTTP service to public network",
|
||||
TCPTunnelDesc: "Expose local TCP service to public network",
|
||||
UDPTunnelDesc: "Expose local UDP service to public network",
|
||||
TCPEntrypointDesc: "Create an entrypoint to the specified TCP tunnel",
|
||||
UDPEntrypointDesc: "Create an entrypoint to the specified UDP tunnel",
|
||||
Settings: "Settings",
|
||||
Language: "Language",
|
||||
Theme: "Theme",
|
||||
Light: "Light",
|
||||
Dark: "Dark",
|
||||
OK: "OK",
|
||||
Cancel: "Cancel",
|
||||
DeleteTunnel: "Delete tunnel?",
|
||||
DeleteEntrypoint: "Delete entrypoint?",
|
||||
ErrInvalidTunnelID: "invalid tunnel ID, should be a valid UUID",
|
||||
ErrInvalidAddr: "invalid address format, should be [IP]:PORT or [HOST]:PORT",
|
||||
ErrDigitOnly: "Must contain only digits",
|
||||
ErrDirectory: "is not a directory",
|
||||
|
||||
English: "English",
|
||||
Chinese: "Chinese",
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package i18n
|
||||
|
||||
import "sync"
|
||||
|
||||
type Lang struct {
|
||||
Name Key
|
||||
Value string
|
||||
content map[Key]string
|
||||
}
|
||||
|
||||
var langs = []Lang{
|
||||
{
|
||||
Name: English,
|
||||
Value: "en_US",
|
||||
content: en_US,
|
||||
},
|
||||
{
|
||||
Name: Chinese,
|
||||
Value: "zh_CN",
|
||||
content: zh_CN,
|
||||
},
|
||||
}
|
||||
|
||||
func Langs() []Lang {
|
||||
return langs
|
||||
}
|
||||
|
||||
var (
|
||||
currentLang Lang = langs[0]
|
||||
mux sync.RWMutex
|
||||
)
|
||||
|
||||
func Current() Lang {
|
||||
mux.RLock()
|
||||
defer mux.RUnlock()
|
||||
|
||||
return currentLang
|
||||
}
|
||||
|
||||
func Set(lang string) {
|
||||
mux.Lock()
|
||||
defer mux.Unlock()
|
||||
|
||||
for i := range langs {
|
||||
if langs[i].Value == lang {
|
||||
currentLang = langs[i]
|
||||
return
|
||||
}
|
||||
}
|
||||
currentLang = langs[0]
|
||||
}
|
||||
|
||||
const (
|
||||
Tunnel Key = "tunnel"
|
||||
Entrypoint Key = "entrypoint"
|
||||
Type Key = "type"
|
||||
Name Key = "name"
|
||||
Address Key = "address"
|
||||
Endpoint Key = "endpoint"
|
||||
BasicAuth Key = "basicAuth"
|
||||
Username Key = "username"
|
||||
Password Key = "password"
|
||||
DirectoryPath Key = "dirPath"
|
||||
CustomHostname Key = "customHostname"
|
||||
Hostname Key = "hostname"
|
||||
EnableTLS Key = "enableTLS"
|
||||
TunnelID Key = "tunnelID"
|
||||
Keepalive Key = "keepalive"
|
||||
FileTunnelDesc Key = "fileTunnelDesc"
|
||||
HTTPTunnelDesc Key = "httpTunnelDesc"
|
||||
TCPTunnelDesc Key = "tcpTunnelDesc"
|
||||
UDPTunnelDesc Key = "udpTunnelDesc"
|
||||
TCPEntrypointDesc Key = "tcpEntrypointDesc"
|
||||
UDPEntrypointDesc Key = "udpEntrypointDesc"
|
||||
Settings Key = "settings"
|
||||
Language Key = "language"
|
||||
Theme Key = "theme"
|
||||
Light Key = "light"
|
||||
Dark Key = "dark"
|
||||
OK Key = "ok"
|
||||
Cancel Key = "cancel"
|
||||
DeleteTunnel Key = "deleteTunnel"
|
||||
DeleteEntrypoint Key = "deleteEntrypoint"
|
||||
ErrInvalidTunnelID Key = "errInvalidTunnelID"
|
||||
ErrInvalidAddr Key = "errInvalidAddr"
|
||||
ErrDigitOnly Key = "errDigitOnly"
|
||||
ErrDirectory Key = "errDir"
|
||||
|
||||
English Key = "english"
|
||||
Chinese Key = "chinese"
|
||||
)
|
||||
|
||||
type Key string
|
||||
|
||||
func (k Key) Value() string {
|
||||
return get(k)
|
||||
}
|
||||
|
||||
func get(key Key) string {
|
||||
mux.RLock()
|
||||
defer mux.RUnlock()
|
||||
|
||||
if v := currentLang.content[key]; v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
return langs[0].content[key]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package i18n
|
||||
|
||||
var zh_CN = map[Key]string{
|
||||
Tunnel: "隧道",
|
||||
Entrypoint: "入口点",
|
||||
Type: "类型",
|
||||
Name: "名称",
|
||||
Address: "地址",
|
||||
Endpoint: "端点",
|
||||
BasicAuth: "基本认证",
|
||||
Username: "用户名",
|
||||
Password: "密码",
|
||||
DirectoryPath: "文件目录路径",
|
||||
CustomHostname: "自定义主机名(重写HTTP Host头)",
|
||||
Hostname: "主机名",
|
||||
EnableTLS: "开启TLS",
|
||||
TunnelID: "隧道ID",
|
||||
Keepalive: "保持连接",
|
||||
FileTunnelDesc: "将本地文件系统暴露到公网",
|
||||
HTTPTunnelDesc: "将本地的一个HTTP服务暴露到公网",
|
||||
TCPTunnelDesc: "将本地的一个TCP服务暴露到公网",
|
||||
UDPTunnelDesc: "将本地的一个UDP服务暴露到公网",
|
||||
TCPEntrypointDesc: "创建一个指定TCP隧道的入口点",
|
||||
UDPEntrypointDesc: "创建一个指定UDP隧道的入口点",
|
||||
Settings: "设置",
|
||||
Language: "语言",
|
||||
Theme: "主题",
|
||||
Light: "浅色",
|
||||
Dark: "深色",
|
||||
OK: "确认",
|
||||
Cancel: "取消",
|
||||
DeleteTunnel: "删除隧道?",
|
||||
DeleteEntrypoint: "删除入口点?",
|
||||
ErrInvalidTunnelID: "无效的隧道ID, 仅支持合法的UUID格式,例如:6bcb409c-dd0f-4ce7-9869-651c52c09d1c",
|
||||
ErrInvalidAddr: "无效的地址格式,仅支持[IP]:PORT或[HOST]:PORT",
|
||||
ErrDigitOnly: "仅能输入数字",
|
||||
ErrDirectory: "不是一个目录",
|
||||
|
||||
English: "英语",
|
||||
Chinese: "中文",
|
||||
}
|
||||
@@ -25,29 +25,42 @@ 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)
|
||||
IconInfo = mustIcon(icons.ActionInfo)
|
||||
IconAlert = mustIcon(icons.AlertErrorOutline)
|
||||
)
|
||||
|
||||
func mustIcon(data []byte) *widget.Icon {
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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 state.editor.Layout(gtx, 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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package entrypoint
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"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 i18n.Key
|
||||
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: i18n.TCPEntrypointDesc,
|
||||
path: page.PageEntrypointTCP,
|
||||
},
|
||||
{
|
||||
name: "UDP",
|
||||
desc: i18n.UDPEntrypointDesc,
|
||||
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, i18n.Entrypoint.Value())
|
||||
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.UniformInset(16).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.Value()).Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
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/i18n"
|
||||
"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
|
||||
|
||||
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,
|
||||
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: i18n.DeleteEntrypoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.tunnelID.Clear()
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.TunnelID.Value()).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(i18n.ErrInvalidTunnelID.Value())
|
||||
}
|
||||
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, i18n.Name.Value()).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, i18n.Entrypoint.Value()).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(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.entrypoint.SetError(err.Error())
|
||||
} else {
|
||||
p.entrypoint.ClearError()
|
||||
}
|
||||
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
ep.Close()
|
||||
}
|
||||
entrypoint.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *tcpPage) delete() {
|
||||
entrypoint.Delete(p.id)
|
||||
entrypoint.SaveConfig()
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
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/i18n"
|
||||
"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
|
||||
|
||||
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,
|
||||
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: i18n.DeleteEntrypoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.tunnelID.Clear()
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.TunnelID.Value()).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(i18n.ErrInvalidTunnelID.Value())
|
||||
}
|
||||
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, i18n.Name.Value()).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, i18n.Entrypoint.Value()).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(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.entrypoint.SetError(err.Error())
|
||||
} else {
|
||||
p.entrypoint.ClearError()
|
||||
}
|
||||
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
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, i18n.Keepalive.Value()).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 i18n.ErrDigitOnly.Value()
|
||||
}
|
||||
}
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
ep.Close()
|
||||
}
|
||||
entrypoint.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *udpPage) delete() {
|
||||
entrypoint.Delete(p.id)
|
||||
entrypoint.SaveConfig()
|
||||
}
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package page
|
||||
|
||||
type EventID string
|
||||
|
||||
const (
|
||||
EventThemeChanged EventID = "event.theme.changed"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
ID EventID
|
||||
}
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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(),
|
||||
})
|
||||
}
|
||||
|
||||
stats := t.Stats()
|
||||
|
||||
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.UniformInset(16).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, t.Name())
|
||||
label.Font.Weight = font.SemiBold
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
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))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body2(th, fmt.Sprintf("%s: %s", i18n.Type.Value(), strings.ToUpper(t.Type()))).Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if createdAt := t.Options().CreatedAt; !createdAt.IsZero() {
|
||||
v, unit := formatDuration(time.Since(createdAt))
|
||||
return material.Body2(th, fmt.Sprintf("%d%s", v, unit)).Layout(gtx)
|
||||
}
|
||||
return material.Body2(th, "N/A").Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Endpoint.Value(), t.Endpoint())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconActionCode.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
current, unitCurrent := format(int64(stats.CurrentConns), 1000)
|
||||
current = float64(int64(current*10)) / 10
|
||||
|
||||
total, unitTotal := format(int64(stats.TotalConns), 1000)
|
||||
total = float64(int64(total*10)) / 10
|
||||
return material.Body2(th, fmt.Sprintf("%s%s / %s%s",
|
||||
strconv.FormatFloat(current, 'f', -1, 64), strings.ToLower(unitCurrent),
|
||||
strconv.FormatFloat(total, 'f', -1, 64), strings.ToLower(unitTotal))).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
rate := stats.RequestRate
|
||||
rate = float64(int64(rate*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s R/s", strconv.FormatFloat(rate, 'f', -1, 64))).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconNavExpandLess.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
v, unit := format(int64(stats.OutputBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
v, unit := format(int64(stats.OutputRateBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB/s", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconNavExpandMore.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
v, unit := format(int64(stats.InputBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
v, unit := format(int64(stats.InputRateBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB/s", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package list
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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(),
|
||||
})
|
||||
}
|
||||
|
||||
stats := t.Stats()
|
||||
|
||||
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.UniformInset(16).Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, t.Name())
|
||||
label.Font.Weight = font.SemiBold
|
||||
return label.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
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))
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body2(th, fmt.Sprintf("%s: %s", i18n.Type.Value(), strings.ToUpper(t.Type()))).Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if createdAt := t.Options().CreatedAt; !createdAt.IsZero() {
|
||||
v, unit := formatDuration(time.Since(createdAt))
|
||||
return material.Body2(th, fmt.Sprintf("%d%s", v, unit)).Layout(gtx)
|
||||
}
|
||||
return material.Body2(th, "N/A").Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Endpoint.Value(), t.Endpoint())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconActionCode.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
current, unitCurrent := format(int64(stats.CurrentConns), 1000)
|
||||
current = float64(int64(current*10)) / 10
|
||||
|
||||
total, unitTotal := format(int64(stats.TotalConns), 1000)
|
||||
total = float64(int64(total*10)) / 10
|
||||
return material.Body2(th, fmt.Sprintf("%s%s / %s%s",
|
||||
strconv.FormatFloat(current, 'f', -1, 64), strings.ToLower(unitCurrent),
|
||||
strconv.FormatFloat(total, 'f', -1, 64), strings.ToLower(unitTotal))).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
rate := stats.RequestRate
|
||||
rate = float64(int64(rate*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s R/s", strconv.FormatFloat(rate, 'f', -1, 64))).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconNavExpandLess.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
v, unit := format(int64(stats.OutputBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
v, unit := format(int64(stats.OutputRateBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB/s", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 4}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconNavExpandMore.Layout(gtx, th.Fg)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 4}.Layout),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
v, unit := format(int64(stats.InputBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
v, unit := format(int64(stats.InputRateBytes), 1024)
|
||||
v = float64(int64(v*100)) / 100
|
||||
return material.Body2(th, fmt.Sprintf("%s %sB/s", strconv.FormatFloat(v, 'f', -1, 64), unit)).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
units = []string{"", "K", "M", "G", "T", "P", "E"}
|
||||
)
|
||||
|
||||
func format(n int64, scale int64) (v float64, unit string) {
|
||||
var remain float64
|
||||
for i := range units {
|
||||
unit = units[i]
|
||||
|
||||
r := n % scale
|
||||
if n = n / scale; n == 0 {
|
||||
v = float64(r) + remain/math.Pow(float64(scale), float64(i))
|
||||
return
|
||||
}
|
||||
remain += float64(r) * math.Pow(float64(scale), float64(i))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
dunits = []string{"s", "m", "h"}
|
||||
)
|
||||
|
||||
func formatDuration(d time.Duration) (v int64, unit string) {
|
||||
if d.Hours() >= 24 {
|
||||
v = int64(d.Hours() / 24)
|
||||
unit = "d"
|
||||
return
|
||||
}
|
||||
|
||||
var scale int64 = 60
|
||||
n := int64(d.Seconds())
|
||||
for i := range dunits {
|
||||
v = n % scale
|
||||
unit = dunits[i]
|
||||
|
||||
if n = n / scale; n == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package home
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"sync/atomic"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"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,
|
||||
pages: []navPage{
|
||||
{
|
||||
list: list.Tunnel(r),
|
||||
path: page.PageTunnel,
|
||||
},
|
||||
{
|
||||
list: list.Entrypoint(r),
|
||||
path: page.PageEntrypoint,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *homePage) Init(opts ...page.PageOption) {
|
||||
p.nav = ui_widget.NewNav(
|
||||
ui_widget.NewNavButton(i18n.Tunnel),
|
||||
ui_widget.NewNavButton(i18n.Entrypoint),
|
||||
)
|
||||
}
|
||||
|
||||
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.Flexed(1, 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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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 p.wgFile.Layout(gtx, 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 p.wgHTTP.Layout(gtx, 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 p.wgTCP.Layout(gtx, 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 p.wgUDP.Layout(gtx, 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 p.wgEntryPointTCP.Layout(gtx, 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 p.wgEntryPointUDP.Layout(gtx, 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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -1,52 +1,38 @@
|
||||
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 {
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
type T = material.Theme
|
||||
|
||||
func WithPageID(id string) PageOption {
|
||||
return func(opts *PageOptions) {
|
||||
opts.ID = id
|
||||
}
|
||||
@@ -54,75 +40,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
|
||||
}
|
||||
|
||||
@@ -1,109 +1,225 @@
|
||||
package page
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/gost.plus/ui/theme"
|
||||
ui_widget "github.com/go-gost/gost.plus/ui/widget"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
const (
|
||||
MaxWidth = 800
|
||||
)
|
||||
|
||||
type Route struct {
|
||||
Path string
|
||||
Path PagePath
|
||||
ID string
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
modal *component.ModalLayer
|
||||
bar *component.AppBar
|
||||
|
||||
pages map[string]Page
|
||||
w *app.Window
|
||||
pages map[PagePath]Page
|
||||
stack routeStack
|
||||
current Route
|
||||
*material.Theme
|
||||
modal *component.ModalLayer
|
||||
notification *ui_widget.Notification
|
||||
events chan Event
|
||||
}
|
||||
|
||||
func NewRouter() *Router {
|
||||
modal := component.NewModal()
|
||||
bar := component.NewAppBar(modal)
|
||||
|
||||
func NewRouter(w *app.Window, th *T) *Router {
|
||||
r := &Router{
|
||||
modal: modal,
|
||||
bar: bar,
|
||||
pages: make(map[string]Page),
|
||||
w: w,
|
||||
pages: make(map[PagePath]Page),
|
||||
Theme: th,
|
||||
modal: component.NewModal(),
|
||||
notification: ui_widget.NewNotification(3*time.Second, func() {
|
||||
w.Invalidate()
|
||||
}),
|
||||
events: make(chan Event, 16),
|
||||
}
|
||||
|
||||
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))
|
||||
logger.Default().WithFields(map[string]any{
|
||||
"kind": "router",
|
||||
"route.id": route.ID,
|
||||
}).Debugf("go to %s", route.Path)
|
||||
}
|
||||
|
||||
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})
|
||||
func (r *Router) Back() {
|
||||
r.stack.Pop()
|
||||
route := r.stack.Peek()
|
||||
|
||||
page := r.pages[route.Path]
|
||||
if page == nil {
|
||||
return
|
||||
}
|
||||
r.current = route
|
||||
|
||||
logger.Default().WithFields(map[string]any{
|
||||
"kind": "router",
|
||||
"route.id": route.ID,
|
||||
}).Debugf("back to %s", route.Path)
|
||||
}
|
||||
|
||||
func (r *Router) Layout(gtx C) D {
|
||||
if r.stack.Peek().Path != PageHome {
|
||||
event.Op(gtx.Ops, r.w)
|
||||
for {
|
||||
ev, ok := gtx.Event(
|
||||
key.Filter{Name: key.NameBack},
|
||||
key.Filter{Name: key.NameEscape},
|
||||
)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
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})
|
||||
switch ev := ev.(type) {
|
||||
case key.Event:
|
||||
if ev.State == key.Press {
|
||||
r.Back()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
defer r.modal.Layout(gtx, th)
|
||||
r.Theme.Palette = theme.Current().Material
|
||||
|
||||
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)
|
||||
}),
|
||||
defer r.modal.Layout(gtx, r.Theme)
|
||||
|
||||
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(MaxWidth)
|
||||
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 layout.Stack{
|
||||
Alignment: layout.N,
|
||||
}.Layout(gtx,
|
||||
layout.Expanded(page.Layout),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 16,
|
||||
Bottom: 16,
|
||||
Right: gtx.Metric.PxToDp(gtx.Constraints.Max.X) / 5,
|
||||
Left: gtx.Metric.PxToDp(gtx.Constraints.Max.X) / 5,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return r.notification.Layout(gtx, r.Theme)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Router) ShowModal(gtx layout.Context, w func(gtx C, th *material.Theme) D) {
|
||||
r.modal.Widget = func(gtx C, th *material.Theme, anim *component.VisibilityAnimation) D {
|
||||
if gtx.Constraints.Max.X > gtx.Dp(MaxWidth) {
|
||||
gtx.Constraints.Max.X = gtx.Dp(MaxWidth)
|
||||
}
|
||||
gtx.Constraints.Max.X = gtx.Constraints.Max.X * 3 / 4
|
||||
|
||||
var clk widget.Clickable
|
||||
return clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return w(gtx, th)
|
||||
})
|
||||
}
|
||||
r.modal.Appear(gtx.Now)
|
||||
}
|
||||
|
||||
func (r *Router) HideModal(gtx C) {
|
||||
r.modal.Disappear(gtx.Now)
|
||||
}
|
||||
|
||||
func (r *Router) Notify(message ui_widget.Message) {
|
||||
r.notification.Show(message)
|
||||
}
|
||||
|
||||
func (r *Router) Emit(event Event) {
|
||||
select {
|
||||
case r.events <- event:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Event() <-chan Event {
|
||||
return r.events
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,286 @@
|
||||
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/i18n"
|
||||
"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
|
||||
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,
|
||||
menu: ui_widget.Menu{
|
||||
List: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
},
|
||||
},
|
||||
list: widget.List{
|
||||
List: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
},
|
||||
},
|
||||
lang: ui_widget.Selector{Title: i18n.Language},
|
||||
theme: ui_widget.Selector{Title: i18n.Theme},
|
||||
}
|
||||
}
|
||||
|
||||
func (p *settingsPage) Init(opts ...page.PageOption) {
|
||||
settings := config.Get().Settings
|
||||
if settings == nil {
|
||||
settings = &config.Settings{}
|
||||
}
|
||||
if settings.Lang == "" {
|
||||
settings.Lang = i18n.Current().Value
|
||||
}
|
||||
if settings.Theme == "" {
|
||||
settings.Theme = theme.Light
|
||||
}
|
||||
|
||||
p.lang.Clear()
|
||||
p.lang.Select(ui_widget.SelectorItem{
|
||||
Name: i18n.Current().Name,
|
||||
Value: i18n.Current().Value,
|
||||
})
|
||||
|
||||
p.theme.Clear()
|
||||
if settings.Theme == theme.Light {
|
||||
p.theme.Select(ui_widget.SelectorItem{
|
||||
Name: i18n.Light,
|
||||
Value: settings.Theme,
|
||||
})
|
||||
} else {
|
||||
p.theme.Select(ui_widget.SelectorItem{
|
||||
Name: i18n.Dark,
|
||||
Value: settings.Theme,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (p *settingsPage) Layout(gtx layout.Context) layout.Dimensions {
|
||||
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 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, i18n.Settings.Value())
|
||||
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 layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(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 {
|
||||
gtx.Constraints.Max.X = gtx.Dp(60)
|
||||
return icons.IconApp.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, "GOST+")
|
||||
label.Font.Weight = font.SemiBold
|
||||
return layout.Center.Layout(gtx, label.Layout)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Center.Layout(gtx, material.Body1(th, version.Version).Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 32}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) 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.UniformInset(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 {
|
||||
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) {
|
||||
var items []ui_widget.MenuItem
|
||||
for _, lang := range i18n.Langs() {
|
||||
items = append(items, ui_widget.MenuItem{
|
||||
Key: lang.Name,
|
||||
Value: lang.Value,
|
||||
})
|
||||
}
|
||||
|
||||
var found bool
|
||||
for i := range items {
|
||||
if found = p.lang.Any(items[i].Key.Value()); found {
|
||||
items[i].Selected = found
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
items[0].Selected = true
|
||||
}
|
||||
|
||||
p.menu.Title = i18n.Language
|
||||
p.menu.Items = items
|
||||
p.menu.Selected = func(index int) {
|
||||
p.lang.Clear()
|
||||
p.lang.Select(ui_widget.SelectorItem{
|
||||
Name: p.menu.Items[index].Key,
|
||||
Value: p.menu.Items[index].Value,
|
||||
})
|
||||
p.router.HideModal(gtx)
|
||||
|
||||
cfg := config.Get()
|
||||
if cfg.Settings == nil {
|
||||
cfg.Settings = &config.Settings{}
|
||||
}
|
||||
cfg.Settings.Lang = p.lang.Item().Value
|
||||
|
||||
config.Set(cfg)
|
||||
cfg.Write()
|
||||
|
||||
i18n.Set(cfg.Settings.Lang)
|
||||
}
|
||||
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.menu.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *settingsPage) showThemeMenu(gtx layout.Context) {
|
||||
items := []ui_widget.MenuItem{
|
||||
{Key: i18n.Light, Value: theme.Light},
|
||||
{Key: i18n.Dark, Value: theme.Dark},
|
||||
}
|
||||
|
||||
var found bool
|
||||
for i := range items {
|
||||
if found = p.theme.Any(items[i].Key.Value()); found {
|
||||
items[i].Selected = found
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
items[0].Selected = true
|
||||
}
|
||||
|
||||
p.menu.Title = i18n.Theme
|
||||
p.menu.Items = items
|
||||
p.menu.Selected = func(index int) {
|
||||
p.theme.Clear()
|
||||
p.theme.Select(ui_widget.SelectorItem{
|
||||
Name: p.menu.Items[index].Key,
|
||||
Value: p.menu.Items[index].Value,
|
||||
})
|
||||
p.router.HideModal(gtx)
|
||||
|
||||
cfg := config.Get()
|
||||
if cfg.Settings == nil {
|
||||
cfg.Settings = &config.Settings{}
|
||||
}
|
||||
cfg.Settings.Theme = p.theme.Item().Value
|
||||
|
||||
config.Set(cfg)
|
||||
cfg.Write()
|
||||
|
||||
switch cfg.Settings.Theme {
|
||||
case theme.Dark:
|
||||
theme.UseDark()
|
||||
default:
|
||||
theme.UseLight()
|
||||
}
|
||||
p.router.Emit(page.Event{ID: page.EventThemeChanged})
|
||||
}
|
||||
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.menu.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -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 state.editor.Layout(gtx, 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))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,570 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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
|
||||
|
||||
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
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
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,
|
||||
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: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).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
|
||||
|
||||
if p.wgID.Clicked(gtx) {
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if tun == nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Name.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
layout.Rigid(material.Body1(th, i18n.Endpoint.Value()).Layout),
|
||||
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 %s", dir, i18n.ErrDirectory.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.endpoint.SetError(err.Error())
|
||||
} else {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.DirectoryPath.Value())
|
||||
}),
|
||||
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, i18n.BasicAuth.Value()).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, i18n.Username.Value())
|
||||
}),
|
||||
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, i18n.Password.Value())
|
||||
})
|
||||
}),
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
}
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *filePage) delete() {
|
||||
tunnel.Delete(p.id)
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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
|
||||
|
||||
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
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
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,
|
||||
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: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).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
|
||||
|
||||
if p.wgID.Clicked(gtx) {
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if tun == nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Name.Value()).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, i18n.Endpoint.Value()).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(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.endpoint.SetError(err.Error())
|
||||
} else {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
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, i18n.BasicAuth.Value()).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, i18n.Username.Value())
|
||||
}),
|
||||
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, i18n.Password.Value())
|
||||
})
|
||||
}),
|
||||
|
||||
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, i18n.CustomHostname.Value()).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, i18n.Hostname.Value())
|
||||
})
|
||||
}),
|
||||
|
||||
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, i18n.EnableTLS.Value()).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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
}
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *httpPage) delete() {
|
||||
tunnel.Delete(p.id)
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package tunnel
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"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 i18n.Key
|
||||
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: i18n.FileTunnelDesc,
|
||||
path: page.PageTunnelFile,
|
||||
},
|
||||
{
|
||||
name: "HTTP",
|
||||
desc: i18n.HTTPTunnelDesc,
|
||||
path: page.PageTunnelHTTP,
|
||||
},
|
||||
{
|
||||
name: "TCP",
|
||||
desc: i18n.TCPTunnelDesc,
|
||||
path: page.PageTunnelTCP,
|
||||
},
|
||||
{
|
||||
name: "UDP",
|
||||
desc: i18n.UDPTunnelDesc,
|
||||
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, i18n.Tunnel.Value())
|
||||
// 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.UniformInset(16).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.Value()).Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return icons.IconForward.Layout(gtx, color.NRGBA(colornames.Grey500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
package tcp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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
|
||||
|
||||
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
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
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,
|
||||
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: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).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
|
||||
|
||||
if p.wgID.Clicked(gtx) {
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if tun == nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Name.Value()).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, i18n.Endpoint.Value()).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(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.endpoint.SetError(err.Error())
|
||||
} else {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
}
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *tcpPage) delete() {
|
||||
tunnel.Delete(p.id)
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
@@ -0,0 +1,464 @@
|
||||
package udp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/i18n"
|
||||
"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
|
||||
|
||||
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
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
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,
|
||||
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: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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.router.HideModal(gtx)
|
||||
}
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
})
|
||||
}
|
||||
|
||||
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{
|
||||
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.UniformInset(16).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
|
||||
|
||||
if p.wgID.Clicked(gtx) {
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if tun == nil {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
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 {
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Name.Value()).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, i18n.Endpoint.Value()).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(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
p.endpoint.SetError(err.Error())
|
||||
} else {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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()
|
||||
p.router.Notify(ui_widget.Message{
|
||||
Type: ui_widget.Error,
|
||||
Content: err.Error(),
|
||||
})
|
||||
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),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
}
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
|
||||
func (p *udpPage) delete() {
|
||||
tunnel.Delete(p.id)
|
||||
tunnel.SaveConfig()
|
||||
}
|
||||
@@ -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")
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
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
|
||||
ItemBg color.NRGBA
|
||||
NotificationBg 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.BlueGrey50),
|
||||
NavButtonContrastBg: color.NRGBA(colornames.BlueGrey100),
|
||||
ItemBg: color.NRGBA(colornames.Grey300),
|
||||
NotificationBg: color.NRGBA(colornames.Grey200),
|
||||
},
|
||||
}
|
||||
|
||||
dark = Theme{
|
||||
Palette: Palette{
|
||||
Material: material.Palette{
|
||||
Fg: color.NRGBA(colornames.White),
|
||||
Bg: color.NRGBA(colornames.Grey900),
|
||||
ContrastBg: color.NRGBA(colornames.Green500),
|
||||
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),
|
||||
ItemBg: color.NRGBA(colornames.Grey600),
|
||||
NotificationBg: color.NRGBA(colornames.Grey700),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -1,36 +1,88 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/app"
|
||||
"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/fonts"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"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
|
||||
w *app.Window
|
||||
router *page.Router
|
||||
}
|
||||
|
||||
func NewUI() *UI {
|
||||
if settings := config.Get().Settings; settings != nil {
|
||||
switch settings.Theme {
|
||||
case theme.Dark:
|
||||
theme.UseDark()
|
||||
default:
|
||||
theme.UseLight()
|
||||
}
|
||||
i18n.Set(settings.Lang)
|
||||
}
|
||||
|
||||
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)
|
||||
th.Palette = theme.Current().Material
|
||||
|
||||
ui := &UI{
|
||||
th: th,
|
||||
router: page.NewRouter(),
|
||||
w := &app.Window{}
|
||||
w.Option(
|
||||
app.Title("GOST"),
|
||||
app.MinSize(800, 600),
|
||||
app.StatusColor(th.Bg),
|
||||
)
|
||||
|
||||
router := page.NewRouter(w, 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{
|
||||
w: w,
|
||||
router: router,
|
||||
}
|
||||
|
||||
return ui
|
||||
}
|
||||
|
||||
func (ui *UI) Layout(gtx C) D {
|
||||
return ui.router.Layout(gtx, ui.th)
|
||||
return ui.router.Layout(gtx)
|
||||
}
|
||||
|
||||
func (ui *UI) Window() *app.Window {
|
||||
return ui.w
|
||||
}
|
||||
|
||||
func (ui *UI) Router() *page.Router {
|
||||
return ui.router
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
Title i18n.Key
|
||||
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 {
|
||||
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.Value()).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: 18,
|
||||
Button: &p.btnCancel,
|
||||
}.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, i18n.Cancel.Value())
|
||||
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: 18,
|
||||
Button: &p.btnOK,
|
||||
}.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, i18n.OK.Value())
|
||||
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)
|
||||
})
|
||||
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"github.com/go-gost/gost.plus/ui/icons"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
List layout.List
|
||||
Items []MenuItem
|
||||
Title i18n.Key
|
||||
Selected func(index int)
|
||||
btnAdd widget.Clickable
|
||||
ShowAdd bool
|
||||
Multiple bool
|
||||
}
|
||||
|
||||
func (p *Menu) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
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.Value()).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 i18n.Key
|
||||
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: 12,
|
||||
Bottom: 12,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.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.Value()).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{}
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
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/i18n"
|
||||
"github.com/go-gost/gost.plus/ui/theme"
|
||||
)
|
||||
|
||||
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
|
||||
} else {
|
||||
btn.background = theme.Current().NavButtonBg
|
||||
}
|
||||
|
||||
return layout.UniformInset(8).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return btn.Layout(gtx, th)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type NavButton struct {
|
||||
btn widget.Clickable
|
||||
cornerRadius unit.Dp
|
||||
background color.NRGBA
|
||||
text i18n.Key
|
||||
}
|
||||
|
||||
func NewNavButton(text i18n.Key) *NavButton {
|
||||
return &NavButton{
|
||||
cornerRadius: 20,
|
||||
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: theme.Current().NavButtonContrastBg,
|
||||
CornerRadius: btn.cornerRadius,
|
||||
}.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 {
|
||||
label := material.Body1(th, btn.text.Value())
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost.plus/ui/icons"
|
||||
"github.com/go-gost/gost.plus/ui/theme"
|
||||
"golang.org/x/exp/shiny/materialdesign/colornames"
|
||||
)
|
||||
|
||||
const (
|
||||
Success string = "success"
|
||||
Info string = "info"
|
||||
Warn string = "warn"
|
||||
Error string = "error"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Type string
|
||||
Content string
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
show atomic.Bool
|
||||
messages chan Message
|
||||
current Message
|
||||
duration time.Duration
|
||||
callback func()
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewNotification(d time.Duration, callback func()) *Notification {
|
||||
p := &Notification{
|
||||
messages: make(chan Message, 16),
|
||||
duration: d,
|
||||
callback: callback,
|
||||
}
|
||||
go p.run()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Notification) Layout(gtx C, th *T) D {
|
||||
if !p.show.Load() {
|
||||
return D{}
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
message := p.current
|
||||
p.mu.RUnlock()
|
||||
|
||||
return component.SurfaceStyle{
|
||||
Theme: th,
|
||||
ShadowStyle: component.ShadowStyle{
|
||||
CornerRadius: 8,
|
||||
},
|
||||
Fill: theme.Current().NotificationBg,
|
||||
}.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,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
switch message.Type {
|
||||
case Success:
|
||||
return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
case Warn:
|
||||
return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Orange500))
|
||||
case Error:
|
||||
return icons.IconAlert.Layout(gtx, color.NRGBA(colornames.Red500))
|
||||
case Info:
|
||||
fallthrough
|
||||
default:
|
||||
return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return material.Body2(th, message.Content).Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Notification) Show(message Message) {
|
||||
select {
|
||||
case p.messages <- message:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Notification) run() {
|
||||
for m := range p.messages {
|
||||
p.mu.Lock()
|
||||
p.current = m
|
||||
p.mu.Unlock()
|
||||
|
||||
p.show.Store(true)
|
||||
if p.callback != nil {
|
||||
p.callback()
|
||||
}
|
||||
|
||||
<-time.After(p.duration)
|
||||
|
||||
p.show.Store(false)
|
||||
if p.callback != nil {
|
||||
p.callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"gioui.org/x/outlay"
|
||||
"github.com/go-gost/gost.plus/ui/i18n"
|
||||
"github.com/go-gost/gost.plus/ui/icons"
|
||||
"github.com/go-gost/gost.plus/ui/theme"
|
||||
)
|
||||
|
||||
type SelectorItem struct {
|
||||
Name i18n.Key
|
||||
Value string
|
||||
}
|
||||
|
||||
type Selector struct {
|
||||
Title i18n.Key
|
||||
items []SelectorItem
|
||||
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: 4,
|
||||
Bottom: 4,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(material.Body1(th, p.Title.Value()).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 {
|
||||
var values []string
|
||||
for _, item := range p.items {
|
||||
if item.Value == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
value := item.Name.Value()
|
||||
if value == "" {
|
||||
value = item.Value
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
|
||||
return outlay.FlowWrap{
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx, len(values), func(gtx layout.Context, i int) layout.Dimensions {
|
||||
return layout.UniformInset(4).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return component.SurfaceStyle{
|
||||
Theme: th,
|
||||
ShadowStyle: component.ShadowStyle{CornerRadius: 14},
|
||||
Fill: theme.Current().ItemBg,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 4,
|
||||
Bottom: 4,
|
||||
Left: 10,
|
||||
Right: 10,
|
||||
}.Layout(gtx, material.Body2(th, values[i]).Layout)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if len(p.items) > 0 {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
return layout.Inset{
|
||||
Top: 4,
|
||||
Bottom: 5,
|
||||
}.Layout(gtx, 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 ...SelectorItem) {
|
||||
for _, item := range items {
|
||||
if item.Name == "" {
|
||||
continue
|
||||
}
|
||||
p.items = append(p.items, item)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Selector) Unselect(name string) {
|
||||
for i := range p.items {
|
||||
if p.items[i].Name.Value() == name {
|
||||
p.items = append(p.items[:i], p.items[i+1:]...)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Selector) Any(names ...string) bool {
|
||||
for _, name := range names {
|
||||
if p.contains(name) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Selector) Item() SelectorItem {
|
||||
if len(p.items) == 0 {
|
||||
return SelectorItem{}
|
||||
}
|
||||
return p.items[0]
|
||||
}
|
||||
|
||||
func (p *Selector) Items() []SelectorItem {
|
||||
return p.items
|
||||
}
|
||||
|
||||
func (p *Selector) contains(name string) bool {
|
||||
if name == "" && len(p.items) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
for i := range p.items {
|
||||
if p.items[i].Name.Value() == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Selector) Clear() {
|
||||
p.items = nil
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget/material"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
type T = material.Theme
|
||||
|
||||
type Widget interface {
|
||||
Layout(gtx C, th *T) D
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
package version
|
||||
|
||||
var Version = "0.2.0"
|
||||
var Version = "0.3.2"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package winres
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
|
||||