diff --git a/Makefile b/Makefile index 89ec4b5..e9277fa 100644 --- a/Makefile +++ b/Makefile @@ -37,7 +37,8 @@ windows-arm64: # go install gioui.org/cmd/gogio@latest android: - gogio -x -work -target android -minsdk 22 -version $(VERSION).7 -name GOST+ -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).aab . + gogio -x -work -target android -minsdk 22 -version $(VERSION).8 -name GOST+ -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).aab . + gogio -x -work -target android -minsdk 22 -version $(VERSION).8 -name GOST+ -signkey build/sign.keystore -signpass android -appid gost.plus -o $(BINDIR)/$(NAME)-$(VERSION).apk . gz_releases=$(addsuffix .gz, $(PLATFORM_LIST)) zip_releases=$(addsuffix .zip, $(WINDOWS_ARCH_LIST)) diff --git a/go.mod b/go.mod index b4ec20c..cf0be74 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/go-gost/gost.plus -go 1.22.0 +go 1.22 + +toolchain go1.22.2 require ( gioui.org v0.6.0 @@ -26,7 +28,7 @@ require ( 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/golang/protobuf v1.5.4 // indirect github.com/gorilla/websocket v1.5.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -36,7 +38,7 @@ require ( github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect github.com/prometheus/client_golang v1.17.0 // indirect - github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 // indirect + github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.11.1 // indirect github.com/rs/xid v1.3.0 // indirect @@ -52,14 +54,14 @@ require ( 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/exp v0.0.0-20240314144324-c7f7c6466f7f // 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/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect - google.golang.org/grpc v1.59.0 // indirect - google.golang.org/protobuf v1.31.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 7145d15..540b2e4 100644 --- a/go.sum +++ b/go.sum @@ -44,12 +44,10 @@ github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h 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/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= @@ -83,8 +81,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16 h1:v7DLqVdK4VrYkVD5diGdl4sxJurKJEMnODWRJlxV9oM= -github.com/prometheus/client_model v0.4.1-0.20230718164431-9a2bf3000d16/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI= @@ -126,29 +124,26 @@ github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+Seva 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 v0.0.0-20240314144324-c7f7c6466f7f h1:3CW0unweImhOzd5FmYuRsD4Y4oQFKZIjAnKbjV4WIrw= +golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc h1:OG+uKOKt/BW+ydf/M7gym7ONo8U+dyIlLazys3du298= golang.org/x/exp/shiny v0.0.0-20240103183307-be819d1f06fc/go.mod h1:3F+MieQB7dRYLTmnncoFbb1crS5lfQoTfDgQy6K4N0o= golang.org/x/image v0.14.0 h1:tNgSxAFe3jC4uYqvZdTr84SZoM1KfwdC9SKIFrLjFn4= golang.org/x/image v0.14.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/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/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= -google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= -google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= -google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/main.go b/main.go index c0601c5..01a51e2 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,8 @@ package main import ( "context" + "fmt" + "log/slog" _ "net" "os" "time" @@ -16,6 +18,9 @@ import ( "github.com/go-gost/gost.plus/tunnel" "github.com/go-gost/gost.plus/tunnel/entrypoint" "github.com/go-gost/gost.plus/ui" + "github.com/go-gost/gost.plus/ui/page" + "github.com/go-gost/gost.plus/ui/theme" + "github.com/go-gost/gost.plus/ui/widget" _ "github.com/go-gost/gost.plus/winres" ) @@ -23,11 +28,7 @@ func main() { Init() go func() { - var w app.Window - w.Option(app.Title("GOST+")) - w.Option(app.MinSize(800, 600)) - err := run(&w) - if err != nil { + if err := run(); err != nil { logger.Default().Fatal(err) } os.Exit(0) @@ -35,16 +36,12 @@ func main() { app.Main() } -func run(w *app.Window) error { - go func() { - for e := range runner.Event() { - if e.TaskID == runner.TaskUpdateStats { - w.Invalidate() - } - } - }() - +func run() error { ui := ui.NewUI() + + go handleEvent(ui) + + w := ui.Window() var ops op.Ops for { switch e := w.Event().(type) { @@ -60,6 +57,34 @@ func run(w *app.Window) error { } } +func handleEvent(ui *ui.UI) { + for { + select { + case e := <-ui.Router().Event(): + switch e.ID { + case page.EventThemeChanged: + slog.Debug("theme changed", "event", e.ID) + ui.Window().Option(app.StatusColor(theme.Current().Material.Bg)) + } + + case e := <-runner.Event(): + switch e.TaskID { + case runner.TaskUpdateStats: + ui.Window().Invalidate() + + default: + if e.Err != nil { + slog.Error(fmt.Sprintf("task: %s", e.Err), "task", e.TaskID) + ui.Router().Notify(widget.Message{ + Type: widget.Error, + Content: e.Err.Error(), + }) + } + } + } + } +} + func Init() { config.Init() tunnel.LoadConfig() diff --git a/ui/fonts/NotoSans-Bold.ttf b/ui/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000..d84248e Binary files /dev/null and b/ui/fonts/NotoSans-Bold.ttf differ diff --git a/ui/fonts/NotoSans-Regular.ttf b/ui/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000..fa4cff5 Binary files /dev/null and b/ui/fonts/NotoSans-Regular.ttf differ diff --git a/ui/fonts/NotoSans-SemiBold.ttf b/ui/fonts/NotoSans-SemiBold.ttf new file mode 100644 index 0000000..d3ed423 Binary files /dev/null and b/ui/fonts/NotoSans-SemiBold.ttf differ diff --git a/ui/fonts/NotoSansMono-Regular.ttf b/ui/fonts/NotoSansMono-Regular.ttf new file mode 100644 index 0000000..c2bbb5a Binary files /dev/null and b/ui/fonts/NotoSansMono-Regular.ttf differ diff --git a/ui/fonts/NotoSansSC-Bold.ttf b/ui/fonts/NotoSansSC-Bold.ttf new file mode 100644 index 0000000..b9010df Binary files /dev/null and b/ui/fonts/NotoSansSC-Bold.ttf differ diff --git a/ui/fonts/NotoSansSC-Regular.ttf b/ui/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000..4d4cadb Binary files /dev/null and b/ui/fonts/NotoSansSC-Regular.ttf differ diff --git a/ui/fonts/NotoSansSC-SemiBold.ttf b/ui/fonts/NotoSansSC-SemiBold.ttf new file mode 100644 index 0000000..d83c2ac Binary files /dev/null and b/ui/fonts/NotoSansSC-SemiBold.ttf differ diff --git a/ui/fonts/font.go b/ui/fonts/font.go new file mode 100644 index 0000000..360e379 --- /dev/null +++ b/ui/fonts/font.go @@ -0,0 +1,45 @@ +package fonts + +import ( + _ "embed" + "fmt" + + "gioui.org/font" + "gioui.org/font/opentype" +) + +var ( + + //go:embed NotoSans-Regular.ttf + notoSansRegular []byte + //go:embed NotoSans-SemiBold.ttf + notoSansSemiBold []byte + + //go:embed NotoSansSC-Regular.ttf + notoSansSCRegular []byte + //go:embed NotoSansSC-SemiBold.ttf + notoSansSCSemiBold []byte +) + +var ( + collection []font.FontFace +) + +func init() { + register(notoSansRegular) + register(notoSansSemiBold) + register(notoSansSCRegular) + register(notoSansSCSemiBold) +} + +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...) +} diff --git a/ui/icons/icons.go b/ui/icons/icons.go index fbdd45a..307580e 100644 --- a/ui/icons/icons.go +++ b/ui/icons/icons.go @@ -60,6 +60,7 @@ var ( IconActionUpdate = mustIcon(icons.ActionUpdate) IconActionHourGlassEmpty = mustIcon(icons.ActionHourglassEmpty) IconInfo = mustIcon(icons.ActionInfo) + IconAlert = mustIcon(icons.AlertErrorOutline) ) func mustIcon(data []byte) *widget.Icon { diff --git a/ui/page/entrypoint/tcp/page.go b/ui/page/entrypoint/tcp/page.go index e644d97..c5bdfc7 100644 --- a/ui/page/entrypoint/tcp/page.go +++ b/ui/page/entrypoint/tcp/page.go @@ -342,6 +342,10 @@ func (p *tcpPage) create() error { if err := ep.Run(); err != nil { ep.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -368,6 +372,10 @@ func (p *tcpPage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := ep.Run(); err != nil { ep.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/page/entrypoint/udp/page.go b/ui/page/entrypoint/udp/page.go index eda76f7..084fcbb 100644 --- a/ui/page/entrypoint/udp/page.go +++ b/ui/page/entrypoint/udp/page.go @@ -399,6 +399,10 @@ func (p *udpPage) create() error { if err := ep.Run(); err != nil { ep.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -429,6 +433,10 @@ func (p *udpPage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := ep.Run(); err != nil { ep.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/page/event.go b/ui/page/event.go new file mode 100644 index 0000000..d5ff387 --- /dev/null +++ b/ui/page/event.go @@ -0,0 +1,11 @@ +package page + +type EventID string + +const ( + EventThemeChanged EventID = "event.theme.changed" +) + +type Event struct { + ID EventID +} diff --git a/ui/page/home/page.go b/ui/page/home/page.go index 3951ff4..18cb7ca 100644 --- a/ui/page/home/page.go +++ b/ui/page/home/page.go @@ -4,7 +4,6 @@ import ( "image/color" "sync/atomic" - "gioui.org/font" "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" @@ -90,14 +89,7 @@ func (p *homePage) Layout(gtx C) D { gtx.Constraints.Max.X = gtx.Dp(50) return icons.IconApp.Layout(gtx) }), - layout.Rigid(layout.Spacer{Width: 8}.Layout), - layout.Flexed(1, func(gtx C) D { - label := material.H6(th, "GOST+") - label.Font.Weight = font.SemiBold - return label.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Width: 8}.Layout), - + layout.Flexed(1, layout.Spacer{Width: 8}.Layout), layout.Rigid(func(gtx C) D { if p.btnFavorite.Clicked(gtx) { p.favorite.Store(!p.favorite.Load()) diff --git a/ui/page/page.go b/ui/page/page.go index f2a8bb2..e36258a 100644 --- a/ui/page/page.go +++ b/ui/page/page.go @@ -2,6 +2,7 @@ package page import ( "gioui.org/layout" + "gioui.org/widget/material" ) type PagePath string @@ -27,6 +28,10 @@ type PageOptions struct { type PageOption func(opts *PageOptions) +type C = layout.Context +type D = layout.Dimensions +type T = material.Theme + func WithPageID(id string) PageOption { return func(opts *PageOptions) { opts.ID = id diff --git a/ui/page/router.go b/ui/page/router.go index 36cd505..c562c52 100644 --- a/ui/page/router.go +++ b/ui/page/router.go @@ -1,6 +1,11 @@ package page import ( + "time" + + "gioui.org/app" + "gioui.org/io/event" + "gioui.org/io/key" "gioui.org/layout" "gioui.org/op/clip" "gioui.org/op/paint" @@ -10,33 +15,39 @@ import ( "gioui.org/x/component" "github.com/go-gost/core/logger" "github.com/go-gost/gost.plus/ui/theme" + ui_widget "github.com/go-gost/gost.plus/ui/widget" ) const ( MaxWidth = 800 ) -type C = layout.Context -type D = layout.Dimensions - type Route struct { Path PagePath ID string } type Router struct { + w *app.Window pages map[PagePath]Page stack routeStack current Route *material.Theme - modal *component.ModalLayer + modal *component.ModalLayer + notification *ui_widget.Notification + events chan Event } -func NewRouter(th *material.Theme) *Router { +func NewRouter(w *app.Window, th *T) *Router { r := &Router{ + w: w, pages: make(map[PagePath]Page), Theme: th, modal: component.NewModal(), + notification: ui_widget.NewNotification(3*time.Second, func() { + w.Invalidate() + }), + events: make(chan Event, 16), } return r @@ -72,7 +83,6 @@ func (r *Router) Back() { } r.current = route - page.Init(WithPageID(route.ID)) logger.Default().WithFields(map[string]any{ "kind": "router", "route.id": route.ID, @@ -80,6 +90,25 @@ func (r *Router) Back() { } func (r *Router) Layout(gtx C) D { + if r.stack.Peek().Path != PageHome { + event.Op(gtx.Ops, r.w) + for { + ev, ok := gtx.Event( + key.Filter{Name: key.NameBack}, + key.Filter{Name: key.NameEscape}, + ) + if !ok { + break + } + switch ev := ev.(type) { + case key.Event: + if ev.State == key.Press { + r.Back() + } + } + } + } + r.Theme.Palette = theme.Current().Material defer r.modal.Layout(gtx, r.Theme) @@ -113,7 +142,21 @@ func (r *Router) Layout(gtx C) D { } return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return page.Layout(gtx) + return layout.Stack{ + Alignment: layout.N, + }.Layout(gtx, + layout.Expanded(page.Layout), + layout.Stacked(func(gtx layout.Context) layout.Dimensions { + return layout.Inset{ + Top: 16, + Bottom: 16, + Right: gtx.Metric.PxToDp(gtx.Constraints.Max.X) / 5, + Left: gtx.Metric.PxToDp(gtx.Constraints.Max.X) / 5, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return r.notification.Layout(gtx, r.Theme) + }) + }), + ) }) }, ) @@ -124,7 +167,7 @@ func (r *Router) ShowModal(gtx layout.Context, w func(gtx C, th *material.Theme) 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 + gtx.Constraints.Max.X = gtx.Constraints.Max.X * 3 / 4 var clk widget.Clickable return clk.Layout(gtx, func(gtx layout.Context) layout.Dimensions { @@ -138,6 +181,21 @@ func (r *Router) HideModal(gtx C) { r.modal.Disappear(gtx.Now) } +func (r *Router) Notify(message ui_widget.Message) { + r.notification.Show(message) +} + +func (r *Router) Emit(event Event) { + select { + case r.events <- event: + default: + } +} + +func (r *Router) Event() <-chan Event { + return r.events +} + type routeStack struct { routes []Route } diff --git a/ui/page/settings/page.go b/ui/page/settings/page.go index 811f406..155674a 100644 --- a/ui/page/settings/page.go +++ b/ui/page/settings/page.go @@ -128,50 +128,64 @@ func (p *settingsPage) Layout(gtx layout.Context) layout.Dimensions { } func (p *settingsPage) layout(gtx layout.Context, th *material.Theme) layout.Dimensions { - return component.SurfaceStyle{ - Theme: th, - ShadowStyle: component.ShadowStyle{ - CornerRadius: 12, - }, - Fill: theme.Current().ContentSurfaceBg, - }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return layout.UniformInset(16).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{ + Axis: layout.Vertical, + Alignment: layout.Middle, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Flex{ Axis: layout.Vertical, Alignment: layout.Middle, }.Layout(gtx, layout.Rigid(func(gtx layout.Context) layout.Dimensions { return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints.Max.X = gtx.Dp(60) return icons.IconApp.Layout(gtx) }) }), - layout.Rigid(layout.Spacer{Height: 8}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - label := material.H6(th, "GOST+") - label.Font.Weight = font.Bold - return label.Layout(gtx) - }), - layout.Rigid(layout.Spacer{Height: 8}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return material.Body1(th, version.Version).Layout(gtx) - }), layout.Rigid(layout.Spacer{Height: 16}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if p.lang.Clicked(gtx) { - p.showLangMenu(gtx) - } - return p.lang.Layout(gtx, th) + label := material.Body1(th, "GOST+") + label.Font.Weight = font.SemiBold + return layout.Center.Layout(gtx, label.Layout) }), + layout.Rigid(layout.Spacer{Height: 8}.Layout), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - if p.theme.Clicked(gtx) { - p.showThemeMenu(gtx) - } - return p.theme.Layout(gtx, th) + return layout.Center.Layout(gtx, material.Body1(th, version.Version).Layout) }), ) - }) - }) + }), + layout.Rigid(layout.Spacer{Height: 32}.Layout), + + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return component.SurfaceStyle{ + Theme: th, + ShadowStyle: component.ShadowStyle{ + CornerRadius: 12, + }, + Fill: theme.Current().ContentSurfaceBg, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(16).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if p.lang.Clicked(gtx) { + p.showLangMenu(gtx) + } + return p.lang.Layout(gtx, th) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if p.theme.Clicked(gtx) { + p.showThemeMenu(gtx) + } + return p.theme.Layout(gtx, th) + }), + ) + }) + }) + }), + ) } func (p *settingsPage) showLangMenu(gtx layout.Context) { @@ -263,6 +277,7 @@ func (p *settingsPage) showThemeMenu(gtx layout.Context) { default: theme.UseLight() } + p.router.Emit(page.Event{ID: page.EventThemeChanged}) } p.router.ShowModal(gtx, func(gtx page.C, th *material.Theme) page.D { diff --git a/ui/page/tunnel/file/page.go b/ui/page/tunnel/file/page.go index 4ffe4d7..a749de4 100644 --- a/ui/page/tunnel/file/page.go +++ b/ui/page/tunnel/file/page.go @@ -495,6 +495,10 @@ func (p *filePage) create() error { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -528,6 +532,10 @@ func (p *filePage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/page/tunnel/http/page.go b/ui/page/tunnel/http/page.go index 42432a8..e500db5 100644 --- a/ui/page/tunnel/http/page.go +++ b/ui/page/tunnel/http/page.go @@ -553,6 +553,10 @@ func (p *httpPage) create() error { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -592,6 +596,10 @@ func (p *httpPage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/page/tunnel/tcp/page.go b/ui/page/tunnel/tcp/page.go index fc0d30b..fcc3060 100644 --- a/ui/page/tunnel/tcp/page.go +++ b/ui/page/tunnel/tcp/page.go @@ -398,6 +398,10 @@ func (p *tcpPage) create() error { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -424,6 +428,10 @@ func (p *tcpPage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/page/tunnel/udp/page.go b/ui/page/tunnel/udp/page.go index 1b4bf9b..fa3f840 100644 --- a/ui/page/tunnel/udp/page.go +++ b/ui/page/tunnel/udp/page.go @@ -398,6 +398,10 @@ func (p *udpPage) create() error { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) return err } @@ -424,6 +428,10 @@ func (p *udpPage) update(opts ...tunnel.Option) tunnel.Tunnel { if err := tun.Run(); err != nil { tun.Close() + p.router.Notify(ui_widget.Message{ + Type: ui_widget.Error, + Content: err.Error(), + }) logger.Default().Error(err) } diff --git a/ui/theme/theme.go b/ui/theme/theme.go index 38f7fdc..cc63767 100644 --- a/ui/theme/theme.go +++ b/ui/theme/theme.go @@ -19,6 +19,8 @@ type Palette struct { ListBg color.NRGBA NavButtonBg color.NRGBA NavButtonContrastBg color.NRGBA + ItemBg color.NRGBA + NotificationBg color.NRGBA } type Theme struct { @@ -41,8 +43,10 @@ var ( }, ContentSurfaceBg: color.NRGBA(colornames.Grey50), ListBg: color.NRGBA(colornames.BlueGrey50), - NavButtonBg: color.NRGBA(colornames.White), + NavButtonBg: color.NRGBA(colornames.BlueGrey50), NavButtonContrastBg: color.NRGBA(colornames.BlueGrey100), + ItemBg: color.NRGBA(colornames.Grey300), + NotificationBg: color.NRGBA(colornames.Grey200), }, } @@ -58,6 +62,8 @@ var ( ListBg: color.NRGBA(colornames.Grey700), NavButtonBg: color.NRGBA(colornames.Grey800), NavButtonContrastBg: color.NRGBA(colornames.Grey600), + ItemBg: color.NRGBA(colornames.Grey600), + NotificationBg: color.NRGBA(colornames.Grey700), }, } ) diff --git a/ui/ui.go b/ui/ui.go index 603c3e1..0f49fff 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,11 +1,12 @@ package ui import ( - "gioui.org/font/gofont" + "gioui.org/app" "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" @@ -25,6 +26,7 @@ type C = layout.Context type D = layout.Dimensions type UI struct { + w *app.Window router *page.Router } @@ -40,10 +42,18 @@ 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) + w := &app.Window{} + w.Option( + app.Title("GOST"), + app.MinSize(800, 600), + app.StatusColor(th.Bg), + ) + + router := page.NewRouter(w, th) router.Register(page.PageHome, home.NewPage(router)) router.Register(page.PageTunnel, tunnel.NewPage(router)) router.Register(page.PageTunnelFile, file.NewPage(router)) @@ -60,6 +70,7 @@ func NewUI() *UI { }) return &UI{ + w: w, router: router, } } @@ -67,3 +78,11 @@ func NewUI() *UI { func (ui *UI) Layout(gtx C) D { return ui.router.Layout(gtx) } + +func (ui *UI) Window() *app.Window { + return ui.w +} + +func (ui *UI) Router() *page.Router { + return ui.router +} diff --git a/ui/widget/dialog.go b/ui/widget/dialog.go index e8bf185..2c5f9c8 100644 --- a/ui/widget/dialog.go +++ b/ui/widget/dialog.go @@ -91,14 +91,14 @@ func (p *Dialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimension return material.ButtonLayoutStyle{ Background: th.Bg, - CornerRadius: 20, + CornerRadius: 18, Button: &p.btnCancel, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{ Top: 8, Bottom: 8, - Left: 24, - Right: 24, + Left: 20, + Right: 20, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { label := material.Body1(th, i18n.Cancel.Value()) label.Color = th.Fg @@ -117,14 +117,14 @@ func (p *Dialog) Layout(gtx layout.Context, th *material.Theme) layout.Dimension return material.ButtonLayoutStyle{ Background: th.Bg, - CornerRadius: 20, + CornerRadius: 18, Button: &p.btnOK, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{ Top: 8, Bottom: 8, - Left: 24, - Right: 24, + Left: 20, + Right: 20, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { label := material.Body1(th, i18n.OK.Value()) label.Color = th.Fg diff --git a/ui/widget/menu.go b/ui/widget/menu.go index 890a286..a5015c8 100644 --- a/ui/widget/menu.go +++ b/ui/widget/menu.go @@ -105,10 +105,10 @@ func (p *MenuItem) Layout(gtx layout.Context, th *material.Theme) layout.Dimensi Button: &p.state, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{ - Top: 8, - Bottom: 8, - Left: 16, - Right: 16, + Top: 12, + Bottom: 12, + Left: 24, + Right: 24, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{ Spacing: layout.SpaceBetween, diff --git a/ui/widget/nav.go b/ui/widget/nav.go index 6cd17d2..5c041c3 100644 --- a/ui/widget/nav.go +++ b/ui/widget/nav.go @@ -45,15 +45,10 @@ func (p *Nav) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { if p.current == index { btn.background = theme.Current().NavButtonContrastBg } else { - btn.background = theme.Current().Material.Bg + btn.background = theme.Current().NavButtonBg } - return layout.Inset{ - Top: 8, - Bottom: 8, - Left: 12, - Right: 12, - }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(8).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return btn.Layout(gtx, th) }) }) @@ -62,15 +57,13 @@ func (p *Nav) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { type NavButton struct { btn widget.Clickable cornerRadius unit.Dp - borderWidth unit.Dp background color.NRGBA text i18n.Key } func NewNavButton(text i18n.Key) *NavButton { return &NavButton{ - cornerRadius: 18, - borderWidth: 1, + cornerRadius: 20, text: text, } } @@ -83,14 +76,13 @@ func (btn *NavButton) Layout(gtx layout.Context, th *material.Theme) layout.Dime }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return widget.Border{ Color: theme.Current().NavButtonContrastBg, - Width: btn.borderWidth, CornerRadius: btn.cornerRadius, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Inset{ Top: 8, Bottom: 8, - Left: 20, - Right: 20, + Left: 16, + Right: 16, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { label := material.Body1(th, btn.text.Value()) return label.Layout(gtx) diff --git a/ui/widget/notification.go b/ui/widget/notification.go new file mode 100644 index 0000000..5cfdbf7 --- /dev/null +++ b/ui/widget/notification.go @@ -0,0 +1,122 @@ +package widget + +import ( + "image/color" + "sync" + "sync/atomic" + "time" + + "gioui.org/layout" + "gioui.org/widget/material" + "gioui.org/x/component" + "github.com/go-gost/gost.plus/ui/icons" + "github.com/go-gost/gost.plus/ui/theme" + "golang.org/x/exp/shiny/materialdesign/colornames" +) + +const ( + Success string = "success" + Info string = "info" + Warn string = "warn" + Error string = "error" +) + +type Message struct { + Type string + Content string +} + +type Notification struct { + show atomic.Bool + messages chan Message + current Message + duration time.Duration + callback func() + mu sync.RWMutex +} + +func NewNotification(d time.Duration, callback func()) *Notification { + p := &Notification{ + messages: make(chan Message, 16), + duration: d, + callback: callback, + } + go p.run() + return p +} + +func (p *Notification) Layout(gtx C, th *T) D { + if !p.show.Load() { + return D{} + } + + p.mu.RLock() + message := p.current + p.mu.RUnlock() + + return component.SurfaceStyle{ + Theme: th, + ShadowStyle: component.ShadowStyle{ + CornerRadius: 8, + }, + Fill: theme.Current().NotificationBg, + }.Layout(gtx, func(gtx C) D { + return layout.Inset{ + Top: 8, + Bottom: 8, + Left: 8, + Right: 8, + }.Layout(gtx, func(gtx C) D { + return layout.Flex{ + Alignment: layout.Middle, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + + switch message.Type { + case Success: + return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Green500)) + case Warn: + return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Orange500)) + case Error: + return icons.IconAlert.Layout(gtx, color.NRGBA(colornames.Red500)) + case Info: + fallthrough + default: + return icons.IconInfo.Layout(gtx, color.NRGBA(colornames.Blue500)) + } + }), + layout.Rigid(layout.Spacer{Width: 8}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return material.Body2(th, message.Content).Layout(gtx) + }), + ) + }) + }) +} + +func (p *Notification) Show(message Message) { + select { + case p.messages <- message: + default: + } +} + +func (p *Notification) run() { + for m := range p.messages { + p.mu.Lock() + p.current = m + p.mu.Unlock() + + p.show.Store(true) + if p.callback != nil { + p.callback() + } + + <-time.After(p.duration) + + p.show.Store(false) + if p.callback != nil { + p.callback() + } + } +} diff --git a/ui/widget/selector.go b/ui/widget/selector.go index 5be7c5b..faf4b7d 100644 --- a/ui/widget/selector.go +++ b/ui/widget/selector.go @@ -1,13 +1,14 @@ package widget import ( - "strings" - "gioui.org/layout" "gioui.org/widget" "gioui.org/widget/material" + "gioui.org/x/component" + "gioui.org/x/outlay" "github.com/go-gost/gost.plus/ui/i18n" "github.com/go-gost/gost.plus/ui/icons" + "github.com/go-gost/gost.plus/ui/theme" ) type SelectorItem struct { @@ -24,8 +25,8 @@ type Selector struct { func (p *Selector) Layout(gtx layout.Context, th *material.Theme) layout.Dimensions { return material.Clickable(gtx, &p.clickable, func(gtx layout.Context) layout.Dimensions { return layout.Inset{ - Top: 8, - Bottom: 8, + Top: 4, + Bottom: 4, }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{ Alignment: layout.Middle, @@ -34,17 +35,49 @@ func (p *Selector) Layout(gtx layout.Context, th *material.Theme) layout.Dimensi layout.Rigid(layout.Spacer{Width: 8}.Layout), layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions { - var names []string + var values []string for _, item := range p.items { - if item.Name != "" { - names = append(names, item.Name.Value()) + if item.Value == "" { + continue + } + + value := item.Name.Value() + if value == "" { + value = item.Value } + values = append(values, value) } - return material.Body2(th, strings.Join(names, ",")).Layout(gtx) + + return outlay.FlowWrap{ + Alignment: layout.Middle, + }.Layout(gtx, len(values), func(gtx layout.Context, i int) layout.Dimensions { + return layout.UniformInset(4).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return component.SurfaceStyle{ + Theme: th, + ShadowStyle: component.ShadowStyle{CornerRadius: 14}, + Fill: theme.Current().ItemBg, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Inset{ + Top: 4, + Bottom: 4, + Left: 10, + Right: 10, + }.Layout(gtx, material.Body2(th, values[i]).Layout) + }) + }) + }) }) }), layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return icons.IconNavRight.Layout(gtx, th.Fg) + if len(p.items) > 0 { + return layout.Dimensions{} + } + return layout.Inset{ + Top: 4, + Bottom: 5, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return icons.IconNavRight.Layout(gtx, th.Fg) + }) }), ) }) diff --git a/ui/widget/widget.go b/ui/widget/widget.go new file mode 100644 index 0000000..0125b8d --- /dev/null +++ b/ui/widget/widget.go @@ -0,0 +1,14 @@ +package widget + +import ( + "gioui.org/layout" + "gioui.org/widget/material" +) + +type C = layout.Context +type D = layout.Dimensions +type T = material.Theme + +type Widget interface { + Layout(gtx C, th *T) D +} diff --git a/version/version.go b/version/version.go index d13f706..9f7588c 100644 --- a/version/version.go +++ b/version/version.go @@ -1,3 +1,3 @@ package version -var Version = "0.3.1" +var Version = "0.3.2"