mirror of
https://github.com/go-gost/gost-plus.git
synced 2024-08-11 17:43:16 +00:00
added service stats
This commit is contained in:
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 33 KiB |
@@ -103,6 +103,7 @@ type Tunnel struct {
|
||||
Keepalive bool `yaml:",omitempty"`
|
||||
TTL int `yaml:"ttl,omitempty"`
|
||||
|
||||
Stats ServiceStats
|
||||
Favorite bool
|
||||
Closed bool
|
||||
CreatedAt time.Time
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -3,19 +3,63 @@ module github.com/go-gost/gost.plus
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
gioui.org v0.5.0
|
||||
gioui.org/x v0.5.0
|
||||
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/natefinch/lumberjack.v2 v2.2.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect
|
||||
gioui.org/shader v1.0.8 // indirect
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-gost/gosocks5 v0.4.0 // indirect
|
||||
github.com/go-gost/plugin v0.0.0-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.1.1 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/gorilla/websocket v1.5.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/prometheus/client_golang v1.17.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/rs/xid v1.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/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-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/image v0.14.0 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect
|
||||
google.golang.org/grpc v1.59.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,31 +1,166 @@
|
||||
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.5.0 h1:07g7/LY1MFuTncfO4A5DIKMMsQV6PkPHyx0MhDqgmYY=
|
||||
gioui.org v0.5.0/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.5.0 h1:NVKTn5AZuYhkAnF7MYcy1dIes36+U1N4gUTsgBhfr4A=
|
||||
gioui.org/x v0.5.0/go.mod h1:X4UBhvanAN+8S16L3K6jDMrVo7Dii7NptgBpOLBD7E4=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372 h1:FQivqchis6bE2/9uF70M2gmmLpe82esEm2QadL0TEJo=
|
||||
github.com/go-text/typesetting v0.0.0-20230803102845-24e03d8b5372/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
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=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/go-gost/core v0.0.0-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-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-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.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.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=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE=
|
||||
github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
|
||||
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM=
|
||||
github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.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=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
|
||||
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
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/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc h1:OG+uKOKt/BW+ydf/M7gym7ONo8U+dyIlLazys3du298=
|
||||
golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o=
|
||||
golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4=
|
||||
golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE=
|
||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc=
|
||||
google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk=
|
||||
google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
_ "gioui.org/app/permission/storage"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
"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"
|
||||
@@ -20,11 +23,10 @@ func main() {
|
||||
Init()
|
||||
|
||||
go func() {
|
||||
w := app.NewWindow(
|
||||
app.Title("GOST+"),
|
||||
app.MinSize(800, 600),
|
||||
)
|
||||
err := run(w)
|
||||
var w app.Window
|
||||
w.Option(app.Title("GOST+"))
|
||||
w.Option(app.MinSize(800, 600))
|
||||
err := run(&w)
|
||||
if err != nil {
|
||||
logger.Default().Fatal(err)
|
||||
}
|
||||
@@ -34,20 +36,26 @@ func main() {
|
||||
}
|
||||
|
||||
func run(w *app.Window) error {
|
||||
go func() {
|
||||
for e := range runner.Event() {
|
||||
if e.TaskID == runner.TaskUpdateStats {
|
||||
w.Invalidate()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ui := ui.NewUI()
|
||||
var ops op.Ops
|
||||
for {
|
||||
switch e := w.NextEvent().(type) {
|
||||
switch e := w.Event().(type) {
|
||||
case app.DestroyEvent:
|
||||
tunnel.SaveConfig()
|
||||
entrypoint.SaveConfig()
|
||||
return e.Err
|
||||
case app.FrameEvent:
|
||||
gtx := app.NewContext(&ops, e)
|
||||
ui.Layout(gtx)
|
||||
e.Frame(gtx.Ops)
|
||||
case key.Event:
|
||||
if e.Name == key.NameBack {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,4 +64,10 @@ 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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,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/gost.plus/tunnel"
|
||||
"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/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() {
|
||||
@@ -196,6 +209,18 @@ func (s *tcpEntryPoint) Status() *xservice.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,6 +13,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"
|
||||
"github.com/go-gost/gost.plus/tunnel"
|
||||
"github.com/go-gost/x/config"
|
||||
chain_parser "github.com/go-gost/x/config/parsing/chain"
|
||||
@@ -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() {
|
||||
@@ -201,6 +213,18 @@ func (s *udpEntryPoint) Status() *xservice.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 {
|
||||
|
||||
+33
-2
@@ -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())
|
||||
}
|
||||
|
||||
@@ -234,6 +253,18 @@ func (s *fileTunnel) Status() *xservice.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 {
|
||||
|
||||
+26
-1
@@ -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,
|
||||
@@ -169,11 +176,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
|
||||
@@ -209,7 +219,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() {
|
||||
@@ -226,6 +239,18 @@ func (s *httpTunnel) Status() *xservice.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 {
|
||||
|
||||
+26
-1
@@ -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() {
|
||||
@@ -192,6 +205,18 @@ func (s *tcpTunnel) Status() *xservice.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 {
|
||||
|
||||
+25
-5
@@ -3,6 +3,7 @@ package tunnel
|
||||
import (
|
||||
"errors"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/gost.plus/config"
|
||||
@@ -39,6 +40,8 @@ type Options struct {
|
||||
EnableTLS bool
|
||||
Keepalive bool
|
||||
TTL int
|
||||
CreatedAt time.Time
|
||||
Stats config.ServiceStats
|
||||
}
|
||||
|
||||
type Option func(opts *Options)
|
||||
@@ -97,6 +100,12 @@ func TTLOption(ttl int) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func CreatedAtOption(createdAt time.Time) Option {
|
||||
return func(opts *Options) {
|
||||
opts.CreatedAt = createdAt
|
||||
}
|
||||
}
|
||||
|
||||
type ServiceStatus interface {
|
||||
Status() *xservice.Status
|
||||
}
|
||||
@@ -110,6 +119,8 @@ type Tunnel interface {
|
||||
Options() Options
|
||||
Run() error
|
||||
Status() *xservice.Status
|
||||
Stats() config.ServiceStats
|
||||
SetStats(stats config.ServiceStats)
|
||||
Favorite(b bool)
|
||||
IsFavorite() bool
|
||||
Close() error
|
||||
@@ -234,6 +245,8 @@ func LoadConfig() {
|
||||
Username: cfg.Username,
|
||||
Password: cfg.Password,
|
||||
EnableTLS: cfg.EnableTLS,
|
||||
CreatedAt: cfg.CreatedAt,
|
||||
Stats: cfg.Stats,
|
||||
})
|
||||
if tun == nil {
|
||||
continue
|
||||
@@ -273,6 +286,8 @@ func SaveConfig() error {
|
||||
EnableTLS: opts.EnableTLS,
|
||||
Favorite: tun.IsFavorite(),
|
||||
Closed: tun.IsClosed(),
|
||||
CreatedAt: opts.CreatedAt,
|
||||
Stats: tun.Stats(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -285,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),
|
||||
@@ -294,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
|
||||
}
|
||||
|
||||
+26
-1
@@ -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() {
|
||||
@@ -192,6 +205,18 @@ func (s *udpTunnel) Status() *xservice.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 {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,36 @@
|
||||
package fonts
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/font/opentype"
|
||||
)
|
||||
|
||||
//go:embed NotoSansSC-Regular.ttf
|
||||
var fontNotoRegular []byte
|
||||
|
||||
//go:embed NotoSansSC-Bold.ttf
|
||||
var fontNotoBold []byte
|
||||
|
||||
var (
|
||||
collection []font.FontFace
|
||||
)
|
||||
|
||||
func init() {
|
||||
register(fontNotoRegular)
|
||||
register(fontNotoBold)
|
||||
}
|
||||
|
||||
func Collection() []font.FontFace {
|
||||
return collection
|
||||
}
|
||||
|
||||
func register(ttf []byte) {
|
||||
faces, err := opentype.ParseCollection(ttf)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to parse font: %v", err))
|
||||
}
|
||||
collection = append(collection, faces[0])
|
||||
}
|
||||
+2
-2
@@ -93,10 +93,10 @@ const (
|
||||
type Key string
|
||||
|
||||
func (k Key) Value() string {
|
||||
return Get(k)
|
||||
return get(k)
|
||||
}
|
||||
|
||||
func Get(key Key) string {
|
||||
func get(key Key) string {
|
||||
mux.RLock()
|
||||
defer mux.RUnlock()
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ var (
|
||||
IconActionCode = mustIcon(icons.ActionCode)
|
||||
IconActionUpdate = mustIcon(icons.ActionUpdate)
|
||||
IconActionHourGlassEmpty = mustIcon(icons.ActionHourglassEmpty)
|
||||
IconInfo = mustIcon(icons.ActionInfo)
|
||||
)
|
||||
|
||||
func mustIcon(data []byte) *widget.Icon {
|
||||
|
||||
@@ -87,7 +87,7 @@ func (p *entrypointPage) Layout(gtx C) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
title := material.H6(th, i18n.Get(i18n.Entrypoint))
|
||||
title := material.H6(th, i18n.Entrypoint.Value())
|
||||
return title.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
|
||||
@@ -27,7 +27,6 @@ type D = layout.Dimensions
|
||||
|
||||
type tcpPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -51,7 +50,6 @@ type tcpPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &tcpPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -72,7 +70,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteEntrypoint),
|
||||
Title: i18n.DeleteEntrypoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -91,6 +89,7 @@ func (p *tcpPage) Init(opts ...page.PageOption) {
|
||||
p.edit = true
|
||||
}
|
||||
|
||||
p.tunnelID.Clear()
|
||||
p.name.Clear()
|
||||
p.entrypoint.Clear()
|
||||
|
||||
@@ -126,18 +125,15 @@ func (p *tcpPage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -273,7 +269,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.TunnelID)).Layout(gtx)
|
||||
return material.Body1(th, i18n.TunnelID.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -282,7 +278,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := uuid.Parse(tid); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidTunnelID))
|
||||
return fmt.Errorf(i18n.ErrInvalidTunnelID.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -299,7 +295,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Name.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "")
|
||||
@@ -307,7 +303,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Entrypoint)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Entrypoint.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -316,7 +312,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidAddr))
|
||||
return fmt.Errorf(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -325,7 +321,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.entrypoint.ClearError()
|
||||
}
|
||||
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Get(i18n.Address))
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
)
|
||||
@@ -390,6 +386,7 @@ func (p *tcpPage) onoff() {
|
||||
tunnel.IDOption(opts.ID),
|
||||
tunnel.NameOption(opts.Name),
|
||||
tunnel.EndpointOption(opts.Endpoint),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
ep.Close()
|
||||
|
||||
@@ -29,7 +29,6 @@ type D = layout.Dimensions
|
||||
|
||||
type udpPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -56,7 +55,6 @@ type udpPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &udpPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -85,7 +83,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteEntrypoint),
|
||||
Title: i18n.DeleteEntrypoint,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -104,6 +102,7 @@ func (p *udpPage) Init(opts ...page.PageOption) {
|
||||
p.edit = true
|
||||
}
|
||||
|
||||
p.tunnelID.Clear()
|
||||
p.name.Clear()
|
||||
p.entrypoint.Clear()
|
||||
|
||||
@@ -144,18 +143,15 @@ func (p *udpPage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -291,7 +287,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.TunnelID)).Layout(gtx)
|
||||
return material.Body1(th, i18n.TunnelID.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -300,7 +296,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := uuid.Parse(tid); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidTunnelID))
|
||||
return fmt.Errorf(i18n.ErrInvalidTunnelID.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -317,7 +313,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Name.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.name.Layout(gtx, th, "")
|
||||
@@ -325,7 +321,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Entrypoint)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Entrypoint.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -334,7 +330,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidAddr))
|
||||
return fmt.Errorf(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -343,7 +339,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.entrypoint.ClearError()
|
||||
}
|
||||
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Get(i18n.Address))
|
||||
return p.entrypoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
@@ -355,7 +351,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, i18n.Get(i18n.Keepalive)).Layout),
|
||||
layout.Flexed(1, material.Body1(th, i18n.Keepalive.Value()).Layout),
|
||||
layout.Rigid(material.Switch(th, &p.keepalive, "keepalive").Layout),
|
||||
)
|
||||
})
|
||||
@@ -369,7 +365,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
if err := func() string {
|
||||
for _, r := range p.ttl.Text() {
|
||||
if !unicode.IsDigit(r) {
|
||||
return i18n.Get(i18n.ErrDigitOnly)
|
||||
return i18n.ErrDigitOnly.Value()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -453,6 +449,7 @@ func (p *udpPage) onoff() {
|
||||
tunnel.EndpointOption(opts.Endpoint),
|
||||
tunnel.KeepaliveOption(opts.Keepalive),
|
||||
tunnel.TTLOption(opts.TTL),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
ep.Close()
|
||||
|
||||
+110
-24
@@ -3,7 +3,9 @@ package list
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/layout"
|
||||
@@ -74,6 +76,8 @@ func (l *entrypointList) Layout(gtx C, th *material.Theme) D {
|
||||
})
|
||||
}
|
||||
|
||||
stats := t.Stats()
|
||||
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
@@ -87,40 +91,122 @@ func (l *entrypointList) Layout(gtx C, th *material.Theme) D {
|
||||
}.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,
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, t.ID())
|
||||
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{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Type), strings.ToUpper(t.Type()))).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Name), t.Name())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Endpoint), t.Endpoint())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Entrypoint), t.Entrypoint())).Layout),
|
||||
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{Width: 8}.Layout),
|
||||
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 {
|
||||
gtx.Constraints.Min.X = gtx.Dp(12)
|
||||
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
|
||||
|
||||
c := colornames.GreenA700
|
||||
if t.Err() != nil {
|
||||
c = colornames.Red600
|
||||
}
|
||||
if t.IsClosed() {
|
||||
c = colornames.Grey600
|
||||
}
|
||||
return icons.IconCircle.Layout(gtx, color.NRGBA(c))
|
||||
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)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
+154
-24
@@ -3,7 +3,10 @@ package list
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/layout"
|
||||
@@ -78,6 +81,8 @@ func (l *tunnelList) Layout(gtx C, th *material.Theme) D {
|
||||
})
|
||||
}
|
||||
|
||||
stats := t.Stats()
|
||||
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
@@ -91,40 +96,122 @@ func (l *tunnelList) Layout(gtx C, th *material.Theme) D {
|
||||
}.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,
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
Alignment: layout.Middle,
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
label := material.Body1(th, t.ID())
|
||||
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{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Type), strings.ToUpper(t.Type()))).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Name), t.Name())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Endpoint), t.Endpoint())).Layout),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
layout.Rigid(material.Body2(th, fmt.Sprintf("%s: %s", i18n.Get(i18n.Entrypoint), t.Entrypoint())).Layout),
|
||||
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{Width: 8}.Layout),
|
||||
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 {
|
||||
gtx.Constraints.Min.X = gtx.Dp(12)
|
||||
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
|
||||
|
||||
c := colornames.GreenA700
|
||||
if t.Err() != nil {
|
||||
c = colornames.Red600
|
||||
}
|
||||
if t.IsClosed() {
|
||||
c = colornames.Grey600
|
||||
}
|
||||
return icons.IconCircle.Layout(gtx, color.NRGBA(c))
|
||||
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)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -132,3 +219,46 @@ func (l *tunnelList) Layout(gtx C, th *material.Theme) D {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+30
-1
@@ -5,11 +5,17 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxWidth = 800
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
@@ -23,12 +29,14 @@ type Router struct {
|
||||
stack routeStack
|
||||
current Route
|
||||
*material.Theme
|
||||
modal *component.ModalLayer
|
||||
}
|
||||
|
||||
func NewRouter(th *material.Theme) *Router {
|
||||
r := &Router{
|
||||
pages: make(map[PagePath]Page),
|
||||
Theme: th,
|
||||
modal: component.NewModal(),
|
||||
}
|
||||
|
||||
return r
|
||||
@@ -74,6 +82,8 @@ func (r *Router) Back() {
|
||||
func (r *Router) Layout(gtx C) D {
|
||||
r.Theme.Palette = theme.Current().Material
|
||||
|
||||
defer r.modal.Layout(gtx, r.Theme)
|
||||
|
||||
return layout.Background{}.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
defer clip.Rect{
|
||||
@@ -96,7 +106,7 @@ func (r *Router) Layout(gtx C) D {
|
||||
}
|
||||
|
||||
inset := layout.Inset{}
|
||||
width := unit.Dp(800)
|
||||
width := unit.Dp(MaxWidth)
|
||||
if x := gtx.Metric.PxToDp(gtx.Constraints.Max.X); x > width {
|
||||
inset.Left = (x - width) / 2
|
||||
inset.Right = inset.Left
|
||||
@@ -109,6 +119,25 @@ func (r *Router) Layout(gtx C) D {
|
||||
)
|
||||
}
|
||||
|
||||
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 * 2 / 3
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
type routeStack struct {
|
||||
routes []Route
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
type settingsPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
menu ui_widget.Menu
|
||||
list widget.List
|
||||
|
||||
@@ -30,7 +29,6 @@ type settingsPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &settingsPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
menu: ui_widget.Menu{
|
||||
List: layout.List{
|
||||
Axis: layout.Vertical,
|
||||
@@ -85,8 +83,6 @@ func (p *settingsPage) Layout(gtx layout.Context) layout.Dimensions {
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -109,7 +105,7 @@ func (p *settingsPage) Layout(gtx layout.Context) layout.Dimensions {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
title := material.H6(th, i18n.Get(i18n.Settings))
|
||||
title := material.H6(th, i18n.Settings.Value())
|
||||
return title.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
@@ -198,7 +194,7 @@ func (p *settingsPage) showLangMenu(gtx layout.Context) {
|
||||
items[0].Selected = true
|
||||
}
|
||||
|
||||
p.menu.Title = i18n.Get(i18n.Language)
|
||||
p.menu.Title = i18n.Language
|
||||
p.menu.Items = items
|
||||
p.menu.Selected = func(index int) {
|
||||
p.lang.Clear()
|
||||
@@ -206,7 +202,7 @@ func (p *settingsPage) showLangMenu(gtx layout.Context) {
|
||||
Name: p.menu.Items[index].Key,
|
||||
Value: p.menu.Items[index].Value,
|
||||
})
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
|
||||
cfg := config.Get()
|
||||
if cfg.Settings == nil {
|
||||
@@ -220,10 +216,9 @@ func (p *settingsPage) showLangMenu(gtx layout.Context) {
|
||||
i18n.Set(cfg.Settings.Lang)
|
||||
}
|
||||
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.menu.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
func (p *settingsPage) showThemeMenu(gtx layout.Context) {
|
||||
@@ -243,7 +238,7 @@ func (p *settingsPage) showThemeMenu(gtx layout.Context) {
|
||||
items[0].Selected = true
|
||||
}
|
||||
|
||||
p.menu.Title = i18n.Get(i18n.Theme)
|
||||
p.menu.Title = i18n.Theme
|
||||
p.menu.Items = items
|
||||
p.menu.Selected = func(index int) {
|
||||
p.theme.Clear()
|
||||
@@ -251,7 +246,7 @@ func (p *settingsPage) showThemeMenu(gtx layout.Context) {
|
||||
Name: p.menu.Items[index].Key,
|
||||
Value: p.menu.Items[index].Value,
|
||||
})
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
|
||||
cfg := config.Get()
|
||||
if cfg.Settings == nil {
|
||||
@@ -270,8 +265,7 @@ func (p *settingsPage) showThemeMenu(gtx layout.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.menu.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
+26
-30
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
@@ -29,7 +30,6 @@ type D = layout.Dimensions
|
||||
|
||||
type filePage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -42,6 +42,8 @@ type filePage struct {
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
name component.TextField
|
||||
endpoint component.TextField
|
||||
@@ -62,7 +64,6 @@ type filePage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &filePage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -88,7 +89,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteTunnel),
|
||||
Title: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -149,18 +150,15 @@ func (p *filePage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -306,9 +304,9 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgID.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
|
||||
})
|
||||
@@ -329,11 +327,10 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -347,9 +344,9 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
|
||||
})
|
||||
@@ -370,11 +367,10 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -383,15 +379,14 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
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.Get(i18n.Endpoint)).Layout(gtx)
|
||||
}),
|
||||
|
||||
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())
|
||||
@@ -408,7 +403,7 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
return err
|
||||
}
|
||||
if !fs.IsDir() {
|
||||
return fmt.Errorf("%s %s", dir, i18n.Get(i18n.ErrDirectory))
|
||||
return fmt.Errorf("%s %s", dir, i18n.ErrDirectory.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -417,7 +412,7 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Get(i18n.DirectoryPath))
|
||||
return p.endpoint.Layout(gtx, th, i18n.DirectoryPath.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 16}.Layout),
|
||||
|
||||
@@ -425,7 +420,7 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, i18n.Get(i18n.BasicAuth)).Layout),
|
||||
layout.Flexed(1, material.Body1(th, i18n.BasicAuth.Value()).Layout),
|
||||
layout.Rigid(material.Switch(th, &p.basicAuth, "basic auth").Layout),
|
||||
)
|
||||
}),
|
||||
@@ -436,7 +431,7 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
p.username.Clear()
|
||||
return D{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, i18n.Get(i18n.Username))
|
||||
return p.username.Layout(gtx, th, i18n.Username.Value())
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.basicAuth.Value {
|
||||
@@ -472,7 +467,7 @@ func (p *filePage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Inset{
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.password.Layout(gtx, th, i18n.Get(i18n.Password))
|
||||
return p.password.Layout(gtx, th, i18n.Password.Value())
|
||||
})
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
@@ -553,6 +548,7 @@ func (p *filePage) onoff() {
|
||||
tunnel.EndpointOption(opts.Endpoint),
|
||||
tunnel.UsernameOption(opts.Username),
|
||||
tunnel.PasswordOption(opts.Password),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
|
||||
+28
-31
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
@@ -29,7 +30,6 @@ type D = layout.Dimensions
|
||||
|
||||
type httpPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -42,6 +42,8 @@ type httpPage struct {
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
name component.TextField
|
||||
endpoint component.TextField
|
||||
@@ -67,7 +69,6 @@ type httpPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &httpPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -98,7 +99,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteTunnel),
|
||||
Title: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -168,18 +169,15 @@ func (p *httpPage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -325,9 +323,9 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgID.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
|
||||
})
|
||||
@@ -348,11 +346,10 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -366,9 +363,9 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
|
||||
})
|
||||
@@ -389,11 +386,10 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -402,14 +398,14 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
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.Get(i18n.Endpoint)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Endpoint.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -418,7 +414,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidAddr))
|
||||
return fmt.Errorf(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -427,7 +423,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Get(i18n.Address))
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
@@ -439,7 +435,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, i18n.Get(i18n.BasicAuth)).Layout),
|
||||
layout.Flexed(1, material.Body1(th, i18n.BasicAuth.Value()).Layout),
|
||||
layout.Rigid(material.Switch(th, &p.basicAuth, "Basic auth").Layout),
|
||||
)
|
||||
})
|
||||
@@ -450,7 +446,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.username.Clear()
|
||||
return D{}
|
||||
}
|
||||
return p.username.Layout(gtx, th, i18n.Get(i18n.Username))
|
||||
return p.username.Layout(gtx, th, i18n.Username.Value())
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if !p.basicAuth.Value {
|
||||
@@ -486,7 +482,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Inset{
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.password.Layout(gtx, th, i18n.Get(i18n.Password))
|
||||
return p.password.Layout(gtx, th, i18n.Password.Value())
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -498,7 +494,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, i18n.Get(i18n.CustomHostname)).Layout),
|
||||
layout.Flexed(1, material.Body1(th, i18n.CustomHostname.Value()).Layout),
|
||||
layout.Rigid(material.Switch(th, &p.rewriteHost, "Hostname").Layout),
|
||||
)
|
||||
})
|
||||
@@ -512,7 +508,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Inset{
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.hostname.Layout(gtx, th, i18n.Get(i18n.Hostname))
|
||||
return p.hostname.Layout(gtx, th, i18n.Hostname.Value())
|
||||
})
|
||||
}),
|
||||
|
||||
@@ -521,7 +517,7 @@ func (p *httpPage) layout(gtx C, th *material.Theme) D {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.Body1(th, i18n.Get(i18n.EnableTLS)).Layout),
|
||||
layout.Flexed(1, material.Body1(th, i18n.EnableTLS.Value()).Layout),
|
||||
layout.Rigid(material.Switch(th, &p.enableTLS, "enable TLS").Layout),
|
||||
)
|
||||
})
|
||||
@@ -618,6 +614,7 @@ func (p *httpPage) onoff() {
|
||||
tunnel.PasswordOption(opts.Password),
|
||||
tunnel.HostnameOption(opts.Hostname),
|
||||
tunnel.EnableTLSOption(opts.EnableTLS),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
|
||||
@@ -96,7 +96,7 @@ func (p *tunnelPage) Layout(gtx C) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
title := material.H6(th, i18n.Get(i18n.Tunnel))
|
||||
title := material.H6(th, i18n.Tunnel.Value())
|
||||
// title.Font.Weight = font.Bold
|
||||
return title.Layout(gtx)
|
||||
}),
|
||||
|
||||
+22
-25
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
@@ -29,7 +30,6 @@ type D = layout.Dimensions
|
||||
|
||||
type tcpPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -42,6 +42,8 @@ type tcpPage struct {
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
name component.TextField
|
||||
endpoint component.TextField
|
||||
@@ -55,7 +57,6 @@ type tcpPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &tcpPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -71,7 +72,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteTunnel),
|
||||
Title: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -123,18 +124,15 @@ func (p *tcpPage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -280,9 +278,9 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgID.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
|
||||
})
|
||||
@@ -303,11 +301,10 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -321,9 +318,9 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
|
||||
})
|
||||
@@ -344,11 +341,10 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -357,14 +353,14 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
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.Get(i18n.Endpoint)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Endpoint.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -373,7 +369,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveTCPAddr("tcp", addr); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidAddr))
|
||||
return fmt.Errorf(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -382,7 +378,7 @@ func (p *tcpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Get(i18n.Address))
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
)
|
||||
@@ -446,6 +442,7 @@ func (p *tcpPage) onoff() {
|
||||
tunnel.NameOption(opts.Name),
|
||||
tunnel.IDOption(opts.ID),
|
||||
tunnel.EndpointOption(opts.Endpoint),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
|
||||
+22
-25
@@ -7,6 +7,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/font"
|
||||
"gioui.org/io/clipboard"
|
||||
@@ -29,7 +30,6 @@ type D = layout.Dimensions
|
||||
|
||||
type udpPage struct {
|
||||
router *page.Router
|
||||
modal *component.ModalLayer
|
||||
|
||||
btnBack widget.Clickable
|
||||
btnState widget.Clickable
|
||||
@@ -42,6 +42,8 @@ type udpPage struct {
|
||||
|
||||
wgID widget.Clickable
|
||||
wgEntrypoint widget.Clickable
|
||||
lastCopyID time.Time
|
||||
lastCopyEP time.Time
|
||||
|
||||
name component.TextField
|
||||
endpoint component.TextField
|
||||
@@ -55,7 +57,6 @@ type udpPage struct {
|
||||
func NewPage(r *page.Router) page.Page {
|
||||
return &udpPage{
|
||||
router: r,
|
||||
modal: component.NewModal(),
|
||||
list: layout.List{
|
||||
// NOTE: the list must be vertical
|
||||
Axis: layout.Vertical,
|
||||
@@ -71,7 +72,7 @@ func NewPage(r *page.Router) page.Page {
|
||||
},
|
||||
},
|
||||
delDialog: ui_widget.Dialog{
|
||||
Title: i18n.Get(i18n.DeleteTunnel),
|
||||
Title: i18n.DeleteTunnel,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -123,18 +124,15 @@ func (p *udpPage) Layout(gtx C) D {
|
||||
p.delete()
|
||||
p.router.Back()
|
||||
}
|
||||
p.modal.Disappear(gtx.Now)
|
||||
p.router.HideModal(gtx)
|
||||
}
|
||||
p.modal.Widget = func(gtx layout.Context, th *material.Theme, anim *component.VisibilityAnimation) layout.Dimensions {
|
||||
p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D {
|
||||
return p.delDialog.Layout(gtx, th)
|
||||
}
|
||||
p.modal.Appear(gtx.Now)
|
||||
})
|
||||
}
|
||||
|
||||
th := p.router.Theme
|
||||
|
||||
defer p.modal.Layout(gtx, th)
|
||||
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
@@ -280,9 +278,9 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgID.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyEP = time.Time{}
|
||||
p.lastCopyID = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.ID())),
|
||||
})
|
||||
@@ -303,11 +301,10 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyID) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -321,9 +318,9 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
|
||||
gtx.Source = src
|
||||
|
||||
copied := false
|
||||
if p.wgEntrypoint.Clicked(gtx) {
|
||||
copied = true
|
||||
p.lastCopyID = time.Time{}
|
||||
p.lastCopyEP = time.Now()
|
||||
gtx.Execute(clipboard.WriteCmd{
|
||||
Data: io.NopCloser(bytes.NewBufferString(tun.Entrypoint())),
|
||||
})
|
||||
@@ -344,11 +341,10 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
c := color.NRGBA(colornames.Blue500)
|
||||
if copied {
|
||||
c = color.NRGBA(colornames.Green500)
|
||||
if time.Since(p.lastCopyEP) < 3*time.Second {
|
||||
return icons.IconDone.Layout(gtx, color.NRGBA(colornames.Green500))
|
||||
}
|
||||
return icons.IconCopy.Layout(gtx, c)
|
||||
return icons.IconCopy.Layout(gtx, color.NRGBA(colornames.Blue500))
|
||||
}),
|
||||
)
|
||||
})
|
||||
@@ -357,14 +353,14 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return material.Body1(th, i18n.Get(i18n.Name)).Layout(gtx)
|
||||
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.Get(i18n.Endpoint)).Layout(gtx)
|
||||
return material.Body1(th, i18n.Endpoint.Value()).Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if err := func() error {
|
||||
@@ -373,7 +369,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
return nil
|
||||
}
|
||||
if _, err := net.ResolveUDPAddr("udp", addr); err != nil {
|
||||
return fmt.Errorf(i18n.Get(i18n.ErrInvalidAddr))
|
||||
return fmt.Errorf(i18n.ErrInvalidAddr.Value())
|
||||
}
|
||||
return nil
|
||||
}(); err != nil {
|
||||
@@ -382,7 +378,7 @@ func (p *udpPage) layout(gtx C, th *material.Theme) D {
|
||||
p.endpoint.ClearError()
|
||||
}
|
||||
|
||||
return p.endpoint.Layout(gtx, th, i18n.Get(i18n.Address))
|
||||
return p.endpoint.Layout(gtx, th, i18n.Address.Value())
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
)
|
||||
@@ -446,6 +442,7 @@ func (p *udpPage) onoff() {
|
||||
tunnel.NameOption(opts.Name),
|
||||
tunnel.IDOption(opts.ID),
|
||||
tunnel.EndpointOption(opts.Endpoint),
|
||||
tunnel.CreatedAtOption(opts.CreatedAt),
|
||||
)
|
||||
} else {
|
||||
tun.Close()
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ var (
|
||||
Material: material.Palette{
|
||||
Fg: color.NRGBA(colornames.White),
|
||||
Bg: color.NRGBA(colornames.Grey900),
|
||||
ContrastBg: color.NRGBA(colornames.LightBlueA400),
|
||||
ContrastBg: color.NRGBA(colornames.Green500),
|
||||
ContrastFg: color.NRGBA(colornames.White),
|
||||
},
|
||||
ContentSurfaceBg: color.NRGBA(colornames.Grey800),
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"gioui.org/font/gofont"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/text"
|
||||
"gioui.org/widget/material"
|
||||
"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"
|
||||
@@ -40,7 +40,8 @@ func NewUI() *UI {
|
||||
}
|
||||
|
||||
th := material.NewTheme()
|
||||
th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
// th.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||
th.Shaper = text.NewShaper(text.WithCollection(fonts.Collection()))
|
||||
th.Palette = theme.Current().Material
|
||||
|
||||
router := page.NewRouter(th)
|
||||
|
||||
+107
-115
@@ -9,7 +9,7 @@ import (
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
Title string
|
||||
Title i18n.Key
|
||||
Body string
|
||||
Widget func(gtx layout.Context, th *material.Theme) layout.Dimensions
|
||||
Clicked func(ok bool)
|
||||
@@ -18,132 +18,124 @@ type Dialog struct {
|
||||
}
|
||||
|
||||
func (p *Dialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
var cl widget.Clickable
|
||||
return cl.Layout(gtx, func(gtx layout.Context) 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,
|
||||
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 component.SurfaceStyle{
|
||||
Theme: th,
|
||||
ShadowStyle: component.ShadowStyle{
|
||||
CornerRadius: 28,
|
||||
},
|
||||
return layout.Inset{
|
||||
Top: 16,
|
||||
Bottom: 16,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.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)
|
||||
})
|
||||
}),
|
||||
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.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(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(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)
|
||||
}
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Width: 8}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if p.btnCancel.Clicked(gtx) && p.Clicked != nil {
|
||||
p.Clicked(false)
|
||||
}
|
||||
|
||||
return material.ButtonLayoutStyle{
|
||||
Background: th.Bg,
|
||||
CornerRadius: 20,
|
||||
Button: &p.btnCancel,
|
||||
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 {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, i18n.Get(i18n.Cancel))
|
||||
label.Color = th.Fg
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
|
||||
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: 20,
|
||||
Button: &p.btnOK,
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Width: 8}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if p.btnOK.Clicked(gtx) && p.Clicked != nil {
|
||||
p.Clicked(true)
|
||||
}
|
||||
|
||||
return material.ButtonLayoutStyle{
|
||||
Background: th.Bg,
|
||||
CornerRadius: 20,
|
||||
Button: &p.btnOK,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, i18n.Get(i18n.OK))
|
||||
label.Color = th.Fg
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
|
||||
label := material.Body1(th, i18n.OK.Value())
|
||||
label.Color = th.Fg
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
+63
-70
@@ -12,7 +12,7 @@ import (
|
||||
type Menu struct {
|
||||
List layout.List
|
||||
Items []MenuItem
|
||||
Title string
|
||||
Title i18n.Key
|
||||
Selected func(index int)
|
||||
btnAdd widget.Clickable
|
||||
ShowAdd bool
|
||||
@@ -20,79 +20,72 @@ type Menu struct {
|
||||
}
|
||||
|
||||
func (p *Menu) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
var cl widget.Clickable
|
||||
return cl.Layout(gtx, func(gtx layout.Context) 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,
|
||||
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 component.SurfaceStyle{
|
||||
Theme: th,
|
||||
ShadowStyle: component.ShadowStyle{
|
||||
CornerRadius: 28,
|
||||
},
|
||||
return layout.Inset{
|
||||
Top: 16,
|
||||
Bottom: 16,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 16,
|
||||
Bottom: 16,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Axis: layout.Vertical,
|
||||
}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.H6(th, p.Title).Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if !p.ShowAdd {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
btn := material.IconButton(th, &p.btnAdd, icons.IconAdd, "Add")
|
||||
btn.Background = th.Bg
|
||||
btn.Color = th.Fg
|
||||
btn.Inset = layout.UniformInset(0)
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.List.Layout(gtx, len(p.Items), func(gtx layout.Context, index int) layout.Dimensions {
|
||||
if p.Items[index].state.Clicked(gtx) {
|
||||
if p.Multiple {
|
||||
p.Items[index].Selected = !p.Items[index].Selected
|
||||
} else {
|
||||
for i := range p.Items {
|
||||
p.Items[i].Selected = false
|
||||
}
|
||||
p.Items[index].Selected = true
|
||||
}
|
||||
if p.Selected != nil {
|
||||
p.Selected(index)
|
||||
}
|
||||
return 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{}
|
||||
}
|
||||
return p.Items[index].Layout(gtx, th)
|
||||
})
|
||||
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)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/go-gost/gost.plus/ui/icons"
|
||||
)
|
||||
|
||||
type MetadataDialog struct {
|
||||
Surface component.SurfaceStyle
|
||||
List material.ListStyle
|
||||
Clicked func(ok bool)
|
||||
btnAdd widget.Clickable
|
||||
btnCancel widget.Clickable
|
||||
btnOK widget.Clickable
|
||||
metadata []*kv
|
||||
}
|
||||
|
||||
func (p *MetadataDialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
if p.btnAdd.Clicked(gtx) {
|
||||
p.metadata = append(p.metadata, &kv{})
|
||||
}
|
||||
|
||||
var cl widget.Clickable
|
||||
return cl.Layout(gtx, func(gtx layout.Context) 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 {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, material.H6(th, "Metadata").Layout),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.IconButton(th, &p.btnAdd, icons.IconAdd, "Add")
|
||||
btn.Background = th.Bg
|
||||
btn.Color = th.Fg
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
for i := range p.metadata {
|
||||
if p.metadata[i].remove.Clicked(gtx) {
|
||||
p.metadata = append(p.metadata[:i], p.metadata[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
gtx.Constraints.Max.Y -= gtx.Dp(80)
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.List.Layout(gtx, len(p.metadata), func(gtx layout.Context, index int) layout.Dimensions {
|
||||
return p.metadata[index].Layout(gtx, th)
|
||||
})
|
||||
})
|
||||
}),
|
||||
|
||||
layout.Rigid(layout.Spacer{Height: 8}.Layout),
|
||||
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Spacing: layout.SpaceBetween,
|
||||
Alignment: layout.Middle,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Width: 8}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if p.btnCancel.Clicked(gtx) && p.Clicked != nil {
|
||||
p.Clicked(false)
|
||||
}
|
||||
|
||||
return material.ButtonLayoutStyle{
|
||||
Background: th.Bg,
|
||||
CornerRadius: 20,
|
||||
Button: &p.btnCancel,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, "Cancel")
|
||||
label.Color = th.Fg
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Spacer{Width: 8}.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if p.btnOK.Clicked(gtx) && p.Clicked != nil {
|
||||
p.Clicked(true)
|
||||
}
|
||||
|
||||
return material.ButtonLayoutStyle{
|
||||
Background: th.Bg,
|
||||
CornerRadius: 20,
|
||||
Button: &p.btnOK,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 8,
|
||||
Bottom: 8,
|
||||
Left: 24,
|
||||
Right: 24,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
label := material.Body1(th, "OK")
|
||||
label.Color = th.Fg
|
||||
return label.Layout(gtx)
|
||||
})
|
||||
|
||||
})
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (p *MetadataDialog) Add(k, v string) {
|
||||
kv := &kv{}
|
||||
kv.k.SetText(k)
|
||||
kv.v.SetText(v)
|
||||
p.metadata = append(p.metadata, kv)
|
||||
}
|
||||
|
||||
func (p *MetadataDialog) Clear() {
|
||||
p.metadata = nil
|
||||
}
|
||||
|
||||
func (p *MetadataDialog) Metadata() []*kv {
|
||||
return p.metadata
|
||||
}
|
||||
|
||||
type kv struct {
|
||||
k component.TextField
|
||||
v component.TextField
|
||||
remove widget.Clickable
|
||||
}
|
||||
|
||||
func (p *kv) Get() (string, string) {
|
||||
return strings.TrimSpace(p.k.Text()), strings.TrimSpace(p.v.Text())
|
||||
}
|
||||
|
||||
func (p *kv) Set(k, v string) {
|
||||
p.k.SetText(k)
|
||||
p.v.SetText(v)
|
||||
}
|
||||
|
||||
func (p *kv) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions {
|
||||
return layout.Inset{
|
||||
Top: 4,
|
||||
Bottom: 4,
|
||||
}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Flex{
|
||||
Alignment: layout.End,
|
||||
}.Layout(gtx,
|
||||
layout.Flexed(0.4, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.k.Layout(gtx, th, "Key")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Flexed(0.4, func(gtx layout.Context) layout.Dimensions {
|
||||
return p.v.Layout(gtx, th, "Value")
|
||||
}),
|
||||
layout.Rigid(layout.Spacer{Width: 8}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
btn := material.IconButton(th, &p.remove, icons.IconDelete, "Remove")
|
||||
btn.Color = th.Fg
|
||||
btn.Background = th.Bg
|
||||
return btn.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user