From 47e3bafaeb2db2e4ab525cdc056a7c53e4d6def0 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:10:49 -0700 Subject: [PATCH 1/8] Add Unbounded widget proxy UI with 3D globe visualization Full feature implementation for the unbounded/broflake widget proxy: Go: FFI and mobile bindings for setUnboundedEnabled/isUnboundedEnabled, event subscription to forward connection state changes to Flutter. Flutter: Unbounded screen with interactive three-globe WebView showing animated arcs as censored users connect, share bandwidth toggle with optimistic update, Riverpod event pipeline from Go through to WebView JS, and geo-IP lookup for peer location visualization. Co-Authored-By: Claude Opus 4.6 --- assets/locales/en.po | 6 + assets/unbounded/globe.html | 256 +++++++++ go.mod | 40 +- go.sum | 70 ++- lantern-core/core.go | 33 +- lantern-core/ffi/ffi.go | 21 + lantern-core/mobile/mobile.go | 19 + .../models/entity/app_setting_entity.dart | 4 + lib/core/router/router.dart | 4 + lib/core/router/router.gr.dart | 405 +++++++------- lib/core/services/db/objectbox-model.json | 7 +- lib/core/services/db/objectbox.g.dart | 23 +- .../home/provider/app_event_notifier.dart | 41 ++ .../home/provider/app_event_notifier.g.dart | 2 +- .../home/provider/app_setting_notifier.dart | 13 + .../home/provider/app_setting_notifier.g.dart | 2 +- lib/features/setting/setting.dart | 7 +- lib/features/unbounded/unbounded.dart | 510 ++++++++++++++++++ lib/lantern/lantern_core_service.dart | 4 + lib/lantern/lantern_ffi_service.dart | 26 + lib/lantern/lantern_generated_bindings.dart | 23 + lib/lantern/lantern_platform_service.dart | 24 + lib/lantern/lantern_service.dart | 16 + pubspec.lock | 16 +- pubspec.yaml | 1 + 25 files changed, 1337 insertions(+), 236 deletions(-) create mode 100644 assets/unbounded/globe.html create mode 100644 lib/features/unbounded/unbounded.dart diff --git a/assets/locales/en.po b/assets/locales/en.po index 2c38dd9cfc..be99d4d412 100644 --- a/assets/locales/en.po +++ b/assets/locales/en.po @@ -31,6 +31,12 @@ msgstr "Unbounded" msgid "help_fight_global_internet_censorship" msgstr "Help Fight Global Internet Censorship" +msgid "share_bandwidth" +msgstr "Share Bandwidth" + +msgid "unbounded_description" +msgstr "When enabled, Unbounded shares a small amount of your bandwidth to help people in censored countries access the open internet." + msgid "vpn_settings" msgstr "VPN Settings" diff --git a/assets/unbounded/globe.html b/assets/unbounded/globe.html new file mode 100644 index 0000000000..378ec3f2b9 --- /dev/null +++ b/assets/unbounded/globe.html @@ -0,0 +1,256 @@ + + + + + +Unbounded Globe + + + +
+ + + + + + + diff --git a/go.mod b/go.mod index bc4c31cc8e..15c113b9fd 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,20 @@ module github.com/getlantern/lantern go 1.25.4 -// replace github.com/getlantern/radiance => ../radiance +replace github.com/getlantern/radiance => ../radiance + +replace github.com/getlantern/common => ../common + +replace github.com/getlantern/broflake => ../unbounded // replace github.com/getlantern/lantern-server-provisioner => ../lantern-server-provisioner // replace github.com/sagernet/sing-box => ../sing-box-minimal +replace github.com/enobufs/go-nats => github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775 + +replace github.com/quic-go/quic-go => github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded + replace github.com/sagernet/sing => github.com/getlantern/sing v0.7.18-lantern replace github.com/sagernet/sing-box => github.com/getlantern/sing-box-minimal v1.12.19-lantern @@ -156,6 +164,7 @@ require ( github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/enobufs/go-nats v0.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/flynn/noise v1.0.1-0.20220214164934-d803f5c4b0f4 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect @@ -164,6 +173,7 @@ require ( github.com/getlantern/algeneva v0.0.0-20250307163401-1824e7b54f52 // indirect github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f // indirect github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 // indirect + github.com/getlantern/broflake v0.0.0-20260221195823-510790b48bbe // indirect github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f // indirect github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d // indirect github.com/getlantern/fronted v0.0.0-20260219001615-7eabaa834efe // indirect @@ -185,6 +195,7 @@ require ( github.com/go-llsqlite/adapter v0.0.0-20230927005056-7f5ce7f0c916 // indirect github.com/go-llsqlite/crawshaw v0.4.0 // indirect github.com/go-resty/resty/v2 v2.16.5 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect github.com/goccy/go-yaml v1.19.0 // indirect @@ -193,6 +204,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect + github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/csrf v1.7.3-0.20250123201450-9dd6af1f6d30 // indirect github.com/gorilla/securecookie v1.1.2 // indirect @@ -222,26 +234,37 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mschoch/smat v0.2.0 // indirect github.com/nwaples/rardecode v1.1.2 // indirect + github.com/onsi/ginkgo/v2 v2.12.0 // indirect github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/dtls/v2 v2.2.12 // indirect + github.com/pion/dtls/v3 v3.0.6 // indirect github.com/pion/ice/v2 v2.3.24 // indirect - github.com/pion/interceptor v0.1.37 // indirect - github.com/pion/logging v0.2.3 // indirect + github.com/pion/ice/v4 v4.0.10 // indirect + github.com/pion/interceptor v0.1.40 // indirect + github.com/pion/logging v0.2.4 // indirect github.com/pion/mdns v0.0.12 // indirect + github.com/pion/mdns/v2 v2.0.7 // indirect github.com/pion/randutil v0.1.0 // indirect github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.12 // indirect - github.com/pion/sctp v1.8.37 // indirect - github.com/pion/sdp/v3 v3.0.11 // indirect + github.com/pion/rtp v1.8.19 // indirect + github.com/pion/sctp v1.8.39 // indirect + github.com/pion/sdp/v3 v3.0.14 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect + github.com/pion/srtp/v3 v3.0.6 // indirect github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.4 // indirect + github.com/pion/stun/v3 v3.0.0 // indirect + github.com/pion/transport v0.14.1 // indirect + github.com/pion/transport/v2 v2.2.10 // indirect github.com/pion/transport/v3 v3.0.7 // indirect + github.com/pion/turn v1.3.7 // indirect github.com/pion/turn/v2 v2.1.3 // indirect + github.com/pion/turn/v4 v4.0.2 // indirect github.com/pion/webrtc/v3 v3.2.40 // indirect + github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus-community/pro-bing v0.4.0 // indirect + github.com/quic-go/quic-go v0.51.0 // indirect github.com/r3labs/sse/v2 v2.10.0 // indirect github.com/refraction-networking/utls v1.8.2 // indirect github.com/refraction-networking/water v0.7.1-alpha // indirect @@ -269,6 +292,7 @@ require ( github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tkuchiki/go-timezone v0.2.0 // indirect github.com/ulikunitz/xz v0.5.10 // indirect + github.com/wlynxg/anet v0.0.5 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xtaci/kcp-go/v5 v5.6.20 // indirect diff --git a/go.sum b/go.sum index 044c08d72b..ab6e1afc6c 100644 --- a/go.sum +++ b/go.sum @@ -193,8 +193,6 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= -github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f h1:EqRKCaOBuvVkFsIjeWUYluE4s4TZtVQSClfIWFqcSks= -github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= @@ -242,8 +240,8 @@ github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175 h1:JWH5BB2o0e github.com/getlantern/osversion v0.0.0-20240418205916-2e84a4a4e175/go.mod h1:h3S9LBmmzN/xM+lwYZHE4abzTtCTtidKtG+nxZcCZX0= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YLAuT8r51ApR5z0d8/qjhHu3TW+divQ2C98Ac= github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= -github.com/getlantern/radiance v0.0.0-20260221215045-6049f134d863 h1:nlx+23+ieMbmXA12ZY3IMuedCsAKgp/s2Fauf8M10mk= -github.com/getlantern/radiance v0.0.0-20260221215045-6049f134d863/go.mod h1:JwM46TRAnU3PCmdhj7gar3bpHH5SQTufj7d2LSdi2tk= +github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded h1:qA1oi5so1/C6psHLPlyPGyq6JhZsPvA4EutsNhjzodc= +github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= github.com/getlantern/samizdat v0.0.2 h1:PkMu6jsfUz7DLZUH2xh548XfzgPASmq5CajZyUKj/9Y= github.com/getlantern/samizdat v0.0.2/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= @@ -297,6 +295,8 @@ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= @@ -357,6 +357,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f h1:pDhu5sgp8yJlEF/g6osliIIpF9K4F5jvkULXa4daRDQ= +github.com/google/pprof v0.0.0-20230821062121-407c9e7a662f/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -488,12 +490,18 @@ github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOl github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= +github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775 h1:CVBqDCqhtrS2etCKGuwruUkwg3f/axVpa2Il5IQQtEs= +github.com/noahlevenson/go-nats v0.0.0-20230720174341-49df1f749775/go.mod h1:dXVvPZcJIwdWDH5ZXQ8oVA7dz+Ecu9TPF+6biaMl1dY= github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d3M= github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.12.0 h1:UIVDowFPwpg6yMUpPjGkYvf06K3RAiJXUhCxEwQVHRI= +github.com/onsi/ginkgo/v2 v2.12.0/go.mod h1:ZNEzXISYlqpb8S36iN71ifqLi3vVD1rVJGvWRCJOUpQ= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw= github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0= @@ -506,45 +514,68 @@ github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4 github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= +github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= +github.com/pion/dtls/v3 v3.0.6 h1:7Hkd8WhAJNbRgq9RgdNh1aaWlZlGpYTzdqjy9x9sK2E= +github.com/pion/dtls/v3 v3.0.6/go.mod h1:iJxNQ3Uhn1NZWOMWlLxEEHAN5yX7GyPvvKw04v9bzYU= github.com/pion/ice/v2 v2.3.24 h1:RYgzhH/u5lH0XO+ABatVKCtRd+4U1GEaCXSMjNr13tI= github.com/pion/ice/v2 v2.3.24/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= -github.com/pion/interceptor v0.1.37 h1:aRA8Zpab/wE7/c0O3fh1PqY0AJI3fCSEM5lRWJVorwI= -github.com/pion/interceptor v0.1.37/go.mod h1:JzxbJ4umVTlZAf+/utHzNesY8tmRkM2lVmkS82TTj8Y= +github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= +github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= +github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= +github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/logging v0.2.3 h1:gHuf0zpoh1GW67Nr6Gj4cv5Z9ZscU7g/EaoC/Ke/igI= -github.com/pion/logging v0.2.3/go.mod h1:z8YfknkquMe1csOrxK5kc+5/ZPAzMxbKLX5aXpbpC90= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= -github.com/pion/rtp v1.8.12 h1:nsKs8Wi0jQyBFHU3qmn/OvtZrhktVfJY0vRxwACsL5U= -github.com/pion/rtp v1.8.12/go.mod h1:8uMBJj32Pa1wwx8Fuv/AsFhn8jsgw+3rUC2PfoBZ8p4= -github.com/pion/sctp v1.8.37 h1:ZDmGPtRPX9mKCiVXtMbTWybFw3z/hVKAZgU81wcOrqs= -github.com/pion/sctp v1.8.37/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.11 h1:VhgVSopdsBKwhCFoyyPmT1fKMeV9nLMrEKxNOdy3IVI= -github.com/pion/sdp/v3 v3.0.11/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= +github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= +github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= +github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= +github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= +github.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI= +github.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= github.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA= +github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= +github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= +github.com/pion/stun v0.3.2/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport v0.8.8/go.mod h1:lpeSM6KJFejVtZf8k0fgeN7zE73APQpTF83WvA1FVP8= +github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= +github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= +github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn v1.3.7 h1:/nyM2XrlZILD7KKfnh0oYEBTRG5JlbH21ibjluRoCeo= +github.com/pion/turn v1.3.7/go.mod h1:js0LBFqMcKAlaWAXoYqNjefGI7kfJCrkCBfHGuTToXE= github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= +github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= +github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= github.com/pion/webrtc/v3 v3.2.40 h1:Wtfi6AZMQg+624cvCXUuSmrKWepSB7zfgYDOYqsSOVU= github.com/pion/webrtc/v3 v3.2.40/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= +github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= +github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -705,6 +736,9 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= +github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= @@ -828,6 +862,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= @@ -878,6 +913,7 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -892,6 +928,7 @@ golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= @@ -907,6 +944,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= diff --git a/lantern-core/core.go b/lantern-core/core.go index 839f5088c7..f7cfc69d62 100644 --- a/lantern-core/core.go +++ b/lantern-core/core.go @@ -33,9 +33,10 @@ import ( type EventType = string const ( - EventTypeConfig EventType = "config" - EventTypeServerLocation EventType = "server-location" - DefaultLogLevel = "trace" + EventTypeConfig EventType = "config" + EventTypeServerLocation EventType = "server-location" + EventTypeUnboundedConnection EventType = "unbounded-connection" + DefaultLogLevel = "trace" ) // LanternCore is the main structure accessing the Lantern backend. @@ -135,6 +136,11 @@ type SmartRouting interface { IsSmartRoutingEnabled() bool } +type Unbounded interface { + SetUnboundedEnabled(bool) error + IsUnboundedEnabled() bool +} + type Core interface { App User @@ -143,6 +149,7 @@ type Core interface { SplitTunnel Ads SmartRouting + Unbounded } // Make sure LanternCore implements the Core interface @@ -216,6 +223,7 @@ func (lc *LanternCore) initialize(opts *utils.Opts, eventEmitter utils.FlutterEv lc.listeningServerLocationChanges() lc.listeningDataCapChanges() + lc.listeningUnboundedConnectionChanges() slog.Debug("LanternCore initialized successfully") // If we have a legacy user ID, fetch user data @@ -260,6 +268,17 @@ func (lc *LanternCore) listeningDataCapChanges() { }) } +func (lc *LanternCore) listeningUnboundedConnectionChanges() { + events.Subscribe(func(evt vpn.UnboundedConnectionEvent) { + jsonBytes, err := json.Marshal(evt) + if err != nil { + slog.Error("Error marshalling unbounded connection event", "error", err) + return + } + lc.notifyFlutter(EventTypeUnboundedConnection, string(jsonBytes)) + }) +} + func (lc *LanternCore) UpdateTelemetryConsent(consent bool) error { slog.Debug("Updating telemetry consent", "consent", consent) if consent { @@ -808,6 +827,14 @@ func (lc *LanternCore) IsSmartRoutingEnabled() bool { return vpn.SmartRoutingEnabled() } +func (lc *LanternCore) SetUnboundedEnabled(enabled bool) error { + return vpn.SetUnbounded(enabled) +} + +func (lc *LanternCore) IsUnboundedEnabled() bool { + return vpn.UnboundedEnabled() +} + func (lc *LanternCore) AddServerBasedOnURLs(urls string, skipCertVerification bool, serverName string) error { slog.Debug("Adding server based on URLs", "urls", urls, "skipCertVerification", skipCertVerification) return lc.serverManager.AddServerBasedOnURLs(context.Background(), urls, skipCertVerification, serverName) diff --git a/lantern-core/ffi/ffi.go b/lantern-core/ffi/ffi.go index 27acd5b73c..050c9357fd 100644 --- a/lantern-core/ffi/ffi.go +++ b/lantern-core/ffi/ffi.go @@ -1002,3 +1002,24 @@ func isSmartRoutingEnabled() C.int { } return 0 } + +//export setUnboundedEnabled +func setUnboundedEnabled(enabled C.int) *C.char { + c, errStr := requireCore() + if errStr != nil { + return errStr + } + if err := c.SetUnboundedEnabled(enabled != 0); err != nil { + return SendError(err) + } + return C.CString("ok") +} + +//export isUnboundedEnabled +func isUnboundedEnabled() C.int { + c, _ := requireCore() + if c != nil && c.IsUnboundedEnabled() { + return 1 + } + return 0 +} diff --git a/lantern-core/mobile/mobile.go b/lantern-core/mobile/mobile.go index 024efd34d2..cfbe05df5b 100644 --- a/lantern-core/mobile/mobile.go +++ b/lantern-core/mobile/mobile.go @@ -493,6 +493,25 @@ func AddServerBasedOnURLs(urls string, skipCertVerification bool, serverName str }) } +// Unbounded Methods + +func SetUnboundedEnabled(enabled bool) error { + slog.Info("unbounded: SetUnboundedEnabled", "enabled", enabled) + return withCore(func(c lanterncore.Core) error { + return c.SetUnboundedEnabled(enabled) + }) +} + +func IsUnboundedEnabled() bool { + ok, err := withCoreR(func(c lanterncore.Core) (bool, error) { + return c.IsUnboundedEnabled(), nil + }) + if err != nil { + return false + } + return ok +} + // Smart Routing Methods // SetSmartRoutingMode sets the smart routing mode. diff --git a/lib/core/models/entity/app_setting_entity.dart b/lib/core/models/entity/app_setting_entity.dart index 1283efbaab..509040ab93 100644 --- a/lib/core/models/entity/app_setting_entity.dart +++ b/lib/core/models/entity/app_setting_entity.dart @@ -22,6 +22,7 @@ class AppSetting { bool onboardingCompleted; String themeMode; String environment; + bool unboundedEnabled; AppSetting({ this.id = 0, @@ -41,6 +42,7 @@ class AppSetting { this.onboardingCompleted = false, this.themeMode = 'system', this.environment = 'prod', + this.unboundedEnabled = false, }); AppSetting copyWith({ @@ -60,6 +62,7 @@ class AppSetting { bool? onboardingCompleted, String? themeMode, String? environment, + bool? unboundedEnabled, }) { return AppSetting( id: id, @@ -79,6 +82,7 @@ class AppSetting { onboardingCompleted: onboardingCompleted ?? this.onboardingCompleted, themeMode: themeMode ?? this.themeMode, environment: environment ?? this.environment, + unboundedEnabled: unboundedEnabled ?? this.unboundedEnabled, ); } diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index 6bb8c84719..a06b7b4154 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -184,6 +184,10 @@ class AppRouter extends RootStackRouter { path: '/smart-routing', page: SmartRouting.page, ), + AutoRoute( + path: '/unbounded', + page: UnboundedScreen.page, + ), AutoRoute( path: '/intro', page: Onboarding.page, diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart index f2e685b236..5cc1f7392f 100644 --- a/lib/core/router/router.gr.dart +++ b/lib/core/router/router.gr.dart @@ -9,10 +9,10 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i44; -import 'package:collection/collection.dart' as _i48; -import 'package:flutter/material.dart' as _i45; -import 'package:lantern/core/common/common.dart' as _i46; +import 'package:auto_route/auto_route.dart' as _i45; +import 'package:collection/collection.dart' as _i49; +import 'package:flutter/material.dart' as _i46; +import 'package:lantern/core/common/common.dart' as _i47; import 'package:lantern/core/widgets/app_webview.dart' as _i3; import 'package:lantern/features/account/account.dart' as _i1; import 'package:lantern/features/account/delete_account.dart' as _i9; @@ -58,27 +58,28 @@ import 'package:lantern/features/setting/follow_us.dart' as _i13; import 'package:lantern/features/setting/invite_friends.dart' as _i15; import 'package:lantern/features/setting/setting.dart' as _i35; import 'package:lantern/features/setting/smart_routing.dart' as _i38; -import 'package:lantern/features/setting/vpn_setting.dart' as _i42; +import 'package:lantern/features/setting/vpn_setting.dart' as _i43; import 'package:lantern/features/split_tunneling/apps_split_tunneling.dart' as _i5; import 'package:lantern/features/split_tunneling/split_tunneling.dart' as _i39; import 'package:lantern/features/split_tunneling/split_tunneling_info.dart' as _i40; import 'package:lantern/features/split_tunneling/website_split_tunneling.dart' - as _i43; + as _i44; import 'package:lantern/features/support/support.dart' as _i41; +import 'package:lantern/features/unbounded/unbounded.dart' as _i42; import 'package:lantern/features/vpn/server_selection.dart' as _i34; -import 'package:lantern/lantern/protos/protos/auth.pb.dart' as _i47; +import 'package:lantern/lantern/protos/protos/auth.pb.dart' as _i48; /// generated route for /// [_i1.Account] -class Account extends _i44.PageRouteInfo { - const Account({List<_i44.PageRouteInfo>? children}) +class Account extends _i45.PageRouteInfo { + const Account({List<_i45.PageRouteInfo>? children}) : super(Account.name, initialChildren: children); static const String name = 'Account'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i1.Account(); @@ -88,12 +89,12 @@ class Account extends _i44.PageRouteInfo { /// generated route for /// [_i2.AddEmail] -class AddEmail extends _i44.PageRouteInfo { +class AddEmail extends _i45.PageRouteInfo { AddEmail({ - _i45.Key? key, - _i46.AuthFlow authFlow = _i46.AuthFlow.signUp, + _i46.Key? key, + _i47.AuthFlow authFlow = _i47.AuthFlow.signUp, String? password, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( AddEmail.name, args: AddEmailArgs(key: key, authFlow: authFlow, password: password), @@ -102,7 +103,7 @@ class AddEmail extends _i44.PageRouteInfo { static const String name = 'AddEmail'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -120,13 +121,13 @@ class AddEmail extends _i44.PageRouteInfo { class AddEmailArgs { const AddEmailArgs({ this.key, - this.authFlow = _i46.AuthFlow.signUp, + this.authFlow = _i47.AuthFlow.signUp, this.password, }); - final _i45.Key? key; + final _i46.Key? key; - final _i46.AuthFlow authFlow; + final _i47.AuthFlow authFlow; final String? password; @@ -150,12 +151,12 @@ class AddEmailArgs { /// generated route for /// [_i3.AppWebView] -class AppWebview extends _i44.PageRouteInfo { +class AppWebview extends _i45.PageRouteInfo { AppWebview({ - _i45.Key? key, + _i46.Key? key, required String title, required String url, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( AppWebview.name, args: AppWebviewArgs(key: key, title: title, url: url), @@ -164,7 +165,7 @@ class AppWebview extends _i44.PageRouteInfo { static const String name = 'AppWebview'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -176,7 +177,7 @@ class AppWebview extends _i44.PageRouteInfo { class AppWebviewArgs { const AppWebviewArgs({this.key, required this.title, required this.url}); - final _i45.Key? key; + final _i46.Key? key; final String title; @@ -200,13 +201,13 @@ class AppWebviewArgs { /// generated route for /// [_i4.Appearance] -class Appearance extends _i44.PageRouteInfo { - const Appearance({List<_i44.PageRouteInfo>? children}) +class Appearance extends _i45.PageRouteInfo { + const Appearance({List<_i45.PageRouteInfo>? children}) : super(Appearance.name, initialChildren: children); static const String name = 'Appearance'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i4.Appearance(); @@ -216,13 +217,13 @@ class Appearance extends _i44.PageRouteInfo { /// generated route for /// [_i5.AppsSplitTunneling] -class AppsSplitTunneling extends _i44.PageRouteInfo { - const AppsSplitTunneling({List<_i44.PageRouteInfo>? children}) +class AppsSplitTunneling extends _i45.PageRouteInfo { + const AppsSplitTunneling({List<_i45.PageRouteInfo>? children}) : super(AppsSplitTunneling.name, initialChildren: children); static const String name = 'AppsSplitTunneling'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i5.AppsSplitTunneling(); @@ -232,13 +233,13 @@ class AppsSplitTunneling extends _i44.PageRouteInfo { /// generated route for /// [_i6.ChoosePaymentMethod] -class ChoosePaymentMethod extends _i44.PageRouteInfo { +class ChoosePaymentMethod extends _i45.PageRouteInfo { ChoosePaymentMethod({ - _i45.Key? key, + _i46.Key? key, required String email, String? code, - required _i46.AuthFlow authFlow, - List<_i44.PageRouteInfo>? children, + required _i47.AuthFlow authFlow, + List<_i45.PageRouteInfo>? children, }) : super( ChoosePaymentMethod.name, args: ChoosePaymentMethodArgs( @@ -252,7 +253,7 @@ class ChoosePaymentMethod extends _i44.PageRouteInfo { static const String name = 'ChoosePaymentMethod'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -274,13 +275,13 @@ class ChoosePaymentMethodArgs { required this.authFlow, }); - final _i45.Key? key; + final _i46.Key? key; final String email; final String? code; - final _i46.AuthFlow authFlow; + final _i47.AuthFlow authFlow; @override String toString() { @@ -304,13 +305,13 @@ class ChoosePaymentMethodArgs { /// generated route for /// [_i7.ConfirmEmail] -class ConfirmEmail extends _i44.PageRouteInfo { +class ConfirmEmail extends _i45.PageRouteInfo { ConfirmEmail({ - _i45.Key? key, + _i46.Key? key, required String email, String? password, - _i46.AuthFlow authFlow = _i46.AuthFlow.signUp, - List<_i44.PageRouteInfo>? children, + _i47.AuthFlow authFlow = _i47.AuthFlow.signUp, + List<_i45.PageRouteInfo>? children, }) : super( ConfirmEmail.name, args: ConfirmEmailArgs( @@ -324,7 +325,7 @@ class ConfirmEmail extends _i44.PageRouteInfo { static const String name = 'ConfirmEmail'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -343,16 +344,16 @@ class ConfirmEmailArgs { this.key, required this.email, this.password, - this.authFlow = _i46.AuthFlow.signUp, + this.authFlow = _i47.AuthFlow.signUp, }); - final _i45.Key? key; + final _i46.Key? key; final String email; final String? password; - final _i46.AuthFlow authFlow; + final _i47.AuthFlow authFlow; @override String toString() { @@ -376,13 +377,13 @@ class ConfirmEmailArgs { /// generated route for /// [_i8.CreatePassword] -class CreatePassword extends _i44.PageRouteInfo { +class CreatePassword extends _i45.PageRouteInfo { CreatePassword({ - _i45.Key? key, + _i46.Key? key, required String email, - required _i46.AuthFlow authFlow, + required _i47.AuthFlow authFlow, required String code, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( CreatePassword.name, args: CreatePasswordArgs( @@ -396,7 +397,7 @@ class CreatePassword extends _i44.PageRouteInfo { static const String name = 'CreatePassword'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -418,11 +419,11 @@ class CreatePasswordArgs { required this.code, }); - final _i45.Key? key; + final _i46.Key? key; final String email; - final _i46.AuthFlow authFlow; + final _i47.AuthFlow authFlow; final String code; @@ -448,13 +449,13 @@ class CreatePasswordArgs { /// generated route for /// [_i9.DeleteAccount] -class DeleteAccount extends _i44.PageRouteInfo { - const DeleteAccount({List<_i44.PageRouteInfo>? children}) +class DeleteAccount extends _i45.PageRouteInfo { + const DeleteAccount({List<_i45.PageRouteInfo>? children}) : super(DeleteAccount.name, initialChildren: children); static const String name = 'DeleteAccount'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i9.DeleteAccount(); @@ -464,13 +465,13 @@ class DeleteAccount extends _i44.PageRouteInfo { /// generated route for /// [_i10.DeveloperMode] -class DeveloperMode extends _i44.PageRouteInfo { - const DeveloperMode({List<_i44.PageRouteInfo>? children}) +class DeveloperMode extends _i45.PageRouteInfo { + const DeveloperMode({List<_i45.PageRouteInfo>? children}) : super(DeveloperMode.name, initialChildren: children); static const String name = 'DeveloperMode'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i10.DeveloperMode(); @@ -480,11 +481,11 @@ class DeveloperMode extends _i44.PageRouteInfo { /// generated route for /// [_i11.DeviceLimitReached] -class DeviceLimitReached extends _i44.PageRouteInfo { +class DeviceLimitReached extends _i45.PageRouteInfo { DeviceLimitReached({ - _i45.Key? key, - required List<_i47.UserResponse_Device> devices, - List<_i44.PageRouteInfo>? children, + _i46.Key? key, + required List<_i48.UserResponse_Device> devices, + List<_i45.PageRouteInfo>? children, }) : super( DeviceLimitReached.name, args: DeviceLimitReachedArgs(key: key, devices: devices), @@ -493,7 +494,7 @@ class DeviceLimitReached extends _i44.PageRouteInfo { static const String name = 'DeviceLimitReached'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -505,9 +506,9 @@ class DeviceLimitReached extends _i44.PageRouteInfo { class DeviceLimitReachedArgs { const DeviceLimitReachedArgs({this.key, required this.devices}); - final _i45.Key? key; + final _i46.Key? key; - final List<_i47.UserResponse_Device> devices; + final List<_i48.UserResponse_Device> devices; @override String toString() { @@ -519,7 +520,7 @@ class DeviceLimitReachedArgs { if (identical(this, other)) return true; if (other is! DeviceLimitReachedArgs) return false; return key == other.key && - const _i48.ListEquality<_i47.UserResponse_Device>().equals( + const _i49.ListEquality<_i48.UserResponse_Device>().equals( devices, other.devices, ); @@ -528,18 +529,18 @@ class DeviceLimitReachedArgs { @override int get hashCode => key.hashCode ^ - const _i48.ListEquality<_i47.UserResponse_Device>().hash(devices); + const _i49.ListEquality<_i48.UserResponse_Device>().hash(devices); } /// generated route for /// [_i12.DownloadLinks] -class DownloadLinks extends _i44.PageRouteInfo { - const DownloadLinks({List<_i44.PageRouteInfo>? children}) +class DownloadLinks extends _i45.PageRouteInfo { + const DownloadLinks({List<_i45.PageRouteInfo>? children}) : super(DownloadLinks.name, initialChildren: children); static const String name = 'DownloadLinks'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i12.DownloadLinks(); @@ -549,13 +550,13 @@ class DownloadLinks extends _i44.PageRouteInfo { /// generated route for /// [_i13.FollowUs] -class FollowUs extends _i44.PageRouteInfo { - const FollowUs({List<_i44.PageRouteInfo>? children}) +class FollowUs extends _i45.PageRouteInfo { + const FollowUs({List<_i45.PageRouteInfo>? children}) : super(FollowUs.name, initialChildren: children); static const String name = 'FollowUs'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i13.FollowUs(); @@ -565,13 +566,13 @@ class FollowUs extends _i44.PageRouteInfo { /// generated route for /// [_i14.Home] -class Home extends _i44.PageRouteInfo { - const Home({List<_i44.PageRouteInfo>? children}) +class Home extends _i45.PageRouteInfo { + const Home({List<_i45.PageRouteInfo>? children}) : super(Home.name, initialChildren: children); static const String name = 'Home'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i14.Home(); @@ -581,13 +582,13 @@ class Home extends _i44.PageRouteInfo { /// generated route for /// [_i15.InviteFriends] -class InviteFriends extends _i44.PageRouteInfo { - const InviteFriends({List<_i44.PageRouteInfo>? children}) +class InviteFriends extends _i45.PageRouteInfo { + const InviteFriends({List<_i45.PageRouteInfo>? children}) : super(InviteFriends.name, initialChildren: children); static const String name = 'InviteFriends'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i15.InviteFriends(); @@ -597,11 +598,11 @@ class InviteFriends extends _i44.PageRouteInfo { /// generated route for /// [_i16.JoinPrivateServer] -class JoinPrivateServer extends _i44.PageRouteInfo { +class JoinPrivateServer extends _i45.PageRouteInfo { JoinPrivateServer({ - _i45.Key? key, + _i46.Key? key, Map? deepLinkData, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( JoinPrivateServer.name, args: JoinPrivateServerArgs(key: key, deepLinkData: deepLinkData), @@ -610,7 +611,7 @@ class JoinPrivateServer extends _i44.PageRouteInfo { static const String name = 'JoinPrivateServer'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -627,7 +628,7 @@ class JoinPrivateServer extends _i44.PageRouteInfo { class JoinPrivateServerArgs { const JoinPrivateServerArgs({this.key, this.deepLinkData}); - final _i45.Key? key; + final _i46.Key? key; final Map? deepLinkData; @@ -641,7 +642,7 @@ class JoinPrivateServerArgs { if (identical(this, other)) return true; if (other is! JoinPrivateServerArgs) return false; return key == other.key && - const _i48.MapEquality().equals( + const _i49.MapEquality().equals( deepLinkData, other.deepLinkData, ); @@ -650,18 +651,18 @@ class JoinPrivateServerArgs { @override int get hashCode => key.hashCode ^ - const _i48.MapEquality().hash(deepLinkData); + const _i49.MapEquality().hash(deepLinkData); } /// generated route for /// [_i17.Language] -class Language extends _i44.PageRouteInfo { - const Language({List<_i44.PageRouteInfo>? children}) +class Language extends _i45.PageRouteInfo { + const Language({List<_i45.PageRouteInfo>? children}) : super(Language.name, initialChildren: children); static const String name = 'Language'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i17.Language(); @@ -671,12 +672,12 @@ class Language extends _i44.PageRouteInfo { /// generated route for /// [_i18.LanternProLicense] -class LanternProLicense extends _i44.PageRouteInfo { +class LanternProLicense extends _i45.PageRouteInfo { LanternProLicense({ - _i45.Key? key, + _i46.Key? key, required String email, required String code, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( LanternProLicense.name, args: LanternProLicenseArgs(key: key, email: email, code: code), @@ -685,7 +686,7 @@ class LanternProLicense extends _i44.PageRouteInfo { static const String name = 'LanternProLicense'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -705,7 +706,7 @@ class LanternProLicenseArgs { required this.code, }); - final _i45.Key? key; + final _i46.Key? key; final String email; @@ -729,13 +730,13 @@ class LanternProLicenseArgs { /// generated route for /// [_i19.Logs] -class Logs extends _i44.PageRouteInfo { - const Logs({List<_i44.PageRouteInfo>? children}) +class Logs extends _i45.PageRouteInfo { + const Logs({List<_i45.PageRouteInfo>? children}) : super(Logs.name, initialChildren: children); static const String name = 'Logs'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i19.Logs(); @@ -745,13 +746,13 @@ class Logs extends _i44.PageRouteInfo { /// generated route for /// [_i20.MacOSExtensionDialog] -class MacOSExtensionDialog extends _i44.PageRouteInfo { - const MacOSExtensionDialog({List<_i44.PageRouteInfo>? children}) +class MacOSExtensionDialog extends _i45.PageRouteInfo { + const MacOSExtensionDialog({List<_i45.PageRouteInfo>? children}) : super(MacOSExtensionDialog.name, initialChildren: children); static const String name = 'MacOSExtensionDialog'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i20.MacOSExtensionDialog(); @@ -761,13 +762,13 @@ class MacOSExtensionDialog extends _i44.PageRouteInfo { /// generated route for /// [_i21.ManagePrivateServer] -class ManagePrivateServer extends _i44.PageRouteInfo { - const ManagePrivateServer({List<_i44.PageRouteInfo>? children}) +class ManagePrivateServer extends _i45.PageRouteInfo { + const ManagePrivateServer({List<_i45.PageRouteInfo>? children}) : super(ManagePrivateServer.name, initialChildren: children); static const String name = 'ManagePrivateServer'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i21.ManagePrivateServer(); @@ -777,13 +778,13 @@ class ManagePrivateServer extends _i44.PageRouteInfo { /// generated route for /// [_i22.ManuallyServerSetup] -class ManuallyServerSetup extends _i44.PageRouteInfo { - const ManuallyServerSetup({List<_i44.PageRouteInfo>? children}) +class ManuallyServerSetup extends _i45.PageRouteInfo { + const ManuallyServerSetup({List<_i45.PageRouteInfo>? children}) : super(ManuallyServerSetup.name, initialChildren: children); static const String name = 'ManuallyServerSetup'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i22.ManuallyServerSetup(); @@ -793,13 +794,13 @@ class ManuallyServerSetup extends _i44.PageRouteInfo { /// generated route for /// [_i23.Onboarding] -class Onboarding extends _i44.PageRouteInfo { - const Onboarding({List<_i44.PageRouteInfo>? children}) +class Onboarding extends _i45.PageRouteInfo { + const Onboarding({List<_i45.PageRouteInfo>? children}) : super(Onboarding.name, initialChildren: children); static const String name = 'Onboarding'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i23.Onboarding(); @@ -809,13 +810,13 @@ class Onboarding extends _i44.PageRouteInfo { /// generated route for /// [_i24.Plans] -class Plans extends _i44.PageRouteInfo { - const Plans({List<_i44.PageRouteInfo>? children}) +class Plans extends _i45.PageRouteInfo { + const Plans({List<_i45.PageRouteInfo>? children}) : super(Plans.name, initialChildren: children); static const String name = 'Plans'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i24.Plans(); @@ -825,13 +826,13 @@ class Plans extends _i44.PageRouteInfo { /// generated route for /// [_i25.PrivateServerAddBilling] -class PrivateServerAddBilling extends _i44.PageRouteInfo { - const PrivateServerAddBilling({List<_i44.PageRouteInfo>? children}) +class PrivateServerAddBilling extends _i45.PageRouteInfo { + const PrivateServerAddBilling({List<_i45.PageRouteInfo>? children}) : super(PrivateServerAddBilling.name, initialChildren: children); static const String name = 'PrivateServerAddBilling'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i25.PrivateServerAddBilling(); @@ -841,11 +842,11 @@ class PrivateServerAddBilling extends _i44.PageRouteInfo { /// generated route for /// [_i26.PrivateServerDeploy] -class PrivateServerDeploy extends _i44.PageRouteInfo { +class PrivateServerDeploy extends _i45.PageRouteInfo { PrivateServerDeploy({ - _i45.Key? key, + _i46.Key? key, required String serverName, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( PrivateServerDeploy.name, args: PrivateServerDeployArgs(key: key, serverName: serverName), @@ -854,7 +855,7 @@ class PrivateServerDeploy extends _i44.PageRouteInfo { static const String name = 'PrivateServerDeploy'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -869,7 +870,7 @@ class PrivateServerDeploy extends _i44.PageRouteInfo { class PrivateServerDeployArgs { const PrivateServerDeployArgs({this.key, required this.serverName}); - final _i45.Key? key; + final _i46.Key? key; final String serverName; @@ -892,14 +893,14 @@ class PrivateServerDeployArgs { /// generated route for /// [_i27.PrivateServerLocation] class PrivateServerLocation - extends _i44.PageRouteInfo { + extends _i45.PageRouteInfo { PrivateServerLocation({ - _i45.Key? key, + _i46.Key? key, required List location, required String? selectedLocation, required dynamic Function(String) onLocationSelected, - required _i46.CloudProvider provider, - List<_i44.PageRouteInfo>? children, + required _i47.CloudProvider provider, + List<_i45.PageRouteInfo>? children, }) : super( PrivateServerLocation.name, args: PrivateServerLocationArgs( @@ -914,7 +915,7 @@ class PrivateServerLocation static const String name = 'PrivateServerLocation'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -938,7 +939,7 @@ class PrivateServerLocationArgs { required this.provider, }); - final _i45.Key? key; + final _i46.Key? key; final List location; @@ -946,7 +947,7 @@ class PrivateServerLocationArgs { final dynamic Function(String) onLocationSelected; - final _i46.CloudProvider provider; + final _i47.CloudProvider provider; @override String toString() { @@ -958,7 +959,7 @@ class PrivateServerLocationArgs { if (identical(this, other)) return true; if (other is! PrivateServerLocationArgs) return false; return key == other.key && - const _i48.ListEquality().equals(location, other.location) && + const _i49.ListEquality().equals(location, other.location) && selectedLocation == other.selectedLocation && provider == other.provider; } @@ -966,20 +967,20 @@ class PrivateServerLocationArgs { @override int get hashCode => key.hashCode ^ - const _i48.ListEquality().hash(location) ^ + const _i49.ListEquality().hash(location) ^ selectedLocation.hashCode ^ provider.hashCode; } /// generated route for /// [_i28.PrivateServerSetup] -class PrivateServerSetup extends _i44.PageRouteInfo { - const PrivateServerSetup({List<_i44.PageRouteInfo>? children}) +class PrivateServerSetup extends _i45.PageRouteInfo { + const PrivateServerSetup({List<_i45.PageRouteInfo>? children}) : super(PrivateServerSetup.name, initialChildren: children); static const String name = 'PrivateServerSetup'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i28.PrivateServerSetup(); @@ -990,13 +991,13 @@ class PrivateServerSetup extends _i44.PageRouteInfo { /// generated route for /// [_i29.PrivateSeverDetails] class PrivateServerDetails - extends _i44.PageRouteInfo { + extends _i45.PageRouteInfo { PrivateServerDetails({ - _i45.Key? key, + _i46.Key? key, required List accounts, - required _i46.CloudProvider provider, + required _i47.CloudProvider provider, bool isPreFilled = false, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( PrivateServerDetails.name, args: PrivateServerDetailsArgs( @@ -1010,7 +1011,7 @@ class PrivateServerDetails static const String name = 'PrivateServerDetails'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1032,11 +1033,11 @@ class PrivateServerDetailsArgs { this.isPreFilled = false, }); - final _i45.Key? key; + final _i46.Key? key; final List accounts; - final _i46.CloudProvider provider; + final _i47.CloudProvider provider; final bool isPreFilled; @@ -1050,7 +1051,7 @@ class PrivateServerDetailsArgs { if (identical(this, other)) return true; if (other is! PrivateServerDetailsArgs) return false; return key == other.key && - const _i48.ListEquality().equals(accounts, other.accounts) && + const _i49.ListEquality().equals(accounts, other.accounts) && provider == other.provider && isPreFilled == other.isPreFilled; } @@ -1058,20 +1059,20 @@ class PrivateServerDetailsArgs { @override int get hashCode => key.hashCode ^ - const _i48.ListEquality().hash(accounts) ^ + const _i49.ListEquality().hash(accounts) ^ provider.hashCode ^ isPreFilled.hashCode; } /// generated route for /// [_i30.QrCodeScanner] -class QrCodeScanner extends _i44.PageRouteInfo { - const QrCodeScanner({List<_i44.PageRouteInfo>? children}) +class QrCodeScanner extends _i45.PageRouteInfo { + const QrCodeScanner({List<_i45.PageRouteInfo>? children}) : super(QrCodeScanner.name, initialChildren: children); static const String name = 'QrCodeScanner'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i30.QrCodeScanner(); @@ -1081,12 +1082,12 @@ class QrCodeScanner extends _i44.PageRouteInfo { /// generated route for /// [_i31.ReportIssue] -class ReportIssue extends _i44.PageRouteInfo { +class ReportIssue extends _i45.PageRouteInfo { ReportIssue({ - _i45.Key? key, + _i46.Key? key, String? description, String? type, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( ReportIssue.name, args: ReportIssueArgs(key: key, description: description, type: type), @@ -1095,7 +1096,7 @@ class ReportIssue extends _i44.PageRouteInfo { static const String name = 'ReportIssue'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -1113,7 +1114,7 @@ class ReportIssue extends _i44.PageRouteInfo { class ReportIssueArgs { const ReportIssueArgs({this.key, this.description, this.type}); - final _i45.Key? key; + final _i46.Key? key; final String? description; @@ -1139,12 +1140,12 @@ class ReportIssueArgs { /// generated route for /// [_i32.ResetPassword] -class ResetPassword extends _i44.PageRouteInfo { +class ResetPassword extends _i45.PageRouteInfo { ResetPassword({ - _i45.Key? key, + _i46.Key? key, required String email, required String code, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( ResetPassword.name, args: ResetPasswordArgs(key: key, email: email, code: code), @@ -1153,7 +1154,7 @@ class ResetPassword extends _i44.PageRouteInfo { static const String name = 'ResetPassword'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1169,7 +1170,7 @@ class ResetPassword extends _i44.PageRouteInfo { class ResetPasswordArgs { const ResetPasswordArgs({this.key, required this.email, required this.code}); - final _i45.Key? key; + final _i46.Key? key; final String email; @@ -1193,11 +1194,11 @@ class ResetPasswordArgs { /// generated route for /// [_i33.ResetPasswordEmail] -class ResetPasswordEmail extends _i44.PageRouteInfo { +class ResetPasswordEmail extends _i45.PageRouteInfo { ResetPasswordEmail({ - _i45.Key? key, + _i46.Key? key, String? email, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( ResetPasswordEmail.name, args: ResetPasswordEmailArgs(key: key, email: email), @@ -1206,7 +1207,7 @@ class ResetPasswordEmail extends _i44.PageRouteInfo { static const String name = 'ResetPasswordEmail'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -1220,7 +1221,7 @@ class ResetPasswordEmail extends _i44.PageRouteInfo { class ResetPasswordEmailArgs { const ResetPasswordEmailArgs({this.key, this.email}); - final _i45.Key? key; + final _i46.Key? key; final String? email; @@ -1242,13 +1243,13 @@ class ResetPasswordEmailArgs { /// generated route for /// [_i34.ServerSelection] -class ServerSelection extends _i44.PageRouteInfo { - const ServerSelection({List<_i44.PageRouteInfo>? children}) +class ServerSelection extends _i45.PageRouteInfo { + const ServerSelection({List<_i45.PageRouteInfo>? children}) : super(ServerSelection.name, initialChildren: children); static const String name = 'ServerSelection'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i34.ServerSelection(); @@ -1258,13 +1259,13 @@ class ServerSelection extends _i44.PageRouteInfo { /// generated route for /// [_i35.Setting] -class Setting extends _i44.PageRouteInfo { - const Setting({List<_i44.PageRouteInfo>? children}) +class Setting extends _i45.PageRouteInfo { + const Setting({List<_i45.PageRouteInfo>? children}) : super(Setting.name, initialChildren: children); static const String name = 'Setting'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i35.Setting(); @@ -1274,13 +1275,13 @@ class Setting extends _i44.PageRouteInfo { /// generated route for /// [_i36.SignInEmail] -class SignInEmail extends _i44.PageRouteInfo { - const SignInEmail({List<_i44.PageRouteInfo>? children}) +class SignInEmail extends _i45.PageRouteInfo { + const SignInEmail({List<_i45.PageRouteInfo>? children}) : super(SignInEmail.name, initialChildren: children); static const String name = 'SignInEmail'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i36.SignInEmail(); @@ -1290,12 +1291,12 @@ class SignInEmail extends _i44.PageRouteInfo { /// generated route for /// [_i37.SignInPassword] -class SignInPassword extends _i44.PageRouteInfo { +class SignInPassword extends _i45.PageRouteInfo { SignInPassword({ - _i45.Key? key, + _i46.Key? key, required String email, bool fromChangeEmail = false, - List<_i44.PageRouteInfo>? children, + List<_i45.PageRouteInfo>? children, }) : super( SignInPassword.name, args: SignInPasswordArgs( @@ -1308,7 +1309,7 @@ class SignInPassword extends _i44.PageRouteInfo { static const String name = 'SignInPassword'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1328,7 +1329,7 @@ class SignInPasswordArgs { this.fromChangeEmail = false, }); - final _i45.Key? key; + final _i46.Key? key; final String email; @@ -1354,13 +1355,13 @@ class SignInPasswordArgs { /// generated route for /// [_i38.SmartRouting] -class SmartRouting extends _i44.PageRouteInfo { - const SmartRouting({List<_i44.PageRouteInfo>? children}) +class SmartRouting extends _i45.PageRouteInfo { + const SmartRouting({List<_i45.PageRouteInfo>? children}) : super(SmartRouting.name, initialChildren: children); static const String name = 'SmartRouting'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i38.SmartRouting(); @@ -1370,13 +1371,13 @@ class SmartRouting extends _i44.PageRouteInfo { /// generated route for /// [_i39.SplitTunneling] -class SplitTunneling extends _i44.PageRouteInfo { - const SplitTunneling({List<_i44.PageRouteInfo>? children}) +class SplitTunneling extends _i45.PageRouteInfo { + const SplitTunneling({List<_i45.PageRouteInfo>? children}) : super(SplitTunneling.name, initialChildren: children); static const String name = 'SplitTunneling'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i39.SplitTunneling(); @@ -1386,13 +1387,13 @@ class SplitTunneling extends _i44.PageRouteInfo { /// generated route for /// [_i40.SplitTunnelingInfo] -class SplitTunnelingInfo extends _i44.PageRouteInfo { - const SplitTunnelingInfo({List<_i44.PageRouteInfo>? children}) +class SplitTunnelingInfo extends _i45.PageRouteInfo { + const SplitTunnelingInfo({List<_i45.PageRouteInfo>? children}) : super(SplitTunnelingInfo.name, initialChildren: children); static const String name = 'SplitTunnelingInfo'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i40.SplitTunnelingInfo(); @@ -1402,13 +1403,13 @@ class SplitTunnelingInfo extends _i44.PageRouteInfo { /// generated route for /// [_i41.Support] -class Support extends _i44.PageRouteInfo { - const Support({List<_i44.PageRouteInfo>? children}) +class Support extends _i45.PageRouteInfo { + const Support({List<_i45.PageRouteInfo>? children}) : super(Support.name, initialChildren: children); static const String name = 'Support'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { return const _i41.Support(); @@ -1417,33 +1418,49 @@ class Support extends _i44.PageRouteInfo { } /// generated route for -/// [_i42.VPNSetting] -class VPNSetting extends _i44.PageRouteInfo { - const VPNSetting({List<_i44.PageRouteInfo>? children}) +/// [_i42.UnboundedScreen] +class UnboundedScreen extends _i45.PageRouteInfo { + const UnboundedScreen({List<_i45.PageRouteInfo>? children}) + : super(UnboundedScreen.name, initialChildren: children); + + static const String name = 'UnboundedScreen'; + + static _i45.PageInfo page = _i45.PageInfo( + name, + builder: (data) { + return const _i42.UnboundedScreen(); + }, + ); +} + +/// generated route for +/// [_i43.VPNSetting] +class VPNSetting extends _i45.PageRouteInfo { + const VPNSetting({List<_i45.PageRouteInfo>? children}) : super(VPNSetting.name, initialChildren: children); static const String name = 'VPNSetting'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { - return const _i42.VPNSetting(); + return const _i43.VPNSetting(); }, ); } /// generated route for -/// [_i43.WebsiteSplitTunneling] -class WebsiteSplitTunneling extends _i44.PageRouteInfo { - const WebsiteSplitTunneling({List<_i44.PageRouteInfo>? children}) +/// [_i44.WebsiteSplitTunneling] +class WebsiteSplitTunneling extends _i45.PageRouteInfo { + const WebsiteSplitTunneling({List<_i45.PageRouteInfo>? children}) : super(WebsiteSplitTunneling.name, initialChildren: children); static const String name = 'WebsiteSplitTunneling'; - static _i44.PageInfo page = _i44.PageInfo( + static _i45.PageInfo page = _i45.PageInfo( name, builder: (data) { - return const _i43.WebsiteSplitTunneling(); + return const _i44.WebsiteSplitTunneling(); }, ); } diff --git a/lib/core/services/db/objectbox-model.json b/lib/core/services/db/objectbox-model.json index 064ce660ae..d5bb2ce7d3 100644 --- a/lib/core/services/db/objectbox-model.json +++ b/lib/core/services/db/objectbox-model.json @@ -59,7 +59,7 @@ }, { "id": "2:687217704776011576", - "lastPropertyId": "23:853140105317312758", + "lastPropertyId": "24:7256187684976579666", "name": "AppSetting", "properties": [ { @@ -147,6 +147,11 @@ "id": "23:853140105317312758", "name": "themeMode", "type": 9 + }, + { + "id": "24:7256187684976579666", + "name": "unboundedEnabled", + "type": 1 } ], "relations": [] diff --git a/lib/core/services/db/objectbox.g.dart b/lib/core/services/db/objectbox.g.dart index 83066d1841..018d74b812 100644 --- a/lib/core/services/db/objectbox.g.dart +++ b/lib/core/services/db/objectbox.g.dart @@ -93,7 +93,7 @@ final _entities = [ obx_int.ModelEntity( id: const obx_int.IdUid(2, 687217704776011576), name: 'AppSetting', - lastPropertyId: const obx_int.IdUid(23, 853140105317312758), + lastPropertyId: const obx_int.IdUid(24, 7256187684976579666), flags: 0, properties: [ obx_int.ModelProperty( @@ -198,6 +198,12 @@ final _entities = [ type: 9, flags: 0, ), + obx_int.ModelProperty( + id: const obx_int.IdUid(24, 7256187684976579666), + name: 'unboundedEnabled', + type: 1, + flags: 0, + ), ], relations: [], backlinks: [], @@ -1014,7 +1020,7 @@ obx_int.ModelDefinition getObjectBoxModel() { final dataCapThresholdOffset = fbb.writeString(object.dataCapThreshold); final environmentOffset = fbb.writeString(object.environment); final themeModeOffset = fbb.writeString(object.themeMode); - fbb.startTable(24); + fbb.startTable(25); fbb.addInt64(0, object.id); fbb.addBool(1, object.isPro); fbb.addBool(2, object.isSplitTunnelingOn); @@ -1032,6 +1038,7 @@ obx_int.ModelDefinition getObjectBoxModel() { fbb.addBool(20, object.onboardingCompleted); fbb.addOffset(21, environmentOffset); fbb.addOffset(22, themeModeOffset); + fbb.addBool(23, object.unboundedEnabled); fbb.finish(fbb.endTable()); return object.id; }, @@ -1119,6 +1126,12 @@ obx_int.ModelDefinition getObjectBoxModel() { final environmentParam = const fb.StringReader( asciiOptimization: true, ).vTableGet(buffer, rootOffset, 46, ''); + final unboundedEnabledParam = const fb.BoolReader().vTableGet( + buffer, + rootOffset, + 50, + false, + ); final object = AppSetting( id: idParam, isPro: isProParam, @@ -1137,6 +1150,7 @@ obx_int.ModelDefinition getObjectBoxModel() { onboardingCompleted: onboardingCompletedParam, themeMode: themeModeParam, environment: environmentParam, + unboundedEnabled: unboundedEnabledParam, ); return object; @@ -2077,6 +2091,11 @@ class AppSetting_ { static final themeMode = obx.QueryStringProperty( _entities[1].properties[16], ); + + /// See [AppSetting.unboundedEnabled]. + static final unboundedEnabled = obx.QueryBooleanProperty( + _entities[1].properties[17], + ); } /// [DeviceEntity] entity fields to define ObjectBox queries. diff --git a/lib/features/home/provider/app_event_notifier.dart b/lib/features/home/provider/app_event_notifier.dart index 591399b029..534b294099 100644 --- a/lib/features/home/provider/app_event_notifier.dart +++ b/lib/features/home/provider/app_event_notifier.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:i18n_extension/default.i18n.dart'; import 'package:lantern/core/common/app_eum.dart'; import 'package:lantern/core/models/datacap_info.dart'; @@ -17,6 +18,37 @@ import 'data_cap_info_provider.dart' show dataCapInfoProvider; part 'app_event_notifier.g.dart'; +/// Represents a consumer connection change from the broflake widget proxy. +class UnboundedConnectionEvent { + final int state; // 1 = connected, -1 = disconnected + final int workerIdx; + final String addr; // IP address + + UnboundedConnectionEvent({ + required this.state, + required this.workerIdx, + required this.addr, + }); + + factory UnboundedConnectionEvent.fromJson(Map json) { + return UnboundedConnectionEvent( + state: json['state'] as int, + workerIdx: json['workerIdx'] as int, + addr: json['addr'] as String? ?? '', + ); + } +} + +/// Global stream controller for unbounded connection events. +final _unboundedConnectionController = + StreamController.broadcast(); + +/// Provider that exposes unbounded connection events as a stream. +final unboundedConnectionProvider = + StreamProvider((ref) { + return _unboundedConnectionController.stream; +}); + /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them /// in one place. @@ -87,6 +119,15 @@ class AppEventNotifier extends _$AppEventNotifier { appLogger.error('Error parsing data-cap-event: $e'); } break; + case 'unbounded-connection': + try { + final data = jsonDecode(event.message); + final connEvent = UnboundedConnectionEvent.fromJson(data); + _unboundedConnectionController.add(connEvent); + } catch (e) { + appLogger.error('Error parsing unbounded-connection event: $e'); + } + break; default: break; } diff --git a/lib/features/home/provider/app_event_notifier.g.dart b/lib/features/home/provider/app_event_notifier.g.dart index 1e2d10aaa8..50612e750e 100644 --- a/lib/features/home/provider/app_event_notifier.g.dart +++ b/lib/features/home/provider/app_event_notifier.g.dart @@ -42,7 +42,7 @@ final class AppEventNotifierProvider AppEventNotifier create() => AppEventNotifier(); } -String _$appEventNotifierHash() => r'836d8ae705eb943827db277d28cb55fc1c4ac19d'; +String _$appEventNotifierHash() => r'c51a71f75787e810f536afc9c340a2c2a330895d'; /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them diff --git a/lib/features/home/provider/app_setting_notifier.dart b/lib/features/home/provider/app_setting_notifier.dart index 5f1597f15a..63a4ee4d66 100644 --- a/lib/features/home/provider/app_setting_notifier.dart +++ b/lib/features/home/provider/app_setting_notifier.dart @@ -106,6 +106,19 @@ class AppSettingNotifier extends _$AppSettingNotifier { }); } + void setUnboundedEnabled(bool value) { + final prev = state.unboundedEnabled; + update(state.copyWith(unboundedEnabled: value)); + + final svc = ref.read(lanternServiceProvider); + svc.setUnboundedEnabled(value).then((res) { + res.match((err) { + appLogger.error('setUnboundedEnabled failed: ${err.error}'); + update(state.copyWith(unboundedEnabled: prev)); + }, (_) {}); + }); + } + void updateAnonymousDataConsent(bool value) { update(state.copyWith(telemetryConsent: value)); updateTelemetryConsent(value); diff --git a/lib/features/home/provider/app_setting_notifier.g.dart b/lib/features/home/provider/app_setting_notifier.g.dart index 5b2f2c05cf..8cbde8c1bd 100644 --- a/lib/features/home/provider/app_setting_notifier.g.dart +++ b/lib/features/home/provider/app_setting_notifier.g.dart @@ -42,7 +42,7 @@ final class AppSettingNotifierProvider } String _$appSettingNotifierHash() => - r'355d669c54fd042f337506102347b94dcf12c03f'; + r'c2db7d1066d396c079bceb80b4c9ac94c04be6c3'; abstract class _$AppSettingNotifier extends $Notifier { AppSetting build(); diff --git a/lib/features/setting/setting.dart b/lib/features/setting/setting.dart index 85db9a06b1..e136cfb49e 100644 --- a/lib/features/setting/setting.dart +++ b/lib/features/setting/setting.dart @@ -229,7 +229,10 @@ class _SettingState extends ConsumerState { minHeight: 72, icon: AppImagePaths.lanternLogoRounded, iconUseThemeColor: false, - trailing: AppImage(path: AppImagePaths.outsideBrowser), + trailing: AppImage( + path: AppImagePaths.arrowForward, + height: 20, + ), label: 'unbounded'.i18n, subtitle: Text( 'help_fight_global_internet_censorship'.i18n, @@ -238,7 +241,7 @@ class _SettingState extends ConsumerState { ), ), onPressed: () { - UrlUtils.openUrl(AppUrls.unbounded); + appRouter.push(const UnboundedScreen()); }, ), ), diff --git a/lib/features/unbounded/unbounded.dart b/lib/features/unbounded/unbounded.dart new file mode 100644 index 0000000000..0ab792cf5b --- /dev/null +++ b/lib/features/unbounded/unbounded.dart @@ -0,0 +1,510 @@ +import 'package:auto_route/annotations.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lantern/core/common/common.dart'; +import 'package:lantern/core/widgets/switch_button.dart'; +import 'package:lantern/features/home/provider/app_event_notifier.dart'; +import 'package:lantern/features/home/provider/app_setting_notifier.dart'; + +@RoutePage(name: 'UnboundedScreen') +class UnboundedScreen extends HookConsumerWidget { + const UnboundedScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSetting = ref.watch(appSettingProvider); + final notifier = ref.read(appSettingProvider.notifier); + final textTheme = Theme.of(context).textTheme; + final isDark = Theme.of(context).brightness == Brightness.dark; + + return BaseScreen( + title: 'unbounded'.i18n, + padded: false, + body: Column( + children: [ + Expanded( + flex: 3, + child: _GlobeView(isDark: isDark), + ), + Expanded( + flex: 2, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AppCard( + padding: EdgeInsets.zero, + child: AppTile( + label: 'share_bandwidth'.i18n, + subtitle: Text( + 'help_fight_global_internet_censorship'.i18n, + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), + ), + icon: AppImagePaths.lanternLogoRounded, + iconUseThemeColor: false, + trailing: SwitchButton( + value: appSetting.unboundedEnabled, + onChanged: (bool? value) { + notifier.setUnboundedEnabled(value ?? false); + }, + ), + onPressed: () { + notifier + .setUnboundedEnabled(!appSetting.unboundedEnabled); + }, + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + 'unbounded_description'.i18n, + style: textTheme.bodySmall!.copyWith( + color: context.textTertiary, + ), + ), + ), + const Spacer(), + Center( + child: TextButton( + onPressed: () { + UrlUtils.openUrl(AppUrls.unbounded); + }, + child: Text( + 'learn_more'.i18n, + style: textTheme.labelLarge!.copyWith( + color: context.textLink, + ), + ), + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _GlobeView extends ConsumerStatefulWidget { + final bool isDark; + + const _GlobeView({required this.isDark}); + + @override + ConsumerState<_GlobeView> createState() => _GlobeViewState(); +} + +class _GlobeViewState extends ConsumerState<_GlobeView> { + InAppWebViewController? _controller; + bool _isLoading = true; + + void _handleConnectionEvent(UnboundedConnectionEvent event) { + if (_controller == null) return; + + _controller?.evaluateJavascript( + source: + "window.unboundedGlobe.handleMessage({type:'connectionEvent',state:${event.state},workerIdx:${event.workerIdx},addr:'${event.addr}'});", + ); + } + + @override + Widget build(BuildContext context) { + ref.listen(unboundedConnectionProvider, (prev, next) { + next.whenData((event) { + _handleConnectionEvent(event); + }); + }); + + return Stack( + children: [ + InAppWebView( + initialData: InAppWebViewInitialData( + data: _globeHtml, + baseUrl: WebUri('https://unpkg.com'), + mimeType: 'text/html', + encoding: 'utf-8', + ), + initialSettings: InAppWebViewSettings( + javaScriptEnabled: true, + transparentBackground: true, + hardwareAcceleration: true, + mediaPlaybackRequiresUserGesture: false, + supportZoom: false, + disableHorizontalScroll: true, + disableVerticalScroll: true, + allowUniversalAccessFromFileURLs: true, + allowFileAccessFromFileURLs: true, + ), + onWebViewCreated: (controller) { + _controller = controller; + }, + onLoadStop: (controller, url) { + setState(() => _isLoading = false); + _sendTheme(); + }, + onConsoleMessage: (controller, consoleMessage) { + debugPrint('Globe JS: ${consoleMessage.message}'); + }, + onReceivedError: (controller, request, error) { + debugPrint('Globe load error: ${error.description} for ${request.url}'); + }, + ), + if (_isLoading) + const Center( + child: CircularProgressIndicator( + color: Color(0xFF00BCD4), + ), + ), + ], + ); + } + + void _sendTheme() { + final theme = widget.isDark ? 'dark' : 'light'; + _controller?.evaluateJavascript( + source: + "window.unboundedGlobe.handleMessage({type:'setTheme',theme:'$theme'});", + ); + } + + @override + void didUpdateWidget(covariant _GlobeView oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.isDark != widget.isDark) { + _sendTheme(); + } + } +} + +// Globe HTML with built-in geo lookup and connection event handling +const _globeHtml = ''' + + + + + + + + +
+ + + + + + + +'''; diff --git a/lib/lantern/lantern_core_service.dart b/lib/lantern/lantern_core_service.dart index 022a5937f1..ded29a6f6d 100644 --- a/lib/lantern/lantern_core_service.dart +++ b/lib/lantern/lantern_core_service.dart @@ -45,6 +45,10 @@ abstract class LanternCoreService { Future> isBlockAdsEnabled(); + Future> setUnboundedEnabled(bool enabled); + + Future> isUnboundedEnabled(); + ///Payments methods Future> stipeSubscriptionPaymentRedirect( {required BillingType type, diff --git a/lib/lantern/lantern_ffi_service.dart b/lib/lantern/lantern_ffi_service.dart index c1527cdb89..483e12a761 100644 --- a/lib/lantern/lantern_ffi_service.dart +++ b/lib/lantern/lantern_ffi_service.dart @@ -1422,6 +1422,32 @@ class LanternFFIService implements LanternCoreService { } } + @override + Future> setUnboundedEnabled(bool enabled) async { + try { + final result = _ffiService + .setUnboundedEnabled(enabled ? 1 : 0) + .cast() + .toDartString(); + checkAPIError(result); + return right(unit); + } catch (e, st) { + appLogger.error('setUnboundedEnabled error: $e', e, st); + return Left(e.toFailure()); + } + } + + @override + Future> isUnboundedEnabled() async { + try { + final res = _ffiService.isUnboundedEnabled(); + return right(res != 0); + } catch (e, st) { + appLogger.error('isUnboundedEnabled error: $e', e, st); + return Left(e.toFailure()); + } + } + @override Future> triggerSystemExtension() { throw Exception("This is not supported on desktop"); diff --git a/lib/lantern/lantern_generated_bindings.dart b/lib/lantern/lantern_generated_bindings.dart index 132a871a13..d9abbe7b9d 100644 --- a/lib/lantern/lantern_generated_bindings.dart +++ b/lib/lantern/lantern_generated_bindings.dart @@ -6077,6 +6077,29 @@ class LanternBindings { _lookup>('isSmartRoutingEnabled'); late final _isSmartRoutingEnabled = _isSmartRoutingEnabledPtr.asFunction(); + + ffi.Pointer setUnboundedEnabled( + int enabled, + ) { + return _setUnboundedEnabled( + enabled, + ); + } + + late final _setUnboundedEnabledPtr = + _lookup Function(ffi.Int)>>( + 'setUnboundedEnabled'); + late final _setUnboundedEnabled = + _setUnboundedEnabledPtr.asFunction Function(int)>(); + + int isUnboundedEnabled() { + return _isUnboundedEnabled(); + } + + late final _isUnboundedEnabledPtr = + _lookup>('isUnboundedEnabled'); + late final _isUnboundedEnabled = + _isUnboundedEnabledPtr.asFunction(); } typedef va_list = ffi.Pointer; diff --git a/lib/lantern/lantern_platform_service.dart b/lib/lantern/lantern_platform_service.dart index 0c9efd9997..6a80a16bea 100644 --- a/lib/lantern/lantern_platform_service.dart +++ b/lib/lantern/lantern_platform_service.dart @@ -245,6 +245,30 @@ class LanternPlatformService implements LanternCoreService { } } + @override + Future> setUnboundedEnabled(bool enabled) async { + try { + await _methodChannel + .invokeMethod('setUnboundedEnabled', {'enabled': enabled}); + return right(unit); + } catch (e, st) { + appLogger.error('setUnboundedEnabled failed', e, st); + return Left(e.toFailure()); + } + } + + @override + Future> isUnboundedEnabled() async { + try { + final res = + await _methodChannel.invokeMethod('isUnboundedEnabled'); + return right(res ?? false); + } catch (e, st) { + appLogger.error('isUnboundedEnabled failed', e, st); + return Left(e.toFailure()); + } + } + List _mapToAppData( Iterable> rawApps, { required EnabledAppsSnapshot enabled, diff --git a/lib/lantern/lantern_service.dart b/lib/lantern/lantern_service.dart index 777f6386b3..7a3888591f 100644 --- a/lib/lantern/lantern_service.dart +++ b/lib/lantern/lantern_service.dart @@ -638,6 +638,22 @@ class LanternService implements LanternCoreService { return _platformService.setBlockAdsEnabled(enabled); } + @override + Future> setUnboundedEnabled(bool enabled) { + if (PlatformUtils.isFFISupported) { + return _ffiService.setUnboundedEnabled(enabled); + } + return _platformService.setUnboundedEnabled(enabled); + } + + @override + Future> isUnboundedEnabled() { + if (PlatformUtils.isFFISupported) { + return _ffiService.isUnboundedEnabled(); + } + return _platformService.isUnboundedEnabled(); + } + @override Future> attachReferralCode(String code) { if (PlatformUtils.isFFISupported) { diff --git a/pubspec.lock b/pubspec.lock index 8ad7171fcd..a93f390516 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1048,10 +1048,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1653,26 +1653,26 @@ packages: dependency: transitive description: name: test - sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" + sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" url: "https://pub.dev" source: hosted - version: "1.26.2" + version: "1.26.3" test_api: dependency: transitive description: name: test_api - sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.6" + version: "0.7.7" test_core: dependency: transitive description: name: test_core - sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" + sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" url: "https://pub.dev" source: hosted - version: "0.6.11" + version: "0.6.12" timezone: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f4b5235bc4..09c4620cc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -157,6 +157,7 @@ flutter: - assets/images/ - assets/images/flags/ - assets/locales/ + - assets/unbounded/ - app.env objectbox: From c47f1517714459da2c5755b98621156e46a7da45 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:25:53 -0700 Subject: [PATCH 2/8] Point to merged common on main, drop local replace Co-Authored-By: Claude Opus 4.6 --- go.mod | 4 +--- go.sum | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 15c113b9fd..880f82de4f 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,6 @@ go 1.25.4 replace github.com/getlantern/radiance => ../radiance -replace github.com/getlantern/common => ../common - replace github.com/getlantern/broflake => ../unbounded // replace github.com/getlantern/lantern-server-provisioner => ../lantern-server-provisioner @@ -174,7 +172,7 @@ require ( github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f // indirect github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 // indirect github.com/getlantern/broflake v0.0.0-20260221195823-510790b48bbe // indirect - github.com/getlantern/common v1.2.1-0.20260121160752-d8ee5791108f // indirect + github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 // indirect github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d // indirect github.com/getlantern/fronted v0.0.0-20260219001615-7eabaa834efe // indirect github.com/getlantern/golog v0.0.0-20230503153817-8e72de7e0a65 // indirect diff --git a/go.sum b/go.sum index ab6e1afc6c..74231a4039 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,8 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= +github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 h1:WhfyxuQ6cf7Ia9NF5PNkhCyfY2nhogXx/CCRRqy7DhI= +github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201 h1:oEZYEpZo28Wdx+5FZo4aU7JFXu0WG/4wJWese5reQSA= github.com/getlantern/context v0.0.0-20220418194847-3d5e7a086201/go.mod h1:Y9WZUHEb+mpra02CbQ/QczLUe6f0Dezxaw5DCJlJQGo= From 148436bdc13ba4d1dc65ef0970e057d5dffca8bb Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 12:52:16 -0700 Subject: [PATCH 3/8] Point to merged broflake on main, drop local replace Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 880f82de4f..8ba7bde376 100644 --- a/go.mod +++ b/go.mod @@ -171,7 +171,7 @@ require ( github.com/getlantern/algeneva v0.0.0-20250307163401-1824e7b54f52 // indirect github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f // indirect github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 // indirect - github.com/getlantern/broflake v0.0.0-20260221195823-510790b48bbe // indirect + github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911 // indirect github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 // indirect github.com/getlantern/dnstt v0.0.0-20260112160750-05100563bd0d // indirect github.com/getlantern/fronted v0.0.0-20260219001615-7eabaa834efe // indirect From df47b3e884d117dfe53dfdf63a864ee6d81eba69 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 14:32:46 -0700 Subject: [PATCH 4/8] Redesign Unbounded screen to match Figma designs - Add info banner, status cards with live/total connection counts, auto-enable checkbox, and welcome dialog to Unbounded screen - Add unbounded stats provider to track active and total connections - Add new AppSetting fields: autoEnableUnbounded, unboundedWelcomeSeen, hideUnbounded with corresponding setters - Create Unbounded Settings sub-page with auto-enable and hide toggles - Add UnboundedSettingsScreen route and link from Settings page - Conditionally hide Unbounded tile when hideUnbounded is enabled - Remove stray root-level flags.dart and system_tray_notifier.dart Co-Authored-By: Claude Opus 4.6 --- .../models/entity/app_setting_entity.dart | 12 + lib/core/router/router.dart | 4 + lib/core/router/router.gr.dart | 411 +++++++++--------- lib/core/services/db/objectbox-model.json | 17 +- lib/core/services/db/objectbox.g.dart | 61 ++- .../home/provider/app_event_notifier.dart | 48 ++ .../home/provider/app_event_notifier.g.dart | 58 +++ .../home/provider/app_setting_notifier.dart | 12 + .../home/provider/app_setting_notifier.g.dart | 2 +- lib/features/setting/setting.dart | 40 +- lib/features/unbounded/unbounded.dart | 283 +++++++++--- .../unbounded/unbounded_settings.dart | 68 +++ 12 files changed, 744 insertions(+), 272 deletions(-) create mode 100644 lib/features/unbounded/unbounded_settings.dart diff --git a/lib/core/models/entity/app_setting_entity.dart b/lib/core/models/entity/app_setting_entity.dart index 509040ab93..ba09feb28c 100644 --- a/lib/core/models/entity/app_setting_entity.dart +++ b/lib/core/models/entity/app_setting_entity.dart @@ -23,6 +23,9 @@ class AppSetting { String themeMode; String environment; bool unboundedEnabled; + bool autoEnableUnbounded; + bool unboundedWelcomeSeen; + bool hideUnbounded; AppSetting({ this.id = 0, @@ -43,6 +46,9 @@ class AppSetting { this.themeMode = 'system', this.environment = 'prod', this.unboundedEnabled = false, + this.autoEnableUnbounded = true, + this.unboundedWelcomeSeen = false, + this.hideUnbounded = false, }); AppSetting copyWith({ @@ -63,6 +69,9 @@ class AppSetting { String? themeMode, String? environment, bool? unboundedEnabled, + bool? autoEnableUnbounded, + bool? unboundedWelcomeSeen, + bool? hideUnbounded, }) { return AppSetting( id: id, @@ -83,6 +92,9 @@ class AppSetting { themeMode: themeMode ?? this.themeMode, environment: environment ?? this.environment, unboundedEnabled: unboundedEnabled ?? this.unboundedEnabled, + autoEnableUnbounded: autoEnableUnbounded ?? this.autoEnableUnbounded, + unboundedWelcomeSeen: unboundedWelcomeSeen ?? this.unboundedWelcomeSeen, + hideUnbounded: hideUnbounded ?? this.hideUnbounded, ); } diff --git a/lib/core/router/router.dart b/lib/core/router/router.dart index a06b7b4154..c1c5c69570 100644 --- a/lib/core/router/router.dart +++ b/lib/core/router/router.dart @@ -188,6 +188,10 @@ class AppRouter extends RootStackRouter { path: '/unbounded', page: UnboundedScreen.page, ), + AutoRoute( + path: '/unbounded-settings', + page: UnboundedSettingsScreen.page, + ), AutoRoute( path: '/intro', page: Onboarding.page, diff --git a/lib/core/router/router.gr.dart b/lib/core/router/router.gr.dart index 5cc1f7392f..47c56dd94b 100644 --- a/lib/core/router/router.gr.dart +++ b/lib/core/router/router.gr.dart @@ -9,10 +9,10 @@ // coverage:ignore-file // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i45; -import 'package:collection/collection.dart' as _i49; -import 'package:flutter/material.dart' as _i46; -import 'package:lantern/core/common/common.dart' as _i47; +import 'package:auto_route/auto_route.dart' as _i46; +import 'package:collection/collection.dart' as _i50; +import 'package:flutter/material.dart' as _i47; +import 'package:lantern/core/common/common.dart' as _i48; import 'package:lantern/core/widgets/app_webview.dart' as _i3; import 'package:lantern/features/account/account.dart' as _i1; import 'package:lantern/features/account/delete_account.dart' as _i9; @@ -58,28 +58,29 @@ import 'package:lantern/features/setting/follow_us.dart' as _i13; import 'package:lantern/features/setting/invite_friends.dart' as _i15; import 'package:lantern/features/setting/setting.dart' as _i35; import 'package:lantern/features/setting/smart_routing.dart' as _i38; -import 'package:lantern/features/setting/vpn_setting.dart' as _i43; +import 'package:lantern/features/setting/vpn_setting.dart' as _i44; import 'package:lantern/features/split_tunneling/apps_split_tunneling.dart' as _i5; import 'package:lantern/features/split_tunneling/split_tunneling.dart' as _i39; import 'package:lantern/features/split_tunneling/split_tunneling_info.dart' as _i40; import 'package:lantern/features/split_tunneling/website_split_tunneling.dart' - as _i44; + as _i45; import 'package:lantern/features/support/support.dart' as _i41; import 'package:lantern/features/unbounded/unbounded.dart' as _i42; +import 'package:lantern/features/unbounded/unbounded_settings.dart' as _i43; import 'package:lantern/features/vpn/server_selection.dart' as _i34; -import 'package:lantern/lantern/protos/protos/auth.pb.dart' as _i48; +import 'package:lantern/lantern/protos/protos/auth.pb.dart' as _i49; /// generated route for /// [_i1.Account] -class Account extends _i45.PageRouteInfo { - const Account({List<_i45.PageRouteInfo>? children}) +class Account extends _i46.PageRouteInfo { + const Account({List<_i46.PageRouteInfo>? children}) : super(Account.name, initialChildren: children); static const String name = 'Account'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i1.Account(); @@ -89,12 +90,12 @@ class Account extends _i45.PageRouteInfo { /// generated route for /// [_i2.AddEmail] -class AddEmail extends _i45.PageRouteInfo { +class AddEmail extends _i46.PageRouteInfo { AddEmail({ - _i46.Key? key, - _i47.AuthFlow authFlow = _i47.AuthFlow.signUp, + _i47.Key? key, + _i48.AuthFlow authFlow = _i48.AuthFlow.signUp, String? password, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( AddEmail.name, args: AddEmailArgs(key: key, authFlow: authFlow, password: password), @@ -103,7 +104,7 @@ class AddEmail extends _i45.PageRouteInfo { static const String name = 'AddEmail'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -121,13 +122,13 @@ class AddEmail extends _i45.PageRouteInfo { class AddEmailArgs { const AddEmailArgs({ this.key, - this.authFlow = _i47.AuthFlow.signUp, + this.authFlow = _i48.AuthFlow.signUp, this.password, }); - final _i46.Key? key; + final _i47.Key? key; - final _i47.AuthFlow authFlow; + final _i48.AuthFlow authFlow; final String? password; @@ -151,12 +152,12 @@ class AddEmailArgs { /// generated route for /// [_i3.AppWebView] -class AppWebview extends _i45.PageRouteInfo { +class AppWebview extends _i46.PageRouteInfo { AppWebview({ - _i46.Key? key, + _i47.Key? key, required String title, required String url, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( AppWebview.name, args: AppWebviewArgs(key: key, title: title, url: url), @@ -165,7 +166,7 @@ class AppWebview extends _i45.PageRouteInfo { static const String name = 'AppWebview'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -177,7 +178,7 @@ class AppWebview extends _i45.PageRouteInfo { class AppWebviewArgs { const AppWebviewArgs({this.key, required this.title, required this.url}); - final _i46.Key? key; + final _i47.Key? key; final String title; @@ -201,13 +202,13 @@ class AppWebviewArgs { /// generated route for /// [_i4.Appearance] -class Appearance extends _i45.PageRouteInfo { - const Appearance({List<_i45.PageRouteInfo>? children}) +class Appearance extends _i46.PageRouteInfo { + const Appearance({List<_i46.PageRouteInfo>? children}) : super(Appearance.name, initialChildren: children); static const String name = 'Appearance'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i4.Appearance(); @@ -217,13 +218,13 @@ class Appearance extends _i45.PageRouteInfo { /// generated route for /// [_i5.AppsSplitTunneling] -class AppsSplitTunneling extends _i45.PageRouteInfo { - const AppsSplitTunneling({List<_i45.PageRouteInfo>? children}) +class AppsSplitTunneling extends _i46.PageRouteInfo { + const AppsSplitTunneling({List<_i46.PageRouteInfo>? children}) : super(AppsSplitTunneling.name, initialChildren: children); static const String name = 'AppsSplitTunneling'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i5.AppsSplitTunneling(); @@ -233,13 +234,13 @@ class AppsSplitTunneling extends _i45.PageRouteInfo { /// generated route for /// [_i6.ChoosePaymentMethod] -class ChoosePaymentMethod extends _i45.PageRouteInfo { +class ChoosePaymentMethod extends _i46.PageRouteInfo { ChoosePaymentMethod({ - _i46.Key? key, + _i47.Key? key, required String email, String? code, - required _i47.AuthFlow authFlow, - List<_i45.PageRouteInfo>? children, + required _i48.AuthFlow authFlow, + List<_i46.PageRouteInfo>? children, }) : super( ChoosePaymentMethod.name, args: ChoosePaymentMethodArgs( @@ -253,7 +254,7 @@ class ChoosePaymentMethod extends _i45.PageRouteInfo { static const String name = 'ChoosePaymentMethod'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -275,13 +276,13 @@ class ChoosePaymentMethodArgs { required this.authFlow, }); - final _i46.Key? key; + final _i47.Key? key; final String email; final String? code; - final _i47.AuthFlow authFlow; + final _i48.AuthFlow authFlow; @override String toString() { @@ -305,13 +306,13 @@ class ChoosePaymentMethodArgs { /// generated route for /// [_i7.ConfirmEmail] -class ConfirmEmail extends _i45.PageRouteInfo { +class ConfirmEmail extends _i46.PageRouteInfo { ConfirmEmail({ - _i46.Key? key, + _i47.Key? key, required String email, String? password, - _i47.AuthFlow authFlow = _i47.AuthFlow.signUp, - List<_i45.PageRouteInfo>? children, + _i48.AuthFlow authFlow = _i48.AuthFlow.signUp, + List<_i46.PageRouteInfo>? children, }) : super( ConfirmEmail.name, args: ConfirmEmailArgs( @@ -325,7 +326,7 @@ class ConfirmEmail extends _i45.PageRouteInfo { static const String name = 'ConfirmEmail'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -344,16 +345,16 @@ class ConfirmEmailArgs { this.key, required this.email, this.password, - this.authFlow = _i47.AuthFlow.signUp, + this.authFlow = _i48.AuthFlow.signUp, }); - final _i46.Key? key; + final _i47.Key? key; final String email; final String? password; - final _i47.AuthFlow authFlow; + final _i48.AuthFlow authFlow; @override String toString() { @@ -377,13 +378,13 @@ class ConfirmEmailArgs { /// generated route for /// [_i8.CreatePassword] -class CreatePassword extends _i45.PageRouteInfo { +class CreatePassword extends _i46.PageRouteInfo { CreatePassword({ - _i46.Key? key, + _i47.Key? key, required String email, - required _i47.AuthFlow authFlow, + required _i48.AuthFlow authFlow, required String code, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( CreatePassword.name, args: CreatePasswordArgs( @@ -397,7 +398,7 @@ class CreatePassword extends _i45.PageRouteInfo { static const String name = 'CreatePassword'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -419,11 +420,11 @@ class CreatePasswordArgs { required this.code, }); - final _i46.Key? key; + final _i47.Key? key; final String email; - final _i47.AuthFlow authFlow; + final _i48.AuthFlow authFlow; final String code; @@ -449,13 +450,13 @@ class CreatePasswordArgs { /// generated route for /// [_i9.DeleteAccount] -class DeleteAccount extends _i45.PageRouteInfo { - const DeleteAccount({List<_i45.PageRouteInfo>? children}) +class DeleteAccount extends _i46.PageRouteInfo { + const DeleteAccount({List<_i46.PageRouteInfo>? children}) : super(DeleteAccount.name, initialChildren: children); static const String name = 'DeleteAccount'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i9.DeleteAccount(); @@ -465,13 +466,13 @@ class DeleteAccount extends _i45.PageRouteInfo { /// generated route for /// [_i10.DeveloperMode] -class DeveloperMode extends _i45.PageRouteInfo { - const DeveloperMode({List<_i45.PageRouteInfo>? children}) +class DeveloperMode extends _i46.PageRouteInfo { + const DeveloperMode({List<_i46.PageRouteInfo>? children}) : super(DeveloperMode.name, initialChildren: children); static const String name = 'DeveloperMode'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i10.DeveloperMode(); @@ -481,11 +482,11 @@ class DeveloperMode extends _i45.PageRouteInfo { /// generated route for /// [_i11.DeviceLimitReached] -class DeviceLimitReached extends _i45.PageRouteInfo { +class DeviceLimitReached extends _i46.PageRouteInfo { DeviceLimitReached({ - _i46.Key? key, - required List<_i48.UserResponse_Device> devices, - List<_i45.PageRouteInfo>? children, + _i47.Key? key, + required List<_i49.UserResponse_Device> devices, + List<_i46.PageRouteInfo>? children, }) : super( DeviceLimitReached.name, args: DeviceLimitReachedArgs(key: key, devices: devices), @@ -494,7 +495,7 @@ class DeviceLimitReached extends _i45.PageRouteInfo { static const String name = 'DeviceLimitReached'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -506,9 +507,9 @@ class DeviceLimitReached extends _i45.PageRouteInfo { class DeviceLimitReachedArgs { const DeviceLimitReachedArgs({this.key, required this.devices}); - final _i46.Key? key; + final _i47.Key? key; - final List<_i48.UserResponse_Device> devices; + final List<_i49.UserResponse_Device> devices; @override String toString() { @@ -520,7 +521,7 @@ class DeviceLimitReachedArgs { if (identical(this, other)) return true; if (other is! DeviceLimitReachedArgs) return false; return key == other.key && - const _i49.ListEquality<_i48.UserResponse_Device>().equals( + const _i50.ListEquality<_i49.UserResponse_Device>().equals( devices, other.devices, ); @@ -529,18 +530,18 @@ class DeviceLimitReachedArgs { @override int get hashCode => key.hashCode ^ - const _i49.ListEquality<_i48.UserResponse_Device>().hash(devices); + const _i50.ListEquality<_i49.UserResponse_Device>().hash(devices); } /// generated route for /// [_i12.DownloadLinks] -class DownloadLinks extends _i45.PageRouteInfo { - const DownloadLinks({List<_i45.PageRouteInfo>? children}) +class DownloadLinks extends _i46.PageRouteInfo { + const DownloadLinks({List<_i46.PageRouteInfo>? children}) : super(DownloadLinks.name, initialChildren: children); static const String name = 'DownloadLinks'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i12.DownloadLinks(); @@ -550,13 +551,13 @@ class DownloadLinks extends _i45.PageRouteInfo { /// generated route for /// [_i13.FollowUs] -class FollowUs extends _i45.PageRouteInfo { - const FollowUs({List<_i45.PageRouteInfo>? children}) +class FollowUs extends _i46.PageRouteInfo { + const FollowUs({List<_i46.PageRouteInfo>? children}) : super(FollowUs.name, initialChildren: children); static const String name = 'FollowUs'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i13.FollowUs(); @@ -566,13 +567,13 @@ class FollowUs extends _i45.PageRouteInfo { /// generated route for /// [_i14.Home] -class Home extends _i45.PageRouteInfo { - const Home({List<_i45.PageRouteInfo>? children}) +class Home extends _i46.PageRouteInfo { + const Home({List<_i46.PageRouteInfo>? children}) : super(Home.name, initialChildren: children); static const String name = 'Home'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i14.Home(); @@ -582,13 +583,13 @@ class Home extends _i45.PageRouteInfo { /// generated route for /// [_i15.InviteFriends] -class InviteFriends extends _i45.PageRouteInfo { - const InviteFriends({List<_i45.PageRouteInfo>? children}) +class InviteFriends extends _i46.PageRouteInfo { + const InviteFriends({List<_i46.PageRouteInfo>? children}) : super(InviteFriends.name, initialChildren: children); static const String name = 'InviteFriends'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i15.InviteFriends(); @@ -598,11 +599,11 @@ class InviteFriends extends _i45.PageRouteInfo { /// generated route for /// [_i16.JoinPrivateServer] -class JoinPrivateServer extends _i45.PageRouteInfo { +class JoinPrivateServer extends _i46.PageRouteInfo { JoinPrivateServer({ - _i46.Key? key, + _i47.Key? key, Map? deepLinkData, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( JoinPrivateServer.name, args: JoinPrivateServerArgs(key: key, deepLinkData: deepLinkData), @@ -611,7 +612,7 @@ class JoinPrivateServer extends _i45.PageRouteInfo { static const String name = 'JoinPrivateServer'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -628,7 +629,7 @@ class JoinPrivateServer extends _i45.PageRouteInfo { class JoinPrivateServerArgs { const JoinPrivateServerArgs({this.key, this.deepLinkData}); - final _i46.Key? key; + final _i47.Key? key; final Map? deepLinkData; @@ -642,7 +643,7 @@ class JoinPrivateServerArgs { if (identical(this, other)) return true; if (other is! JoinPrivateServerArgs) return false; return key == other.key && - const _i49.MapEquality().equals( + const _i50.MapEquality().equals( deepLinkData, other.deepLinkData, ); @@ -651,18 +652,18 @@ class JoinPrivateServerArgs { @override int get hashCode => key.hashCode ^ - const _i49.MapEquality().hash(deepLinkData); + const _i50.MapEquality().hash(deepLinkData); } /// generated route for /// [_i17.Language] -class Language extends _i45.PageRouteInfo { - const Language({List<_i45.PageRouteInfo>? children}) +class Language extends _i46.PageRouteInfo { + const Language({List<_i46.PageRouteInfo>? children}) : super(Language.name, initialChildren: children); static const String name = 'Language'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i17.Language(); @@ -672,12 +673,12 @@ class Language extends _i45.PageRouteInfo { /// generated route for /// [_i18.LanternProLicense] -class LanternProLicense extends _i45.PageRouteInfo { +class LanternProLicense extends _i46.PageRouteInfo { LanternProLicense({ - _i46.Key? key, + _i47.Key? key, required String email, required String code, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( LanternProLicense.name, args: LanternProLicenseArgs(key: key, email: email, code: code), @@ -686,7 +687,7 @@ class LanternProLicense extends _i45.PageRouteInfo { static const String name = 'LanternProLicense'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -706,7 +707,7 @@ class LanternProLicenseArgs { required this.code, }); - final _i46.Key? key; + final _i47.Key? key; final String email; @@ -730,13 +731,13 @@ class LanternProLicenseArgs { /// generated route for /// [_i19.Logs] -class Logs extends _i45.PageRouteInfo { - const Logs({List<_i45.PageRouteInfo>? children}) +class Logs extends _i46.PageRouteInfo { + const Logs({List<_i46.PageRouteInfo>? children}) : super(Logs.name, initialChildren: children); static const String name = 'Logs'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i19.Logs(); @@ -746,13 +747,13 @@ class Logs extends _i45.PageRouteInfo { /// generated route for /// [_i20.MacOSExtensionDialog] -class MacOSExtensionDialog extends _i45.PageRouteInfo { - const MacOSExtensionDialog({List<_i45.PageRouteInfo>? children}) +class MacOSExtensionDialog extends _i46.PageRouteInfo { + const MacOSExtensionDialog({List<_i46.PageRouteInfo>? children}) : super(MacOSExtensionDialog.name, initialChildren: children); static const String name = 'MacOSExtensionDialog'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i20.MacOSExtensionDialog(); @@ -762,13 +763,13 @@ class MacOSExtensionDialog extends _i45.PageRouteInfo { /// generated route for /// [_i21.ManagePrivateServer] -class ManagePrivateServer extends _i45.PageRouteInfo { - const ManagePrivateServer({List<_i45.PageRouteInfo>? children}) +class ManagePrivateServer extends _i46.PageRouteInfo { + const ManagePrivateServer({List<_i46.PageRouteInfo>? children}) : super(ManagePrivateServer.name, initialChildren: children); static const String name = 'ManagePrivateServer'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i21.ManagePrivateServer(); @@ -778,13 +779,13 @@ class ManagePrivateServer extends _i45.PageRouteInfo { /// generated route for /// [_i22.ManuallyServerSetup] -class ManuallyServerSetup extends _i45.PageRouteInfo { - const ManuallyServerSetup({List<_i45.PageRouteInfo>? children}) +class ManuallyServerSetup extends _i46.PageRouteInfo { + const ManuallyServerSetup({List<_i46.PageRouteInfo>? children}) : super(ManuallyServerSetup.name, initialChildren: children); static const String name = 'ManuallyServerSetup'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i22.ManuallyServerSetup(); @@ -794,13 +795,13 @@ class ManuallyServerSetup extends _i45.PageRouteInfo { /// generated route for /// [_i23.Onboarding] -class Onboarding extends _i45.PageRouteInfo { - const Onboarding({List<_i45.PageRouteInfo>? children}) +class Onboarding extends _i46.PageRouteInfo { + const Onboarding({List<_i46.PageRouteInfo>? children}) : super(Onboarding.name, initialChildren: children); static const String name = 'Onboarding'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i23.Onboarding(); @@ -810,13 +811,13 @@ class Onboarding extends _i45.PageRouteInfo { /// generated route for /// [_i24.Plans] -class Plans extends _i45.PageRouteInfo { - const Plans({List<_i45.PageRouteInfo>? children}) +class Plans extends _i46.PageRouteInfo { + const Plans({List<_i46.PageRouteInfo>? children}) : super(Plans.name, initialChildren: children); static const String name = 'Plans'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i24.Plans(); @@ -826,13 +827,13 @@ class Plans extends _i45.PageRouteInfo { /// generated route for /// [_i25.PrivateServerAddBilling] -class PrivateServerAddBilling extends _i45.PageRouteInfo { - const PrivateServerAddBilling({List<_i45.PageRouteInfo>? children}) +class PrivateServerAddBilling extends _i46.PageRouteInfo { + const PrivateServerAddBilling({List<_i46.PageRouteInfo>? children}) : super(PrivateServerAddBilling.name, initialChildren: children); static const String name = 'PrivateServerAddBilling'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i25.PrivateServerAddBilling(); @@ -842,11 +843,11 @@ class PrivateServerAddBilling extends _i45.PageRouteInfo { /// generated route for /// [_i26.PrivateServerDeploy] -class PrivateServerDeploy extends _i45.PageRouteInfo { +class PrivateServerDeploy extends _i46.PageRouteInfo { PrivateServerDeploy({ - _i46.Key? key, + _i47.Key? key, required String serverName, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( PrivateServerDeploy.name, args: PrivateServerDeployArgs(key: key, serverName: serverName), @@ -855,7 +856,7 @@ class PrivateServerDeploy extends _i45.PageRouteInfo { static const String name = 'PrivateServerDeploy'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -870,7 +871,7 @@ class PrivateServerDeploy extends _i45.PageRouteInfo { class PrivateServerDeployArgs { const PrivateServerDeployArgs({this.key, required this.serverName}); - final _i46.Key? key; + final _i47.Key? key; final String serverName; @@ -893,14 +894,14 @@ class PrivateServerDeployArgs { /// generated route for /// [_i27.PrivateServerLocation] class PrivateServerLocation - extends _i45.PageRouteInfo { + extends _i46.PageRouteInfo { PrivateServerLocation({ - _i46.Key? key, + _i47.Key? key, required List location, required String? selectedLocation, required dynamic Function(String) onLocationSelected, - required _i47.CloudProvider provider, - List<_i45.PageRouteInfo>? children, + required _i48.CloudProvider provider, + List<_i46.PageRouteInfo>? children, }) : super( PrivateServerLocation.name, args: PrivateServerLocationArgs( @@ -915,7 +916,7 @@ class PrivateServerLocation static const String name = 'PrivateServerLocation'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -939,7 +940,7 @@ class PrivateServerLocationArgs { required this.provider, }); - final _i46.Key? key; + final _i47.Key? key; final List location; @@ -947,7 +948,7 @@ class PrivateServerLocationArgs { final dynamic Function(String) onLocationSelected; - final _i47.CloudProvider provider; + final _i48.CloudProvider provider; @override String toString() { @@ -959,7 +960,7 @@ class PrivateServerLocationArgs { if (identical(this, other)) return true; if (other is! PrivateServerLocationArgs) return false; return key == other.key && - const _i49.ListEquality().equals(location, other.location) && + const _i50.ListEquality().equals(location, other.location) && selectedLocation == other.selectedLocation && provider == other.provider; } @@ -967,20 +968,20 @@ class PrivateServerLocationArgs { @override int get hashCode => key.hashCode ^ - const _i49.ListEquality().hash(location) ^ + const _i50.ListEquality().hash(location) ^ selectedLocation.hashCode ^ provider.hashCode; } /// generated route for /// [_i28.PrivateServerSetup] -class PrivateServerSetup extends _i45.PageRouteInfo { - const PrivateServerSetup({List<_i45.PageRouteInfo>? children}) +class PrivateServerSetup extends _i46.PageRouteInfo { + const PrivateServerSetup({List<_i46.PageRouteInfo>? children}) : super(PrivateServerSetup.name, initialChildren: children); static const String name = 'PrivateServerSetup'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i28.PrivateServerSetup(); @@ -991,13 +992,13 @@ class PrivateServerSetup extends _i45.PageRouteInfo { /// generated route for /// [_i29.PrivateSeverDetails] class PrivateServerDetails - extends _i45.PageRouteInfo { + extends _i46.PageRouteInfo { PrivateServerDetails({ - _i46.Key? key, + _i47.Key? key, required List accounts, - required _i47.CloudProvider provider, + required _i48.CloudProvider provider, bool isPreFilled = false, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( PrivateServerDetails.name, args: PrivateServerDetailsArgs( @@ -1011,7 +1012,7 @@ class PrivateServerDetails static const String name = 'PrivateServerDetails'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1033,11 +1034,11 @@ class PrivateServerDetailsArgs { this.isPreFilled = false, }); - final _i46.Key? key; + final _i47.Key? key; final List accounts; - final _i47.CloudProvider provider; + final _i48.CloudProvider provider; final bool isPreFilled; @@ -1051,7 +1052,7 @@ class PrivateServerDetailsArgs { if (identical(this, other)) return true; if (other is! PrivateServerDetailsArgs) return false; return key == other.key && - const _i49.ListEquality().equals(accounts, other.accounts) && + const _i50.ListEquality().equals(accounts, other.accounts) && provider == other.provider && isPreFilled == other.isPreFilled; } @@ -1059,20 +1060,20 @@ class PrivateServerDetailsArgs { @override int get hashCode => key.hashCode ^ - const _i49.ListEquality().hash(accounts) ^ + const _i50.ListEquality().hash(accounts) ^ provider.hashCode ^ isPreFilled.hashCode; } /// generated route for /// [_i30.QrCodeScanner] -class QrCodeScanner extends _i45.PageRouteInfo { - const QrCodeScanner({List<_i45.PageRouteInfo>? children}) +class QrCodeScanner extends _i46.PageRouteInfo { + const QrCodeScanner({List<_i46.PageRouteInfo>? children}) : super(QrCodeScanner.name, initialChildren: children); static const String name = 'QrCodeScanner'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i30.QrCodeScanner(); @@ -1082,12 +1083,12 @@ class QrCodeScanner extends _i45.PageRouteInfo { /// generated route for /// [_i31.ReportIssue] -class ReportIssue extends _i45.PageRouteInfo { +class ReportIssue extends _i46.PageRouteInfo { ReportIssue({ - _i46.Key? key, + _i47.Key? key, String? description, String? type, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( ReportIssue.name, args: ReportIssueArgs(key: key, description: description, type: type), @@ -1096,7 +1097,7 @@ class ReportIssue extends _i45.PageRouteInfo { static const String name = 'ReportIssue'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -1114,7 +1115,7 @@ class ReportIssue extends _i45.PageRouteInfo { class ReportIssueArgs { const ReportIssueArgs({this.key, this.description, this.type}); - final _i46.Key? key; + final _i47.Key? key; final String? description; @@ -1140,12 +1141,12 @@ class ReportIssueArgs { /// generated route for /// [_i32.ResetPassword] -class ResetPassword extends _i45.PageRouteInfo { +class ResetPassword extends _i46.PageRouteInfo { ResetPassword({ - _i46.Key? key, + _i47.Key? key, required String email, required String code, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( ResetPassword.name, args: ResetPasswordArgs(key: key, email: email, code: code), @@ -1154,7 +1155,7 @@ class ResetPassword extends _i45.PageRouteInfo { static const String name = 'ResetPassword'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1170,7 +1171,7 @@ class ResetPassword extends _i45.PageRouteInfo { class ResetPasswordArgs { const ResetPasswordArgs({this.key, required this.email, required this.code}); - final _i46.Key? key; + final _i47.Key? key; final String email; @@ -1194,11 +1195,11 @@ class ResetPasswordArgs { /// generated route for /// [_i33.ResetPasswordEmail] -class ResetPasswordEmail extends _i45.PageRouteInfo { +class ResetPasswordEmail extends _i46.PageRouteInfo { ResetPasswordEmail({ - _i46.Key? key, + _i47.Key? key, String? email, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( ResetPasswordEmail.name, args: ResetPasswordEmailArgs(key: key, email: email), @@ -1207,7 +1208,7 @@ class ResetPasswordEmail extends _i45.PageRouteInfo { static const String name = 'ResetPasswordEmail'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs( @@ -1221,7 +1222,7 @@ class ResetPasswordEmail extends _i45.PageRouteInfo { class ResetPasswordEmailArgs { const ResetPasswordEmailArgs({this.key, this.email}); - final _i46.Key? key; + final _i47.Key? key; final String? email; @@ -1243,13 +1244,13 @@ class ResetPasswordEmailArgs { /// generated route for /// [_i34.ServerSelection] -class ServerSelection extends _i45.PageRouteInfo { - const ServerSelection({List<_i45.PageRouteInfo>? children}) +class ServerSelection extends _i46.PageRouteInfo { + const ServerSelection({List<_i46.PageRouteInfo>? children}) : super(ServerSelection.name, initialChildren: children); static const String name = 'ServerSelection'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i34.ServerSelection(); @@ -1259,13 +1260,13 @@ class ServerSelection extends _i45.PageRouteInfo { /// generated route for /// [_i35.Setting] -class Setting extends _i45.PageRouteInfo { - const Setting({List<_i45.PageRouteInfo>? children}) +class Setting extends _i46.PageRouteInfo { + const Setting({List<_i46.PageRouteInfo>? children}) : super(Setting.name, initialChildren: children); static const String name = 'Setting'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i35.Setting(); @@ -1275,13 +1276,13 @@ class Setting extends _i45.PageRouteInfo { /// generated route for /// [_i36.SignInEmail] -class SignInEmail extends _i45.PageRouteInfo { - const SignInEmail({List<_i45.PageRouteInfo>? children}) +class SignInEmail extends _i46.PageRouteInfo { + const SignInEmail({List<_i46.PageRouteInfo>? children}) : super(SignInEmail.name, initialChildren: children); static const String name = 'SignInEmail'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i36.SignInEmail(); @@ -1291,12 +1292,12 @@ class SignInEmail extends _i45.PageRouteInfo { /// generated route for /// [_i37.SignInPassword] -class SignInPassword extends _i45.PageRouteInfo { +class SignInPassword extends _i46.PageRouteInfo { SignInPassword({ - _i46.Key? key, + _i47.Key? key, required String email, bool fromChangeEmail = false, - List<_i45.PageRouteInfo>? children, + List<_i46.PageRouteInfo>? children, }) : super( SignInPassword.name, args: SignInPasswordArgs( @@ -1309,7 +1310,7 @@ class SignInPassword extends _i45.PageRouteInfo { static const String name = 'SignInPassword'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { final args = data.argsAs(); @@ -1329,7 +1330,7 @@ class SignInPasswordArgs { this.fromChangeEmail = false, }); - final _i46.Key? key; + final _i47.Key? key; final String email; @@ -1355,13 +1356,13 @@ class SignInPasswordArgs { /// generated route for /// [_i38.SmartRouting] -class SmartRouting extends _i45.PageRouteInfo { - const SmartRouting({List<_i45.PageRouteInfo>? children}) +class SmartRouting extends _i46.PageRouteInfo { + const SmartRouting({List<_i46.PageRouteInfo>? children}) : super(SmartRouting.name, initialChildren: children); static const String name = 'SmartRouting'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i38.SmartRouting(); @@ -1371,13 +1372,13 @@ class SmartRouting extends _i45.PageRouteInfo { /// generated route for /// [_i39.SplitTunneling] -class SplitTunneling extends _i45.PageRouteInfo { - const SplitTunneling({List<_i45.PageRouteInfo>? children}) +class SplitTunneling extends _i46.PageRouteInfo { + const SplitTunneling({List<_i46.PageRouteInfo>? children}) : super(SplitTunneling.name, initialChildren: children); static const String name = 'SplitTunneling'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i39.SplitTunneling(); @@ -1387,13 +1388,13 @@ class SplitTunneling extends _i45.PageRouteInfo { /// generated route for /// [_i40.SplitTunnelingInfo] -class SplitTunnelingInfo extends _i45.PageRouteInfo { - const SplitTunnelingInfo({List<_i45.PageRouteInfo>? children}) +class SplitTunnelingInfo extends _i46.PageRouteInfo { + const SplitTunnelingInfo({List<_i46.PageRouteInfo>? children}) : super(SplitTunnelingInfo.name, initialChildren: children); static const String name = 'SplitTunnelingInfo'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i40.SplitTunnelingInfo(); @@ -1403,13 +1404,13 @@ class SplitTunnelingInfo extends _i45.PageRouteInfo { /// generated route for /// [_i41.Support] -class Support extends _i45.PageRouteInfo { - const Support({List<_i45.PageRouteInfo>? children}) +class Support extends _i46.PageRouteInfo { + const Support({List<_i46.PageRouteInfo>? children}) : super(Support.name, initialChildren: children); static const String name = 'Support'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i41.Support(); @@ -1419,13 +1420,13 @@ class Support extends _i45.PageRouteInfo { /// generated route for /// [_i42.UnboundedScreen] -class UnboundedScreen extends _i45.PageRouteInfo { - const UnboundedScreen({List<_i45.PageRouteInfo>? children}) +class UnboundedScreen extends _i46.PageRouteInfo { + const UnboundedScreen({List<_i46.PageRouteInfo>? children}) : super(UnboundedScreen.name, initialChildren: children); static const String name = 'UnboundedScreen'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { return const _i42.UnboundedScreen(); @@ -1434,33 +1435,49 @@ class UnboundedScreen extends _i45.PageRouteInfo { } /// generated route for -/// [_i43.VPNSetting] -class VPNSetting extends _i45.PageRouteInfo { - const VPNSetting({List<_i45.PageRouteInfo>? children}) +/// [_i43.UnboundedSettingsScreen] +class UnboundedSettingsScreen extends _i46.PageRouteInfo { + const UnboundedSettingsScreen({List<_i46.PageRouteInfo>? children}) + : super(UnboundedSettingsScreen.name, initialChildren: children); + + static const String name = 'UnboundedSettingsScreen'; + + static _i46.PageInfo page = _i46.PageInfo( + name, + builder: (data) { + return const _i43.UnboundedSettingsScreen(); + }, + ); +} + +/// generated route for +/// [_i44.VPNSetting] +class VPNSetting extends _i46.PageRouteInfo { + const VPNSetting({List<_i46.PageRouteInfo>? children}) : super(VPNSetting.name, initialChildren: children); static const String name = 'VPNSetting'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { - return const _i43.VPNSetting(); + return const _i44.VPNSetting(); }, ); } /// generated route for -/// [_i44.WebsiteSplitTunneling] -class WebsiteSplitTunneling extends _i45.PageRouteInfo { - const WebsiteSplitTunneling({List<_i45.PageRouteInfo>? children}) +/// [_i45.WebsiteSplitTunneling] +class WebsiteSplitTunneling extends _i46.PageRouteInfo { + const WebsiteSplitTunneling({List<_i46.PageRouteInfo>? children}) : super(WebsiteSplitTunneling.name, initialChildren: children); static const String name = 'WebsiteSplitTunneling'; - static _i45.PageInfo page = _i45.PageInfo( + static _i46.PageInfo page = _i46.PageInfo( name, builder: (data) { - return const _i44.WebsiteSplitTunneling(); + return const _i45.WebsiteSplitTunneling(); }, ); } diff --git a/lib/core/services/db/objectbox-model.json b/lib/core/services/db/objectbox-model.json index d5bb2ce7d3..d39b299ede 100644 --- a/lib/core/services/db/objectbox-model.json +++ b/lib/core/services/db/objectbox-model.json @@ -59,7 +59,7 @@ }, { "id": "2:687217704776011576", - "lastPropertyId": "24:7256187684976579666", + "lastPropertyId": "27:1490069847328736408", "name": "AppSetting", "properties": [ { @@ -152,6 +152,21 @@ "id": "24:7256187684976579666", "name": "unboundedEnabled", "type": 1 + }, + { + "id": "25:4271307681061492505", + "name": "autoEnableUnbounded", + "type": 1 + }, + { + "id": "26:6311603266760598594", + "name": "unboundedWelcomeSeen", + "type": 1 + }, + { + "id": "27:1490069847328736408", + "name": "hideUnbounded", + "type": 1 } ], "relations": [] diff --git a/lib/core/services/db/objectbox.g.dart b/lib/core/services/db/objectbox.g.dart index 018d74b812..d839ab083b 100644 --- a/lib/core/services/db/objectbox.g.dart +++ b/lib/core/services/db/objectbox.g.dart @@ -93,7 +93,7 @@ final _entities = [ obx_int.ModelEntity( id: const obx_int.IdUid(2, 687217704776011576), name: 'AppSetting', - lastPropertyId: const obx_int.IdUid(24, 7256187684976579666), + lastPropertyId: const obx_int.IdUid(27, 1490069847328736408), flags: 0, properties: [ obx_int.ModelProperty( @@ -204,6 +204,24 @@ final _entities = [ type: 1, flags: 0, ), + obx_int.ModelProperty( + id: const obx_int.IdUid(25, 4271307681061492505), + name: 'autoEnableUnbounded', + type: 1, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(26, 6311603266760598594), + name: 'unboundedWelcomeSeen', + type: 1, + flags: 0, + ), + obx_int.ModelProperty( + id: const obx_int.IdUid(27, 1490069847328736408), + name: 'hideUnbounded', + type: 1, + flags: 0, + ), ], relations: [], backlinks: [], @@ -1020,7 +1038,7 @@ obx_int.ModelDefinition getObjectBoxModel() { final dataCapThresholdOffset = fbb.writeString(object.dataCapThreshold); final environmentOffset = fbb.writeString(object.environment); final themeModeOffset = fbb.writeString(object.themeMode); - fbb.startTable(25); + fbb.startTable(28); fbb.addInt64(0, object.id); fbb.addBool(1, object.isPro); fbb.addBool(2, object.isSplitTunnelingOn); @@ -1039,6 +1057,9 @@ obx_int.ModelDefinition getObjectBoxModel() { fbb.addOffset(21, environmentOffset); fbb.addOffset(22, themeModeOffset); fbb.addBool(23, object.unboundedEnabled); + fbb.addBool(24, object.autoEnableUnbounded); + fbb.addBool(25, object.unboundedWelcomeSeen); + fbb.addBool(26, object.hideUnbounded); fbb.finish(fbb.endTable()); return object.id; }, @@ -1132,6 +1153,24 @@ obx_int.ModelDefinition getObjectBoxModel() { 50, false, ); + final autoEnableUnboundedParam = const fb.BoolReader().vTableGet( + buffer, + rootOffset, + 52, + false, + ); + final unboundedWelcomeSeenParam = const fb.BoolReader().vTableGet( + buffer, + rootOffset, + 54, + false, + ); + final hideUnboundedParam = const fb.BoolReader().vTableGet( + buffer, + rootOffset, + 56, + false, + ); final object = AppSetting( id: idParam, isPro: isProParam, @@ -1151,6 +1190,9 @@ obx_int.ModelDefinition getObjectBoxModel() { themeMode: themeModeParam, environment: environmentParam, unboundedEnabled: unboundedEnabledParam, + autoEnableUnbounded: autoEnableUnboundedParam, + unboundedWelcomeSeen: unboundedWelcomeSeenParam, + hideUnbounded: hideUnboundedParam, ); return object; @@ -2096,6 +2138,21 @@ class AppSetting_ { static final unboundedEnabled = obx.QueryBooleanProperty( _entities[1].properties[17], ); + + /// See [AppSetting.autoEnableUnbounded]. + static final autoEnableUnbounded = obx.QueryBooleanProperty( + _entities[1].properties[18], + ); + + /// See [AppSetting.unboundedWelcomeSeen]. + static final unboundedWelcomeSeen = obx.QueryBooleanProperty( + _entities[1].properties[19], + ); + + /// See [AppSetting.hideUnbounded]. + static final hideUnbounded = obx.QueryBooleanProperty( + _entities[1].properties[20], + ); } /// [DeviceEntity] entity fields to define ObjectBox queries. diff --git a/lib/features/home/provider/app_event_notifier.dart b/lib/features/home/provider/app_event_notifier.dart index 534b294099..c17f5151fd 100644 --- a/lib/features/home/provider/app_event_notifier.dart +++ b/lib/features/home/provider/app_event_notifier.dart @@ -49,6 +49,54 @@ final unboundedConnectionProvider = return _unboundedConnectionController.stream; }); +/// Tracks live and cumulative connection counts for Unbounded. +class UnboundedStats { + final int activeCount; + final int totalCount; + + const UnboundedStats({this.activeCount = 0, this.totalCount = 0}); +} + +/// Provider that tracks unbounded connection stats (active + total). +final unboundedStatsProvider = Provider((ref) { + ref.watch(unboundedConnectionProvider); + return _UnboundedStatsAccumulator.stats; +}); + +/// Simple static accumulator for unbounded stats. Updated by the listener +/// installed below. We use a static because the stats must survive provider +/// rebuilds. +class _UnboundedStatsAccumulator { + static UnboundedStats stats = const UnboundedStats(); +} + +/// A keep-alive provider whose sole job is to listen to connection events and +/// accumulate stats in [_UnboundedStatsAccumulator]. +@Riverpod(keepAlive: true) +class UnboundedStatsListener extends _$UnboundedStatsListener { + @override + void build() { + ref.listen(unboundedConnectionProvider, (prev, next) { + next.whenData((event) { + final s = _UnboundedStatsAccumulator.stats; + if (event.state == 1) { + _UnboundedStatsAccumulator.stats = UnboundedStats( + activeCount: s.activeCount + 1, + totalCount: s.totalCount + 1, + ); + } else if (event.state == -1) { + _UnboundedStatsAccumulator.stats = UnboundedStats( + activeCount: (s.activeCount - 1).clamp(0, s.activeCount), + totalCount: s.totalCount, + ); + } + // Invalidate the stats provider so watchers rebuild. + ref.invalidate(unboundedStatsProvider); + }); + }); + } +} + /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them /// in one place. diff --git a/lib/features/home/provider/app_event_notifier.g.dart b/lib/features/home/provider/app_event_notifier.g.dart index 50612e750e..f440a13da1 100644 --- a/lib/features/home/provider/app_event_notifier.g.dart +++ b/lib/features/home/provider/app_event_notifier.g.dart @@ -8,6 +8,64 @@ part of 'app_event_notifier.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning +/// A keep-alive provider whose sole job is to listen to connection events and +/// accumulate stats in [_UnboundedStatsAccumulator]. + +@ProviderFor(UnboundedStatsListener) +const unboundedStatsListenerProvider = UnboundedStatsListenerProvider._(); + +/// A keep-alive provider whose sole job is to listen to connection events and +/// accumulate stats in [_UnboundedStatsAccumulator]. +final class UnboundedStatsListenerProvider + extends $NotifierProvider { + /// A keep-alive provider whose sole job is to listen to connection events and + /// accumulate stats in [_UnboundedStatsAccumulator]. + const UnboundedStatsListenerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'unboundedStatsListenerProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$unboundedStatsListenerHash(); + + @$internal + @override + UnboundedStatsListener create() => UnboundedStatsListener(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$unboundedStatsListenerHash() => + r'7e38010a65e7b4d8ddd441a2bdef65f515d7a6af'; + +/// A keep-alive provider whose sole job is to listen to connection events and +/// accumulate stats in [_UnboundedStatsAccumulator]. + +abstract class _$UnboundedStatsListener extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref; + final element = ref.element as $ClassProviderElement< + AnyNotifier, void, Object?, Object?>; + element.handleValue(ref, null); + } +} + /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them /// in one place. diff --git a/lib/features/home/provider/app_setting_notifier.dart b/lib/features/home/provider/app_setting_notifier.dart index 63a4ee4d66..846016b8cd 100644 --- a/lib/features/home/provider/app_setting_notifier.dart +++ b/lib/features/home/provider/app_setting_notifier.dart @@ -119,6 +119,18 @@ class AppSettingNotifier extends _$AppSettingNotifier { }); } + void setAutoEnableUnbounded(bool value) { + update(state.copyWith(autoEnableUnbounded: value)); + } + + void setUnboundedWelcomeSeen(bool value) { + update(state.copyWith(unboundedWelcomeSeen: value)); + } + + void setHideUnbounded(bool value) { + update(state.copyWith(hideUnbounded: value)); + } + void updateAnonymousDataConsent(bool value) { update(state.copyWith(telemetryConsent: value)); updateTelemetryConsent(value); diff --git a/lib/features/home/provider/app_setting_notifier.g.dart b/lib/features/home/provider/app_setting_notifier.g.dart index 8cbde8c1bd..91b8fb52f3 100644 --- a/lib/features/home/provider/app_setting_notifier.g.dart +++ b/lib/features/home/provider/app_setting_notifier.g.dart @@ -42,7 +42,7 @@ final class AppSettingNotifierProvider } String _$appSettingNotifierHash() => - r'c2db7d1066d396c079bceb80b4c9ac94c04be6c3'; + r'6830d1af12a28788b3cd599e7fe6d957a38498b2'; abstract class _$AppSettingNotifier extends $Notifier { AppSetting build(); diff --git a/lib/features/setting/setting.dart b/lib/features/setting/setting.dart index e136cfb49e..04b9c81fde 100644 --- a/lib/features/setting/setting.dart +++ b/lib/features/setting/setting.dart @@ -224,24 +224,36 @@ class _SettingState extends ConsumerState { ), ), const SizedBox(height: 4), - Card( + if (!appSetting.hideUnbounded) + Card( + child: AppTile( + minHeight: 72, + icon: AppImagePaths.lanternLogoRounded, + iconUseThemeColor: false, + trailing: AppImage( + path: AppImagePaths.arrowForward, + height: 20, + ), + label: 'unbounded'.i18n, + subtitle: Text( + 'help_fight_global_internet_censorship'.i18n, + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), + ), + onPressed: () { + appRouter.push(const UnboundedScreen()); + }, + ), + ), + AppCard( + padding: EdgeInsets.zero, child: AppTile( - minHeight: 72, + label: 'Unbounded Settings', icon: AppImagePaths.lanternLogoRounded, iconUseThemeColor: false, - trailing: AppImage( - path: AppImagePaths.arrowForward, - height: 20, - ), - label: 'unbounded'.i18n, - subtitle: Text( - 'help_fight_global_internet_censorship'.i18n, - style: textTheme.labelMedium!.copyWith( - color: context.textTertiary, - ), - ), onPressed: () { - appRouter.push(const UnboundedScreen()); + appRouter.push(const UnboundedSettingsScreen()); }, ), ), diff --git a/lib/features/unbounded/unbounded.dart b/lib/features/unbounded/unbounded.dart index 0ab792cf5b..b3b615d58a 100644 --- a/lib/features/unbounded/unbounded.dart +++ b/lib/features/unbounded/unbounded.dart @@ -15,76 +15,209 @@ class UnboundedScreen extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final appSetting = ref.watch(appSettingProvider); final notifier = ref.read(appSettingProvider.notifier); - final textTheme = Theme.of(context).textTheme; final isDark = Theme.of(context).brightness == Brightness.dark; + final stats = ref.watch(unboundedStatsProvider); + + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!appSetting.unboundedWelcomeSeen) { + notifier.setUnboundedWelcomeSeen(true); + _showWelcomeDialog(context); + } + }); return BaseScreen( title: 'unbounded'.i18n, padded: false, body: Column( children: [ + _InfoBanner(), Expanded( flex: 3, child: _GlobeView(isDark: isDark), ), - Expanded( - flex: 2, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AppCard( - padding: EdgeInsets.zero, - child: AppTile( - label: 'share_bandwidth'.i18n, - subtitle: Text( - 'help_fight_global_internet_censorship'.i18n, - style: textTheme.labelMedium!.copyWith( - color: context.textTertiary, - ), - ), - icon: AppImagePaths.lanternLogoRounded, - iconUseThemeColor: false, - trailing: SwitchButton( - value: appSetting.unboundedEnabled, - onChanged: (bool? value) { - notifier.setUnboundedEnabled(value ?? false); - }, - ), - onPressed: () { - notifier - .setUnboundedEnabled(!appSetting.unboundedEnabled); - }, - ), - ), - const SizedBox(height: 16), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: Text( - 'unbounded_description'.i18n, - style: textTheme.bodySmall!.copyWith( - color: context.textTertiary, - ), - ), - ), - const Spacer(), - Center( - child: TextButton( - onPressed: () { - UrlUtils.openUrl(AppUrls.unbounded); - }, - child: Text( - 'learn_more'.i18n, - style: textTheme.labelLarge!.copyWith( - color: context.textLink, - ), - ), - ), - ), - ], + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + children: [ + _StatusSection( + enabled: appSetting.unboundedEnabled, + onToggle: notifier.setUnboundedEnabled, + helpingNow: stats.activeCount, + totalHelped: stats.totalCount, + ), + const SizedBox(height: 12), + _AutoEnableRow( + value: appSetting.autoEnableUnbounded, + onChanged: notifier.setAutoEnableUnbounded, + ), + const SizedBox(height: 16), + ], + ), + ), + ], + ), + ); + } + + void _showWelcomeDialog(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + AppDialog.customDialog( + context: context, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 24), + Icon( + Icons.handshake_outlined, + size: 48, + color: context.textLink, + ), + const SizedBox(height: 16), + Text( + 'Welcome to Unbounded', + style: textTheme.headlineSmall, + textAlign: TextAlign.center, + ), + const SizedBox(height: 12), + Text( + 'Unbounded lets you share a small amount of your internet bandwidth to help people in censored countries access the open web.', + style: textTheme.bodyMedium!.copyWith( + color: context.textSecondary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 8), + Text( + 'Your connection stays secure and private. You control when sharing is active.', + style: textTheme.bodyMedium!.copyWith( + color: context.textSecondary, + ), + textAlign: TextAlign.center, + ), + ], + ), + action: [ + AppTextButton( + label: 'learn_more'.i18n, + onPressed: () { + UrlUtils.openUrl(AppUrls.unbounded); + }, + ), + AppTextButton( + label: 'Got It', + onPressed: () { + appRouter.maybePop(); + }, + ), + ], + ); + } +} + +class _InfoBanner extends StatelessWidget { + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + return Padding( + padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: context.bgCallout, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + Icons.info_outline, + size: 18, + color: context.textSecondary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Help others bypass censorship by securely sharing your connection.', + style: textTheme.bodySmall!.copyWith( + color: context.textSecondary, + ), ), ), + ], + ), + ), + ); + } +} + +class _StatusSection extends StatelessWidget { + final bool enabled; + final ValueChanged onToggle; + final int helpingNow; + final int totalHelped; + + const _StatusSection({ + required this.enabled, + required this.onToggle, + required this.helpingNow, + required this.totalHelped, + }); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + final statusColor = enabled ? AppColors.green5 : context.textTertiary; + final countColor = AppColors.green5; + + return AppCard( + padding: const EdgeInsets.symmetric(horizontal: 0), + child: Column( + children: [ + ListTile( + dense: true, + leading: Icon(Icons.language, color: context.textSecondary, size: 22), + title: Row( + children: [ + Text( + 'Status: ', + style: textTheme.titleSmall, + ), + Text( + enabled ? 'Enabled' : 'Disabled', + style: textTheme.titleSmall!.copyWith(color: statusColor), + ), + ], + ), + trailing: SwitchButton( + value: enabled, + onChanged: onToggle, + ), + ), + DividerSpace(), + ListTile( + dense: true, + leading: Icon(Icons.person_outline, color: context.textSecondary, size: 22), + title: Text( + 'People you are helping right now:', + style: textTheme.bodySmall!.copyWith(color: context.textSecondary), + ), + trailing: Text( + '$helpingNow', + style: textTheme.titleSmall!.copyWith(color: countColor), + ), + ), + DividerSpace(), + ListTile( + dense: true, + leading: Icon(Icons.people_outline, color: context.textSecondary, size: 22), + title: Text( + 'Total people helped to date:', + style: textTheme.bodySmall!.copyWith(color: context.textSecondary), + ), + trailing: Text( + '$totalHelped', + style: textTheme.titleSmall!.copyWith(color: countColor), + ), ), ], ), @@ -92,6 +225,42 @@ class UnboundedScreen extends HookConsumerWidget { } } +class _AutoEnableRow extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const _AutoEnableRow({ + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return AppCard( + padding: EdgeInsets.zero, + child: ListTile( + leading: Icon(Icons.settings_outlined, color: context.textSecondary, size: 22), + title: Text( + 'Auto-enable Unbounded', + style: textTheme.titleSmall, + ), + subtitle: Text( + 'Turn on automatically when Lantern is open', + style: textTheme.bodySmall!.copyWith(color: context.textTertiary), + ), + trailing: Checkbox( + value: value, + onChanged: (val) => onChanged(val ?? false), + activeColor: context.textLink, + ), + onTap: () => onChanged(!value), + ), + ); + } +} + class _GlobeView extends ConsumerStatefulWidget { final bool isDark; diff --git a/lib/features/unbounded/unbounded_settings.dart b/lib/features/unbounded/unbounded_settings.dart new file mode 100644 index 0000000000..0e11b4ef64 --- /dev/null +++ b/lib/features/unbounded/unbounded_settings.dart @@ -0,0 +1,68 @@ +import 'package:auto_route/annotations.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lantern/core/common/common.dart'; +import 'package:lantern/core/widgets/switch_button.dart'; +import 'package:lantern/features/home/provider/app_setting_notifier.dart'; + +@RoutePage(name: 'UnboundedSettingsScreen') +class UnboundedSettingsScreen extends HookConsumerWidget { + const UnboundedSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appSetting = ref.watch(appSettingProvider); + final notifier = ref.read(appSettingProvider.notifier); + final textTheme = Theme.of(context).textTheme; + + return BaseScreen( + title: 'Unbounded Settings', + body: Column( + children: [ + const SizedBox(height: defaultSize), + AppCard( + padding: EdgeInsets.zero, + child: AppTile( + label: 'Auto-enable Unbounded', + subtitle: Text( + 'Turn on automatically when Lantern is open', + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), + ), + icon: Icon(Icons.play_circle_outline, size: 22), + trailing: Checkbox( + value: appSetting.autoEnableUnbounded, + onChanged: (val) => + notifier.setAutoEnableUnbounded(val ?? false), + activeColor: context.textLink, + ), + onPressed: () => notifier + .setAutoEnableUnbounded(!appSetting.autoEnableUnbounded), + ), + ), + const SizedBox(height: defaultSize), + AppCard( + padding: EdgeInsets.zero, + child: AppTile( + label: 'Hide Unbounded', + subtitle: Text( + 'Removes Unbounded from the UI', + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), + ), + icon: Icon(Icons.visibility_off_outlined, size: 22), + trailing: SwitchButton( + value: appSetting.hideUnbounded, + onChanged: notifier.setHideUnbounded, + ), + onPressed: () => + notifier.setHideUnbounded(!appSetting.hideUnbounded), + ), + ), + ], + ), + ); + } +} From 48bbd1dd16e7771e710d5fcd8524bb89f435c05f Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Mon, 23 Feb 2026 14:37:02 -0700 Subject: [PATCH 5/8] Use unbounded on main --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 8ba7bde376..0685006514 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.25.4 replace github.com/getlantern/radiance => ../radiance -replace github.com/getlantern/broflake => ../unbounded +// replace github.com/getlantern/broflake => ../unbounded // replace github.com/getlantern/lantern-server-provisioner => ../lantern-server-provisioner diff --git a/go.sum b/go.sum index 74231a4039..30b58c3d63 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,8 @@ github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f h1:NLGftemDrbGf7Wce github.com/getlantern/amp v0.0.0-20260113204224-600f8e8dfe5f/go.mod h1:qnMv9szb8JK3kA9W4N2FlYUMj1GkA0x7QEUEPD7tk4o= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01 h1:Mmeh4/DA1OKN9tVWRAvTL5efFx4c7v9/55hoK17NclA= github.com/getlantern/appdir v0.0.0-20250324200952-507a0625eb01/go.mod h1:3vR6+jQdWfWojZ77w+htCqEF5MO/Y2twJOpAvFuM9po= +github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911 h1:HmTK1otLrb3zOi3b+7VEODbKdMKKxs0Ka6Ux17PjgLc= +github.com/getlantern/broflake v0.0.0-20260223195036-4065257e0911/go.mod h1:b40tuN7lS/j6OXOlUyo4m/CY+JbZ/dj1SBOtGfqPUKI= github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7 h1:WhfyxuQ6cf7Ia9NF5PNkhCyfY2nhogXx/CCRRqy7DhI= github.com/getlantern/common v1.2.1-0.20260223192400-cc00002ef6c7/go.mod h1:eSSuV4bMPgQJnczBw+KWWqWNo1itzmVxC++qUBPRTt0= github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY= From 7d6139b55b02a7579385ae5e2809268fdc376c75 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Tue, 24 Feb 2026 13:27:07 -0700 Subject: [PATCH 6/8] Point to radiance adam/unbounded-widget-proxy, drop local replace Co-Authored-By: Claude Opus 4.6 --- go.mod | 6 +++--- go.sum | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 0685006514..fa432c2863 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/getlantern/lantern go 1.25.4 -replace github.com/getlantern/radiance => ../radiance +// replace github.com/getlantern/radiance => ../radiance // replace github.com/getlantern/broflake => ../unbounded @@ -28,7 +28,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/alecthomas/assert/v2 v2.3.0 github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 - github.com/getlantern/radiance v0.0.0-20260221215045-6049f134d863 + github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031 github.com/sagernet/sing-box v1.12.22 golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 golang.org/x/sys v0.40.0 @@ -179,7 +179,7 @@ require ( github.com/getlantern/iptool v0.0.0-20230112135223-c00e863b2696 // indirect github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d // indirect github.com/getlantern/kindling v0.0.0-20260219202502-df15c15dc5fb // indirect - github.com/getlantern/lantern-box v0.0.6-0.20260220213333-4b20583e43ff // indirect + github.com/getlantern/lantern-box v0.0.6-0.20260224193811-b50ea1f2e9db // indirect github.com/getlantern/lantern-water v0.0.0-20260130212632-d5ea08838250 // indirect github.com/getlantern/mtime v0.0.0-20200417132445-23682092d1f7 // indirect github.com/getlantern/netx v0.0.0-20240830183145-c257516187f0 // indirect diff --git a/go.sum b/go.sum index 30b58c3d63..a4121524e7 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,8 @@ github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d h1:2/9rPC1x github.com/getlantern/keepcurrent v0.0.0-20240126172110-2e0264ca385d/go.mod h1:enUAvxkJ15QUtTKOKoO9WJV2L5u33P8YmqkC+iu8iT4= github.com/getlantern/kindling v0.0.0-20260219202502-df15c15dc5fb h1:ATakfsW9OOvlnoU++kz/zG2kK9JCQdUdFWd2N2QwI2w= github.com/getlantern/kindling v0.0.0-20260219202502-df15c15dc5fb/go.mod h1:8t/DQxsfk7LBzHFwpv+qOLlj2bG8vL5Ckw73Y0/FY1s= -github.com/getlantern/lantern-box v0.0.6-0.20260220213333-4b20583e43ff h1:r6iJ6hcsemDVG62zpVDgAGscObWPZcO8NlL5T2ZpNWw= -github.com/getlantern/lantern-box v0.0.6-0.20260220213333-4b20583e43ff/go.mod h1:OnSmUR2+rpmGcS5DA0iUyEPwfPEEftnEtj2A6rBq+ko= +github.com/getlantern/lantern-box v0.0.6-0.20260224193811-b50ea1f2e9db h1:N+qr2VV6w8hpXBQuDHI/u4/3fQzgFRhV1dM47GyNi5M= +github.com/getlantern/lantern-box v0.0.6-0.20260224193811-b50ea1f2e9db/go.mod h1:OnSmUR2+rpmGcS5DA0iUyEPwfPEEftnEtj2A6rBq+ko= github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 h1:6seyD2f9tz2am0YQd/Qn+q7LFiiQgnmxgwWFnVceGZw= github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9/go.mod h1:s0VKrlJf/z+M0U8IKHFL2hfuflocRw3SINmMacrTlMA= github.com/getlantern/lantern-water v0.0.0-20260130212632-d5ea08838250 h1:xculJyC6hS0kNSQKWBP1FQbpSVmeJyhUGID804jgKCA= @@ -246,6 +246,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded h1:qA1oi5so1/C6psHLPlyPGyq6JhZsPvA4EutsNhjzodc= github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= +github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031 h1:7I8fcTXTzUwNrLm+XYzGbsus8TsVhtMUjeczTV8Tymw= +github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031/go.mod h1:bdUNd0PV0wR1/VdEWjTuFfkp2JSMvLTTyizz2BFMd+4= github.com/getlantern/samizdat v0.0.2 h1:PkMu6jsfUz7DLZUH2xh548XfzgPASmq5CajZyUKj/9Y= github.com/getlantern/samizdat v0.0.2/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= From 44f1a14c6dce766edf08ef816cac13889a750612 Mon Sep 17 00:00:00 2001 From: Adam Fisk Date: Tue, 24 Feb 2026 13:37:47 -0700 Subject: [PATCH 7/8] Update radiance to latest on adam/unbounded-widget-proxy (7520e33) Co-Authored-By: Claude Opus 4.6 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index fa432c2863..e8af6502a3 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/Microsoft/go-winio v0.6.2 github.com/alecthomas/assert/v2 v2.3.0 github.com/getlantern/lantern-server-provisioner v0.0.0-20251031121934-8ea031fccfa9 - github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031 + github.com/getlantern/radiance v0.0.0-20260224202913-7520e330886d github.com/sagernet/sing-box v1.12.22 golang.org/x/mobile v0.0.0-20250711185624-d5bb5ecc55c0 golang.org/x/sys v0.40.0 diff --git a/go.sum b/go.sum index a4121524e7..599bf44767 100644 --- a/go.sum +++ b/go.sum @@ -246,8 +246,8 @@ github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535 h1:rtDmW8YL github.com/getlantern/pluriconfig v0.0.0-20251126214241-8cc8bc561535/go.mod h1:WKJEdjMOD4IuTRYwjQHjT4bmqDl5J82RShMLxPAvi0Q= github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded h1:qA1oi5so1/C6psHLPlyPGyq6JhZsPvA4EutsNhjzodc= github.com/getlantern/quic-go-unbounded-fork v0.51.3-unbounded/go.mod h1:MFlGGpcpJqRAfmYi6NC2cptDPSxRWTOGNuP4wqrWmzQ= -github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031 h1:7I8fcTXTzUwNrLm+XYzGbsus8TsVhtMUjeczTV8Tymw= -github.com/getlantern/radiance v0.0.0-20260223202618-48046679d031/go.mod h1:bdUNd0PV0wR1/VdEWjTuFfkp2JSMvLTTyizz2BFMd+4= +github.com/getlantern/radiance v0.0.0-20260224202913-7520e330886d h1:Xnc+eYMyAPzzyJ4Tzn/rZbRZScQ/ZwXZasiaD+c32dY= +github.com/getlantern/radiance v0.0.0-20260224202913-7520e330886d/go.mod h1:bdUNd0PV0wR1/VdEWjTuFfkp2JSMvLTTyizz2BFMd+4= github.com/getlantern/samizdat v0.0.2 h1:PkMu6jsfUz7DLZUH2xh548XfzgPASmq5CajZyUKj/9Y= github.com/getlantern/samizdat v0.0.2/go.mod h1:uEeykQSW2/6rTjfPlj3MTTo59poSHXfAHTGgzYDkbr0= github.com/getlantern/sing v0.7.18-lantern h1:QKGgIUA3LwmKYP/7JlQTRkxj9jnP4cX2Q/B+nd8XEjo= From 3828bf98cd57c7d00aa15cec380987a1dd92b629 Mon Sep 17 00:00:00 2001 From: jigar-f <132374182+jigar-f@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:32:11 +0530 Subject: [PATCH 8/8] Unbounded changes (#8493) * added platfrom methods and refactor to improve maintainability * added localization and fixed UI issues * connect more dots * more localizetion * UI fixes * fix setting ui issue * Replace webview with widget --- .../lantern/handler/MethodHandler.kt | 17 + assets/images/auto_mode.svg | 8 + assets/locales/en.po | 27 + assets/unbounded/globe.html | 256 ------ assets/unbounded/uv-map-dark.png | Bin 0 -> 104139 bytes assets/unbounded/uv-map.png | Bin 0 -> 69958 bytes ios/Runner/Handlers/MethodHandler.swift | 40 + lib/core/common/app_image_paths.dart | 4 + lib/core/common/app_semantic_colors.dart | 136 +-- .../models/unbounded_connection_event.dart | 28 + lib/core/services/geo_lookup_service.dart | 173 ++++ .../home/provider/app_event_notifier.dart | 85 +- .../home/provider/app_event_notifier.g.dart | 58 -- .../home/provider/app_setting_notifier.g.dart | 2 +- lib/features/setting/setting.dart | 58 +- .../provider/unbounded_notifier.dart | 40 + .../provider/unbounded_notifier.g.dart | 60 ++ lib/features/unbounded/unbounded.dart | 829 ++++++------------ macos/Runner/Handlers/MethodHandler.swift | 34 + pubspec.lock | 26 +- pubspec.yaml | 2 + 21 files changed, 804 insertions(+), 1079 deletions(-) create mode 100644 assets/images/auto_mode.svg delete mode 100644 assets/unbounded/globe.html create mode 100644 assets/unbounded/uv-map-dark.png create mode 100644 assets/unbounded/uv-map.png create mode 100644 lib/core/models/unbounded_connection_event.dart create mode 100644 lib/core/services/geo_lookup_service.dart create mode 100644 lib/features/unbounded/provider/unbounded_notifier.dart create mode 100644 lib/features/unbounded/provider/unbounded_notifier.g.dart diff --git a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt index 9e8301b8a2..2cebbcf97c 100644 --- a/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt +++ b/android/app/src/main/kotlin/org/getlantern/lantern/handler/MethodHandler.kt @@ -114,6 +114,10 @@ enum class Methods(val method: String) { // Smart routing SetRoutingMode("setRoutingMode"), + + // Unbounded + SetUnboundedEnabled("setUnboundedEnabled"), + IsUnboundedEnabled("isUnboundedEnabled"), } class MethodHandler : FlutterPlugin, @@ -1039,6 +1043,19 @@ class MethodHandler : FlutterPlugin, } } + Methods.SetUnboundedEnabled.method -> { + scope.handleResult(result, "SetUnboundedEnabled") { + val enabled = call.argument("enabled") ?: error("Missing enabled") + Mobile.setUnboundedEnabled(enabled) + } + } + + Methods.IsUnboundedEnabled.method -> { + scope.handleValue(result, "IsUnboundedEnabled") { + Mobile.isUnboundedEnabled() + } + } + else -> { result.notImplemented() } diff --git a/assets/images/auto_mode.svg b/assets/images/auto_mode.svg new file mode 100644 index 0000000000..4c4e9d8ff9 --- /dev/null +++ b/assets/images/auto_mode.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/locales/en.po b/assets/locales/en.po index be99d4d412..5958a397d9 100644 --- a/assets/locales/en.po +++ b/assets/locales/en.po @@ -1487,6 +1487,33 @@ msgstr "Dark" msgid "system" msgstr "System" +msgid "help_others_bypass_censorship" +msgstr "Help others bypass censorship by securely sharing your connection." + +msgid "people_you_are_helping_right_now" +msgstr "People you are helping right now:" + +msgid "total_people_helped_to_date" +msgstr "Total people helped to date:" + +msgid "status" +msgstr "Status" + +msgid "auto_enable_unbounded" +msgstr "Auto-enable Unbounded" + +msgid "turn_on_automatically_when_lantern_is_open" +msgstr "Turn on automatically when Lantern is open" + +msgid "welcome_to_unbounded" +msgstr "Welcome to Unbounded!" + +msgid "unbounded_description" +msgstr "Unbounded lets you share a small amount of your internet bandwidth to help people in censored countries access the open web." + +msgid "your_connection_stays_secure_and_private" +msgstr "Your connection stays secure and private. You control when sharing is active." + diff --git a/assets/unbounded/globe.html b/assets/unbounded/globe.html deleted file mode 100644 index 378ec3f2b9..0000000000 --- a/assets/unbounded/globe.html +++ /dev/null @@ -1,256 +0,0 @@ - - - - - -Unbounded Globe - - - -
- - - - - - - diff --git a/assets/unbounded/uv-map-dark.png b/assets/unbounded/uv-map-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..afe9a07568135b06ee2dd2ed13e13f590bef0052 GIT binary patch literal 104139 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 literal 0 HcmV?d00001 diff --git a/assets/unbounded/uv-map.png b/assets/unbounded/uv-map.png new file mode 100644 index 0000000000000000000000000000000000000000..98953b96df72b5536bdc44bdb0c6ecba9d8dc83d GIT binary patch literal 69958 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 ae4XNc-t*N?EnpEq*2iprulUXTU;hCokchJY literal 0 HcmV?d00001 diff --git a/ios/Runner/Handlers/MethodHandler.swift b/ios/Runner/Handlers/MethodHandler.swift index 447354900d..9791a0e271 100644 --- a/ios/Runner/Handlers/MethodHandler.swift +++ b/ios/Runner/Handlers/MethodHandler.swift @@ -214,6 +214,16 @@ class MethodHandler { return } self.setSmartRouteMode(mode: mode, result: result) + + // Unbounded + case "setUnboundedEnabled": + let data = call.arguments as? [String: Any] + let enabled = data?["enabled"] as? Bool ?? false + self.setUnboundedEnabled(result: result, enabled: enabled) + + case "isUnboundedEnabled": + self.isUnboundedEnabled(result: result) + default: result(FlutterMethodNotImplemented) } @@ -970,6 +980,36 @@ class MethodHandler { } + // MARK: - Unbounded + + func setUnboundedEnabled(result: @escaping FlutterResult, enabled: Bool) { + Task { + var error: NSError? + MobileSetUnboundedEnabled(enabled, &error) + if let error { + await self.handleFlutterError(error, result: result, code: "SET_UNBOUNDED_ENABLED_ERROR") + return + } + await MainActor.run { + result("ok") + } + } + } + + func isUnboundedEnabled(result: @escaping FlutterResult) { + Task { + var error: NSError? + let enabled = MobileIsUnboundedEnabled() + if let error { + await self.handleFlutterError(error, result: result, code: "IS_UNBOUNDED_ENABLED_ERROR") + return + } + await MainActor.run { + result(enabled) + } + } + } + // MARK: - Utils /// Helper for handling Flutter errors diff --git a/lib/core/common/app_image_paths.dart b/lib/core/common/app_image_paths.dart index 84250edfd6..c7961b37fd 100644 --- a/lib/core/common/app_image_paths.dart +++ b/lib/core/common/app_image_paths.dart @@ -111,6 +111,10 @@ class AppImagePaths { static const automatic = 'assets/images/automatic.svg'; static const lightMode = 'assets/images/light_mode.svg'; static const darkMode = 'assets/images/dark_mode.svg'; + static const autoMode = 'assets/images/auto_mode.svg'; + static const darkMap = 'assets/unbounded/uv-map-dark.png'; + static const lightMap = 'assets/unbounded/uv-map.png'; + /// Validates and returns a safe flag path for the given country code. /// Returns null if the country code is invalid or the flag asset doesn't exist. diff --git a/lib/core/common/app_semantic_colors.dart b/lib/core/common/app_semantic_colors.dart index 74b15e2d5a..446e18cc79 100644 --- a/lib/core/common/app_semantic_colors.dart +++ b/lib/core/common/app_semantic_colors.dart @@ -6,30 +6,30 @@ import 'app_colors.dart'; /// /// Usage: context.textPrimary, context.bgElevated, context.borderInput … extension AppSemanticColors on BuildContext { - bool get _isDark => Theme.of(this).brightness == Brightness.dark; + bool get isDark => Theme.of(this).brightness == Brightness.dark; // ── Text ──────────────────────────────────────────────────────────────────── /// text.primary Gray.900 light / Gray.200 dark - Color get textPrimary => _isDark ? AppColors.gray2 : AppColors.gray9; + Color get textPrimary => isDark ? AppColors.gray2 : AppColors.gray9; /// text.secondary Gray.800 light / Gray.300 dark - Color get textSecondary => _isDark ? AppColors.gray3 : AppColors.gray8; + Color get textSecondary => isDark ? AppColors.gray3 : AppColors.gray8; /// text.tertiary Gray.700 light / Gray.400 dark - Color get textTertiary => _isDark ? AppColors.gray4 : AppColors.gray7; + Color get textTertiary => isDark ? AppColors.gray4 : AppColors.gray7; /// text.link Blue.700 light / Blue.200 dark - Color get textLink => _isDark ? AppColors.blue2 : AppColors.blue7; + Color get textLink => isDark ? AppColors.blue2 : AppColors.blue7; /// text.disabled Gray.600 light / Gray.500 dark - Color get textDisabled => _isDark ? AppColors.gray5 : AppColors.gray6; + Color get textDisabled => isDark ? AppColors.gray5 : AppColors.gray6; /// text.inverse Gray.100 light / Gray.900 dark - Color get textInverse => _isDark ? AppColors.gray9 : AppColors.gray1; + Color get textInverse => isDark ? AppColors.gray9 : AppColors.gray1; /// text.inverse-color Blue.400 light / Blue.600 dark - Color get textInverseColor => _isDark ? AppColors.blue6 : AppColors.blue4; + Color get textInverseColor => isDark ? AppColors.blue6 : AppColors.blue4; /// text.promo-icon Yellow.300 both modes Color get textPromoIcon => AppColors.yellow3; @@ -37,48 +37,48 @@ extension AppSemanticColors on BuildContext { // ── Background ────────────────────────────────────────────────────────────── /// bg.surface Gray.100 light / Gray.900 dark - Color get bgSurface => _isDark ? AppColors.gray9 : AppColors.gray1; + Color get bgSurface => isDark ? AppColors.gray9 : AppColors.gray1; /// bg.elevated White light / Gray.850 dark - Color get bgElevated => _isDark ? AppColors.gray850 : AppColors.white; + Color get bgElevated => isDark ? AppColors.gray850 : AppColors.white; /// bg.input White light / Gray.850 dark - Color get bgInput => _isDark ? AppColors.gray850 : AppColors.white; + Color get bgInput => isDark ? AppColors.gray850 : AppColors.white; /// bg.hover Blue.100 light / Blue.900 dark - Color get bgHover => _isDark ? AppColors.blue9 : AppColors.blue1; + Color get bgHover => isDark ? AppColors.blue9 : AppColors.blue1; /// bg.overlay Gray.100 light / Gray.900 dark - Color get bgOverlay => _isDark ? AppColors.gray9 : AppColors.gray1; + Color get bgOverlay => isDark ? AppColors.gray9 : AppColors.gray1; /// bg.callout Gray.200 light / Gray.800 dark - Color get bgCallout => _isDark ? AppColors.gray8 : AppColors.gray2; + Color get bgCallout => isDark ? AppColors.gray8 : AppColors.gray2; /// bg.snackbar Blue.900 light / Blue.200 dark - Color get bgSnackbar => _isDark ? AppColors.blue2 : AppColors.blue9; + Color get bgSnackbar => isDark ? AppColors.blue2 : AppColors.blue9; /// bg.snackbar-error Red.700 light / Red.500 dark - Color get bgSnackbarError => _isDark ? AppColors.red5 : AppColors.red7; + Color get bgSnackbarError => isDark ? AppColors.red5 : AppColors.red7; /// bg.promo Yellow.100 light / Gray.900 dark - Color get bgPromo => _isDark ? AppColors.gray9 : AppColors.yellow1; + Color get bgPromo => isDark ? AppColors.gray9 : AppColors.yellow1; // ── Border ────────────────────────────────────────────────────────────────── /// border.default Gray.200 light / Gray.800 dark - Color get borderDefault => _isDark ? AppColors.gray8 : AppColors.gray2; + Color get borderDefault => isDark ? AppColors.gray8 : AppColors.gray2; /// border.input Gray.300 light / Gray.700 dark - Color get borderInput => _isDark ? AppColors.gray7 : AppColors.gray3; + Color get borderInput => isDark ? AppColors.gray7 : AppColors.gray3; /// border.input-focus Blue.800 light / Blue.200 dark - Color get borderInputFocus => _isDark ? AppColors.blue2 : AppColors.blue8; + Color get borderInputFocus => isDark ? AppColors.blue2 : AppColors.blue8; /// border.input-filled Gray.900 light / Gray.400 dark - Color get borderInputFilled => _isDark ? AppColors.gray4 : AppColors.gray9; + Color get borderInputFilled => isDark ? AppColors.gray4 : AppColors.gray9; /// border.error Red.600 light / Red.500 dark - Color get borderError => _isDark ? AppColors.red5 : AppColors.red6; + Color get borderError => isDark ? AppColors.red5 : AppColors.red6; /// border.promo Yellow.500 both modes Color get borderPromo => AppColors.yellow5; @@ -86,125 +86,125 @@ extension AppSemanticColors on BuildContext { // ── Status ────────────────────────────────────────────────────────────────── /// status.error-text Red.800 light / Red.200 dark - Color get statusErrorText => _isDark ? AppColors.red2 : AppColors.red8; + Color get statusErrorText => isDark ? AppColors.red2 : AppColors.red8; /// status.error-bg Red.200 light / Red.800 dark - Color get statusErrorBg => _isDark ? AppColors.red8 : AppColors.red2; + Color get statusErrorBg => isDark ? AppColors.red8 : AppColors.red2; /// status.error-border Red.400 light / Red.600 dark - Color get statusErrorBorder => _isDark ? AppColors.red6 : AppColors.red4; + Color get statusErrorBorder => isDark ? AppColors.red6 : AppColors.red4; /// status.success-text Green.800 light / Green.300 dark - Color get statusSuccessText => _isDark ? AppColors.green3 : AppColors.green8; + Color get statusSuccessText => isDark ? AppColors.green3 : AppColors.green8; /// status.success-bg Green.200 light / Green.700 dark - Color get statusSuccessBg => _isDark ? AppColors.green7 : AppColors.green2; + Color get statusSuccessBg => isDark ? AppColors.green7 : AppColors.green2; /// status.success-border Green.400 light / Green.600 dark Color get statusSuccessBorder => - _isDark ? AppColors.green6 : AppColors.green4; + isDark ? AppColors.green6 : AppColors.green4; /// status.warning-text Yellow.500 light / Yellow.200 dark - Color get statusWarningText => _isDark ? AppColors.yellow2 : AppColors.yellow5; + Color get statusWarningText => isDark ? AppColors.yellow2 : AppColors.yellow5; /// status.warning-bg-dot Yellow.300 light / Yellow.500 dark Color get statusWarningBgDot => - _isDark ? AppColors.yellow5 : AppColors.yellow3; + isDark ? AppColors.yellow5 : AppColors.yellow3; /// status.neutral-text Gray.600 light / Gray.200 dark - Color get statusNeutralText => _isDark ? AppColors.gray2 : AppColors.gray6; + Color get statusNeutralText => isDark ? AppColors.gray2 : AppColors.gray6; /// status.informational-text Blue.800 light / Blue.200 dark - Color get statusInfoText => _isDark ? AppColors.blue2 : AppColors.blue8; + Color get statusInfoText => isDark ? AppColors.blue2 : AppColors.blue8; /// status.Informational-bg Blue.200 light / Blue.700 dark - Color get statusInfoBg => _isDark ? AppColors.blue7 : AppColors.blue2; + Color get statusInfoBg => isDark ? AppColors.blue7 : AppColors.blue2; /// status.Informational-border Blue.400 light / Blue.600 dark - Color get statusInfoBorder => _isDark ? AppColors.blue6 : AppColors.blue4; + Color get statusInfoBorder => isDark ? AppColors.blue6 : AppColors.blue4; /// status.error-bg-dot Red.600 light / Red.800 dark - Color get statusErrorBgDot => _isDark ? AppColors.red8 : AppColors.red6; + Color get statusErrorBgDot => isDark ? AppColors.red8 : AppColors.red6; /// status.error-border-dot Red.300 light / Red.500 dark - Color get statusErrorBorderDot => _isDark ? AppColors.red5 : AppColors.red3; + Color get statusErrorBorderDot => isDark ? AppColors.red5 : AppColors.red3; /// status.warning-border-dot Yellow.200 light / Yellow.400 dark Color get statusWarningBorderDot => - _isDark ? AppColors.yellow4 : AppColors.yellow2; + isDark ? AppColors.yellow4 : AppColors.yellow2; /// status.success-bg-dot Green.600 light / Green.700 dark Color get statusSuccessBgDot => - _isDark ? AppColors.green7 : AppColors.green6; + isDark ? AppColors.green7 : AppColors.green6; /// status.success-border-dot Green.300 light / Green.500 dark Color get statusSuccessBorderDot => - _isDark ? AppColors.green5 : AppColors.green3; + isDark ? AppColors.green5 : AppColors.green3; /// status.neutral-bg-dot Gray.500 light / Gray.700 dark - Color get statusNeutralBgDot => _isDark ? AppColors.gray7 : AppColors.gray5; + Color get statusNeutralBgDot => isDark ? AppColors.gray7 : AppColors.gray5; /// status.neutral-border-dot Gray.300 light / Gray.500 dark Color get statusNeutralBorderDot => - _isDark ? AppColors.gray5 : AppColors.gray3; + isDark ? AppColors.gray5 : AppColors.gray3; // ── Action / Primary ──────────────────────────────────────────────────────── /// action.primary.primary-bg Blue.1000 light / Blue.600 dark - Color get actionPrimaryBg => _isDark ? AppColors.blue6 : AppColors.blue10; + Color get actionPrimaryBg => isDark ? AppColors.blue6 : AppColors.blue10; /// action.primary.primary-bg-hover Blue.800 light / Blue.500 dark - Color get actionPrimaryBgHover => _isDark ? AppColors.blue5 : AppColors.blue8; + Color get actionPrimaryBgHover => isDark ? AppColors.blue5 : AppColors.blue8; /// action.primary.primary-text Gray.100 both modes Color get actionPrimaryText => AppColors.gray1; /// action.primary.primary-disabled-bg Gray.200 light / Gray.700 dark Color get actionPrimaryDisabledBg => - _isDark ? AppColors.gray7 : AppColors.gray2; + isDark ? AppColors.gray7 : AppColors.gray2; /// action.primary.primary-disabled-text Gray.500 both modes Color get actionPrimaryDisabledText => AppColors.gray5; /// action.primary.primary-disabled-border Gray.400 light / Gray.500 dark Color get actionPrimaryDisabledBorder => - _isDark ? AppColors.gray5 : AppColors.gray4; + isDark ? AppColors.gray5 : AppColors.gray4; // ── Action / Secondary ────────────────────────────────────────────────────── /// action.secondary.secondary-bg Gray.100 light / Gray.900 dark - Color get actionSecondaryBg => _isDark ? AppColors.gray9 : AppColors.gray1; + Color get actionSecondaryBg => isDark ? AppColors.gray9 : AppColors.gray1; /// action.secondary.secondary-bg-hover Gray.200 light / Gray.800 dark Color get actionSecondaryBgHover => - _isDark ? AppColors.gray8 : AppColors.gray2; + isDark ? AppColors.gray8 : AppColors.gray2; /// action.secondary.secondary-text Gray.900 light / Gray.100 dark - Color get actionSecondaryText => _isDark ? AppColors.gray1 : AppColors.gray9; + Color get actionSecondaryText => isDark ? AppColors.gray1 : AppColors.gray9; /// action.secondary.secondary-border Gray.500 light / Gray.600 dark Color get actionSecondaryBorder => - _isDark ? AppColors.gray6 : AppColors.gray5; + isDark ? AppColors.gray6 : AppColors.gray5; /// action.secondary.secondary-disabled-bg Gray.200 light / Gray.900 dark Color get actionSecondaryDisabledBg => - _isDark ? AppColors.gray9 : AppColors.gray2; + isDark ? AppColors.gray9 : AppColors.gray2; /// action.secondary.secondary-disabled-text Gray.500 both modes Color get actionSecondaryDisabledText => AppColors.gray5; /// action.secondary.secondary-disabled-border Gray.300 light / Gray.700 dark Color get actionSecondaryDisabledBorder => - _isDark ? AppColors.gray7 : AppColors.gray3; + isDark ? AppColors.gray7 : AppColors.gray3; // ── Action / Tertiary ─────────────────────────────────────────────────────── /// action.tertiary.tertiary-text Gray.900 light / Gray.100 dark - Color get actionTertiaryText => _isDark ? AppColors.gray1 : AppColors.gray9; + Color get actionTertiaryText => isDark ? AppColors.gray1 : AppColors.gray9; /// action.tertiary.tertiary-hover-bg Gray.200 light / Gray.800 dark Color get actionTertiaryHoverBg => - _isDark ? AppColors.gray8 : AppColors.gray2; + isDark ? AppColors.gray8 : AppColors.gray2; /// action.tertiary.tertiary-disabled-text Gray.500 both modes Color get actionTertiaryDisabledText => AppColors.gray5; @@ -212,61 +212,61 @@ extension AppSemanticColors on BuildContext { // ── Action / Tonal ────────────────────────────────────────────────────────── /// action.tonal.tonal-bg Blue.100 light / Blue.700 dark - Color get actionTonalBg => _isDark ? AppColors.blue7 : AppColors.blue1; + Color get actionTonalBg => isDark ? AppColors.blue7 : AppColors.blue1; /// action.tonal.tonal-border Gray.200 light / Gray.800 dark - Color get actionTonalBorder => _isDark ? AppColors.gray8 : AppColors.gray2; + Color get actionTonalBorder => isDark ? AppColors.gray8 : AppColors.gray2; /// action.tonal.tonal-bg-hover Blue.200 light / Blue.600 dark - Color get actionTonalBgHover => _isDark ? AppColors.blue6 : AppColors.blue2; + Color get actionTonalBgHover => isDark ? AppColors.blue6 : AppColors.blue2; /// action.tonal.tonal-text Gray.900 light / Gray.100 dark - Color get actionTonalText => _isDark ? AppColors.gray1 : AppColors.gray9; + Color get actionTonalText => isDark ? AppColors.gray1 : AppColors.gray9; /// action.tonal.tonal-disabled-bg Gray.100 light / Gray.800 dark Color get actionTonalDisabledBg => - _isDark ? AppColors.gray8 : AppColors.gray1; + isDark ? AppColors.gray8 : AppColors.gray1; /// action.tonal.tonal-disabled-border Gray.200 light / Gray.800 dark Color get actionTonalDisabledBorder => - _isDark ? AppColors.gray8 : AppColors.gray2; + isDark ? AppColors.gray8 : AppColors.gray2; /// action.tonal.tonal-disabled-text Gray.400 light / Gray.500 dark Color get actionTonalDisabledText => - _isDark ? AppColors.gray5 : AppColors.gray4; + isDark ? AppColors.gray5 : AppColors.gray4; // ── Action / Toggle ───────────────────────────────────────────────────────── /// action.toggle.toggle-active-bg Green.500 light / Green.700 dark Color get actionToggleActiveBg => - _isDark ? AppColors.green7 : AppColors.green5; + isDark ? AppColors.green7 : AppColors.green5; /// action.toggle.toggle-brand-active-bg Blue.400 light / Blue.600 dark Color get actionToggleBrandActiveBg => - _isDark ? AppColors.blue6 : AppColors.blue4; + isDark ? AppColors.blue6 : AppColors.blue4; /// action.toggle.toggle-disabled-bg Gray.700 both modes Color get actionToggleDisabledBg => AppColors.gray7; /// action.toggle.toggle-knob-bg Gray.000 light / Gray.100 dark - Color get actionToggleKnobBg => _isDark ? AppColors.gray1 : AppColors.gray0; + Color get actionToggleKnobBg => isDark ? AppColors.gray1 : AppColors.gray0; /// action.toggle.toggle-border Gray.200 light / Gray.700 dark - Color get actionToggleBorder => _isDark ? AppColors.gray7 : AppColors.gray2; + Color get actionToggleBorder => isDark ? AppColors.gray7 : AppColors.gray2; // ── Action / Tabbar ───────────────────────────────────────────────────────── /// action.tabbar.tabbar-bg Blue.200 light / Blue.800 dark - Color get actionTabbarBg => _isDark ? AppColors.blue8 : AppColors.blue2; + Color get actionTabbarBg => isDark ? AppColors.blue8 : AppColors.blue2; /// action.tabbar.tabbar-border Blue.300 light / Blue.700 dark - Color get actionTabbarBorder => _isDark ? AppColors.blue7 : AppColors.blue3; + Color get actionTabbarBorder => isDark ? AppColors.blue7 : AppColors.blue3; /// action.tabbar.tabbar-selected-text Blue.1000 light / Blue.100 dark Color get actionTabbarSelectedText => - _isDark ? AppColors.blue1 : AppColors.blue10; + isDark ? AppColors.blue1 : AppColors.blue10; /// action.tabbar.tabbar-disabled-text Gray.600 light / Gray.200 dark Color get actionTabbarDisabledText => - _isDark ? AppColors.gray2 : AppColors.gray6; + isDark ? AppColors.gray2 : AppColors.gray6; } diff --git a/lib/core/models/unbounded_connection_event.dart b/lib/core/models/unbounded_connection_event.dart new file mode 100644 index 0000000000..eb643f255a --- /dev/null +++ b/lib/core/models/unbounded_connection_event.dart @@ -0,0 +1,28 @@ +/// Represents a consumer connection change from the broflake widget proxy. +class UnboundedConnectionEvent { + final int state; // 1 = connected, -1 = disconnected + final int workerIdx; + final String addr; // IP address + + UnboundedConnectionEvent({ + required this.state, + required this.workerIdx, + required this.addr, + }); + + factory UnboundedConnectionEvent.fromJson(Map json) { + return UnboundedConnectionEvent( + state: json['state'] as int, + workerIdx: json['workerIdx'] as int, + addr: json['addr'] as String? ?? '', + ); + } +} + +/// Tracks live and cumulative connection counts for Unbounded. +class UnboundedStats { + final int activeCount; + final int totalCount; + + const UnboundedStats({this.activeCount = 0, this.totalCount = 0}); +} diff --git a/lib/core/services/geo_lookup_service.dart b/lib/core/services/geo_lookup_service.dart new file mode 100644 index 0000000000..50150fe375 --- /dev/null +++ b/lib/core/services/geo_lookup_service.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; + +import 'package:flutter_earth_globe/globe_coordinates.dart'; +import 'package:http/http.dart' as http; + +class GeoLookupService { + static const _geoUrl = 'https://geo.getiantem.org'; + + // ISO country code → approximate centre coordinates + static const _countries = { + 'AF': (lat: 33.0, lng: 65.0), + 'AL': (lat: 41.0, lng: 20.0), + 'DZ': (lat: 28.0, lng: 3.0), + 'AD': (lat: 42.5, lng: 1.6), + 'AO': (lat: -12.5, lng: 18.5), + 'AR': (lat: -34.0, lng: -64.0), + 'AM': (lat: 40.0, lng: 45.0), + 'AU': (lat: -27.0, lng: 133.0), + 'AT': (lat: 47.33, lng: 13.33), + 'AZ': (lat: 40.5, lng: 47.5), + 'BD': (lat: 24.0, lng: 90.0), + 'BY': (lat: 53.0, lng: 28.0), + 'BE': (lat: 50.83, lng: 4.0), + 'BJ': (lat: 9.5, lng: 2.25), + 'BO': (lat: -17.0, lng: -65.0), + 'BA': (lat: 44.0, lng: 18.0), + 'BR': (lat: -10.0, lng: -55.0), + 'BG': (lat: 43.0, lng: 25.0), + 'KH': (lat: 13.0, lng: 105.0), + 'CM': (lat: 6.0, lng: 12.0), + 'CA': (lat: 60.0, lng: -95.0), + 'CL': (lat: -30.0, lng: -71.0), + 'CN': (lat: 35.0, lng: 105.0), + 'CO': (lat: 4.0, lng: -72.0), + 'CD': (lat: 0.0, lng: 25.0), + 'CR': (lat: 10.0, lng: -84.0), + 'HR': (lat: 45.17, lng: 15.5), + 'CU': (lat: 21.5, lng: -80.0), + 'CZ': (lat: 49.75, lng: 15.5), + 'DK': (lat: 56.0, lng: 10.0), + 'DO': (lat: 19.0, lng: -70.67), + 'EC': (lat: -2.0, lng: -77.5), + 'EG': (lat: 27.0, lng: 30.0), + 'SV': (lat: 13.83, lng: -88.92), + 'EE': (lat: 59.0, lng: 26.0), + 'ET': (lat: 8.0, lng: 38.0), + 'FI': (lat: 64.0, lng: 26.0), + 'FR': (lat: 46.0, lng: 2.0), + 'GE': (lat: 42.0, lng: 43.5), + 'DE': (lat: 51.0, lng: 9.0), + 'GH': (lat: 8.0, lng: -2.0), + 'GR': (lat: 39.0, lng: 22.0), + 'GT': (lat: 15.5, lng: -90.25), + 'HN': (lat: 15.0, lng: -86.5), + 'HK': (lat: 22.25, lng: 114.17), + 'HU': (lat: 47.0, lng: 20.0), + 'IS': (lat: 65.0, lng: -18.0), + 'IN': (lat: 20.0, lng: 77.0), + 'ID': (lat: -5.0, lng: 120.0), + 'IR': (lat: 32.0, lng: 53.0), + 'IQ': (lat: 33.0, lng: 44.0), + 'IE': (lat: 53.0, lng: -8.0), + 'IL': (lat: 31.5, lng: 34.75), + 'IT': (lat: 42.83, lng: 12.83), + 'CI': (lat: 8.0, lng: -5.0), + 'JP': (lat: 36.0, lng: 138.0), + 'JO': (lat: 31.0, lng: 36.0), + 'KZ': (lat: 48.0, lng: 68.0), + 'KE': (lat: 1.0, lng: 38.0), + 'KR': (lat: 37.0, lng: 127.5), + 'KW': (lat: 29.34, lng: 47.66), + 'KG': (lat: 41.0, lng: 75.0), + 'LA': (lat: 18.0, lng: 105.0), + 'LV': (lat: 57.0, lng: 25.0), + 'LB': (lat: 33.83, lng: 35.83), + 'LT': (lat: 56.0, lng: 24.0), + 'MG': (lat: -20.0, lng: 47.0), + 'MY': (lat: 2.5, lng: 112.5), + 'ML': (lat: 17.0, lng: -4.0), + 'MX': (lat: 23.0, lng: -102.0), + 'MD': (lat: 47.0, lng: 29.0), + 'MN': (lat: 46.0, lng: 105.0), + 'MA': (lat: 32.0, lng: -5.0), + 'MZ': (lat: -18.25, lng: 35.0), + 'MM': (lat: 22.0, lng: 98.0), + 'NP': (lat: 28.0, lng: 84.0), + 'NL': (lat: 52.5, lng: 5.75), + 'NZ': (lat: -41.0, lng: 174.0), + 'NI': (lat: 13.0, lng: -85.0), + 'NG': (lat: 10.0, lng: 8.0), + 'NO': (lat: 62.0, lng: 10.0), + 'OM': (lat: 21.0, lng: 57.0), + 'PK': (lat: 30.0, lng: 70.0), + 'PA': (lat: 9.0, lng: -80.0), + 'PY': (lat: -23.0, lng: -58.0), + 'PE': (lat: -10.0, lng: -76.0), + 'PH': (lat: 13.0, lng: 122.0), + 'PL': (lat: 52.0, lng: 20.0), + 'PT': (lat: 39.5, lng: -8.0), + 'QA': (lat: 25.5, lng: 51.25), + 'RO': (lat: 46.0, lng: 25.0), + 'RU': (lat: 60.0, lng: 100.0), + 'SA': (lat: 25.0, lng: 45.0), + 'SN': (lat: 14.0, lng: -14.0), + 'RS': (lat: 44.0, lng: 21.0), + 'SG': (lat: 1.37, lng: 103.8), + 'SK': (lat: 48.67, lng: 19.5), + 'SI': (lat: 46.0, lng: 15.0), + 'ZA': (lat: -29.0, lng: 24.0), + 'ES': (lat: 40.0, lng: -4.0), + 'LK': (lat: 7.0, lng: 81.0), + 'SE': (lat: 62.0, lng: 15.0), + 'CH': (lat: 47.0, lng: 8.0), + 'SY': (lat: 35.0, lng: 38.0), + 'TW': (lat: 23.5, lng: 121.0), + 'TJ': (lat: 39.0, lng: 71.0), + 'TZ': (lat: -6.0, lng: 35.0), + 'TH': (lat: 15.0, lng: 100.0), + 'TN': (lat: 34.0, lng: 9.0), + 'TR': (lat: 39.0, lng: 35.0), + 'TM': (lat: 40.0, lng: 60.0), + 'UA': (lat: 49.0, lng: 32.0), + 'AE': (lat: 24.0, lng: 54.0), + 'GB': (lat: 54.0, lng: -2.0), + 'US': (lat: 38.0, lng: -97.0), + 'UY': (lat: -33.0, lng: -56.0), + 'UZ': (lat: 41.0, lng: 64.0), + 'VE': (lat: 8.0, lng: -66.0), + 'VN': (lat: 16.0, lng: 106.0), + 'YE': (lat: 15.0, lng: 48.0), + 'ZM': (lat: -15.0, lng: 30.0), + 'ZW': (lat: -20.0, lng: 30.0), + }; + + static GlobeCoordinates _isoToCoords(String iso) { + final c = _countries[iso] ?? _countries['US']!; + return GlobeCoordinates(c.lat, c.lng); + } + + /// Looks up the current device's location (no IP argument). + static Future selfLookup() async { + try { + final response = await http + .get(Uri.parse('$_geoUrl/')) + .timeout(const Duration(seconds: 5)); + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final iso = + (data['Country'] as Map?)?['IsoCode'] as String? ?? + 'US'; + return _isoToCoords(iso); + } + } catch (_) {} + return _isoToCoords('US'); + } + + /// Looks up the country for a peer [ip] address. + static Future peerLookup(String ip) async { + try { + final response = await http + .get(Uri.parse('$_geoUrl/$ip')) + .timeout(const Duration(seconds: 5)); + if (response.statusCode == 200) { + final data = jsonDecode(response.body) as Map; + final iso = + (data['Country'] as Map?)?['IsoCode'] as String? ?? + 'IR'; + return _isoToCoords(iso); + } + } catch (_) {} + return _isoToCoords('IR'); + } +} diff --git a/lib/features/home/provider/app_event_notifier.dart b/lib/features/home/provider/app_event_notifier.dart index c17f5151fd..12d8ad1a92 100644 --- a/lib/features/home/provider/app_event_notifier.dart +++ b/lib/features/home/provider/app_event_notifier.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:i18n_extension/default.i18n.dart'; import 'package:lantern/core/common/app_eum.dart'; import 'package:lantern/core/models/datacap_info.dart'; import 'package:lantern/core/models/entity/server_location_entity.dart'; +import 'package:lantern/core/models/unbounded_connection_event.dart'; import 'package:lantern/core/services/logger_service.dart'; import 'package:lantern/features/home/provider/home_notifier.dart'; import 'package:lantern/features/vpn/provider/available_servers_notifier.dart'; @@ -15,88 +15,11 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../../../core/models/available_servers.dart'; import 'data_cap_info_provider.dart' show dataCapInfoProvider; +import '../../unbounded/provider/unbounded_notifier.dart' + show unboundedProvider; part 'app_event_notifier.g.dart'; -/// Represents a consumer connection change from the broflake widget proxy. -class UnboundedConnectionEvent { - final int state; // 1 = connected, -1 = disconnected - final int workerIdx; - final String addr; // IP address - - UnboundedConnectionEvent({ - required this.state, - required this.workerIdx, - required this.addr, - }); - - factory UnboundedConnectionEvent.fromJson(Map json) { - return UnboundedConnectionEvent( - state: json['state'] as int, - workerIdx: json['workerIdx'] as int, - addr: json['addr'] as String? ?? '', - ); - } -} - -/// Global stream controller for unbounded connection events. -final _unboundedConnectionController = - StreamController.broadcast(); - -/// Provider that exposes unbounded connection events as a stream. -final unboundedConnectionProvider = - StreamProvider((ref) { - return _unboundedConnectionController.stream; -}); - -/// Tracks live and cumulative connection counts for Unbounded. -class UnboundedStats { - final int activeCount; - final int totalCount; - - const UnboundedStats({this.activeCount = 0, this.totalCount = 0}); -} - -/// Provider that tracks unbounded connection stats (active + total). -final unboundedStatsProvider = Provider((ref) { - ref.watch(unboundedConnectionProvider); - return _UnboundedStatsAccumulator.stats; -}); - -/// Simple static accumulator for unbounded stats. Updated by the listener -/// installed below. We use a static because the stats must survive provider -/// rebuilds. -class _UnboundedStatsAccumulator { - static UnboundedStats stats = const UnboundedStats(); -} - -/// A keep-alive provider whose sole job is to listen to connection events and -/// accumulate stats in [_UnboundedStatsAccumulator]. -@Riverpod(keepAlive: true) -class UnboundedStatsListener extends _$UnboundedStatsListener { - @override - void build() { - ref.listen(unboundedConnectionProvider, (prev, next) { - next.whenData((event) { - final s = _UnboundedStatsAccumulator.stats; - if (event.state == 1) { - _UnboundedStatsAccumulator.stats = UnboundedStats( - activeCount: s.activeCount + 1, - totalCount: s.totalCount + 1, - ); - } else if (event.state == -1) { - _UnboundedStatsAccumulator.stats = UnboundedStats( - activeCount: (s.activeCount - 1).clamp(0, s.activeCount), - totalCount: s.totalCount, - ); - } - // Invalidate the stats provider so watchers rebuild. - ref.invalidate(unboundedStatsProvider); - }); - }); - } -} - /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them /// in one place. @@ -171,7 +94,7 @@ class AppEventNotifier extends _$AppEventNotifier { try { final data = jsonDecode(event.message); final connEvent = UnboundedConnectionEvent.fromJson(data); - _unboundedConnectionController.add(connEvent); + ref.read(unboundedProvider.notifier).onConnectionEvent(connEvent); } catch (e) { appLogger.error('Error parsing unbounded-connection event: $e'); } diff --git a/lib/features/home/provider/app_event_notifier.g.dart b/lib/features/home/provider/app_event_notifier.g.dart index f440a13da1..50612e750e 100644 --- a/lib/features/home/provider/app_event_notifier.g.dart +++ b/lib/features/home/provider/app_event_notifier.g.dart @@ -8,64 +8,6 @@ part of 'app_event_notifier.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -/// A keep-alive provider whose sole job is to listen to connection events and -/// accumulate stats in [_UnboundedStatsAccumulator]. - -@ProviderFor(UnboundedStatsListener) -const unboundedStatsListenerProvider = UnboundedStatsListenerProvider._(); - -/// A keep-alive provider whose sole job is to listen to connection events and -/// accumulate stats in [_UnboundedStatsAccumulator]. -final class UnboundedStatsListenerProvider - extends $NotifierProvider { - /// A keep-alive provider whose sole job is to listen to connection events and - /// accumulate stats in [_UnboundedStatsAccumulator]. - const UnboundedStatsListenerProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'unboundedStatsListenerProvider', - isAutoDispose: false, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$unboundedStatsListenerHash(); - - @$internal - @override - UnboundedStatsListener create() => UnboundedStatsListener(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(void value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$unboundedStatsListenerHash() => - r'7e38010a65e7b4d8ddd441a2bdef65f515d7a6af'; - -/// A keep-alive provider whose sole job is to listen to connection events and -/// accumulate stats in [_UnboundedStatsAccumulator]. - -abstract class _$UnboundedStatsListener extends $Notifier { - void build(); - @$mustCallSuper - @override - void runBuild() { - build(); - final ref = this.ref as $Ref; - final element = ref.element as $ClassProviderElement< - AnyNotifier, void, Object?, Object?>; - element.handleValue(ref, null); - } -} - /// Listens for application-wide events and triggers corresponding actions. /// This can be used for all listening to events that go sends and handling them /// in one place. diff --git a/lib/features/home/provider/app_setting_notifier.g.dart b/lib/features/home/provider/app_setting_notifier.g.dart index 04531964c4..91b8fb52f3 100644 --- a/lib/features/home/provider/app_setting_notifier.g.dart +++ b/lib/features/home/provider/app_setting_notifier.g.dart @@ -42,7 +42,7 @@ final class AppSettingNotifierProvider } String _$appSettingNotifierHash() => - r'4e2663a63cead2d7429e8ae9d728439d2d9e3d21'; + r'6830d1af12a28788b3cd599e7fe6d957a38498b2'; abstract class _$AppSettingNotifier extends $Notifier { AppSetting build(); diff --git a/lib/features/setting/setting.dart b/lib/features/setting/setting.dart index 3df65394f2..439c5ee66e 100644 --- a/lib/features/setting/setting.dart +++ b/lib/features/setting/setting.dart @@ -216,37 +216,39 @@ class _SettingState extends ConsumerState { const SizedBox(height: 4), if (!appSetting.hideUnbounded) Card( - child: AppTile( - minHeight: 72, - icon: AppImagePaths.lanternLogoRounded, - iconUseThemeColor: false, - trailing: AppImage( - path: AppImagePaths.arrowForward, - height: 20, - ), - label: 'unbounded'.i18n, - subtitle: Text( - 'help_fight_global_internet_censorship'.i18n, - style: textTheme.labelMedium!.copyWith( - color: context.textTertiary, + child: Column( + children: [ + AppTile( + minHeight: 72, + icon: AppImagePaths.lanternLogoRounded, + iconUseThemeColor: false, + trailing: AppImage( + path: AppImagePaths.arrowForward, + height: 20, + ), + label: 'unbounded'.i18n, + subtitle: Text( + 'help_fight_global_internet_censorship'.i18n, + style: textTheme.labelMedium!.copyWith( + color: context.textTertiary, + ), + ), + onPressed: () { + appRouter.push(const UnboundedScreen()); + }, ), - ), - onPressed: () { - appRouter.push(const UnboundedScreen()); - }, + DividerSpace(), + AppTile( + label: 'Unbounded Settings', + icon: AppImagePaths.lanternLogoRounded, + iconUseThemeColor: false, + onPressed: () { + appRouter.push(const UnboundedSettingsScreen()); + }, + ), + ], ), ), - AppCard( - padding: EdgeInsets.zero, - child: AppTile( - label: 'Unbounded Settings', - icon: AppImagePaths.lanternLogoRounded, - iconUseThemeColor: false, - onPressed: () { - appRouter.push(const UnboundedSettingsScreen()); - }, - ), - ), SizedBox(height: defaultSize), ], ), diff --git a/lib/features/unbounded/provider/unbounded_notifier.dart b/lib/features/unbounded/provider/unbounded_notifier.dart new file mode 100644 index 0000000000..bec491bdaf --- /dev/null +++ b/lib/features/unbounded/provider/unbounded_notifier.dart @@ -0,0 +1,40 @@ +import 'dart:async'; + +import 'package:lantern/core/models/unbounded_connection_event.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'unbounded_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class UnboundedNotifier extends _$UnboundedNotifier { + final _eventController = + StreamController.broadcast(); + + /// Individual connection events for the globe view to animate arcs. + Stream get connectionEvents => + _eventController.stream; + + @override + UnboundedStats build() { + ref.onDispose(_eventController.close); + return const UnboundedStats(); + } + + /// Called by [AppEventNotifier] whenever an `unbounded-connection` event + /// arrives from the Go bridge. + void onConnectionEvent(UnboundedConnectionEvent event) { + final s = state; + if (event.state == 1) { + state = UnboundedStats( + activeCount: s.activeCount + 1, + totalCount: s.totalCount + 1, + ); + } else if (event.state == -1) { + state = UnboundedStats( + activeCount: (s.activeCount - 1).clamp(0, s.activeCount), + totalCount: s.totalCount, + ); + } + _eventController.add(event); + } +} diff --git a/lib/features/unbounded/provider/unbounded_notifier.g.dart b/lib/features/unbounded/provider/unbounded_notifier.g.dart new file mode 100644 index 0000000000..f340d335fb --- /dev/null +++ b/lib/features/unbounded/provider/unbounded_notifier.g.dart @@ -0,0 +1,60 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'unbounded_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(UnboundedNotifier) +const unboundedProvider = UnboundedNotifierProvider._(); + +final class UnboundedNotifierProvider + extends $NotifierProvider { + const UnboundedNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'unboundedProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$unboundedNotifierHash(); + + @$internal + @override + UnboundedNotifier create() => UnboundedNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(UnboundedStats value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$unboundedNotifierHash() => r'f5d26079a98de84fe0b96f881a0030c28902c067'; + +abstract class _$UnboundedNotifier extends $Notifier { + UnboundedStats build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = ref.element as $ClassProviderElement< + AnyNotifier, + UnboundedStats, + Object?, + Object?>; + element.handleValue(ref, created); + } +} diff --git a/lib/features/unbounded/unbounded.dart b/lib/features/unbounded/unbounded.dart index b3b615d58a..0dca42861a 100644 --- a/lib/features/unbounded/unbounded.dart +++ b/lib/features/unbounded/unbounded.dart @@ -1,11 +1,23 @@ +import 'dart:async'; +import 'dart:math'; + import 'package:auto_route/annotations.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:flutter_earth_globe/flutter_earth_globe.dart'; +import 'package:flutter_earth_globe/flutter_earth_globe_controller.dart'; +import 'package:flutter_earth_globe/globe_coordinates.dart'; +import 'package:flutter_earth_globe/point.dart'; +import 'package:flutter_earth_globe/point_connection.dart'; +import 'package:flutter_earth_globe/point_connection_style.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lantern/core/common/common.dart'; +import 'package:lantern/core/models/entity/app_setting_entity.dart'; +import 'package:lantern/core/models/unbounded_connection_event.dart'; +import 'package:lantern/core/services/geo_lookup_service.dart'; +import 'package:lantern/core/widgets/info_row.dart'; import 'package:lantern/core/widgets/switch_button.dart'; -import 'package:lantern/features/home/provider/app_event_notifier.dart'; import 'package:lantern/features/home/provider/app_setting_notifier.dart'; +import 'package:lantern/features/unbounded/provider/unbounded_notifier.dart'; @RoutePage(name: 'UnboundedScreen') class UnboundedScreen extends HookConsumerWidget { @@ -13,52 +25,127 @@ class UnboundedScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final textTheme = TextTheme.of(context); final appSetting = ref.watch(appSettingProvider); - final notifier = ref.read(appSettingProvider.notifier); - final isDark = Theme.of(context).brightness == Brightness.dark; - final stats = ref.watch(unboundedStatsProvider); + final settingNotifier = ref.read(appSettingProvider.notifier); + final stats = ref.watch(unboundedProvider); WidgetsBinding.instance.addPostFrameCallback((_) { if (!appSetting.unboundedWelcomeSeen) { - notifier.setUnboundedWelcomeSeen(true); + settingNotifier.setUnboundedWelcomeSeen(true); _showWelcomeDialog(context); } }); return BaseScreen( title: 'unbounded'.i18n, - padded: false, body: Column( children: [ - _InfoBanner(), - Expanded( - flex: 3, - child: _GlobeView(isDark: isDark), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( + InfoRow(text: 'help_others_bypass_censorship'.i18n), + SizedBox(height: defaultSize), + Expanded(flex: 3, child: _GlobeView()), + SizedBox(height: defaultSize), + _buildStatusCard( + context, appSetting, stats, settingNotifier, textTheme), + SizedBox(height: defaultSize), + _buildAutoEnableCard(context, appSetting, settingNotifier, textTheme), + SizedBox(height: defaultSize), + ], + ), + ); + } + + // ── Card builders ────────────────────────────────────────────────────────── + + Widget _buildStatusCard( + BuildContext context, + AppSetting appSetting, + UnboundedStats stats, + AppSettingNotifier notifier, + TextTheme textTheme, + ) { + return AppCard( + padding: EdgeInsets.zero, + child: Column( + children: [ + AppTile( + label: 'status'.i18n, + labelWidget: Row( children: [ - _StatusSection( - enabled: appSetting.unboundedEnabled, - onToggle: notifier.setUnboundedEnabled, - helpingNow: stats.activeCount, - totalHelped: stats.totalCount, + Text( + '${'status'.i18n}: ', + style: + textTheme.bodyLarge!.copyWith(color: context.textPrimary), ), - const SizedBox(height: 12), - _AutoEnableRow( - value: appSetting.autoEnableUnbounded, - onChanged: notifier.setAutoEnableUnbounded, + Text( + appSetting.unboundedEnabled + ? 'enabled'.i18n + : 'disabled'.i18n, + style: textTheme.titleMedium!.copyWith( + color: appSetting.unboundedEnabled + ? AppColors.green6 + : context.textTertiary, + ), ), - const SizedBox(height: 16), ], ), + tileTextStyle: textTheme.bodyLarge, + icon: Icon(Icons.language, color: context.textPrimary), + trailing: SwitchButton( + value: appSetting.unboundedEnabled, + onChanged: notifier.setUnboundedEnabled, + ), + ), + DividerSpace(), + AppTile( + label: 'people_you_are_helping_right_now'.i18n, + tileTextStyle: textTheme.bodyLarge, + icon: Icon(Icons.person_outline, color: context.textPrimary), + trailing: Text( + stats.activeCount.toString(), + style: textTheme.titleMedium!.copyWith(color: context.textLink), + ), + ), + DividerSpace(), + AppTile( + label: 'total_people_helped_to_date'.i18n, + tileTextStyle: textTheme.bodyLarge!, + icon: Icon(Icons.people, color: context.textPrimary), + trailing: Text( + stats.totalCount.toString(), + style: textTheme.titleMedium!.copyWith(color: context.textLink), + ), ), ], ), ); } + Widget _buildAutoEnableCard( + BuildContext context, + AppSetting appSetting, + AppSettingNotifier notifier, + TextTheme textTheme, + ) { + return AppCard( + padding: EdgeInsets.zero, + child: AppTile( + icon: AppImagePaths.autoMode, + label: 'auto_enable_unbounded'.i18n, + subtitle: Text( + 'turn_on_automatically_when_lantern_is_open'.i18n, + style: textTheme.labelMedium!.copyWith(color: context.textTertiary), + ), + trailing: Checkbox( + value: appSetting.autoEnableUnbounded, + onChanged: (value) => notifier.setAutoEnableUnbounded(value!), + ), + ), + ); + } + + // ── Welcome dialog ───────────────────────────────────────────────────────── + void _showWelcomeDialog(BuildContext context) { final textTheme = Theme.of(context).textTheme; AppDialog.customDialog( @@ -67,31 +154,23 @@ class UnboundedScreen extends HookConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ const SizedBox(height: 24), - Icon( - Icons.handshake_outlined, - size: 48, - color: context.textLink, - ), + Icon(Icons.handshake_outlined, size: 48, color: context.textLink), const SizedBox(height: 16), Text( - 'Welcome to Unbounded', + 'welcome_to_unbounded'.i18n, style: textTheme.headlineSmall, textAlign: TextAlign.center, ), const SizedBox(height: 12), Text( - 'Unbounded lets you share a small amount of your internet bandwidth to help people in censored countries access the open web.', - style: textTheme.bodyMedium!.copyWith( - color: context.textSecondary, - ), + 'unbounded_description'.i18n, + style: textTheme.bodyMedium!.copyWith(color: context.textSecondary), textAlign: TextAlign.center, ), const SizedBox(height: 8), Text( - 'Your connection stays secure and private. You control when sharing is active.', - style: textTheme.bodyMedium!.copyWith( - color: context.textSecondary, - ), + 'your_connection_stays_secure_and_private'.i18n, + style: textTheme.bodyMedium!.copyWith(color: context.textSecondary), textAlign: TextAlign.center, ), ], @@ -99,581 +178,175 @@ class UnboundedScreen extends HookConsumerWidget { action: [ AppTextButton( label: 'learn_more'.i18n, - onPressed: () { - UrlUtils.openUrl(AppUrls.unbounded); - }, + onPressed: () => UrlUtils.openUrl(AppUrls.unbounded), ), AppTextButton( - label: 'Got It', - onPressed: () { - appRouter.maybePop(); - }, + label: 'got_it'.i18n, + onPressed: () => appRouter.maybePop(), ), ], ); } } -class _InfoBanner extends StatelessWidget { - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - return Padding( - padding: const EdgeInsets.fromLTRB(16, 8, 16, 4), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: context.bgCallout, - borderRadius: BorderRadius.circular(12), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon( - Icons.info_outline, - size: 18, - color: context.textSecondary, - ), - const SizedBox(width: 8), - Expanded( - child: Text( - 'Help others bypass censorship by securely sharing your connection.', - style: textTheme.bodySmall!.copyWith( - color: context.textSecondary, - ), - ), - ), - ], - ), - ), - ); - } -} - -class _StatusSection extends StatelessWidget { - final bool enabled; - final ValueChanged onToggle; - final int helpingNow; - final int totalHelped; - - const _StatusSection({ - required this.enabled, - required this.onToggle, - required this.helpingNow, - required this.totalHelped, - }); - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - final statusColor = enabled ? AppColors.green5 : context.textTertiary; - final countColor = AppColors.green5; - - return AppCard( - padding: const EdgeInsets.symmetric(horizontal: 0), - child: Column( - children: [ - ListTile( - dense: true, - leading: Icon(Icons.language, color: context.textSecondary, size: 22), - title: Row( - children: [ - Text( - 'Status: ', - style: textTheme.titleSmall, - ), - Text( - enabled ? 'Enabled' : 'Disabled', - style: textTheme.titleSmall!.copyWith(color: statusColor), - ), - ], - ), - trailing: SwitchButton( - value: enabled, - onChanged: onToggle, - ), - ), - DividerSpace(), - ListTile( - dense: true, - leading: Icon(Icons.person_outline, color: context.textSecondary, size: 22), - title: Text( - 'People you are helping right now:', - style: textTheme.bodySmall!.copyWith(color: context.textSecondary), - ), - trailing: Text( - '$helpingNow', - style: textTheme.titleSmall!.copyWith(color: countColor), - ), - ), - DividerSpace(), - ListTile( - dense: true, - leading: Icon(Icons.people_outline, color: context.textSecondary, size: 22), - title: Text( - 'Total people helped to date:', - style: textTheme.bodySmall!.copyWith(color: context.textSecondary), - ), - trailing: Text( - '$totalHelped', - style: textTheme.titleSmall!.copyWith(color: countColor), - ), - ), - ], - ), - ); - } -} - -class _AutoEnableRow extends StatelessWidget { - final bool value; - final ValueChanged onChanged; - - const _AutoEnableRow({ - required this.value, - required this.onChanged, - }); - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - - return AppCard( - padding: EdgeInsets.zero, - child: ListTile( - leading: Icon(Icons.settings_outlined, color: context.textSecondary, size: 22), - title: Text( - 'Auto-enable Unbounded', - style: textTheme.titleSmall, - ), - subtitle: Text( - 'Turn on automatically when Lantern is open', - style: textTheme.bodySmall!.copyWith(color: context.textTertiary), - ), - trailing: Checkbox( - value: value, - onChanged: (val) => onChanged(val ?? false), - activeColor: context.textLink, - ), - onTap: () => onChanged(!value), - ), - ); - } -} +// ── Globe widget ─────────────────────────────────────────────────────────────── class _GlobeView extends ConsumerStatefulWidget { - final bool isDark; - - const _GlobeView({required this.isDark}); - @override ConsumerState<_GlobeView> createState() => _GlobeViewState(); } class _GlobeViewState extends ConsumerState<_GlobeView> { - InAppWebViewController? _controller; - bool _isLoading = true; - - void _handleConnectionEvent(UnboundedConnectionEvent event) { - if (_controller == null) return; - - _controller?.evaluateJavascript( - source: - "window.unboundedGlobe.handleMessage({type:'connectionEvent',state:${event.state},workerIdx:${event.workerIdx},addr:'${event.addr}'});", - ); - } + // Arc: blue4 at 75 % opacity + static final _arcColor = AppColors.blue4.withValues(alpha: 0.75); + + // Dots: blue4 (origin) and yellow3 (peer) both at 15 % opacity + static final _originPointColor = AppColors.blue4.withValues(alpha: 0.15); + static final _peerPointColor = AppColors.yellow3.withValues(alpha: 0.15); + + // Atmosphere: solid blue4 (dark theme) / blue6 (light theme) + static const _atmosphereDark = AppColors.blue4; + static const _atmosphereLight = AppColors.blue6; + + final FlutterEarthGlobeController _globeController = + FlutterEarthGlobeController( + isRotating: true, + rotationSpeed: 0.04, + zoom: 0, + isZoomEnabled: false, + showAtmosphere: true, + atmosphereColor: _atmosphereDark, + atmosphereOpacity: 0.2, + atmosphereBlur: 20, + ); + + StreamSubscription? _eventSub; + GlobeCoordinates? _originCoords; @override - Widget build(BuildContext context) { - ref.listen(unboundedConnectionProvider, (prev, next) { - next.whenData((event) { - _handleConnectionEvent(event); - }); - }); + void initState() { + super.initState(); - return Stack( - children: [ - InAppWebView( - initialData: InAppWebViewInitialData( - data: _globeHtml, - baseUrl: WebUri('https://unpkg.com'), - mimeType: 'text/html', - encoding: 'utf-8', - ), - initialSettings: InAppWebViewSettings( - javaScriptEnabled: true, - transparentBackground: true, - hardwareAcceleration: true, - mediaPlaybackRequiresUserGesture: false, - supportZoom: false, - disableHorizontalScroll: true, - disableVerticalScroll: true, - allowUniversalAccessFromFileURLs: true, - allowFileAccessFromFileURLs: true, - ), - onWebViewCreated: (controller) { - _controller = controller; - }, - onLoadStop: (controller, url) { - setState(() => _isLoading = false); - _sendTheme(); - }, - onConsoleMessage: (controller, consoleMessage) { - debugPrint('Globe JS: ${consoleMessage.message}'); - }, - onReceivedError: (controller, request, error) { - debugPrint('Globe load error: ${error.description} for ${request.url}'); - }, - ), - if (_isLoading) - const Center( - child: CircularProgressIndicator( - color: Color(0xFF00BCD4), - ), - ), - ], - ); - } + _globeController.onLoaded = () { + if (!mounted) return; + _applyTheme(); + }; - void _sendTheme() { - final theme = widget.isDark ? 'dark' : 'light'; - _controller?.evaluateJavascript( - source: - "window.unboundedGlobe.handleMessage({type:'setTheme',theme:'$theme'});", - ); + listingToConnectionEvents(); + _initOrigin(); } - @override - void didUpdateWidget(covariant _GlobeView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.isDark != widget.isDark) { - _sendTheme(); - } + void listingToConnectionEvents() { + _eventSub = ref + .read(unboundedProvider.notifier) + .connectionEvents + .listen(_handleConnectionEvent); } -} -// Globe HTML with built-in geo lookup and connection event handling -const _globeHtml = ''' - - - - - - - - -

