Compare commits

..

52 Commits

Author SHA1 Message Date
chen08209
52d61b15fd Fix the problem that the download of remote resources failed after GeodataMode was turned on, which caused the application to flash back.
Fix edit profile error
2024-06-06 10:15:51 +08:00
chen08209
fea3c14608 Fix quickStart change proxy error 2024-06-05 17:59:53 +08:00
chen08209
84be01a38a Fix core version 2024-06-05 17:59:50 +08:00
chen08209
93da242148 Fix core version 2024-06-05 14:13:54 +08:00
chen08209
bb7e44da30 Update file_picker
Add resources page

Optimize more detail
2024-06-05 13:44:11 +08:00
chen08209
01f1b2d72f Add access selected sorted 2024-06-05 13:43:44 +08:00
chen08209
3074b1299e Fix notification duplicate creation issue
Fix AccessControl click issue
2024-06-03 11:48:50 +08:00
chen08209
bd5470b863 Fix Workflow 2024-05-31 22:34:46 +08:00
chen08209
83fafa4b68 Fix Linux unable to open 2024-05-31 22:12:41 +08:00
chen08209
b7fb969301 Update README.md 3 2024-05-31 15:41:08 +08:00
chen08209
3ef3190785 Create LICENSE 2024-05-31 15:11:15 +08:00
chen08209
b1c763fcfa Update README.md 2 2024-05-31 15:08:51 +08:00
chen08209
9452ffa514 Update README.md 2024-05-31 15:02:18 +08:00
chen08209
c9291e5027 Optimize workFlow 2024-05-31 14:25:15 +08:00
chen08209
3ba86dc9c2 optimize checkUpdate 2024-05-31 10:07:51 +08:00
chen08209
fd3040283c Fix submit error 2024-05-30 17:22:23 +08:00
chen08209
91d30c0f0e add WebDAV
add Auto check updates

Optimize more details
2024-05-30 17:11:15 +08:00
chen08209
c4b470ffaf optimize delayTest 2024-05-17 19:54:57 +08:00
chen08209
9a07c785f2 upgrade flutter version 2024-05-15 20:34:59 +08:00
chen08209
a134c32493 Update kernel
Add import profile via QR code image
2024-05-15 20:21:02 +08:00
chen08209
472cea9037 Add compatibility mode and adapt clash scheme. 2024-05-11 14:10:06 +08:00
chen08209
08d07498b9 update Version 2024-05-07 18:32:21 +08:00
chen08209
d5aa09949a Reconstruction application proxy logic 2024-05-07 18:31:14 +08:00
chen08209
fd1dfe5c60 Fix Tab destroy error 2024-05-06 19:05:27 +08:00
chen08209
9f89fe8b29 Optimize repeat healthcheck 2024-05-06 17:17:26 +08:00
chen08209
78081a12e8 Optimize Direct mode ui 2024-05-06 15:27:37 +08:00
chen08209
6896837f28 Optimize Healthcheck 2024-05-06 14:32:23 +08:00
chen08209
85eb903402 Remove proxies position animation, improve performance
Add Telegram Link
2024-05-06 14:32:22 +08:00
chen08209
9aa9180f1f Update healthcheck policy 2024-05-06 14:32:21 +08:00
chen08209
feb9688a29 New Check URLTest 2024-05-06 14:32:21 +08:00
chen08209
5c71992174 Fix the problem of invalid auto-selection 2024-05-05 16:14:34 +08:00
chen08209
74c3d0ae25 New Async UpdateConfig 2024-05-05 03:13:52 +08:00
chen08209
ecd1bcafd5 add changeProfileDebounce 2024-05-05 03:13:51 +08:00
chen08209
184d2d117a Update Workflow 2024-05-05 03:13:50 +08:00
chen08209
89e6f17794 Fix ChangeProfile block 2024-05-05 03:13:49 +08:00
chen08209
aef50fe0e3 Fix Release Message Error 2024-05-04 16:14:03 +08:00
chen08209
fc0767ed25 Update Selector 2 2024-05-04 01:14:56 +08:00
chen08209
dbf1724cca Update Version 2024-05-04 00:14:34 +08:00
chen08209
909aa4038e Fix Proxies Select Error 2024-05-04 00:14:07 +08:00
chen08209
2d0a7d8d46 Fix the problem that the proxy group is empty in global mode. 2024-05-03 23:08:06 +08:00
chen08209
ca96cd1d82 Fix the problem that the proxy group is empty in global mode. 2024-05-03 23:07:38 +08:00
chen08209
91ab1e5dac Add ProxyProvider2 2024-05-03 21:48:22 +08:00
chen08209
b3a5f74df8 Add ProxyProvider 2024-05-03 21:28:22 +08:00
chen08209
1f98be8ad8 Update Version 2024-05-03 15:33:46 +08:00
chen08209
453c7c98d0 Update ProxyGroup Sort 2024-05-03 15:33:45 +08:00
chen08209
91faed35c0 Fix Android quickStart VpnService some problems 2024-05-02 00:32:11 +08:00
chen08209
07bbaf6b6f Update version 2024-05-01 23:40:04 +08:00
chen08209
e8feb7c431 Set Android notification low importance 2024-05-01 23:40:03 +08:00
chen08209
4d16820526 Fix the issue that VpnService can't be closed correctly in special cases 2024-05-01 23:40:00 +08:00
chen08209
92294b49c6 Fix the problem that TileService is not destroyed correctly in some cases
Adjust tab animation defaults
2024-05-01 23:39:59 +08:00
chen08209
8a188a37c9 Add Telegram in README_zh_CN.md 2024-05-01 21:52:07 +08:00
chen08209
48af16c265 Add Telegram 2024-05-01 21:50:26 +08:00
55 changed files with 32769 additions and 474 deletions

View File