- - - - - - - -'''; + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + // RotatingGlobe uses MediaQuery.size to center the sphere on screen. + // We override it so the globe centers within this widget's bounds + // rather than the full screen. + final widgetSize = Size(constraints.maxWidth, constraints.maxHeight); + + // 70 % of the shorter edge keeps the atmosphere glow from being clipped. + final radius = + min(constraints.maxWidth, constraints.maxHeight) / 2 * 0.7; + + // ClipRect prevents arcs from painting outside this widget's box. + return ClipRect( + child: MediaQuery( + data: MediaQueryData(size: widgetSize), + child: Stack( + children: [ + Positioned.fill( + child: FlutterEarthGlobe( + controller: _globeController, + radius: radius, + alignment: const Alignment(0.0, 0.1), + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/macos/Runner/Handlers/MethodHandler.swift b/macos/Runner/Handlers/MethodHandler.swift index be1536bef9..4e34412055 100644 --- a/macos/Runner/Handlers/MethodHandler.swift +++ b/macos/Runner/Handlers/MethodHandler.swift @@ -278,6 +278,14 @@ class MethodHandler { let enable = self.decodeValue(from: call.arguments, result: result) as Bool? self.setRoutingMode(result: result, enable: enable ?? false) + // Unbounded + case "setUnboundedEnabled": + let enabled: Bool = requireArg(call: call, name: "enabled", result: result)! + self.setUnboundedEnabled(result: result, enabled: enabled) + + case "isUnboundedEnabled": + self.isUnboundedEnabled(result: result) + default: result(FlutterMethodNotImplemented) } @@ -1165,6 +1173,32 @@ class MethodHandler { } } + // Unbounded + + private func setUnboundedEnabled(result: @escaping FlutterResult, enabled: Bool) { + Task.detached { + var error: NSError? + MobileSetUnboundedEnabled(enabled, &error) + if let err = error { + await self.handleFlutterError(err, result: result, code: "SET_UNBOUNDED_ENABLED_FAILED") + return + } + await MainActor.run { result("ok") } + } + } + + private func isUnboundedEnabled(result: @escaping FlutterResult) { + Task.detached { + var error: NSError? + let enabled = MobileIsUnboundedEnabled() + if let err = error { + await self.handleFlutterError(err, result: result, code: "IS_UNBOUNDED_ENABLED_FAILED") + return + } + await MainActor.run { result(enabled) } + } + } + // MARK: - Utils /// Helper for handling Flutter errors diff --git a/pubspec.lock b/pubspec.lock index a93f390516..cab581b4dd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -531,6 +531,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_earth_globe: + dependency: "direct main" + description: + name: flutter_earth_globe + sha256: d5b2d9a9ae9ad667808b0e5efc59dd3155051218f409cdb645b89a2d722e1745 + url: "https://pub.dev" + source: hosted + version: "2.2.0" flutter_hooks: dependency: "direct main" description: @@ -800,7 +808,7 @@ packages: source: hosted version: "4.3.0" http: - dependency: transitive + dependency: "direct main" description: name: http sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" @@ -1048,10 +1056,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1653,26 +1661,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "65e29d831719be0591f7b3b1a32a3cda258ec98c58c7b25f7b84241bc31215bb" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.26.2" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "80bf5a02b60af04b09e14f6fe68b921aad119493e26e490deaca5993fef1b05a" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.11" timezone: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5d2be6eefa..e8e308c619 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -109,6 +109,8 @@ dependencies: device_info_plus: ^12.2.0 flutter_inappwebview: ^6.1.5 + flutter_earth_globe: ^2.2.0 + http: ^1.2.2 desktop_webview_window: ^0.2.3 store_checker: ^1.8.0 # payment