@@ -34,6 +34,8 @@ on Mobile:
💡 Based on Material You Design, [Surfboard](https://github.com/getsurfboard/surfboard)-like UI
☁️ Supports data sync via WebDAV
✨ Support subscription link, Dark mode
## Contact

View File

@@ -34,6 +34,8 @@ on Mobile:
💡 基本 Material You 设计, 类[Surfboard](https://github.com/getsurfboard/surfboard)用户界面
☁️ 支持通过WebDAV同步数据
✨ 支持一键导入订阅, 深色模式
## Contact

BIN
assets/data/ASN.mmdb Normal file

Binary file not shown.

31589
assets/data/GeoSite.dat Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@@ -22,6 +22,7 @@ import (
"strings"
"sync"
"syscall"
"time"
)
type healthCheckSchema struct {
@@ -93,6 +94,13 @@ type Now struct {
Value string `json:"value"`
}
type ExternalProvider struct {
Name string `json:"name"`
Type string `json:"type"`
VehicleType string `json:"vehicle-type"`
UpdateAt time.Time `json:"update-at"`
}
func restartExecutable(execPath string) {
var err error
executor.Shutdown()
@@ -317,11 +325,14 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi
targetConfig.ExternalUI = ""
targetConfig.Interface = ""
targetConfig.ExternalUIURL = ""
targetConfig.GeodataMode = false
//targetConfig.IPv6 = patchConfig.IPv6
targetConfig.LogLevel = patchConfig.LogLevel
targetConfig.Port = 0
targetConfig.SocksPort = 0
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.FindProcessMode = process.FindProcessAlways
targetConfig.AllowLan = patchConfig.AllowLan
targetConfig.MixedPort = patchConfig.MixedPort
targetConfig.Mode = patchConfig.Mode
targetConfig.Tun.Enable = patchConfig.Tun.Enable
targetConfig.Tun.Device = patchConfig.Tun.Device

View File

@@ -8,7 +8,7 @@ require (
github.com/Kr328/tun2socket v0.0.0-20220414050025-d07c78d06d34
github.com/metacubex/mihomo v1.17.1
github.com/miekg/dns v1.1.59
golang.org/x/net v0.24.0
golang.org/x/net v0.25.0
golang.org/x/sync v0.7.0
)
@@ -21,7 +21,7 @@ require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cilium/ebpf v0.12.3 // indirect
github.com/cloudflare/circl v1.3.6 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/coreos/go-iptables v0.7.0 // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect
@@ -34,8 +34,8 @@ require (
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.3.2 // indirect
github.com/gofrs/uuid/v5 v5.1.0 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/gofrs/uuid/v5 v5.2.0 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
@@ -51,14 +51,15 @@ require (
github.com/mdlayher/socket v0.4.1 // indirect
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 // indirect
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec // indirect
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 // indirect
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d // indirect
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e // indirect
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 // indirect
github.com/metacubex/sing-shadowsocks v0.2.6 // indirect
github.com/metacubex/sing-shadowsocks2 v0.2.0 // indirect
github.com/metacubex/sing-tun v0.2.6 // indirect
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec // indirect
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f // indirect
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 // indirect
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 // indirect
github.com/metacubex/utls v1.6.6 // indirect
github.com/mroth/weightedrand/v2 v2.1.0 // indirect
github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 // indirect
github.com/onsi/ginkgo/v2 v2.9.5 // indirect
@@ -75,10 +76,9 @@ require (
github.com/sagernet/sing-mux v0.2.1-0.20240124034317-9bfb33698bb6 // indirect
github.com/sagernet/sing-shadowtls v0.1.4 // indirect
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 // indirect
github.com/sagernet/utls v1.5.4 // indirect
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e // indirect
github.com/samber/lo v1.39.0 // indirect
github.com/shirou/gopsutil/v3 v3.24.3 // indirect
github.com/shirou/gopsutil/v3 v3.24.4 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b // indirect
github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c // indirect
@@ -94,14 +94,14 @@ require (
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.17.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.20.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
golang.org/x/tools v0.21.0 // indirect
google.golang.org/protobuf v1.34.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/blake3 v1.2.2 // indirect
lukechampine.com/blake3 v1.3.0 // indirect
)

View File

@@ -21,8 +21,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg=
github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -57,10 +57,10 @@ github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.3.2 h1:zlnbNHxumkRvfPWgfXu8RBwyNR1x8wh9cf5PTOCqs9Q=
github.com/gobwas/ws v1.3.2/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
github.com/gofrs/uuid/v5 v5.1.0 h1:S5rqVKIigghZTCBKPCw0Y+bXkn26K3TB5mvQq2Ix8dk=
github.com/gofrs/uuid/v5 v5.1.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
@@ -105,22 +105,24 @@ github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvO
github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec h1:HxreOiFTUrJXJautEo8rnE1uKTVGY8wtZepY1Tii/Nc=
github.com/metacubex/gvisor v0.0.0-20240320004321-933faba989ec/go.mod h1:8BVmQ+3cxjqzWElafm24rb2Ae4jRI6vAXNXWqWjfrXw=
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98 h1:oMLlJV4a9AylNo8ZLBNUiqZ02Vme6GLLHjuWJz8amSk=
github.com/metacubex/quic-go v0.42.1-0.20240418003344-f006b5735d98/go.mod h1:iGx3Y1zynls/FjFgykLSqDcM81U0IKePRTXEz5g3iiQ=
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d h1:RAe0ND8J5SOPGI623oEXfaHKaaUrrzHx+U1DN9Awcco=
github.com/metacubex/sing-quic v0.0.0-20240418004036-814c531c378d/go.mod h1:WyY0zYxv+o+18R/Ece+QFontlgXoobKbNqbtYn2zjz8=
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e h1:Nzwe08FNIJpExWpy9iXkG336dN/8nJqn69yijB7vJ8g=
github.com/metacubex/quic-go v0.43.2-0.20240518033621-2c3d14c6b38e/go.mod h1:uXHODgJFUfUnkkCMWLd5Er6L5QY/LFRZb9LD5jyyhsk=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72 h1:Wr4g1HCb5Z/QIFwFiVNjO2qL+dRu25+Mdn9xtAZZ+ew=
github.com/metacubex/sing-quic v0.0.0-20240518034124-7696d3f7da72/go.mod h1:g7Mxj7b7zm7YVqD975mk/hSmrb0A0G4bVvIMr2MMzn8=
github.com/metacubex/sing-shadowsocks v0.2.6 h1:6oEB3QcsFYnNiFeoevcXrCwJ3sAablwVSgtE9R3QeFQ=
github.com/metacubex/sing-shadowsocks v0.2.6/go.mod h1:zIkMeSnb8Mbf4hdqhw0pjzkn1d99YJ3JQm/VBg5WMTg=
github.com/metacubex/sing-shadowsocks2 v0.2.0 h1:hqwT/AfI5d5UdPefIzR6onGHJfDXs5zgOM5QSgaM/9A=
github.com/metacubex/sing-shadowsocks2 v0.2.0/go.mod h1:LCKF6j1P94zN8ZS+LXRK1gmYTVGB3squivBSXAFnOg8=
github.com/metacubex/sing-tun v0.2.6 h1:frc58BqnIClqcC9KcYBfVAn5bgO6WW1ANKvZW3/HYAQ=
github.com/metacubex/sing-tun v0.2.6/go.mod h1:4VsMwZH1IlgPGFK1ZbBomZ/B2MYkTgs2+gnBAr5GOIo=
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec h1:K4Wq3GOdLZ/xcqwyzAt4kmYQrjokyKQ3u/Xh5Yft14U=
github.com/metacubex/sing-tun v0.2.7-0.20240512075008-89e7c6208eec/go.mod h1:4VsMwZH1IlgPGFK1ZbBomZ/B2MYkTgs2+gnBAr5GOIo=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f h1:QjXrHKbTMBip/C+R79bvbfr42xH1gZl3uFb0RELdZiQ=
github.com/metacubex/sing-vmess v0.1.9-0.20231207122118-72303677451f/go.mod h1:olVkD4FChQ5gKMHG4ZzuD7+fMkJY1G8vwOKpRehjrmY=
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63 h1:AGyIB55UfQm/0ZH0HtQO9u3l//yjtHUpjeRjjPGfGRI=
github.com/metacubex/sing-wireguard v0.0.0-20240321042214-224f96122a63/go.mod h1:uY+BYb0UEknLrqvbGcwi9i++KgrKxsurysgI6G1Pveo=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66 h1:as/aO/fM8nv4W4pOr9EETP6kV/Oaujk3fUNyQSJK61c=
github.com/metacubex/tfo-go v0.0.0-20240228025757-be1269474a66/go.mod h1:c7bVFM9f5+VzeZ/6Kg77T/jrg1Xp8QpqlSHvG/aXVts=
github.com/metacubex/utls v1.6.6 h1:3D12YKHTf2Z41UPhQU2dWerNWJ5TVQD9gKoQ+H+iLC8=
github.com/metacubex/utls v1.6.6/go.mod h1:+WLFUnXjcpdxXCnyX25nggw8C6YonZ8zOK2Zm/oRvdo=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
@@ -166,14 +168,12 @@ github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnV
github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7 h1:DImB4lELfQhplLTxeq2z31Fpv8CQqqrUwTbrIRumZqQ=
github.com/sagernet/smux v0.0.0-20231208180855-7041f6ea79e7/go.mod h1:FP9X2xjT/Az1EsG/orYYoC+5MojWnuI7hrffz8fGwwo=
github.com/sagernet/utls v1.5.4 h1:KmsEGbB2dKUtCNC+44NwAdNAqnqQ6GA4pTO0Yik56co=
github.com/sagernet/utls v1.5.4/go.mod h1:CTGxPWExIloRipK3XFpYv0OVyhO8kk3XCGW/ieyTh1s=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e h1:iGH0RMv2FzELOFNFQtvsxH7NPmlo7X5JizEK51UCojo=
github.com/sagernet/wireguard-go v0.0.0-20231209092712-9a439356a62e/go.mod h1:YbL4TKHRR6APYQv3U2RGfwLDpPYSyWz6oUlpISBEzBE=
github.com/samber/lo v1.39.0 h1:4gTz1wUhNYLhFSKl6O+8peW0v2F4BCY034GRpU9WnuA=
github.com/samber/lo v1.39.0/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/shirou/gopsutil/v3 v3.24.3 h1:eoUGJSmdfLzJ3mxIhmOAhgKEKgQkeOwKpz1NbhVnuPE=
github.com/shirou/gopsutil/v3 v3.24.3/go.mod h1:JpND7O217xa72ewWz9zN2eIIkPWsDN/3pl0H8Qt0uwg=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -223,18 +223,18 @@ go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBs
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
@@ -254,28 +254,28 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY=
golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg=
golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw=
golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
lukechampine.com/blake3 v1.2.2 h1:wEAbSg0IVU4ih44CVlpMqMZMpzr5hf/6aqodLlevd/w=
lukechampine.com/blake3 v1.2.2/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
lukechampine.com/blake3 v1.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=

View File

@@ -10,10 +10,13 @@ import (
"github.com/metacubex/mihomo/adapter/provider"
"github.com/metacubex/mihomo/common/structure"
"github.com/metacubex/mihomo/common/utils"
"github.com/metacubex/mihomo/component/mmdb"
"github.com/metacubex/mihomo/config"
"github.com/metacubex/mihomo/constant"
cp "github.com/metacubex/mihomo/constant/provider"
"github.com/metacubex/mihomo/hub/executor"
"github.com/metacubex/mihomo/log"
rp "github.com/metacubex/mihomo/rules/provider"
"github.com/metacubex/mihomo/tunnel"
"github.com/metacubex/mihomo/tunnel/statistic"
"golang.org/x/net/context"
@@ -58,10 +61,17 @@ func shutdownClash() bool {
}
//export validateConfig
func validateConfig(s *C.char) bool {
bytes := []byte(C.GoString(s))
_, err := config.UnmarshalRawConfig(bytes)
return err == nil
func validateConfig(s *C.char, port C.longlong) {
i := int64(port)
go func() {
bytes := []byte(C.GoString(s))
_, err := config.UnmarshalRawConfig(bytes)
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
bridge.SendToPort(i, "")
}()
}
//export updateConfig
@@ -222,7 +232,7 @@ func asyncTestDelay(s *C.char) {
func getVersionInfo() *C.char {
versionInfo := map[string]string{
"clashName": constant.Name,
"version": constant.Version,
"version": "1.18.5",
}
data, err := json.Marshal(versionInfo)
if err != nil {
@@ -286,6 +296,84 @@ func getProvider(name *C.char) *C.char {
return C.CString(string(data))
}
//export getExternalProviders
func getExternalProviders() *C.char {
externalProviders := make([]ExternalProvider, 0)
providers := tunnel.Providers()
for n, p := range providers {
if p.VehicleType() != cp.Compatible {
p := p.(*provider.ProxySetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
for n, p := range tunnel.RuleProviders() {
if p.VehicleType() != cp.Compatible {
p := p.(*rp.RuleSetProvider)
externalProviders = append(externalProviders, ExternalProvider{
Name: n,
Type: p.Type().String(),
VehicleType: p.VehicleType().String(),
UpdateAt: p.UpdatedAt,
})
}
}
data, err := json.Marshal(externalProviders)
if err != nil {
return C.CString("")
}
return C.CString(string(data))
}
//export updateExternalProvider
func updateExternalProvider(providerName *C.char, providerType *C.char, port C.longlong) {
i := int64(port)
go func() {
providerNameString := C.GoString(providerName)
providerTypeString := C.GoString(providerType)
switch providerTypeString {
case "Proxy":
providers := tunnel.Providers()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "Rule":
providers := tunnel.RuleProviders()
err := providers[providerNameString].Update()
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoIp":
err := mmdb.DownloadMMDB(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "GeoSite":
err := mmdb.DownloadGeoSite(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
case "ASN":
err := mmdb.DownloadASN(constant.Path.Resolve(providerNameString))
if err != nil {
bridge.SendToPort(i, err.Error())
return
}
}
bridge.SendToPort(i, "")
}()
}
//export healthcheck
func healthcheck() {
hcCompatibleProvider(tunnel.Providers())

View File

@@ -82,7 +82,6 @@ class ApplicationState extends State<Application> {
super.initState();
globalState.appController = AppController(context);
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
globalState.appController.updateViewWidth();
globalState.appController.afterInit();
globalState.appController.initLink();
_updateGroups();

View File

@@ -5,9 +5,9 @@ import 'dart:io';
import 'dart:isolate';
import 'package:ffi/ffi.dart';
import '../enum/enum.dart';
import '../models/models.dart';
import '../common/common.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/models.dart';
import 'generated/clash_ffi.dart';
class ClashCore {
@@ -58,11 +58,20 @@ class ClashCore {
bool get isInit => clashFFI.getIsInit() == 1;
bool validateConfig(String data) {
return clashFFI.validateConfig(
data.toNativeUtf8().cast(),
) ==
1;
Future<String> validateConfig(String data) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.validateConfig(
data.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
return completer.future;
}
Future<String> updateConfig(UpdateConfigParams updateConfigParams) {
@@ -107,31 +116,41 @@ class ClashCore {
});
}
Future<DelayMap> getDelayMap() {
final proxiesRaw = clashFFI.getProxies();
final proxiesRawString = proxiesRaw.cast<Utf8>().toDartString();
return Isolate.run<DelayMap>(() {
final proxies = json.decode(proxiesRawString) as Map<String, dynamic>;
return proxies.map<String, int?>(
(k, v) {
final history = v["history"] as List<dynamic>;
if (history.isEmpty) {
return MapEntry(
k,
null,
);
} else {
final delay = history.last["delay"];
return MapEntry(
k,
delay != 0 ? delay : -1,
);
}
},
);
Future<List<ExternalProvider>> getExternalProviders() {
final externalProvidersRaw = clashFFI.getExternalProviders();
final externalProvidersRawString =
externalProvidersRaw.cast<Utf8>().toDartString();
return Isolate.run<List<ExternalProvider>>(() {
final externalProviders =
(json.decode(externalProvidersRawString) as List<dynamic>)
.map(
(item) => ExternalProvider.fromJson(item),
)
.toList();
return externalProviders;
});
}
Future<String> updateExternalProvider({
required String providerName,
required String providerType,
}) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
clashFFI.updateExternalProvider(
providerName.toNativeUtf8().cast(),
providerType.toNativeUtf8().cast(),
receiver.sendPort.nativePort,
);
return completer.future;
}
bool changeProxy(ChangeProxyParams changeProxyParams) {
final params = json.encode(changeProxyParams);
return clashFFI.changeProxy(params.toNativeUtf8().cast()) == 1;

View File

@@ -893,19 +893,22 @@ class ClashFFI {
_lookup<ffi.NativeFunction<GoUint8 Function()>>('shutdownClash');
late final _shutdownClash = _shutdownClashPtr.asFunction<int Function()>();
int validateConfig(
void validateConfig(
ffi.Pointer<ffi.Char> s,
int port,
) {
return _validateConfig(
s,
port,
);
}
late final _validateConfigPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
'validateConfig');
late final _validateConfig =
_validateConfigPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
late final _validateConfigPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('validateConfig');
late final _validateConfig = _validateConfigPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void updateConfig(
ffi.Pointer<ffi.Char> s,
@@ -1054,6 +1057,35 @@ class ClashFFI {
late final _getProvider = _getProviderPtr
.asFunction<ffi.Pointer<ffi.Char> Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getExternalProviders() {
return _getExternalProviders();
}
late final _getExternalProvidersPtr =
_lookup<ffi.NativeFunction<ffi.Pointer<ffi.Char> Function()>>(
'getExternalProviders');
late final _getExternalProviders =
_getExternalProvidersPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void updateExternalProvider(
ffi.Pointer<ffi.Char> providerName,
ffi.Pointer<ffi.Char> providerType,
int port,
) {
return _updateExternalProvider(
providerName,
providerType,
port,
);
}
late final _updateExternalProviderPtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>,
ffi.LongLong)>>('updateExternalProvider');
late final _updateExternalProvider = _updateExternalProviderPtr.asFunction<
void Function(ffi.Pointer<ffi.Char>, ffi.Pointer<ffi.Char>, int)>();
void healthcheck() {
return _healthcheck();
}

View File

@@ -1,24 +1,41 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'core.dart';
class ClashService {
Future<bool> initMmdb() async {
final mmdbPath = await appPath.getMMDBPath();
var mmdbFile = File(mmdbPath);
final isExists = await mmdbFile.exists();
if (isExists) return true;
Future<void> initGeo() async {
final homePath = await appPath.getHomeDirPath();
final homeDir = Directory(homePath);
final isExists = await homeDir.exists();
if (!isExists) {
await homeDir.create(recursive: true);
}
const geoFileNameList = [
mmdbFileName,
geoSiteFileName,
asnFileName,
];
try {
mmdbFile = await mmdbFile.create(recursive: true);
ByteData data = await rootBundle.load('assets/data/geoip.metadb');
List<int> bytes = data.buffer.asUint8List();
await mmdbFile.writeAsBytes(bytes, flush: true);
return true;
} catch (_) {
return false;
for (final geoFileName in geoFileNameList) {
final geoFile = File(
join(homePath, geoFileName),
);
final isExists = await geoFile.exists();
if (isExists) {
continue;
}
final data = await rootBundle.load('assets/data/$geoFileName');
List<int> bytes = data.buffer.asUint8List();
await geoFile.writeAsBytes(bytes, flush: true);
}
} catch (e) {
debugPrint("$e");
exit(0);
}
}
@@ -26,8 +43,7 @@ class ClashService {
required ClashConfig clashConfig,
required Config config,
}) async {
final isInitMmdb = await initMmdb();
if (!isInitMmdb) return false;
await initGeo();
final homeDirPath = await appPath.getHomeDirPath();
final isInit = clashCore.init(homeDirPath);
return isInit;

View File

@@ -9,6 +9,8 @@ const httpTimeoutDuration = Duration(milliseconds: 5000);
const moreDuration = Duration(milliseconds: 100);
const defaultUpdateDuration = Duration(days: 1);
const mmdbFileName = "geoip.metadb";
const geoSiteFileName = "GeoSite.dat";
const asnFileName = "ASN.mmdb";
const profilesDirectoryName = "profiles";
const localhost = "127.0.0.1";
const clashConfigKey = "clash_config";

View File

@@ -1,5 +1,7 @@
import 'package:fl_clash/common/app_localizations.dart';
extension DateTimeExtension on DateTime {
bool isBeforeNow() {
bool get isBeforeNow {
return isBefore(DateTime.now());
}
@@ -9,4 +11,28 @@ extension DateTimeExtension on DateTime {
}
return true;
}
String get lastUpdateTimeDesc {
final currentDateTime = DateTime.now();
final difference = currentDateTime.difference(this);
final days = difference.inDays;
if (days >= 365) {
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
}
if (days >= 30) {
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
}
if (days >= 1) {
return "$days ${appLocalizations.days}${appLocalizations.ago}";
}
final hours = difference.inHours;
if (hours >= 1) {
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
}
final minutes = difference.inMinutes;
if (minutes >= 1) {
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
}
return appLocalizations.just;
}
}

View File

@@ -29,6 +29,14 @@ class Navigation {
label: "profiles",
fragment: ProfilesFragment(),
),
const NavigationItem(
icon: Icon(Icons.swap_vert_circle),
label: "resources",
description: "resourcesDesc",
keep: false,
fragment: Resources(),
modes: [NavigationItemMode.desktop, NavigationItemMode.more],
),
NavigationItem(
icon: const Icon(Icons.adb),
label: "logs",

View File

@@ -2,6 +2,8 @@ import 'dart:io';
import 'dart:isolate';
import 'dart:typed_data';
import 'package:fl_clash/common/constant.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:flutter/material.dart';
import 'package:zxing2/qrcode.dart';
import 'package:image/image.dart' as img;
@@ -178,6 +180,12 @@ class Other {
.where((item) => item.isNotEmpty)
.toList();
}
ViewMode getViewMode(double viewWidth){
if (viewWidth <= maxMobileWidth) return ViewMode.mobile;
if (viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop;
}
}
final other = Other();

View File

@@ -36,11 +36,6 @@ class AppPath {
final directory = await getProfilesPath();
return join(directory, "$id.yaml");
}
Future<String> getMMDBPath() async {
var directory = await applicationSupportDirectoryCompleter.future;
return join(directory.path, mmdbFileName);
}
}
final appPath = AppPath();

View File

@@ -3,10 +3,9 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:fl_clash/common/common.dart';
import 'package:image_picker/image_picker.dart';
import 'package:fl_clash/models/models.dart';
class Picker {
Future<Result<PlatformFile>> pickerConfigFile() async {
Future<PlatformFile?> pickerConfigFile() async {
FilePickerResult? filePickerResult;
if (Platform.isAndroid) {
filePickerResult = await FilePicker.platform.pickFiles(
@@ -23,20 +22,20 @@ class Picker {
}
final file = filePickerResult?.files.first;
if (file == null) {
return Result.error(appLocalizations.pleaseUploadFile);
return null;
}
return Result.success(file);
return file;
}
Future<Result<String>> pickerConfigQRCode() async {
Future<String?> pickerConfigQRCode() async {
final xFile = await ImagePicker().pickImage(source: ImageSource.gallery);
final bytes = await xFile?.readAsBytes();
if (bytes == null) return Result.error();
if (bytes == null) return null;
final result = await other.parseQRCode(bytes);
if (result == null || !result.isUrl) {
return Result.error(appLocalizations.pleaseUploadValidQrcode);
throw appLocalizations.pleaseUploadValidQrcode;
}
return Result.success(result);
return result;
}
}

View File

@@ -11,7 +11,7 @@ class ProxyManager {
_proxy = proxy ?? proxy_plugin.Proxy();
}
bool get isStart => startTime != null && startTime!.isBeforeNow();
bool get isStart => startTime != null && startTime!.isBeforeNow;
DateTime? get startTime => _proxy.startTime;

View File

@@ -1,36 +1,78 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:fl_clash/common/common.dart';
import 'package:http/http.dart';
import '../models/models.dart';
import 'package:fl_clash/state.dart';
class Request {
static Future<Result<Response>> getFileResponseForUrl(String url) async {
final headers = {'User-Agent': coreName};
try {
final response = await get(Uri.parse(url), headers: headers).timeout(
httpTimeoutDuration,
late final Dio _dio;
int? _port;
Request() {
_dio = Dio(
BaseOptions(
connectTimeout: httpTimeoutDuration,
sendTimeout: httpTimeoutDuration,
receiveTimeout: httpTimeoutDuration,
headers: {"User-Agent": coreName},
),
);
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
_syncProxy();
return handler.next(options); // 继续请求
},
));
}
_syncProxy(){
final port = globalState.appController.clashConfig.mixedPort;
if (_port != port) {
_port = port;
_dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};
return client;
},
);
return Result.success(response);
} catch (err) {
return Result.error(err.toString());
}
}
static Future<Result<Map<String,dynamic>>> checkForUpdate() async {
final response = await get(
Uri.parse(
"https://api.github.com/repos/$repository/releases/latest",
Future<Response> getFileResponseForUrl(String url) async {
final response = await _dio
.get(
url,
options: Options(
responseType: ResponseType.bytes,
),
)
.timeout(
httpTimeoutDuration,
);
return response;
}
Future<Map<String, dynamic>?> checkForUpdate() async {
final response = await _dio.get(
"https://api.github.com/repos/$repository/releases/latest",
options: Options(
responseType: ResponseType.json,
),
);
if (response.statusCode != 200) return Result.error();
final body = json.decode(response.body) as Map<String,dynamic>;
final remoteVersion = body['tag_name'];
if (response.statusCode != 200) return null;
final data = response.data as Map<String, dynamic>;
final remoteVersion = data['tag_name'];
final packageInfo = await appPackage.packageInfoCompleter.future;
final version = packageInfo.version;
final hasUpdate =
other.compareVersions(remoteVersion.replaceAll('v', ''), version) > 0;
if (!hasUpdate) return Result.error();
return Result.success(body);
if (!hasUpdate) return null;
return data;
}
}
final request = Request();

View File

@@ -6,7 +6,6 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher.dart';
import 'clash/core.dart';
import 'enum/enum.dart';
import 'models/models.dart';
import 'common/common.dart';
@@ -78,10 +77,10 @@ class AppController {
);
}
addProfile(Profile profile) {
addProfile(Profile profile) async {
config.setProfile(profile);
if (config.currentProfileId != null) return;
changeProfile(profile.id);
await changeProfile(profile.id);
}
deleteProfile(String id) async {
@@ -102,15 +101,14 @@ class AppController {
updateProfile(String id) async {
final profile = config.getCurrentProfileForId(id);
if (profile != null) {
final res = await profile.update();
if (res.type == ResultType.success) {
config.setProfile(profile);
}
final tempProfile = profile.copyWith();
await tempProfile.update();
config.setProfile(tempProfile);
}
}
Future<String> updateClashConfig({bool isPatch = true}) async {
return await globalState.updateClashConfig(
Future<void> updateClashConfig({bool isPatch = true}) async {
await globalState.updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: isPatch,
@@ -118,24 +116,15 @@ class AppController {
}
Future applyProfile() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
}
Function? _changeProfileDebounce;
changeProfileDebounce(String? profileId) {
if (profileId == config.currentProfileId) return;
config.currentProfileId = profileId;
_changeProfileDebounce ??= debounce<Function(String?)>((profileId) async {
await applyProfile();
appState.delayMap = {};
saveConfigPreferences();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(() async {
await globalState.applyProfile(
appState: appState,
config: config,
clashConfig: clashConfig,
);
});
_changeProfileDebounce!([profileId]);
}
changeProfile(String? value) async {
@@ -153,8 +142,10 @@ class AppController {
?.add(
profile.autoUpdateDuration,
)
.isBeforeNow();
if (isNotNeedUpdate == false) continue;
.isBeforeNow;
if (isNotNeedUpdate == false ||
profile.url == null ||
profile.url!.isEmpty) continue;
await profile.update();
}
}
@@ -211,18 +202,17 @@ class AppController {
autoCheckUpdate() async {
if (!config.autoCheckUpdate) return;
final res = await Request.checkForUpdate();
checkUpdateResultHandle(result: res);
final res = await request.checkForUpdate();
checkUpdateResultHandle(data: res);
}
checkUpdateResultHandle({
Result<Map<String, dynamic>>? result,
bool handleError = false
}) async {
if (result == null) return;
if (result.type == ResultType.success) {
final tagName = result.data?['tag_name'];
final body = result.data?['body'];
Map<String, dynamic>? data,
bool handleError = false,
}) async {
if (data != null) {
final tagName = data['tag_name'];
final body = data['body'];
final submits = other.parseReleaseBody(body);
globalState.showMessage(
title: appLocalizations.discoverNewVersion,
@@ -248,7 +238,7 @@ class AppController {
},
confirmText: appLocalizations.goDownload,
);
} else if(handleError){
} else if (handleError) {
globalState.showMessage(
title: appLocalizations.checkUpdate,
message: TextSpan(
@@ -290,11 +280,10 @@ class AppController {
appState.setDelay(delay);
}
updateDelayMap() async {
appState.delayMap = await clashCore.getDelayMap();
}
toPage(int index, {bool hasAnimate = false}) {
if (index > appState.currentNavigationItems.length - 1) {
return;
}
appState.currentLabel = appState.currentNavigationItems[index].label;
if ((config.isAnimateToPage || hasAnimate)) {
globalState.pageController?.animateToPage(
@@ -350,70 +339,50 @@ class AppController {
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(
final profile = await commonScaffoldState?.loadingRun<Profile>(
() async {
await Future.delayed(const Duration(milliseconds: 300));
final profile = Profile(
url: url,
);
final res = await profile.update();
if (res.type == ResultType.success) {
addProfile(profile);
} else {
debugPrint(res.message);
globalState.showMessage(
title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan(text: res.message!),
);
}
await profile.update();
return profile;
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
addProfileFormFile() async {
final result = await picker.pickerConfigFile();
if (result.type == ResultType.error) return;
final platformFile = await globalState.safeRun(picker.pickerConfigFile);
if (!context.mounted) return;
globalState.navigatorKey.currentState?.popUntil((route) => route.isFirst);
toProfiles();
final commonScaffoldState = globalState.homeScaffoldKey.currentState;
if (commonScaffoldState?.mounted != true) return;
commonScaffoldState?.loadingRun(
final profile = await commonScaffoldState?.loadingRun<Profile?>(
() async {
await Future.delayed(const Duration(milliseconds: 300));
final bytes = result.data?.bytes;
final bytes = platformFile?.bytes;
if (bytes == null) {
return;
return null;
}
final profile = Profile(label: result.data?.name);
final sRes = await profile.saveFile(bytes);
if (sRes.type == ResultType.error) {
debugPrint(sRes.message);
globalState.showMessage(
title: "${appLocalizations.add}${appLocalizations.profile}",
message: TextSpan(text: sRes.message!),
);
return;
}
addProfile(profile);
final profile = Profile(label: platformFile?.name);
await profile.saveFile(bytes);
return profile;
},
title: "${appLocalizations.add}${appLocalizations.profile}",
);
if (profile != null) {
await addProfile(profile);
}
}
addProfileFormQrCode() async {
final result = await picker.pickerConfigQRCode();
if (result.type == ResultType.error) {
if (result.message != null) {
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(
text: result.message,
),
);
}
return;
}
addProfileFormURL(result.data!);
final url = await globalState.safeRun(picker.pickerConfigQRCode);
if (url == null) return;
addProfileFormURL(url);
}
clearShowProxyDelay() {
@@ -432,12 +401,9 @@ class AppController {
}
}
updateViewWidth() {
appState.viewWidth = context.width;
if (appState.viewWidth == 0) {
Future.delayed(moreDuration, () {
updateViewWidth();
});
}
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
appState.viewWidth = width;
});
}
}

View File

@@ -1,5 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/common.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
@@ -11,13 +10,13 @@ class AboutFragment extends StatelessWidget {
_checkUpdate(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final res =
await commonScaffoldState?.loadingRun<Result<Map<String, dynamic>>>(
Request.checkForUpdate,
final data =
await commonScaffoldState?.loadingRun<Map<String, dynamic>?>(
request.checkForUpdate,
title: appLocalizations.checkUpdate,
);
globalState.appController.checkUpdateResultHandle(
result: res,
data: data,
handleError: true,
);
}

View File

@@ -219,6 +219,25 @@ class _AccessFragmentState extends State<AccessFragment> {
return ValueListenableBuilder(
valueListenable: packagesListenable,
builder: (_, packages, ___) {
final accessControl = globalState.appController.config.accessControl;
final acceptList = accessControl.acceptList;
final rejectList = accessControl.rejectList;
final acceptPackages = packages.sorted((a, b) {
final isSelectA = acceptList.contains(a.packageName);
final isSelectB = acceptList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
final rejectPackages = packages.sorted((a, b) {
final isSelectA = rejectList.contains(a.packageName);
final isSelectB = rejectList.contains(b.packageName);
if (isSelectA && isSelectB) return 0;
if (isSelectA) return -1;
if (isSelectB) return 1;
return 0;
});
return Selector<Config, PackageListSelectorState>(
selector: (_, config) => PackageListSelectorState(
accessControl: config.accessControl,
@@ -228,18 +247,22 @@ class _AccessFragmentState extends State<AccessFragment> {
final accessControl = state.accessControl;
final isAccessControl = state.isAccessControl;
final isFilterSystemApp = accessControl.isFilterSystemApp;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
final accessControlMode = accessControl.mode;
final packages =
accessControlMode == AccessControlMode.acceptSelected
? acceptPackages
: rejectPackages;
final currentList =
accessControlMode == AccessControlMode.acceptSelected
? accessControl.acceptList
: accessControl.rejectList;
final currentPackages = isFilterSystemApp
? packages
.where((element) => element.isSystem == false)
.toList()
: packages;
final packageNameList =
currentPackages.map((e) => e.packageName).toList();
final valueList = currentList.intersection(packageNameList);
final describe =
accessControlMode == AccessControlMode.acceptSelected

View File

@@ -8,4 +8,5 @@ export 'access.dart';
export 'config.dart';
export 'application_setting.dart';
export 'about.dart';
export 'backup_and_recovery.dart';
export 'backup_and_recovery.dart';
export 'resources.dart';

View File

@@ -1,4 +1,5 @@
import 'package:collection/collection.dart';
import 'dart:async';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/state.dart';
@@ -7,10 +8,40 @@ import 'package:provider/provider.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
class LogsFragment extends StatelessWidget {
class LogsFragment extends StatefulWidget {
const LogsFragment({super.key});
_initActions(BuildContext context) {
@override
State<LogsFragment> createState() => _LogsFragmentState();
}
class _LogsFragmentState extends State<LogsFragment> {
final logsNotifier = ValueNotifier<List<Log>>([]);
Timer? timer;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
logsNotifier.value = context.read<AppState>().logs;
if (timer != null) {
timer?.cancel();
timer = null;
}
timer = Timer.periodic(const Duration(milliseconds: 200), (timer) {
logsNotifier.value = globalState.appController.appState.logs;
});
});
}
@override
void dispose() {
super.dispose();
timer?.cancel();
timer = null;
}
_initActions() {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
@@ -20,7 +51,7 @@ class LogsFragment extends StatelessWidget {
showSearch(
context: context,
delegate: LogsSearchDelegate(
logs: globalState.appController.appState.logs.reversed.toList(),
logs: logsNotifier.value.reversed.toList(),
),
);
},
@@ -31,10 +62,8 @@ class LogsFragment extends StatelessWidget {
}
_buildList() {
return Selector<AppState, List<Log>>(
selector: (_, appState) => appState.logs,
shouldRebuild: (prev, next) =>
!const ListEquality<Log>().equals(prev, next),
return ValueListenableBuilder<List<Log>>(
valueListenable: logsNotifier,
builder: (_, List<Log> logs, __) {
if (logs.isEmpty) {
return NullStatus(
@@ -48,7 +77,6 @@ class LogsFragment extends StatelessWidget {
itemBuilder: (BuildContext context, int index) {
final log = logs[index];
return LogItem(
key: ValueKey(log.dateTime),
log: log,
);
},
@@ -65,14 +93,13 @@ class LogsFragment extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Selector<AppState, bool?>(
selector: (_, appState) {
return appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools";
},
selector: (_, appState) =>
appState.currentLabel == 'logs' ||
appState.viewMode == ViewMode.mobile &&
appState.currentLabel == "tools",
builder: (_, isCurrent, child) {
if (isCurrent == null || isCurrent) {
_initActions(context);
_initActions();
}
return child!;
},

View File

@@ -40,7 +40,7 @@ class _EditProfileState extends State<EditProfile> {
_handleConfirm() {
if (!_formKey.currentState!.validate()) return;
final config = widget.context.read<Config>();
final hasUpdate = widget.profile.url != urlController.text;
final hasUpdate = urlController.text.isNotEmpty && widget.profile.url != urlController.text;
widget.profile.url = urlController.text;
widget.profile.label = labelController.text;
widget.profile.autoUpdate = autoUpdate;
@@ -82,7 +82,7 @@ class _EditProfileState extends State<EditProfile> {
},
),
),
if (widget.profile.url != null)...[
if (widget.profile.url != null && widget.profile.url!.isNotEmpty == true)...[
ListItem(
title: TextFormField(
controller: urlController,

View File

@@ -25,7 +25,6 @@ class ProfilesFragment extends StatefulWidget {
}
class _ProfilesFragmentState extends State<ProfilesFragment> {
_handleDeleteProfile(String id) async {
globalState.appController.deleteProfile(id);
}
@@ -140,9 +139,10 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
),
child: Selector2<AppState, Config, ProfilesSelectorState>(
selector: (_, appState, config) => ProfilesSelectorState(
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode),
profiles: config.profiles,
currentProfileId: config.currentProfileId,
viewMode: appState.viewMode,
),
builder: (context, state, child) {
if (state.profiles.isEmpty) {
return NullStatus(
@@ -176,30 +176,6 @@ class ProfileItem extends StatelessWidget {
required this.onChanged,
});
String _getLastUpdateTimeDifference(DateTime lastDateTime) {
final currentDateTime = DateTime.now();
final difference = currentDateTime.difference(lastDateTime);
final days = difference.inDays;
if (days >= 365) {
return "${(days / 365).floor()} ${appLocalizations.years}${appLocalizations.ago}";
}
if (days >= 30) {
return "${(days / 30).floor()} ${appLocalizations.months}${appLocalizations.ago}";
}
if (days >= 1) {
return "$days ${appLocalizations.days}${appLocalizations.ago}";
}
final hours = difference.inHours;
if (hours >= 1) {
return "$hours ${appLocalizations.hours}${appLocalizations.ago}";
}
final minutes = difference.inMinutes;
if (minutes >= 1) {
return "$minutes ${appLocalizations.minutes}${appLocalizations.ago}";
}
return appLocalizations.just;
}
@override
Widget build(BuildContext context) {
String useShow;
@@ -246,9 +222,7 @@ class ProfileItem extends StatelessWidget {
),
Flexible(
child: Text(
profile.lastUpdateDate != null
? _getLastUpdateTimeDifference(profile.lastUpdateDate!)
: '',
profile.lastUpdateDate?.lastUpdateTimeDesc ?? '',
style: Theme.of(context).textTheme.labelMedium?.toLight(),
),
),

View File

@@ -399,7 +399,6 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
late Animation<double> _opacity;
_healthcheck() async {
if (globalState.healthcheckLock) return;
@@ -416,7 +415,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 300,
milliseconds: 600,
),
);
_scale = Tween<double>(
@@ -432,19 +431,6 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
),
),
);
_opacity = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
curve: Curves.easeIn,
),
),
);
}
@override
@@ -465,10 +451,7 @@ class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
height: 56,
child: Transform.scale(
scale: _scale.value,
child: Opacity(
opacity: _opacity.value,
child: child!,
),
child: child,
),
);
},

View File

@@ -0,0 +1,172 @@
import 'dart:io';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/ffi.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:path/path.dart' hide context;
@immutable
class GeoItem {
final String label;
final String fileName;
const GeoItem({
required this.label,
required this.fileName,
});
}
class Resources extends StatefulWidget {
const Resources({super.key});
@override
State<Resources> createState() => _ResourcesState();
}
class _ResourcesState extends State<Resources> {
_updateExternalProvider(
String providerName,
String providerType,
) async {
final commonScaffoldState = context.commonScaffoldState;
await commonScaffoldState?.loadingRun(() async {
final message = await clashCore.updateExternalProvider(
providerName: providerName,
providerType: providerType,
);
if (message.isNotEmpty) throw message;
});
setState(() {});
}
Future<DateTime> _getGeoFileLastModified(String fileName) async {
final homePath = await appPath.getHomeDirPath();
return await File(join(homePath, fileName)).lastModified();
}
Widget _buildExternalProviderSection() {
return FutureBuilder<List<ExternalProvider>>(
future: () async {
await Future.delayed(const Duration(milliseconds: 200));
return await clashCore.getExternalProviders();
}(),
builder: (_, snapshot) {
return Center(
child: FadeBox(
key: const Key("external_providers"),
child: snapshot.data == null || snapshot.data!.isEmpty
? Container()
: Section(
title: appLocalizations.externalResources,
child: Column(
children: [
for (final externalProvider in snapshot.data!)
ListItem(
title: Text(externalProvider.name),
subtitle: Text(
"${externalProvider.type} (${externalProvider.vehicleType})",
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
externalProvider.updateAt.lastUpdateTimeDesc,
style: context.textTheme.bodyMedium,
),
const Padding(
padding: EdgeInsets.only(left: 12,right: 4),
child: VerticalDivider(
endIndent: 2,
width: 4,
indent: 2,
),
),
externalProvider.vehicleType == "HTTP"
? IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
externalProvider.name,
externalProvider.type,
);
},
)
: Container(),
],
),
)
],
),
),
),
);
},
);
}
Widget _buildGeoDataSection() {
const geoItems = <GeoItem>[
GeoItem(label: "GeoIp", fileName: mmdbFileName),
GeoItem(label: "GeoSite", fileName: geoSiteFileName),
GeoItem(label: "ASN", fileName: asnFileName),
];
return Section(
title: appLocalizations.geoData,
child: Column(
children: [
for (final geoItem in geoItems)
ListItem(
title: Text(geoItem.label),
subtitle: FutureBuilder<DateTime>(
future: () async {
await Future.delayed(const Duration(milliseconds: 200));
return await _getGeoFileLastModified(geoItem.fileName);
}(),
builder: (_, snapshot) {
return Container(
alignment: Alignment.centerLeft,
height: 24,
child: FadeBox(
key: Key("fade_box_${geoItem.label}"),
child: snapshot.data == null
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: Text(
snapshot.data!.lastUpdateTimeDesc,
),
),
);
},
),
trailing: IconButton(
icon: const Icon(Icons.sync),
onPressed: () {
_updateExternalProvider(
geoItem.fileName,
geoItem.label,
);
},
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return ListView(
children: [
_buildGeoDataSection(),
_buildExternalProviderSection(),
],
);
}
}

View File

@@ -9,6 +9,8 @@
"tools": "Tools",
"logs": "Logs",
"logsDesc": "Log capture records",
"resources": "Resources",
"resourcesDesc": "External resource related info",
"trafficUsage": "Traffic usage",
"coreInfo": "Core info",
"nullCoreInfoDesc": "Unable to obtain core info",
@@ -151,5 +153,8 @@
"checkUpdate": "Check for updates",
"discoverNewVersion": "Discover the new version",
"checkUpdateError": "The current application is already the latest version",
"goDownload": "Go to download"
"goDownload": "Go to download",
"unknown": "Unknown",
"geoData": "GeoData",
"externalResources": "External resources"
}

View File

@@ -9,6 +9,8 @@
"tools": "工具",
"logs": "日志",
"logsDesc": "日志捕获记录",
"resources": "资源",
"resourcesDesc": "外部资源相关信息",
"trafficUsage": "流量统计",
"coreInfo": "内核信息",
"nullCoreInfoDesc": "无法获取内核信息",
@@ -151,5 +153,8 @@
"checkUpdate": "检查更新",
"discoverNewVersion": "发现新版本",
"checkUpdateError": "当前应用已经是最新版了",
"goDownload": "前往下载"
"goDownload": "前往下载",
"unknown": "未知",
"geoData": "地理数据",
"externalResources": "外部资源"
}

View File

@@ -109,11 +109,14 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("Edit"),
"en": MessageLookupByLibrary.simpleMessage("English"),
"exit": MessageLookupByLibrary.simpleMessage("Exit"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
"fileDesc":
MessageLookupByLibrary.simpleMessage("Directly upload profile"),
"filterSystemApp":
MessageLookupByLibrary.simpleMessage("Filter system app"),
"geoData": MessageLookupByLibrary.simpleMessage("GeoData"),
"global": MessageLookupByLibrary.simpleMessage("Global"),
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
@@ -199,6 +202,9 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Only recovery profiles"),
"recoverySuccess":
MessageLookupByLibrary.simpleMessage("Recovery success"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"),
"rule": MessageLookupByLibrary.simpleMessage("Rule"),
"save": MessageLookupByLibrary.simpleMessage("Save"),
"selectAll": MessageLookupByLibrary.simpleMessage("Select all"),
@@ -229,6 +235,7 @@ class MessageLookup extends MessageLookupByLibrary {
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage(
"unable to update current profile"),
"unknown": MessageLookupByLibrary.simpleMessage("Unknown"),
"update": MessageLookupByLibrary.simpleMessage("Update"),
"upload": MessageLookupByLibrary.simpleMessage("Upload"),
"url": MessageLookupByLibrary.simpleMessage("URL"),

View File

@@ -90,9 +90,11 @@ class MessageLookup extends MessageLookupByLibrary {
"edit": MessageLookupByLibrary.simpleMessage("编辑"),
"en": MessageLookupByLibrary.simpleMessage("英语"),
"exit": MessageLookupByLibrary.simpleMessage("退出"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"),
"geoData": MessageLookupByLibrary.simpleMessage("地理数据"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
@@ -161,6 +163,8 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryDesc": MessageLookupByLibrary.simpleMessage("从WebDAV恢复数据"),
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
"save": MessageLookupByLibrary.simpleMessage("保存"),
"selectAll": MessageLookupByLibrary.simpleMessage("全选"),
@@ -187,6 +191,7 @@ class MessageLookup extends MessageLookupByLibrary {
"tunDesc": MessageLookupByLibrary.simpleMessage("仅在管理员模式生效"),
"unableToUpdateCurrentProfileDesc":
MessageLookupByLibrary.simpleMessage("无法更新当前配置文件"),
"unknown": MessageLookupByLibrary.simpleMessage("未知"),
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),

View File

@@ -150,6 +150,26 @@ class AppLocalizations {
);
}
/// `Resources`
String get resources {
return Intl.message(
'Resources',
name: 'resources',
desc: '',
args: [],
);
}
/// `External resource related info`
String get resourcesDesc {
return Intl.message(
'External resource related info',
name: 'resourcesDesc',
desc: '',
args: [],
);
}
/// `Traffic usage`
String get trafficUsage {
return Intl.message(
@@ -1579,6 +1599,36 @@ class AppLocalizations {
args: [],
);
}
/// `Unknown`
String get unknown {
return Intl.message(
'Unknown',
name: 'unknown',
desc: '',
args: [],
);
}
/// `GeoData`
String get geoData {
return Intl.message(
'GeoData',
name: 'geoData',
desc: '',
args: [],
);
}
/// `External resources`
String get externalResources {
return Intl.message(
'External resources',
name: 'externalResources',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -21,7 +21,6 @@ Future<void> main() async {
mode: clashConfig.mode,
isCompatible: config.isCompatible,
selectedMap: config.currentSelectedMap,
viewWidth: other.getViewWidth(),
);
await globalState.init(
appState: appState,

View File

@@ -32,7 +32,6 @@ class AppState with ChangeNotifier {
AppState({
required Mode mode,
double? viewWidth,
required bool isCompatible,
required SelectedMap selectedMap,
}) : _navigationItems = [],
@@ -40,7 +39,7 @@ class AppState with ChangeNotifier {
_currentLabel = "dashboard",
_traffics = [],
_logs = [],
_viewWidth = viewWidth ?? 0,
_viewWidth = 0,
_selectedMap = selectedMap,
_sortNum = 0,
_mode = mode,
@@ -167,6 +166,9 @@ class AppState with ChangeNotifier {
addLog(Log log) {
_logs.add(log);
if(_logs.length > 60){
_logs = _logs.sublist(_logs.length - 60);
}
notifyListeners();
}
@@ -179,7 +181,6 @@ class AppState with ChangeNotifier {
}
}
List<Group> get groups => _groups;
set groups(List<Group> value) {
@@ -253,11 +254,7 @@ class AppState with ChangeNotifier {
}
}
ViewMode get viewMode {
if (_viewWidth <= maxMobileWidth) return ViewMode.mobile;
if (_viewWidth <= maxLaptopWidth) return ViewMode.laptop;
return ViewMode.desktop;
}
ViewMode get viewMode => other.getViewMode(_viewWidth);
DelayMap get delayMap {
return _delayMap;

View File

@@ -1,24 +0,0 @@
import 'package:fl_clash/enum/enum.dart';
class Result<T> {
String? message;
ResultType type;
T? data;
Result({
this.message,
required this.type,
this.data,
});
Result.success([this.data]) : type = ResultType.success,
message = null;
Result.error([this.message]) : type = ResultType.error,
data = null;
@override
String toString() {
return 'Result{message: $message, type: $type, data: $data}';
}
}

View File

@@ -50,7 +50,7 @@ class Config extends ChangeNotifier {
_autoRun = false,
_themeMode = ThemeMode.system,
_openLog = false,
_isCompatible = false,
_isCompatible = true,
_primaryColor = defaultPrimaryColor.value,
_proxiesSortType = ProxiesSortType.none,
_isMinimizeOnExit = true,
@@ -90,7 +90,7 @@ class Config extends ChangeNotifier {
_setProfile(Profile profile) {
final List<Profile> profilesTemp = List.from(_profiles);
final index =
profilesTemp.indexWhere((element) => element.id == profile.id);
profilesTemp.indexWhere((element) => element.id == profile.id);
final updateProfile = profile.copyWith(
label: _getLabel(profile.label, profile.id),
);
@@ -281,7 +281,7 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: false)
@JsonKey(defaultValue: true)
bool get isCompatible {
return _isCompatible;
}

View File

@@ -75,3 +75,16 @@ class Process with _$Process {
factory Process.fromJson(Map<String, Object?> json) =>
_$ProcessFromJson(json);
}
@freezed
class ExternalProvider with _$ExternalProvider {
const factory ExternalProvider({
required String name,
required String type,
@JsonKey(name: "vehicle-type") required String vehicleType,
@JsonKey(name: "update-at") required DateTime updateAt,
}) = _ExternalProvider;
factory ExternalProvider.fromJson(Map<String, Object?> json) =>
_$ExternalProviderFromJson(json);
}

View File

@@ -1029,3 +1029,214 @@ abstract class _Process implements Process {
_$$ProcessImplCopyWith<_$ProcessImpl> get copyWith =>
throw _privateConstructorUsedError;
}
ExternalProvider _$ExternalProviderFromJson(Map<String, dynamic> json) {
return _ExternalProvider.fromJson(json);
}
/// @nodoc
mixin _$ExternalProvider {
String get name => throw _privateConstructorUsedError;
String get type => throw _privateConstructorUsedError;
@JsonKey(name: "vehicle-type")
String get vehicleType => throw _privateConstructorUsedError;
@JsonKey(name: "update-at")
DateTime get updateAt => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ExternalProviderCopyWith<ExternalProvider> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ExternalProviderCopyWith<$Res> {
factory $ExternalProviderCopyWith(
ExternalProvider value, $Res Function(ExternalProvider) then) =
_$ExternalProviderCopyWithImpl<$Res, ExternalProvider>;
@useResult
$Res call(
{String name,
String type,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
/// @nodoc
class _$ExternalProviderCopyWithImpl<$Res, $Val extends ExternalProvider>
implements $ExternalProviderCopyWith<$Res> {
_$ExternalProviderCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? type = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
return _then(_value.copyWith(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
as String,
updateAt: null == updateAt
? _value.updateAt
: updateAt // ignore: cast_nullable_to_non_nullable
as DateTime,
) as $Val);
}
}
/// @nodoc
abstract class _$$ExternalProviderImplCopyWith<$Res>
implements $ExternalProviderCopyWith<$Res> {
factory _$$ExternalProviderImplCopyWith(_$ExternalProviderImpl value,
$Res Function(_$ExternalProviderImpl) then) =
__$$ExternalProviderImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{String name,
String type,
@JsonKey(name: "vehicle-type") String vehicleType,
@JsonKey(name: "update-at") DateTime updateAt});
}
/// @nodoc
class __$$ExternalProviderImplCopyWithImpl<$Res>
extends _$ExternalProviderCopyWithImpl<$Res, _$ExternalProviderImpl>
implements _$$ExternalProviderImplCopyWith<$Res> {
__$$ExternalProviderImplCopyWithImpl(_$ExternalProviderImpl _value,
$Res Function(_$ExternalProviderImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? name = null,
Object? type = null,
Object? vehicleType = null,
Object? updateAt = null,
}) {
return _then(_$ExternalProviderImpl(
name: null == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
as String,
type: null == type
? _value.type
: type // ignore: cast_nullable_to_non_nullable
as String,
vehicleType: null == vehicleType
? _value.vehicleType
: vehicleType // ignore: cast_nullable_to_non_nullable
as String,
updateAt: null == updateAt
? _value.updateAt
: updateAt // ignore: cast_nullable_to_non_nullable
as DateTime,
));
}
}
/// @nodoc
@JsonSerializable()
class _$ExternalProviderImpl implements _ExternalProvider {
const _$ExternalProviderImpl(
{required this.name,
required this.type,
@JsonKey(name: "vehicle-type") required this.vehicleType,
@JsonKey(name: "update-at") required this.updateAt});
factory _$ExternalProviderImpl.fromJson(Map<String, dynamic> json) =>
_$$ExternalProviderImplFromJson(json);
@override
final String name;
@override
final String type;
@override
@JsonKey(name: "vehicle-type")
final String vehicleType;
@override
@JsonKey(name: "update-at")
final DateTime updateAt;
@override
String toString() {
return 'ExternalProvider(name: $name, type: $type, vehicleType: $vehicleType, updateAt: $updateAt)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ExternalProviderImpl &&
(identical(other.name, name) || other.name == name) &&
(identical(other.type, type) || other.type == type) &&
(identical(other.vehicleType, vehicleType) ||
other.vehicleType == vehicleType) &&
(identical(other.updateAt, updateAt) ||
other.updateAt == updateAt));
}
@JsonKey(ignore: true)
@override
int get hashCode =>
Object.hash(runtimeType, name, type, vehicleType, updateAt);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ExternalProviderImplCopyWith<_$ExternalProviderImpl> get copyWith =>
__$$ExternalProviderImplCopyWithImpl<_$ExternalProviderImpl>(
this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$ExternalProviderImplToJson(
this,
);
}
}
abstract class _ExternalProvider implements ExternalProvider {
const factory _ExternalProvider(
{required final String name,
required final String type,
@JsonKey(name: "vehicle-type") required final String vehicleType,
@JsonKey(name: "update-at") required final DateTime updateAt}) =
_$ExternalProviderImpl;
factory _ExternalProvider.fromJson(Map<String, dynamic> json) =
_$ExternalProviderImpl.fromJson;
@override
String get name;
@override
String get type;
@override
@JsonKey(name: "vehicle-type")
String get vehicleType;
@override
@JsonKey(name: "update-at")
DateTime get updateAt;
@override
@JsonKey(ignore: true)
_$$ExternalProviderImplCopyWith<_$ExternalProviderImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@@ -94,3 +94,21 @@ Map<String, dynamic> _$$ProcessImplToJson(_$ProcessImpl instance) =>
'source': instance.source,
'target': instance.target,
};
_$ExternalProviderImpl _$$ExternalProviderImplFromJson(
Map<String, dynamic> json) =>
_$ExternalProviderImpl(
name: json['name'] as String,
type: json['type'] as String,
vehicleType: json['vehicle-type'] as String,
updateAt: DateTime.parse(json['update-at'] as String),
);
Map<String, dynamic> _$$ExternalProviderImplToJson(
_$ExternalProviderImpl instance) =>
<String, dynamic>{
'name': instance.name,
'type': instance.type,
'vehicle-type': instance.vehicleType,
'update-at': instance.updateAt.toIso8601String(),
};

View File

@@ -20,6 +20,7 @@ mixin _$NavigationItem {
String get label => throw _privateConstructorUsedError;
String? get description => throw _privateConstructorUsedError;
Widget get fragment => throw _privateConstructorUsedError;
bool get keep => throw _privateConstructorUsedError;
String? get path => throw _privateConstructorUsedError;
List<NavigationItemMode> get modes => throw _privateConstructorUsedError;
@@ -39,6 +40,7 @@ abstract class $NavigationItemCopyWith<$Res> {
String label,
String? description,
Widget fragment,
bool keep,
String? path,
List<NavigationItemMode> modes});
}
@@ -60,6 +62,7 @@ class _$NavigationItemCopyWithImpl<$Res, $Val extends NavigationItem>
Object? label = null,
Object? description = freezed,
Object? fragment = null,
Object? keep = null,
Object? path = freezed,
Object? modes = null,
}) {
@@ -80,6 +83,10 @@ class _$NavigationItemCopyWithImpl<$Res, $Val extends NavigationItem>
? _value.fragment
: fragment // ignore: cast_nullable_to_non_nullable
as Widget,
keep: null == keep
? _value.keep
: keep // ignore: cast_nullable_to_non_nullable
as bool,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
@@ -105,6 +112,7 @@ abstract class _$$NavigationItemImplCopyWith<$Res>
String label,
String? description,
Widget fragment,
bool keep,
String? path,
List<NavigationItemMode> modes});
}
@@ -124,6 +132,7 @@ class __$$NavigationItemImplCopyWithImpl<$Res>
Object? label = null,
Object? description = freezed,
Object? fragment = null,
Object? keep = null,
Object? path = freezed,
Object? modes = null,
}) {
@@ -144,6 +153,10 @@ class __$$NavigationItemImplCopyWithImpl<$Res>
? _value.fragment
: fragment // ignore: cast_nullable_to_non_nullable
as Widget,
keep: null == keep
? _value.keep
: keep // ignore: cast_nullable_to_non_nullable
as bool,
path: freezed == path
? _value.path
: path // ignore: cast_nullable_to_non_nullable
@@ -164,6 +177,7 @@ class _$NavigationItemImpl implements _NavigationItem {
required this.label,
this.description,
required this.fragment,
this.keep = true,
this.path,
final List<NavigationItemMode> modes = const [
NavigationItemMode.mobile,
@@ -180,6 +194,9 @@ class _$NavigationItemImpl implements _NavigationItem {
@override
final Widget fragment;
@override
@JsonKey()
final bool keep;
@override
final String? path;
final List<NavigationItemMode> _modes;
@override
@@ -192,7 +209,7 @@ class _$NavigationItemImpl implements _NavigationItem {
@override
String toString() {
return 'NavigationItem(icon: $icon, label: $label, description: $description, fragment: $fragment, path: $path, modes: $modes)';
return 'NavigationItem(icon: $icon, label: $label, description: $description, fragment: $fragment, keep: $keep, path: $path, modes: $modes)';
}
@override
@@ -206,13 +223,14 @@ class _$NavigationItemImpl implements _NavigationItem {
other.description == description) &&
(identical(other.fragment, fragment) ||
other.fragment == fragment) &&
(identical(other.keep, keep) || other.keep == keep) &&
(identical(other.path, path) || other.path == path) &&
const DeepCollectionEquality().equals(other._modes, _modes));
}
@override
int get hashCode => Object.hash(runtimeType, icon, label, description,
fragment, path, const DeepCollectionEquality().hash(_modes));
fragment, keep, path, const DeepCollectionEquality().hash(_modes));
@JsonKey(ignore: true)
@override
@@ -228,6 +246,7 @@ abstract class _NavigationItem implements NavigationItem {
required final String label,
final String? description,
required final Widget fragment,
final bool keep,
final String? path,
final List<NavigationItemMode> modes}) = _$NavigationItemImpl;
@@ -240,6 +259,8 @@ abstract class _NavigationItem implements NavigationItem {
@override
Widget get fragment;
@override
bool get keep;
@override
String? get path;
@override
List<NavigationItemMode> get modes;

View File

@@ -9,7 +9,6 @@ export 'log.dart';
export 'system_color_scheme.dart';
export 'connection.dart';
export 'package.dart';
export 'common.dart';
export 'ffi.dart';
export 'selector.dart';
export 'navigation.dart';

View File

@@ -11,6 +11,7 @@ class NavigationItem with _$NavigationItem {
required String label,
final String? description,
required Widget fragment,
@Default(true) bool keep,
String? path,
@Default([NavigationItemMode.mobile, NavigationItemMode.desktop])
List<NavigationItemMode> modes,

View File

@@ -5,11 +5,8 @@ import 'dart:typed_data';
import 'package:fl_clash/clash/core.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:json_annotation/json_annotation.dart';
import 'common.dart';
part 'generated/profile.g.dart';
typedef SelectedMap = Map<String, String>;
@@ -83,44 +80,28 @@ class Profile {
Duration? autoUpdateDuration,
this.autoUpdate = true,
}) : id = id ?? DateTime.now().millisecondsSinceEpoch.toString(),
autoUpdateDuration =
autoUpdateDuration ?? defaultUpdateDuration,
autoUpdateDuration = autoUpdateDuration ?? defaultUpdateDuration,
selectedMap = selectedMap ?? {};
ProfileType get type => url == null ? ProfileType.file : ProfileType.url;
Future<Result<bool>> checkAndUpdate() async {
Future<void> checkAndUpdate() async {
final isExists = await check();
if(!isExists){
if(url != null){
if (!isExists) {
if (url != null) {
return await update();
}
return Result.error();
}
return Result.success();
}
Future<Result<bool>> update() async {
if (url == null) {
return Result.error(
appLocalizations.unableToUpdateCurrentProfileDesc,
);
}
final responseResult = await Request.getFileResponseForUrl(url!);
final response = responseResult.data;
if (responseResult.type != ResultType.success || response == null) {
return Result.error(responseResult.message);
}
final disposition = response.headers['content-disposition'];
Future<void> update() async {
final response = await request.getFileResponseForUrl(url!);
final disposition = response.headers.value("content-disposition");
label ??= other.getFileNameForDisposition(disposition) ?? id;
final userinfo = response.headers['subscription-userinfo'];
final userinfo = response.headers.value('subscription-userinfo');
userInfo = UserInfo.formHString(userinfo);
final saveResult = await saveFile(response.bodyBytes);
if (saveResult.type == ResultType.error) {
return Result.error(saveResult.message);
}
await saveFile(response.data);
lastUpdateDate = DateTime.now();
return Result.success();
}
Future<bool> check() async {
@@ -128,10 +109,10 @@ class Profile {
return await File(profilePath!).exists();
}
Future<Result<void>> saveFile(Uint8List bytes) async {
final isValidate = clashCore.validateConfig(utf8.decode(bytes));
if (!isValidate) {
return Result.error(appLocalizations.profileParseErrorDesc);
Future<void> saveFile(Uint8List bytes) async {
final message = await clashCore.validateConfig(utf8.decode(bytes));
if (message.isNotEmpty) {
throw message;
}
final path = await appPath.getProfilePath(id);
final file = File(path!);
@@ -141,7 +122,6 @@ class Profile {
}
await file.writeAsBytes(bytes);
lastUpdateDate = DateTime.now();
return Result.success();
}
Map<String, dynamic> toJson() {

View File

@@ -1,3 +1,4 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
@@ -56,54 +57,65 @@ class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return PopContainer(
child: Selector2<AppState, Config, HomeSelectorState>(
selector: (_, appState, config) => HomeSelectorState(
currentLabel: appState.currentLabel,
navigationItems: appState.currentNavigationItems,
viewMode: appState.viewMode,
locale: config.locale,
),
builder: (_, state, child) {
final viewMode = state.viewMode;
final navigationItems = state.navigationItems;
final currentLabel = state.currentLabel;
final index = navigationItems.lastIndexWhere(
(element) => element.label == currentLabel,
);
final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar(
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
Widget body;
if (viewMode != ViewMode.mobile) {
body = Row(
children: [
navigationBar,
Expanded(
flex: 1,
child: child!,
)
],
);
} else {
body = child!;
child: LayoutBuilder(
builder: (_, container) {
final appController = globalState.appController;
final maxWidth = container.maxWidth;
if (appController.appState.viewWidth != maxWidth) {
globalState.appController.updateViewWidth(maxWidth);
}
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
return Selector2<AppState, Config, HomeSelectorState>(
selector: (_, appState, config) {
return HomeSelectorState(
currentLabel: appState.currentLabel,
navigationItems: appState.currentNavigationItems,
viewMode: other.getViewMode(maxWidth),
locale: config.locale,
);
},
builder: (_, state, child) {
final viewMode = state.viewMode;
final navigationItems = state.navigationItems;
final currentLabel = state.currentLabel;
final index = navigationItems.lastIndexWhere(
(element) => element.label == currentLabel,
);
final currentIndex = index == -1 ? 0 : index;
final navigationBar = _getNavigationBar(
viewMode: viewMode,
navigationItems: navigationItems,
currentIndex: currentIndex,
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
Widget body;
if (viewMode != ViewMode.mobile) {
body = Row(
children: [
navigationBar,
Expanded(
flex: 1,
child: child!,
)
],
);
} else {
body = child!;
}
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
body: body,
bottomNavigationBar: bottomNavigationBar,
);
},
child: const HomeBody(
key: Key("home_boy"),
),
body: body,
bottomNavigationBar: bottomNavigationBar,
);
},
child: const HomeBody(
key: Key("home_boy"),
),
),
);
}
@@ -120,7 +132,7 @@ class HomeBody extends StatelessWidget {
final currentIndex = index == -1 ? 0 : index;
if (globalState.pageController != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
globalState.appController.toPage(currentIndex);
globalState.appController.toPage(currentIndex, hasAnimate: true);
});
} else {
globalState.pageController = PageController(
@@ -146,6 +158,7 @@ class HomeBody extends StatelessWidget {
itemBuilder: (_, index) {
final navigationItem = navigationItems[index];
return KeepContainer(
keep: navigationItem.keep,
key: Key(navigationItem.label),
child: navigationItem.fragment,
);

View File

@@ -70,7 +70,7 @@ class Proxy extends ProxyPlatform {
});
}
bool get isStart => startTime != null && startTime!.isBeforeNow();
bool get isStart => startTime != null && startTime!.isBeforeNow;
startAfterHook(int? fd) {
if (!isStart && fd != null) {

View File

@@ -38,7 +38,7 @@ class GlobalState {
timer?.cancel();
}
Future<String> updateClashConfig({
Future<void> updateClashConfig({
required ClashConfig clashConfig,
required Config config,
bool isPatch = true,
@@ -46,12 +46,13 @@ class GlobalState {
final profilePath = await appPath.getProfilePath(config.currentProfileId);
await config.currentProfile?.checkAndUpdate();
debugPrint("update config");
return clashCore.updateConfig(UpdateConfigParams(
final res = await clashCore.updateConfig(UpdateConfigParams(
profilePath: profilePath,
config: clashConfig,
isPatch: isPatch,
isCompatible: config.isCompatible,
));
if (res.isNotEmpty) throw res;
}
updateCoreVersionInfo(AppState appState) {
@@ -76,17 +77,16 @@ class GlobalState {
stopListenUpdate();
}
applyProfile({
Future<void> applyProfile({
required AppState appState,
required Config config,
required ClashConfig clashConfig,
}) async {
final res = await updateClashConfig(
await updateClashConfig(
clashConfig: clashConfig,
config: config,
isPatch: false,
);
if (res.isNotEmpty) return Result.error(res);
await updateGroups(appState);
changeProxy(
appState: appState,
@@ -121,19 +121,17 @@ class GlobalState {
required Config config,
required ClashConfig clashConfig,
}) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
if (config.profiles.isEmpty) {
stopSystemProxy();
return;
}
config.currentSelectedMap.forEach((key, value) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: key,
proxyName: value,
),
);
});
}
@@ -167,9 +165,7 @@ class GlobalState {
title: Text(title),
content: Container(
width: 300,
constraints: const BoxConstraints(
maxHeight: 200
),
constraints: const BoxConstraints(maxHeight: 200),
child: SingleChildScrollView(
child: RichText(
overflow: TextOverflow.visible,
@@ -270,6 +266,24 @@ class GlobalState {
});
updateCurrentDelayDebounce!([proxyName]);
}
Future<T?> safeRun<T>(
FutureOr<T> Function() futureFunction, {
String? title,
}) async {
try {
final res = await futureFunction();
return res;
} catch (e) {
showMessage(
title: title ?? appLocalizations.tip,
message: TextSpan(
text: e.toString(),
),
);
return null;
}
}
}
final globalState = GlobalState();

View File

@@ -2,8 +2,13 @@ import 'package:flutter/material.dart';
class KeepContainer extends StatefulWidget {
final Widget child;
final bool keep;
const KeepContainer({super.key, required this.child});
const KeepContainer({
super.key,
required this.child,
this.keep = true,
});
@override
State<KeepContainer> createState() => _KeepContainerState();
@@ -11,7 +16,6 @@ class KeepContainer extends StatefulWidget {
class _KeepContainerState extends State<KeepContainer>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
@@ -19,5 +23,5 @@ class _KeepContainerState extends State<KeepContainer>
}
@override
bool get wantKeepAlive => true;
bool get wantKeepAlive => widget.keep;
}

View File

@@ -27,11 +27,6 @@ class _WindowContainerState extends State<WindowContainer>
windowManager.addListener(this);
}
@override
void onWindowResize() {
globalState.appController.updateViewWidth();
}
@override
void onWindowClose() async {
await globalState.appController.handleBackOrExit();

View File

@@ -218,7 +218,7 @@ packages:
source: hosted
version: "2.3.6"
dio:
dependency: transitive
dependency: "direct main"
description:
name: dio
sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5"
@@ -269,10 +269,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4"
sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.2.1"
version: "8.0.3"
file_selector_linux:
dependency: transitive
description:
@@ -398,7 +398,7 @@ packages:
source: hosted
version: "2.1.0"
http:
dependency: "direct main"
dependency: transitive
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
@@ -433,10 +433,10 @@ packages:
dependency: "direct main"
description:
name: image_picker
sha256: "33974eca2e87e8b4e3727f1b94fa3abcb25afe80b6bc2c4d449a0e150aedf720"
sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.1"
version: "1.1.2"
image_picker_android:
dependency: transitive
description:

View File

@@ -1,7 +1,7 @@
name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none'
version: 0.8.8
version: 0.8.12
environment:
sdk: '>=3.1.0 <4.0.0'
@@ -23,8 +23,7 @@ dependencies:
launch_at_startup: ^0.2.2
windows_single_instance: ^1.0.1
json_annotation: ^4.9.0
http: ^1.2.0
file_picker: ^6.1.1
file_picker: ^8.0.3
mobile_scanner: 5.0.1
app_links: ^3.5.0
win32_registry: ^1.1.2
@@ -34,10 +33,11 @@ dependencies:
package_info_plus: ^7.0.0
url_launcher: ^6.2.6
freezed_annotation: ^2.4.1
image_picker: ^1.1.1
image_picker: ^1.1.2
zxing2: ^0.2.3
image: ^4.1.7
webdav_client: ^1.2.2
dio: ^5.4.3+1
dev_dependencies:
flutter_test:
sdk: flutter
@@ -51,7 +51,7 @@ dev_dependencies:
flutter:
uses-material-design: true
assets:
- assets/data/geoip.metadb
- assets/data/
- assets/images/
ffigen:
name: "ClashFFI"

View File

@@ -1,5 +1,4 @@
// ignore_for_file: avoid_print
void main() async {
String input = """