alroyso 1 рік тому
батько
коміт
f3d16562da
42 змінених файлів з 2686 додано та 2065 видалено
  1. 258 0
      assets/dep/example.yaml
  2. BIN
      assets/tp/clash/Country.mmdb
  3. 0 16
      assets/tp/clash/config.yaml
  4. BIN
      assets/tp/clash/geoip.dat
  5. 0 779
      assets/tp/clash/geosite.dat
  6. 0 294
      core/go.sum
  7. 1 1
      core/lib.go
  8. 63 0
      lib/app/bean/ClashServiceInfo.dart
  9. 0 35
      lib/app/bean/clash_config_entity.dart
  10. 0 141
      lib/app/bean/clash_config_generator.dart
  11. 87 0
      lib/app/bean/clash_core.dart
  12. 119 0
      lib/app/bean/config.dart
  13. 163 0
      lib/app/bean/connect.dart
  14. 162 0
      lib/app/bean/proxie.dart
  15. 0 18
      lib/app/bean/proxy_group.dart
  16. 111 6
      lib/app/bean/rule.dart
  17. 1 0
      lib/app/component/connection_status.dart
  18. 84 0
      lib/app/const/const.dart
  19. 190 0
      lib/app/controller/GlobalController.dart
  20. 162 0
      lib/app/controller/config.dart
  21. 51 0
      lib/app/controller/controllers.dart
  22. 143 0
      lib/app/controller/core.dart
  23. 36 0
      lib/app/controller/protocol.dart
  24. 242 0
      lib/app/controller/service.dart
  25. 138 0
      lib/app/controller/tray.dart
  26. 45 0
      lib/app/controller/window.dart
  27. 0 18
      lib/app/global_controller/GlobalController.dart
  28. 17 17
      lib/app/modules/home/controllers/home_controller.dart
  29. 0 3
      lib/app/modules/home/views/home_view.dart
  30. 1 1
      lib/app/modules/node/controllers/node_controller.dart
  31. 0 698
      lib/app/service/clash_service.dart
  32. 23 0
      lib/app/utils/event.dart
  33. 56 0
      lib/app/utils/logger.dart
  34. 39 0
      lib/app/utils/shell.dart
  35. 44 0
      lib/app/utils/system_dns.dart
  36. 188 0
      lib/app/utils/system_proxy.dart
  37. 66 0
      lib/app/utils/utils.dart
  38. 87 28
      lib/main.dart
  39. 1 1
      macos/Podfile.lock
  40. 0 8
      macos/Runner.xcodeproj/project.pbxproj
  41. 8 1
      pubspec.yaml
  42. 100 0
      scripts/init.dart

+ 258 - 0
assets/dep/example.yaml

@@ -0,0 +1,258 @@
+# HTTP 代理端口
+port: 7890
+
+# SOCKS5 代理端口
+socks-port: 7891
+
+# Linux 和 macOS 的 redir 代理端口
+redir-port: 7892
+mixed-port: 7893
+
+# 允许局域网的连接
+allow-lan: true
+
+# 规则模式:Rule(规则) / Global(全局代理)/ Direct(全局直连)
+# mode: rule
+mode: Rule
+
+# 设置日志输出级别 (默认级别:silent,即不输出任何内容,以避免因日志内容过大而导致程序内存溢出)。
+# 5 个级别:silent / info / warning / error / debug。级别越高日志输出量越大,越倾向于调试,若需要请自行开启。
+log-level: debug
+# Clash 的 RESTful API
+external-controller: "127.0.0.1:9090"
+
+# RESTful API 的口令
+secret: ""
+
+# 您可以将静态网页资源(如 clash-dashboard)放置在一个目录中,clash 将会服务于 `RESTful API/ui`
+# 参数应填写配置目录的相对路径或绝对路径。
+# external-ui: folder
+
+# DNS 设置
+
+dns:
+  nameserver:
+    - 114.114.114.114
+    - 119.29.29.29
+    - https://doh.pub/dns-query
+    - https://dns.alidns.com/dns-query
+  fallback:
+    - https://dns.cloudflare.com/dns-query
+    - "[2001:da8::666]:53"
+    - https://public.dns.iij.jp/dns-query
+    - https://jp.tiar.app/dns-query
+    - https://jp.tiarap.org/dns-query
+    - tls://dot.tiar.app
+  enable: true
+  ipv6: false
+  # enhanced-mode: redir-host
+  enhanced-mode: fake-ip
+  fake-ip-range: 198.18.0.1/16
+  listen: 0.0.0.0:53
+  fake-ip-filter:
+    - "*.lan"
+  default-nameserver:
+    - 114.114.114.114
+    - 119.29.29.29
+    - "[2001:da8::666]:53"
+
+tun:
+  enable: false
+  stack: system
+  # stack: gvisor
+  dns-hijack:
+    - 198.18.0.2:53 # when `fake-ip-range` is 198.18.0.1/16, should hijack 198.18.0.2:53
+  auto-route: true # auto set global route for Windows
+  # It is recommended to use `interface-name`
+  auto-detect-interface: true # auto detect interface, conflict with `interface-name`
+
+# 服务器节点订阅
+# proxy-providers:
+#   sspool:
+#     type: http
+#     url: "xxx"
+#     interval: 3600
+#     path: ./proxy/sspool.yaml
+#     health-check:
+#       enable: true
+#       interval: 600
+#       url: http://www.gstatic.com/generate_204
+
+# proxies:
+#   - name: "proxie"
+#     type: vmess
+#     server: xxx
+#     port: 8080
+#     uuid: xxx
+#     alterId: 1
+#     cipher: auto
+#     udp: true
+#     tls: false
+#     network: ws
+#     ws-opts:
+#       path: /y831
+
+proxy-groups:
+  - name: proxy
+    type: select
+    proxies:
+      - REJECT
+      - DIRECT
+
+
+
+rule-providers:
+# name: # Provider 名称
+#   type: http # http 或 file
+#   behavior: classical # 或 ipcidr、domain
+#   path: # 文件路径
+#   url: # 只有当类型为 HTTP 时才可用,您不需要在本地空间中创建新文件。
+#   interval: # 自动更新间隔,仅在类型为 HTTP 时可用
+
+# reject:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/reject.txt"
+#   path: ./ruleset/reject.yaml
+#   interval: 86400
+
+# icloud:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/icloud.txt"
+#   path: ./ruleset/icloud.yaml
+#   interval: 86400
+
+# apple:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/apple.txt"
+#   path: ./ruleset/apple.yaml
+#   interval: 86400
+
+# google:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/google.txt"
+#   path: ./ruleset/google.yaml
+#   interval: 86400
+
+# direct:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/direct.txt"
+#   path: ./ruleset/direct.yaml
+#   interval: 86400
+
+# private:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/private.txt"
+#   path: ./ruleset/private.yaml
+#   interval: 86400
+
+# gfw:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/gfw.txt"
+#   path: ./ruleset/gfw.yaml
+#   interval: 86400
+
+# greatfire:
+#   type: http
+#   behavior: domain
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/greatfire.txt"
+#   path: ./ruleset/greatfire.yaml
+#   interval: 86400
+
+# lancidr:
+#   type: http
+#   behavior: ipcidr
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/Loyalsoldier/clash-rules/release/lancidr.txt"
+#   path: ./ruleset/lancidr.yaml
+#   interval: 86400
+
+# china:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/China.yaml"
+#   path: ./ruleset/china.yaml
+#   interval: 86400
+
+# streamingcn:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/StreamingMedia/StreamingCN.yaml"
+#   path: ./ruleset/streamingcn.yaml
+#   interval: 86400
+
+# streaming:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/StreamingMedia/Streaming.yaml"
+#   path: ./ruleset/streaming.yaml
+#   interval: 86400
+
+# telegram:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/Extra/Telegram/Telegram.yaml"
+#   path: ./ruleset/telegram.yaml
+#   interval: 86400
+
+# privacy:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/Guard/Privacy.yaml"
+#   path: ./ruleset/hijacking.yaml
+#   interval: 86400
+
+# hijacking:
+#   type: http
+#   behavior: classical
+#   url: "https://ghproxy.com/https://raw.githubusercontent.com/DivineEngine/Profiles/master/Clash/RuleSet/Guard/Hijacking.yaml"
+#   path: ./ruleset/hijacking.yaml
+#   interval: 86400
+
+# 规则
+# rules:
+# Unbreak
+# - PROCESS-NAME,China,DIRECT
+# - PROCESS-NAME,google,DIRECT
+# - RULE-SET,"🎯 全球直连",DIRECT
+# - RULE-SET,China,DIRECT
+# - RULE-SET,google,DIRECT
+# - MATCH,MATCH
+# - GEOIP,CN,DIRECT
+rules:
+  # - DOMAIN,pub.dev,🌎 国外网站
+  # - DOMAIN,translate.googleapis.com,🌎 国外网站
+  # # https://github.com/Dreamacro/clash/issues/1663#issuecomment-1075815348
+  # - DOMAIN-SUFFIX,msftconnecttest.com,🌎 国外网站
+
+  # - RULE-SET,private,DIRECT
+  # - RULE-SET,lancidr,DIRECT,no-resolve
+  # - RULE-SET,applications,DIRECT
+
+  # - RULE-SET,reject,🛑 广告拦截
+  # - RULE-SET,privacy,🛑 隐私保护
+  # - RULE-SET,hijacking,🛑 网络劫持
+
+  # - RULE-SET,icloud,🍎 苹果服务
+  # - RULE-SET,apple,🍎 苹果服务
+
+  # - RULE-SET,china,🌏 国内网站
+  # - RULE-SET,streamingcn,🌏 国内网站
+  # - GEOIP,CN,🌏 国内网站
+
+  # - RULE-SET,google,🎯 全球直连
+  # - RULE-SET,direct,🎯 全球直连
+
+  # - RULE-SET,telegram,📲 电报信息
+
+  # - RULE-SET,gfw,🌎 国外网站
+  # - RULE-SET,proxy,🌎 国外网站
+  # - RULE-SET,greatfire,🌎 国外网站
+  # - RULE-SET,streaming,🌎 国外网站
+
+  - MATCH,proxy

BIN
assets/tp/clash/Country.mmdb


+ 0 - 16
assets/tp/clash/config.yaml

@@ -1,16 +0,0 @@
-external-controller: 127.0.0.1:22345
-
-mixed-port: 22346
-
-rules:
-  # localhost rule
-  - DOMAIN-KEYWORD,announce,DIRECT
-  - DOMAIN-KEYWORD,torrent,DIRECT
-  - DOMAIN-KEYWORD,tracker,DIRECT
-  - DOMAIN-SUFFIX,smtp,DIRECT
-  - DOMAIN-SUFFIX,local,DIRECT
-  - IP-CIDR,192.168.0.0/16,DIRECT
-  - IP-CIDR,10.0.0.0/8,DIRECT
-  - IP-CIDR,172.16.0.0/12,DIRECT
-  - IP-CIDR,127.0.0.0/8,DIRECT
-  - IP-CIDR,100.64.0.0/10,DIRECT

BIN
assets/tp/clash/geoip.dat


Різницю між файлами не показано, бо вона завелика
+ 0 - 779
assets/tp/clash/geosite.dat


+ 0 - 294
core/go.sum

@@ -1,294 +0,0 @@
-github.com/3andne/restls-client-go v0.1.6 h1:tRx/YilqW7iHpgmEL4E1D8dAsuB0tFF3uvncS+B6I08=
-github.com/3andne/restls-client-go v0.1.6/go.mod h1:iEdTZNt9kzPIxjIGSMScUFSBrUH6bFRNg0BWlP4orEY=
-github.com/MetaCubeX/Clash.Meta v1.16.0 h1:iXyrWiNXW2KTeVavNF6RYT+ZttN1GcA2RZA4yBOJy4g=
-github.com/MetaCubeX/Clash.Meta v1.16.0/go.mod h1:KjXZh8AsC2LLtL9iUvBeHG+GofSfTMj4WLddhuVcKrs=
-github.com/RyuaNerin/go-krypto v1.0.2 h1:9KiZrrBs+tDrQ66dNy4nrX6SzntKtSKdm0wKHhdB4WM=
-github.com/RyuaNerin/go-krypto v1.0.2/go.mod h1:17LzMeJCgzGTkPH3TmfzRnEJ/yA7ErhTPp9sxIqONtA=
-github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok=
-github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
-github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
-github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
-github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
-github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
-github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
-github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
-github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
-github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
-github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
-github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
-github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y=
-github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs=
-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=
-github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
-github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
-github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8=
-github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I=
-github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391 h1:8j2RH289RJplhA6WfdaPqzg1MjH2K8wX5e0uhAxrw2g=
-github.com/ericlagergren/polyval v0.0.0-20220411101811-e25bc10ba391/go.mod h1:K2R7GhgxrlJzHw2qiPWsCZXf/kXEJN9PLnQK73Ll0po=
-github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c h1:RUzBDdZ+e/HEe2Nh8lYsduiPAZygUfVXJn0Ncj5sHMg=
-github.com/ericlagergren/saferand v0.0.0-20220206064634-960a4dd2bc5c/go.mod h1:ETASDWf/FmEb6Ysrtd1QhjNedUU/ZQxBCRLh60bQ/UI=
-github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 h1:tlDMEdcPRQKBEz5nGDMvswiajqh7k8ogWRlhRwKy5mY=
-github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1/go.mod h1:4RfsapbGx2j/vU5xC/5/9qB3kn9Awp1YDiEnN43QrJ4=
-github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010 h1:fuGucgPk5dN6wzfnxl3D0D3rVLw4v2SbBT9jb4VnxzA=
-github.com/ericlagergren/subtle v0.0.0-20220507045147-890d697da010/go.mod h1:JtBcj7sBuTTRupn7c2bFspMDIObMJsVK8TeUvpShPok=
-github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
-github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
-github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
-github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
-github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
-github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
-github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
-github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
-github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
-github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
-github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
-github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
-github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
-github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
-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/gofrs/uuid/v5 v5.0.0 h1:p544++a97kEL+svbcFbCQVM9KFu0Yo25UoISXGNNH9M=
-github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
-github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
-github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
-github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
-github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
-github.com/google/tink/go v1.6.1 h1:t7JHqO8Ath2w2ig5vjwQYJzhGEZymedQc90lQXUBa4I=
-github.com/google/tink/go v1.6.1/go.mod h1:IGW53kTgag+st5yPhKKwJ6u2l+SSp5/v9XF7spovjlY=
-github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
-github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
-github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
-github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
-github.com/insomniacslk/dhcp v0.0.0-20231012130842-9b5d35ae8e55 h1:dmjBtYDUZ5eGvu/PaA5T3I56XSFMgOfx1y6ewalrkDs=
-github.com/insomniacslk/dhcp v0.0.0-20231012130842-9b5d35ae8e55/go.mod h1:yuSAiSXw3A4R2pJZhqGa4I7/gBSOZ9aGYP5MBe/Zwoo=
-github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
-github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
-github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
-github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
-github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
-github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
-github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
-github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
-github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
-github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
-github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
-github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
-github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
-github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
-github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
-github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
-github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
-github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
-github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
-github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI=
-github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 h1:cjd4biTvOzK9ubNCCkQ+ldc4YSH/rILn53l/xGBFHHI=
-github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759/go.mod h1:UHOv2xu+RIgLwpXca7TLrXleEd4oR3sPatW6IF8wU88=
-github.com/metacubex/gvisor v0.0.0-20230611153922-78842f086475 h1:qSEOvPPaMrWggFyFhFYGyMR8i1HKyhXjdi1QYUAa2ww=
-github.com/metacubex/gvisor v0.0.0-20230611153922-78842f086475/go.mod h1:wehEpqiogdeyncfhckJP5gD2LtBgJW0wnDC24mJ+8Jg=
-github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf h1:hflzPbb2M+3uUOZEVO72MKd2R62xEermoVaNhJOzBR8=
-github.com/metacubex/quic-go v0.38.1-0.20230909013832-033f6a2115cf/go.mod h1:7RCcKJJk1DMeNQQNnYKS+7FqftqPfG031oP8jrYRMw8=
-github.com/metacubex/sing-quic v0.0.0-20230921160948-82175eb07a81 h1:6g+ohVa8FQLXz/ATmped/4kWuK0HKvhy1hwzQXyF0EI=
-github.com/metacubex/sing-quic v0.0.0-20230921160948-82175eb07a81/go.mod h1:oGpQmqe5tj3sPdPWCNLbBoUSwqd+Z6SqVO7TlMNVnH4=
-github.com/metacubex/sing-shadowsocks v0.2.5 h1:O2RRSHlKGEpAVG/OHJQxyHqDy8uvvdCW/oW2TDBOIhc=
-github.com/metacubex/sing-shadowsocks v0.2.5/go.mod h1:Xz2uW9BEYGEoA8B4XEpoxt7ERHClFCwsMAvWaruoyMo=
-github.com/metacubex/sing-shadowsocks2 v0.1.4 h1:OOCf8lgsVcpTOJUeaFAMzyKVebaQOBnKirDdUdBoKIE=
-github.com/metacubex/sing-shadowsocks2 v0.1.4/go.mod h1:Qz028sLfdY3qxGRm9FDI+IM2Ae3ty2wR7HIzD/56h/k=
-github.com/metacubex/sing-tun v0.1.12 h1:Jgmz0k3ddRiJ8zfS4X7j6B/iSy6GnOdDEU0nhqiZcK4=
-github.com/metacubex/sing-tun v0.1.12/go.mod h1:X2P/H1HqXwqGcguGXWDVDhSS1GmDxVi13OmbtDedZ2M=
-github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74 h1:FtupiyFkaVjFvRa7B/uDtRWg5BNsoyPC9MTev3sDasY=
-github.com/metacubex/sing-vmess v0.1.9-0.20230921005247-a0488d7dac74/go.mod h1:8EWBZpc+qNvf5gmvjAtMHK1/DpcWqzfcBL842K00BsM=
-github.com/metacubex/sing-wireguard v0.0.0-20230611155257-1498ae315a28 h1:mXFpxfR/1nADh+GoT8maWEvc6LO6uatPsARD8WzUDMA=
-github.com/metacubex/sing-wireguard v0.0.0-20230611155257-1498ae315a28/go.mod h1:KrDPq/dE793jGIJw9kcIvjA/proAfU0IeU7WlMXW7rs=
-github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
-github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
-github.com/mroth/weightedrand/v2 v2.1.0 h1:o1ascnB1CIVzsqlfArQQjeMy1U0NcIbBO5rfd5E/OeU=
-github.com/mroth/weightedrand/v2 v2.1.0/go.mod h1:f2faGsfOGOwc1p94wzHKKZyTpcJUW7OJ/9U4yfiNAOU=
-github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7 h1:1102pQc2SEPp5+xrS26wEaeb26sZy6k9/ZXlZN+eXE4=
-github.com/oasisprotocol/deoxysii v0.0.0-20220228165953-2091330c22b7/go.mod h1:UqoUn6cHESlliMhOnKLWr+CBH+e3bazUPvFj1XZwAjs=
-github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
-github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
-github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
-github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
-github.com/openacid/errors v0.8.1/go.mod h1:GUQEJJOJE3W9skHm8E8Y4phdl2LLEN8iD7c5gcGgdx0=
-github.com/openacid/low v0.1.21 h1:Tr2GNu4N/+rGRYdOsEHOE89cxUIaDViZbVmKz29uKGo=
-github.com/openacid/low v0.1.21/go.mod h1:q+MsKI6Pz2xsCkzV4BLj7NR5M4EX0sGz5AqotpZDVh0=
-github.com/openacid/must v0.1.3/go.mod h1:luPiXCuJlEo3UUFQngVQokV0MPGryeYvtCbQPs3U1+I=
-github.com/openacid/testkeys v0.1.6/go.mod h1:MfA7cACzBpbiwekivj8StqX0WIRmqlMsci1c37CA3Do=
-github.com/oschwald/maxminddb-golang v1.12.0 h1:9FnTOD0YOhP7DGxGsq4glzpGy5+w7pq50AS6wALUMYs=
-github.com/oschwald/maxminddb-golang v1.12.0/go.mod h1:q0Nob5lTCqyQ8WT6FYgS1L7PXKVVbgiymefNwIjPzgY=
-github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
-github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
-github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
-github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
-github.com/puzpuzpuz/xsync/v2 v2.5.0 h1:2k4qrO/orvmEXZ3hmtHqIy9XaQtPTwzMZk1+iErpE8c=
-github.com/puzpuzpuz/xsync/v2 v2.5.0/go.mod h1:gD2H2krq/w52MfPLE+Uy64TzJDVY7lP2znR9qmR35kU=
-github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo=
-github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A=
-github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
-github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
-github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
-github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
-github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61 h1:5+m7c6AkmAylhauulqN/c5dnh8/KssrE9c93TQrXldA=
-github.com/sagernet/go-tun2socks v1.16.12-0.20220818015926-16cb67876a61/go.mod h1:QUQ4RRHD6hGGHdFMEtR8T2P6GS6R3D/CXKdaYHKKXms=
-github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97 h1:iL5gZI3uFp0X6EslacyapiRz7LLSJyr4RajF/BhMVyE=
-github.com/sagernet/netlink v0.0.0-20220905062125-8043b4a9aa97/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
-github.com/sagernet/sing v0.0.0-20220817130738-ce854cda8522/go.mod h1:QVsS5L/ZA2Q5UhQwLrn0Trw+msNd/NPGEhBKR/ioWiY=
-github.com/sagernet/sing v0.1.8/go.mod h1:jt1w2u7lJQFFSGLiRrRIs5YWmx4kAPfWuOejuDW9qMk=
-github.com/sagernet/sing v0.2.11 h1:mu0S6d8y/xSVxilOqRd32Fmire5SZz9nT3t9NEHwUMY=
-github.com/sagernet/sing v0.2.11/go.mod h1:GQ673iPfUnkbK/dIPkfd1Xh1MjOGo36gkl/mkiHY7Jg=
-github.com/sagernet/sing-mux v0.1.3 h1:fAf7PZa2A55mCeh0KKM02f1k2Y4vEmxuZZ/51ahkkLA=
-github.com/sagernet/sing-mux v0.1.3/go.mod h1:wGeIeiiFLx4HUM5LAg65wrNZ/X1muOimqK0PEhNbPi0=
-github.com/sagernet/sing-shadowtls v0.1.4 h1:aTgBSJEgnumzFenPvc+kbD9/W0PywzWevnVpEx6Tw3k=
-github.com/sagernet/sing-shadowtls v0.1.4/go.mod h1:F8NBgsY5YN2beQavdgdm1DPlhaKQlaL6lpDdcBglGK4=
-github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37 h1:HuE6xSwco/Xed8ajZ+coeYLmioq0Qp1/Z2zczFaV8as=
-github.com/sagernet/smux v0.0.0-20230312102458-337ec2a5af37/go.mod h1:3skNSftZDJWTGVtVaM2jfbce8qHnmH/AGDRe62iNOg0=
-github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6 h1:Px+hN4Vzgx+iCGVnWH5A8eR7JhNnIV3rGQmBxA7cw6Q=
-github.com/sagernet/tfo-go v0.0.0-20230816093905-5a5c285d44a6/go.mod h1:zovq6vTvEM6ECiqE3Eeb9rpIylPpamPcmrJ9tv0Bt0M=
-github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2 h1:kDUqhc9Vsk5HJuhfIATJ8oQwBmpOZJuozQG7Vk88lL4=
-github.com/sagernet/utls v0.0.0-20230309024959-6732c2ab36f2/go.mod h1:JKQMZq/O2qnZjdrt+B57olmfgEmLtY9iiSIEYtWvoSM=
-github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f h1:Kvo8w8Y9lzFGB/7z09MJ3TR99TFtfI/IuY87Ygcycho=
-github.com/sagernet/wireguard-go v0.0.0-20230807125731-5d4a7ef2dc5f/go.mod h1:mySs0abhpc/gLlvhoq7HP1RzOaRmIXVeZGCh++zoApk=
-github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
-github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
-github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9 h1:rc/CcqLH3lh8n+csdOuDfP+NuykE0U6AeYSJJHKDgSg=
-github.com/scjalliance/comshim v0.0.0-20230315213746-5e51f40bd3b9/go.mod h1:a/83NAfUXvEuLpmxDssAXxgUgrEy12MId3Wd7OTs76s=
-github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
-github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
-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=
-github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
-github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b h1:rXHg9GrUEtWZhEkrykicdND3VPjlVbYiLdX9J7gimS8=
-github.com/sina-ghaderi/poly1305 v0.0.0-20220724002748-c5926b03988b/go.mod h1:X7qrxNQViEaAN9LNZOPl9PfvQtp3V3c7LTo0dvGi0fM=
-github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c h1:DjKMC30y6yjG3IxDaeAj3PCoRr+IsO+bzyT+Se2m2Hk=
-github.com/sina-ghaderi/rabaead v0.0.0-20220730151906-ab6e06b96e8c/go.mod h1:NV/a66PhhWYVmUMaotlXJ8fIEFB98u+c8l/CQIEFLrU=
-github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIFG3i4Gi093BQITvwH9znsz2VUZmnmwHvpIo=
-github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8=
-github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
-github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
-github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
-github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
-github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
-github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg=
-github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
-github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
-github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
-github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
-github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
-github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
-github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
-github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
-github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig=
-github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc=
-gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo=
-gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ=
-go.etcd.io/bbolt v1.3.7 h1:j+zJOnnEjF/kyHlDDgGnVL/AIqIJPq8UoB2GSNfkUfQ=
-go.etcd.io/bbolt v1.3.7/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
-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.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
-golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
-golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
-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.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
-golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
-golang.org/x/mod v0.13.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.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
-golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
-golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220731174439-a90be440212d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.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.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
-golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
-golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
-golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
-golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
-golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
-golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-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=
-golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
-google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
-gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/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.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
-lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=

+ 1 - 1
core/lib.go

@@ -351,4 +351,4 @@ func get_configs() *C.char {
 
 func main() {
 	fmt.Println("hello fclash")
-}
+}

+ 63 - 0
lib/app/bean/ClashServiceInfo.dart

@@ -0,0 +1,63 @@
+class ClashServiceInfo {
+  ClashServiceInfo({
+    required this.code,
+    required this.mode,
+    required this.status,
+    required this.version,
+  });
+  late int code;
+  late String mode;
+  late String status;
+  late String version;
+
+  ClashServiceInfo.fromJson(Map<String, dynamic> json) {
+    code = json['code'];
+    mode = json['mode'];
+    status = json['status'];
+    version = json['version'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['code'] = code;
+    data['mode'] = mode;
+    data['status'] = status;
+    data['version'] = version;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ClashServiceLog {
+  ClashServiceLog({
+    required this.time,
+    required this.type,
+    required this.msg,
+  });
+  late String time;
+  late String type;
+  late String msg;
+
+  ClashServiceLog.fromJson(Map<String, dynamic> json) {
+    time = json['time'];
+    type = json['type'];
+    msg = json['msg'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['time'] = time;
+    data['type'] = type;
+    data['msg'] = msg;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}

+ 0 - 35
lib/app/bean/clash_config_entity.dart

@@ -1,35 +0,0 @@
-import 'dart:convert';
-
-import 'package:dart_json_mapper/dart_json_mapper.dart';
-import 'package:naiyouwl/app/bean/rule.dart';
-
-import '../data/model/NodeMode.dart';
-import 'clash_config_generator.dart';
-
-@jsonSerializable
-class ClashConfigEntity {
-  int? port;
-  @JsonProperty(name: "socks-port")
-  int? socksPort;
-  @JsonProperty(name: "redir-port")
-  int? redirPort;
-  @JsonProperty(name: "tproxy-port")
-  int? tproxyPort;
-  @JsonProperty(name: "mixed-port")
-  int? mixedPort;
-  List<dynamic>? authentication;
-  @JsonProperty(name: "allow-lan")
-  bool? allowLan;
-  @JsonProperty(name: "bind-address")
-  String? bindAddress;
-  String? mode;
-  @JsonProperty(name: "log-level")
-  String? logLevel;
-  bool? ipv6;
-
-  ClashConfigEntity(this.port,this.socksPort,this.redirPort,this.tproxyPort,this.mixedPort,this.authentication,this.allowLan,this.bindAddress,this.mode,this.logLevel,this.ipv6);
-
-
-
-
-}

+ 0 - 141
lib/app/bean/clash_config_generator.dart

@@ -1,141 +0,0 @@
-// 代理类
-class Proxy {
-  final String name;
-  final String type;
-  final String server;
-  final int port;
-  final String uuid;
-  final int udp;
-  final String? flow;
-  final String? servername;
-  final bool? tls;
-
-  Proxy({
-    required this.name,
-    required this.type,
-    required this.server,
-    required this.port,
-    required this.uuid,
-    required this.udp,
-    this.flow,
-    this.servername,
-    this.tls,
-  });
-
-  String toYamlString() {
-    var lines = [
-      'name: $name',
-      'type: $type',
-      'server: $server',
-      'port: $port',
-      'uuid: $uuid',
-      'udp: $udp',
-    ];
-
-    if (flow != null) lines.add('flow: $flow');
-    if (servername != null) lines.add('servername: $servername');
-    if (tls != null) lines.add('tls: $tls');
-
-    return lines.map((line) => '  - $line').join('\n');
-  }
-}
-
-// 代理组类
-class ProxyGroup {
-  final String name;
-  final String type;
-  final List<String> proxies;
-
-  ProxyGroup({
-    required this.name,
-    required this.type,
-    required this.proxies,
-  });
-
-  String toYamlString() {
-    return '  - name: $name\n  type: $type\n  proxies: [${proxies.join(', ')}]';
-  }
-}
-
-// 主配置类
-class YamlConfig {
-  final int mixedPort;
-  final bool allowLan;
-  final String bindAddress;
-  final String mode;
-  final String logLevel;
-  final String externalController;
-  final List<Proxy> proxies;
-  final List<ProxyGroup> proxyGroups;
-  final List<String> rules;
-
-  YamlConfig({
-    required this.mixedPort,
-    required this.allowLan,
-    required this.bindAddress,
-    required this.mode,
-    required this.logLevel,
-    required this.externalController,
-    required this.proxies,
-    required this.proxyGroups,
-    required this.rules,
-  });
-
-  String toYamlString() {
-    return '''mixed-port: $mixedPort
-allow-lan: $allowLan
-bind-address: '$bindAddress'
-mode: $mode
-log-level: $logLevel
-external-controller: '$externalController'
-proxies:
-${proxies.map((proxy) => proxy.toYamlString()).join('\n')}
-proxy-groups:
-${proxyGroups.map((group) => group.toYamlString()).join('\n')}
-rules:
-- ${rules.join('\n- ')}''';
-  }
-
-
-
-}
-
-// void main() {
-//   // 为示例,这里仅添加了一些代理和代理组。
-//   var exampleProxies = [
-//     Proxy(
-//       name: '香港22-vless',
-//       type: 'vless',
-//       server: 'sz.ip2000.top',
-//       port: 27399,
-//       uuid: '459b4a80-bd61-4ecd-a26b-e9c1809d9e45',
-//       udp: 1,
-//       flow: 'xtls-rprx-vision',
-//       servername: 'www.amazon.com',
-//       tls: true,
-//     ),
-//     // ... 可以根据需要添加更多代理
-//   ];
-//
-//   var exampleProxyGroup = ProxyGroup(
-//     name: 'proxy',
-//     type: 'select',
-//     proxies: ['香港22-vless', '香港原生61D'],
-//   );
-//
-//   var config = YamlConfig(
-//     mixedPort: 7890,
-//     allowLan: true,
-//     bindAddress: '*',
-//     mode: 'rule',
-//     logLevel: 'info',
-//     externalController: '127.0.0.1:9090',
-//     proxies: exampleProxies,
-//     proxyGroups: [exampleProxyGroup],
-//     rules: ['MATCH,用户中心'],
-//   );
-//
-//   // var fileName = 'config_output.yaml';
-//   // File(fileName).writeAsStringSync(config.toYamlString());
-//   // print('YAML config saved to $fileName');
-// }

+ 87 - 0
lib/app/bean/clash_core.dart

@@ -0,0 +1,87 @@
+class ClashCoreVersion {
+  ClashCoreVersion({
+    required this.premium,
+    required this.version,
+  });
+  late bool premium;
+  late String version;
+
+  ClashCoreVersion.fromJson(Map<String, dynamic> json) {
+    premium = json['premium'];
+    version = json['version'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['premium'] = premium;
+    data['version'] = version;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ClashCoreConfig {
+  ClashCoreConfig({
+    required this.port,
+    required this.socksPort,
+    required this.redirPort,
+    required this.tproxyPort,
+    required this.mixedPort,
+    required this.authentication,
+    required this.allowLan,
+    required this.bindAddress,
+    required this.mode,
+    required this.logLevel,
+    required this.ipv6,
+  });
+  late int port;
+  late int socksPort;
+  late int redirPort;
+  late int tproxyPort;
+  late int mixedPort;
+  late List<String> authentication;
+  late bool allowLan;
+  late String bindAddress;
+  late String mode;
+  late String logLevel;
+  late bool ipv6;
+
+  ClashCoreConfig.fromJson(Map<String, dynamic> json) {
+    port = json['port'];
+    socksPort = json['socks-port'];
+    redirPort = json['redir-port'];
+    tproxyPort = json['tproxy-port'];
+    mixedPort = json['mixed-port'];
+    authentication = List.castFrom<dynamic, String>(json['authentication']);
+    allowLan = json['allow-lan'];
+    bindAddress = json['bind-address'];
+    mode = json['mode'];
+    logLevel = json['log-level'];
+    ipv6 = json['ipv6'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['port'] = port;
+    data['socks-port'] = socksPort;
+    data['redir-port'] = redirPort;
+    data['tproxy-port'] = tproxyPort;
+    data['mixed-port'] = mixedPort;
+    data['authentication'] = authentication;
+    data['allow-lan'] = allowLan;
+    data['bind-address'] = bindAddress;
+    data['mode'] = mode;
+    data['log-level'] = logLevel;
+    data['ipv6'] = ipv6;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}

+ 119 - 0
lib/app/bean/config.dart

@@ -0,0 +1,119 @@
+class Config {
+  Config({
+    required this.selected,
+    required this.updateInterval,
+    required this.updateSubsAtStart,
+    required this.setSystemProxy,
+    required this.startAtLogin,
+    required this.breakConnections,
+    required this.subs,
+    required this.language,
+  });
+  late String selected;
+  late int updateInterval;
+  late bool updateSubsAtStart;
+  late bool setSystemProxy;
+  late bool startAtLogin;
+  late bool breakConnections;
+  late String language;
+  late List<ConfigSub> subs;
+
+  Config.fromJson(Map<String, dynamic> json) {
+    selected = json['selected'];
+    updateInterval = json['updateInterval'];
+    updateSubsAtStart = json['updateSubsAtStart'];
+    setSystemProxy = json['setSystemProxy'];
+    startAtLogin = json['startAtLogin'];
+    breakConnections = json['breakConnections'];
+    language = json['language'];
+    subs = List.from(json['subs']).map((e) => ConfigSub.fromJson(e)).toList();
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['selected'] = selected;
+    data['updateInterval'] = updateInterval;
+    data['updateSubsAtStart'] = updateSubsAtStart;
+    data['setSystemProxy'] = setSystemProxy;
+    data['startAtLogin'] = startAtLogin;
+    data['breakConnections'] = breakConnections;
+    data['language'] = language;
+    data['subs'] = subs.map((e) => e.toJson()).toList();
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ConfigSub {
+  ConfigSub({
+    required this.name,
+    this.url,
+    this.updateTime,
+    this.info,
+  });
+
+  late String name;
+  String? url;
+  int? updateTime;
+  ConfigSubInfo? info;
+
+  ConfigSub.fromJson(Map<String, dynamic> json) {
+    name = json['name'];
+    url = json['url'];
+    updateTime = json['updateTime'];
+    if (json['info'] != null) info = ConfigSubInfo.fromJson(json['info']);
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'name': name,
+      'url': url,
+      'updateTime': updateTime,
+      'info': info?.toJson(),
+    };
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ConfigSubInfo {
+  ConfigSubInfo({
+    this.upload,
+    this.download,
+    this.total,
+    this.expire,
+  });
+
+  int? upload;
+  int? download;
+  int? total;
+  int? expire;
+
+  ConfigSubInfo.fromJson(Map<dynamic, dynamic> json) {
+    upload = json['upload'];
+    download = json['download'];
+    total = json['total'];
+    expire = json['expire'];
+  }
+
+  Map<String, dynamic> toJson() {
+    return {
+      'upload': upload,
+      'download': download,
+      'total': total,
+      'expire': expire,
+    };
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}

+ 163 - 0
lib/app/bean/connect.dart

@@ -0,0 +1,163 @@
+class Connect {
+  Connect({
+    required this.downloadTotal,
+    required this.uploadTotal,
+    required this.connections,
+  });
+  late int downloadTotal;
+  late int uploadTotal;
+  late List<ConnectConnection> connections;
+
+  Connect.fromJson(Map<String, dynamic> json) {
+    downloadTotal = json['downloadTotal'];
+    uploadTotal = json['uploadTotal'];
+    connections = List.from(json['connections']).map((e) => ConnectConnection.fromJson(e)).toList();
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['downloadTotal'] = downloadTotal;
+    data['uploadTotal'] = uploadTotal;
+    data['connections'] = connections.map((e) => e.toJson()).toList();
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ConnectConnection {
+  ConnectConnection({
+    required this.id,
+    required this.speed,
+    required this.metadata,
+    required this.upload,
+    required this.download,
+    required this.start,
+    required this.chains,
+    required this.rule,
+    required this.rulePayload,
+  });
+  late String id;
+  late ConnectConnectionSpeed speed;
+  late ConnectConnectionMetadata metadata;
+  late int upload;
+  late int download;
+  late String start;
+  late List<String> chains;
+  late String rule;
+  late String rulePayload;
+
+  ConnectConnection.fromJson(Map<String, dynamic> json) {
+    id = json['id'];
+    speed = ConnectConnectionSpeed.fromJson(json['speed']);
+    metadata = ConnectConnectionMetadata.fromJson(json['metadata']);
+    upload = json['upload'];
+    download = json['download'];
+    start = json['start'];
+    chains = List.castFrom<dynamic, String>(json['chains']);
+    rule = json['rule'];
+    rulePayload = json['rulePayload'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['id'] = id;
+    data['speed'] = speed.toJson();
+    data['metadata'] = metadata.toJson();
+    data['upload'] = upload;
+    data['download'] = download;
+    data['start'] = start;
+    data['chains'] = chains;
+    data['rule'] = rule;
+    data['rulePayload'] = rulePayload;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ConnectConnectionSpeed {
+  ConnectConnectionSpeed({
+    required this.download,
+    required this.upload,
+  });
+  late int download;
+  late int upload;
+
+  ConnectConnectionSpeed.fromJson(Map<String, dynamic>? json) {
+    download = json?['download'] ?? 0;
+    upload = json?['upload'] ?? 0;
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['download'] = download;
+    data['upload'] = upload;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ConnectConnectionMetadata {
+  ConnectConnectionMetadata({
+    required this.network,
+    required this.type,
+    required this.sourceIP,
+    required this.destinationIP,
+    required this.sourcePort,
+    required this.destinationPort,
+    required this.host,
+    required this.dnsMode,
+    required this.processPath,
+  });
+  late String network;
+  late String type;
+  late String sourceIP;
+  late String destinationIP;
+  late String sourcePort;
+  late String destinationPort;
+  late String host;
+  late String dnsMode;
+  late String processPath;
+
+  ConnectConnectionMetadata.fromJson(Map<String, dynamic> json) {
+    network = json['network'];
+    type = json['type'];
+    sourceIP = json['sourceIP'];
+    destinationIP = json['destinationIP'];
+    sourcePort = json['sourcePort'];
+    destinationPort = json['destinationPort'];
+    host = json['host'];
+    dnsMode = json['dnsMode'];
+    processPath = json['processPath'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['network'] = network;
+    data['type'] = type;
+    data['sourceIP'] = sourceIP;
+    data['destinationIP'] = destinationIP;
+    data['sourcePort'] = sourcePort;
+    data['destinationPort'] = destinationPort;
+    data['host'] = host;
+    data['dnsMode'] = dnsMode;
+    data['processPath'] = processPath;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}

+ 162 - 0
lib/app/bean/proxie.dart

@@ -0,0 +1,162 @@
+class Proxie {
+  Proxie({
+    required this.proxies,
+  });
+  late Map<String, ProxieProxiesItem> proxies;
+
+  Proxie.fromJson(Map<String, dynamic> json) {
+    proxies = (json['proxies'] as Map<String, dynamic>).map((key, value) => MapEntry(key, ProxieProxiesItem.fromJson(value)));
+  }
+
+// Map<String, dynamic> toJson() {
+//   final _data = <String, dynamic>{};
+//   _data['proxies'] = proxies.toJson();
+//   return _data;
+// }
+
+// @override
+// String toString() {
+//   return toJson().toString();
+// }
+}
+
+class ProxieProxiesItem {
+  ProxieProxiesItem({
+    this.all,
+    required this.history,
+    required this.name,
+    this.now,
+    required this.type,
+    required this.udp,
+  });
+  late List<String>? all;
+  late List<ProxieProxiesItemHistory> history;
+  late String name;
+  late String? now;
+  late String type;
+  late bool udp;
+
+  get delay {
+    return history.isEmpty ? 0 : history.last.delay;
+  }
+
+  ProxieProxiesItem.fromJson(Map<String, dynamic> json) {
+    all = json['all'] == null ? null : List.castFrom<dynamic, String>(json['all']);
+    history = List.from(json['history']).map((e) => ProxieProxiesItemHistory.fromJson(e)).toList();
+    name = json['name'];
+    now = json['now'];
+    type = json['type'];
+    udp = json['udp'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['all'] = all;
+    data['history'] = history.map((e) => e.toJson()).toList();
+    data['name'] = name;
+    data['now'] = now;
+    data['type'] = type;
+    data['udp'] = udp;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ProxieProxiesItemHistory {
+  ProxieProxiesItemHistory({
+    required this.time,
+    required this.delay,
+  });
+  late String time;
+  late int delay;
+
+  ProxieProxiesItemHistory.fromJson(Map<String, dynamic> json) {
+    time = json['time'];
+    delay = json['delay'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['time'] = time;
+    data['delay'] = delay;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ProxieProvider {
+  ProxieProvider({
+    required this.providers,
+  });
+  late Map<String, ProxieProviderItem> providers;
+
+  ProxieProvider.fromJson(Map<String, dynamic> json) {
+    providers = (json['providers'] as Map<String, dynamic>).map((key, value) => MapEntry(key, ProxieProviderItem.fromJson(value)));
+  }
+
+// Map<String, dynamic> toJson() {
+//   final _data = <String, dynamic>{};
+//   _data['providers'] = providers.toJson();
+//   return _data;
+// }
+
+// @override
+// String toString() {
+//   return toJson().toString();
+// }
+}
+
+class ProxieProviderItem {
+  ProxieProviderItem({
+    required this.name,
+    required this.proxies,
+    required this.type,
+    required this.vehicleType,
+    this.updatedAt,
+  });
+  late String name;
+  late List<ProxieProxiesItem> proxies;
+  late String type;
+  late String vehicleType;
+  late String? updatedAt;
+
+  ProxieProviderItem.fromJson(Map<String, dynamic> json) {
+    name = json['name'];
+    proxies = List.from(json['proxies']).map((e) => ProxieProxiesItem.fromJson(e)).toList();
+    type = json['type'];
+    vehicleType = json['vehicleType'];
+    updatedAt = json['updatedAt'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['name'] = name;
+    data['proxies'] = proxies.map((e) => e.toJson()).toList();
+    data['type'] = type;
+    data['vehicleType'] = vehicleType;
+    data['updatedAt'] = updatedAt;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class ProxieProxieType {
+  static const String selector = 'Selector';
+  static const String urltest = 'URLTest';
+  static const String fallback = 'Fallback';
+  static const String loadbalance = 'LoadBalance';
+  static const String direct = 'Direct';
+  static const String reject = 'Reject';
+}

+ 0 - 18
lib/app/bean/proxy_group.dart

@@ -1,18 +0,0 @@
-import 'package:dart_json_mapper/dart_json_mapper.dart';
-
-@jsonSerializable
-class ProxyGroup {
-  String? name;
-  String type;
-  List<String> proxies;
-
-  ProxyGroup({required this.name, required this.type, required this.proxies});
-
-  Map<String, dynamic> toYamlMap() {
-    return {
-      "name": name,
-      "type": type,
-      "proxies": proxies,
-    };
-  }
-}

+ 111 - 6
lib/app/bean/rule.dart

@@ -1,10 +1,115 @@
-import 'package:dart_json_mapper/dart_json_mapper.dart';
+class RuleProvider {
+  RuleProvider({
+    required this.providers,
+  });
+  late Map<String, RuleProvidersProvidersItem> providers;
+
+  RuleProvider.fromJson(Map<String, dynamic> json) {
+    providers = (json['providers'] as Map<String, dynamic>).map((key, value) => MapEntry(key, RuleProvidersProvidersItem.fromJson(value)));
+  }
+
+// Map<String, dynamic> toJson() {
+//   final _data = <String, dynamic>{};
+//   _data['providers'] = providers.toJson();
+//   return _data;
+// }
+
+// @override
+// String toString() {
+//   return toJson().toString();
+// }
+}
+
+class RuleProvidersProvidersItem {
+  RuleProvidersProvidersItem({
+    required this.behavior,
+    required this.name,
+    required this.ruleCount,
+    required this.type,
+    required this.updatedAt,
+    required this.vehicleType,
+  });
+  late String behavior;
+  late String name;
+  late int ruleCount;
+  late String type;
+  late String updatedAt;
+  late String vehicleType;
+
+  RuleProvidersProvidersItem.fromJson(Map<String, dynamic> json) {
+    behavior = json['behavior'];
+    name = json['name'];
+    ruleCount = json['ruleCount'];
+    type = json['type'];
+    updatedAt = json['updatedAt'];
+    vehicleType = json['vehicleType'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['behavior'] = behavior;
+    data['name'] = name;
+    data['ruleCount'] = ruleCount;
+    data['type'] = type;
+    data['updatedAt'] = updatedAt;
+    data['vehicleType'] = vehicleType;
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
 
-@jsonSerializable
 class Rule {
-  String rule;
+  Rule({
+    required this.rules,
+  });
+  late List<RuleRule> rules;
+
+  Rule.fromJson(Map<String, dynamic> json) {
+    rules = List.from(json['rules']).map((e) => RuleRule.fromJson(e)).toList();
+  }
+
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['rules'] = rules.map((e) => e.toJson()).toList();
+    return data;
+  }
+
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}
+
+class RuleRule {
+  RuleRule({
+    required this.type,
+    required this.payload,
+    required this.proxy,
+  });
+  late String type;
+  late String payload;
+  late String proxy;
+
+  RuleRule.fromJson(Map<String, dynamic> json) {
+    type = json['type'];
+    payload = json['payload'];
+    proxy = json['proxy'];
+  }
 
-  Rule({required this.rule});
+  Map<String, dynamic> toJson() {
+    final data = <String, dynamic>{};
+    data['type'] = type;
+    data['payload'] = payload;
+    data['proxy'] = proxy;
+    return data;
+  }
 
-  String toYamlString() => rule;
-}
+  @override
+  String toString() {
+    return toJson().toString();
+  }
+}

+ 1 - 0
lib/app/component/connection_status.dart

@@ -1 +1,2 @@
 enum ConnectionStatus { disconnected, connecting, stopped }
+

+ 84 - 0
lib/app/const/const.dart

@@ -0,0 +1,84 @@
+import 'dart:io';
+import 'package:path/path.dart' as path;
+import 'package:process_run/shell_run.dart';
+
+class ClashName {
+  static String get platform {
+    if (Platform.isWindows) return 'windows';
+    if (Platform.isMacOS) return 'darwin';
+    if (Platform.isLinux) return 'linux';
+    return 'unknown';
+  }
+
+  static String get arch {
+    return const String.fromEnvironment('OS_ARCH', defaultValue: 'amd64');
+  }
+
+  static String get ext {
+    if (Platform.isWindows) return '.exe';
+    return '';
+  }
+
+  static String get name {
+    return 'clash-$platform-$arch$ext';
+  }
+}
+
+class Paths {
+  static Directory get assets {
+    File mainFile = File(Platform.resolvedExecutable);
+    String assetsPath = '../data/flutter_assets/assets';
+    if (Platform.isMacOS) assetsPath = '../../Frameworks/App.framework/Resources/flutter_assets/assets';
+    return Directory(path.normalize(path.join(mainFile.path, assetsPath)));
+  }
+
+  static Directory get assetsBin {
+    return Directory(path.join(assets.path, 'bin'));
+  }
+
+  static Directory get assetsDep {
+    return Directory(path.join(assets.path, 'dep'));
+  }
+
+  static Directory get config {
+    return Directory(path.join(userHomePath, '.config', 'naiyou'));
+  }
+}
+
+class Files {
+  static File get assetsClashCore {
+    return File(path.join(Paths.assetsBin.path, ClashName.name));
+  }
+
+  static File get assetsClashService {
+    return File(path.join(Paths.assetsBin.path, 'naiyou-service-${ClashName.platform}-${ClashName.arch}${ClashName.ext}'));
+  }
+
+  static File get assetsCountryMmdb {
+    return File(path.join(Paths.assetsDep.path, 'Country.mmdb'));
+  }
+
+  static File get assetsWintun {
+    return File(path.join(Paths.assetsDep.path, 'wintun.dll'));
+  }
+
+  static File get assetsExample {
+    return File(path.join(Paths.assetsDep.path, 'example.yaml'));
+  }
+
+  static File get configConfig {
+    return File(path.join(Paths.config.path, '.config.json'));
+  }
+
+  static File get configCountryMmdb {
+    return File(path.join(Paths.config.path, 'Country.mmdb'));
+  }
+
+  static File get configWintun {
+    return File(path.join(Paths.config.path, 'wintun.dll'));
+  }
+
+  static File get configExample {
+    return File(path.join(Paths.config.path, 'example.yaml'));
+  }
+}

+ 190 - 0
lib/app/controller/GlobalController.dart

@@ -0,0 +1,190 @@
+import 'dart:io';
+import 'package:flutter/widgets.dart';
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/bean/proxie.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:naiyouwl/app/data/model/NodeMode.dart';
+import 'package:naiyouwl/app/network/api_service.dart';
+import 'package:naiyouwl/app/utils/logger.dart';
+import 'package:naiyouwl/app/utils/system_proxy.dart';
+import 'package:naiyouwl/app/utils/utils.dart';
+import 'package:tray_manager/tray_manager.dart';
+import 'package:window_manager/window_manager.dart';
+
+class GlobalController extends GetxController {
+
+  late BuildContext context;
+
+  var nodeModes = <NodeMode>[].obs;
+  var isLoading = false.obs;
+  var errorMsg = ''.obs;
+  var systemProxySwitchIng = false.obs;
+
+  // 策略组
+  var proxieGroups = <ProxieProxiesItem>[].obs;
+  // 代理集
+  var proxieProviders = <ProxieProviderItem>[].obs;
+  // 代理
+  var proxieProxies = <ProxieProxiesItem>[].obs;
+  // 所有节点
+  var allProxies = <String, ProxieProxiesItem>{}.obs;
+
+  final List<String> groupInternalTypes = ['DIRECT', 'REJECT', 'GLOBAL'];
+  final List<String> groupTypes = [
+    ProxieProxieType.selector,
+    ProxieProxieType.urltest,
+    ProxieProxieType.fallback,
+    ProxieProxieType.loadbalance,
+  ];
+  Future<void> init(BuildContext context) async {
+    this.context = context;
+    watchExit();
+
+    // init plugins
+    await controllers.tray.initTray();
+    controllers.window.initWindow();
+    //controllers.protocol.initProtocol();
+
+    // init config
+    await controllers.config.initConfig();
+    final language = controllers.config.config.value.language.split('_');
+    //await controllers.pageSetting.applyLanguage(Locale(language[0], language[1]));
+
+    // init service
+    await controllers.service.startService();
+    if (controllers.service.serviceStatus.value != RunningState.running) return;
+
+    // init clash core
+    await controllers.service.startClashCore();
+    if (controllers.service.coreStatus.value != RunningState.running) return;
+    await controllers.core.updateVersion();
+    // await controllers.pageProxie.updateDate();
+
+    initRegularlyUpdate();
+  }
+
+  Future<void> fetchNodes() async {
+    nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
+  }
+
+  Future<void> systemProxySwitch(bool open) async {
+    systemProxySwitchIng.value = true;
+
+    await SystemProxy.instance.set(open ? controllers.core.proxyConfig : SystemProxyConfig());
+    await controllers.config.setSystemProxy(open);
+    systemProxySwitchIng.value = false;
+  }
+
+  Future<dynamic> _updateProxie() async {
+    final proxie = await controllers.core.fetchProxie();
+    final global = proxie.proxies["GLOBAL"]!;
+    proxieGroups.value = global.all!
+        .where((it) => !groupInternalTypes.contains(it) && groupTypes.contains(proxie.proxies[it]!.type))
+        .map((it) => proxie.proxies[it]!)
+        .toList();
+    proxieProxies.value = global.all!
+        .where((it) => !groupInternalTypes.contains(it) && !groupTypes.contains(proxie.proxies[it]!.type))
+        .map((it) => proxie.proxies[it]!)
+        .toList();
+    if (controllers.core.config.value.mode == 'global') proxieGroups.insert(0, global);
+  }
+
+  Future<dynamic> _updateProxieProvider() async {
+    proxieProviders.value = (await controllers.core.fetchProxieProvider()).providers.values.where((it) => it.vehicleType != 'Compatible').toList();
+    for (final it in proxieProviders) {
+      it.proxies.sort((a, b) {
+        if (a.delay == 0) return 1;
+        if (b.delay == 0) return -1;
+        return a.delay - b.delay;
+      });
+    }
+  }
+
+  Future<void> updateDate() async {
+    log.debug('controller.proxie.updateDate()');
+
+    await controllers.core.updateConfig();
+    await _updateProxie();
+    await _updateProxieProvider();
+    allProxies.clear();
+    for (final provide in proxieProviders) {
+      for (final it in provide.proxies) {
+        allProxies[it.name] = it;
+      }
+    }
+    for (final it in proxieProxies) {
+      allProxies[it.name] = it;
+    }
+    for (final it in proxieGroups) {
+      allProxies[it.name] = it;
+    }
+    proxieGroups.refresh();
+    proxieProxies.refresh();
+    proxieProviders.refresh();
+    allProxies.refresh();
+  }
+
+
+  Future<void> handleSetProxieGroup(ProxieProxiesItem proxie, String value) async {
+    if (proxie.now == value) return;
+    await controllers.core.fetchSetProxieGroup(proxie.name, value);
+    await updateDate();
+    if (controllers.config.config.value.breakConnections) {
+      final conn = await controllers.core.fetchConnection();
+      for (final it in conn.connections) {
+        if (it.chains.contains(proxie.name)) controllers.core.fetchCloseConnections(it.id);
+      }
+    }
+  }
+
+  void watchExit() {
+    // watch process kill
+    // ref https://github.com/dart-lang/sdk/issues/12170
+    if (Platform.isMacOS) {
+      // windows not support https://github.com/dart-lang/sdk/issues/28603
+      // for macos 任务管理器退出进程
+      ProcessSignal.sigterm.watch().listen((_) {
+        stdout.writeln('exit: sigterm');
+        handleExit();
+      });
+    }
+    // for macos, windows ctrl+c
+    ProcessSignal.sigint.watch().listen((_) {
+      stdout.writeln('exit: sigint');
+      handleExit();
+    });
+  }
+
+  void initRegularlyUpdate() {
+    Future.delayed(const Duration(minutes: 5)).then((_) async {
+      for (final it in controllers.config.config.value.subs) {
+        try {
+          if (it.url == null || it.url!.isEmpty) continue;
+          if (((DateTime.now().millisecondsSinceEpoch ~/ 1000) - (it.updateTime ?? 0)) < controllers.config.config.value.updateInterval) continue;
+          final chenged = await controllers.config.updateSub(it);
+          if (!chenged) continue;
+          if (it.name != controllers.config.config.value.selected) continue;
+          // restart clash core
+          await controllers.service.reloadClashCore();
+          await Future.delayed(const Duration(seconds: 20));
+        } catch (_) {}
+      }
+      initRegularlyUpdate();
+    });
+  }
+
+  Future<void> handleExit() async {
+    await controllers.service.stopService();
+    await trayManager.destroy();
+    await windowManager.destroy();
+    // exit(0);
+  }
+
+  @override
+  void dispose() {
+    controllers.tray.dispose();
+    controllers.window.dispose();
+    //controllers.protocol.dispose();
+    super.dispose();
+  }
+}

+ 162 - 0
lib/app/controller/config.dart

@@ -0,0 +1,162 @@
+import 'dart:io';
+import 'dart:convert';
+import 'package:dio/dio.dart';
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/bean/config.dart';
+import 'package:naiyouwl/app/const/const.dart';
+import 'package:yaml/yaml.dart';
+import 'package:path/path.dart' as path;
+import 'package:flutter_emoji/flutter_emoji.dart';
+
+final Map<String, dynamic> _defaultConfig = {
+  'selected': 'example.yaml',
+  'updateInterval': 86400,
+  'updateSubsAtStart': false,
+  'setSystemProxy': false,
+  'startAtLogin': false,
+  'breakConnections': false,
+  'language': 'zh_CN',
+  'subs': [],
+};
+
+class ConfigController extends GetxController {
+  late final dio;
+  var config = Config.fromJson(_defaultConfig).obs;
+
+  var clashCoreApiAddress = '127.0.0.1:9090'.obs;
+  var clashCoreApiSecret = ''.obs;
+  var clashCoreDns = ''.obs;
+  var clashCoreTunEnable = false.obs;
+
+  Future<void> initConfig() async {
+    //dio.addSentry();
+    dio = Dio(BaseOptions(baseUrl: clashCoreApiAddress.value));
+    if (!await Paths.config.exists()) await Paths.config.create(recursive: true);
+    if (!await Files.configCountryMmdb.exists()) await Files.assetsCountryMmdb.copy(Files.configCountryMmdb.path);
+    if (Platform.isWindows && !await Files.configWintun.exists()) await Files.assetsWintun.copy(Files.configWintun.path);
+    final locale = Get.deviceLocale!;
+    _defaultConfig['language'] = '${locale.languageCode}_${locale.countryCode}';
+
+    if (await Files.configConfig.exists()) {
+      final local = json.decode(await Files.configConfig.readAsString());
+      config.value = Config.fromJson({..._defaultConfig, ...local});
+    } else {
+      config.value = Config.fromJson(_defaultConfig);
+    }
+    if (config.value.subs.isEmpty) {
+      if (!await Files.configExample.exists()) await Files.assetsExample.copy(Files.configExample.path);
+      config.value.subs.add(ConfigSub(name: 'example.yaml', url: '', updateTime: 0));
+      config.value.selected = 'example.yaml';
+    }
+    await readClashCoreApi();
+  }
+
+  Future<void> save() async {
+    await Files.configConfig.writeAsString(json.encode(config.toJson()));
+  }
+
+  Future<void> readClashCoreApi() async {
+    final configStr = await File(path.join(Paths.config.path, config.value.selected)).readAsString();
+    // final emoji = EmojiParser();
+    // final b = emoji.unemojify(_config);
+    final configJson = loadYaml(configStr.replaceAll(EmojiParser.REGEX_EMOJI, 'emoji'));
+    // print(_json["external-controller"]);
+    // https://github.com/dart-lang/yaml/issues/53
+    // final _extControl = RegExp(r'''(?<!#\s*)external-controller:\s+['"]?([^'"]+?)['"]?\s''').firstMatch(_config)?.group(1);
+    // final _secret = RegExp(r'''(?<!#\s*)secret:\s+['"]?([^'"]+?)['"]?\s''').firstMatch(_config)?.group(1);
+    clashCoreApiAddress.value = (configJson["external-controller"] ?? '127.0.0.1:9090').replaceAll('0.0.0.0', '127.0.0.1');
+    clashCoreApiSecret.value = (configJson["secret"] ?? '');
+    clashCoreTunEnable.value = configJson["tun"]?["enable"] == true;
+    clashCoreDns.value = '';
+    if (configJson["dns"]?["enable"] == true && (configJson["dns"]["listen"] ?? '').isNotEmpty) {
+      final dns = (configJson["dns"]["listen"] as String).split(":");
+      final ip = dns[0];
+      final port = dns[1];
+      if (port == '53') {
+        clashCoreDns.value = ip == '0.0.0.0' ? '127.0.0.1' : ip;
+      }
+    }
+  }
+
+  Future<void> setLanguage(String language) async {
+    config.value.language = language;
+    await save();
+    config.refresh();
+  }
+
+  Future<void> setSystemProxy(bool open) async {
+    config.value.setSystemProxy = open;
+    await save();
+    config.refresh();
+  }
+
+  Future<void> setUpdateInterval(int value) async {
+    config.value.updateInterval = value;
+    await save();
+    config.refresh();
+  }
+
+  Future<void> setUpdateSubsAtStart(bool value) async {
+    config.value.updateSubsAtStart = value;
+    await save();
+    config.refresh();
+  }
+
+  Future<void> setSelectd(String selected) async {
+    config.value.selected = selected;
+    await save();
+    config.refresh();
+  }
+
+  Future<bool> updateSub(ConfigSub sub) async {
+    if ((sub.url ?? '').isEmpty) return false;
+    final res = await dio.get(sub.url!);
+    final subInfo = res.headers['subscription-userinfo'];
+    final file = File(path.join(Paths.config.path, sub.name));
+    final oldConfig = await file.exists() ? await file.readAsString() : '';
+    final changed = oldConfig != res.data;
+    if (changed) await file.writeAsString(res.data);
+    sub.updateTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
+    sub.info = null;
+    if (subInfo != null) {
+      final info = Map.fromEntries(
+          subInfo.first.split(RegExp(r';\s*')).where((s) => s.isNotEmpty).map((e) => e.split('=')).map((e) => MapEntry(e[0], int.parse(e[1]))));
+      sub.info = ConfigSubInfo.fromJson(info);
+    }
+    await setSub(sub.name, sub);
+    return changed;
+  }
+
+  Future<void> setSub(String subName, ConfigSub sub) async {
+    final idx = config.value.subs.indexWhere((it) => it.name == subName);
+    config.value.subs[idx] = sub;
+    if (subName != sub.name) {
+      final file = File(path.join(Paths.config.path, subName));
+      if (await file.exists()) await file.rename(path.join(Paths.config.path, sub.name));
+    }
+    await save();
+    config.refresh();
+  }
+
+  Future<void> addSub(ConfigSub sub) async {
+    config.value.subs.add(sub);
+    final file = File(path.join(Paths.config.path, sub.name));
+    if (!await file.exists()) await file.create();
+    await save();
+    config.refresh();
+  }
+
+  Future<void> deleteSub(String subName) async {
+    final file = File(path.join(Paths.config.path, subName));
+    if (await file.exists()) await file.delete();
+    config.value.subs.removeWhere((it) => it.name == subName);
+    await save();
+    config.refresh();
+  }
+
+  Future<void> setBreakConnections(bool value) async {
+    config.value.breakConnections = value;
+    await save();
+    config.refresh();
+  }
+}

+ 51 - 0
lib/app/controller/controllers.dart

@@ -0,0 +1,51 @@
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/controller/GlobalController.dart';
+import 'package:naiyouwl/app/controller/config.dart';
+import 'package:naiyouwl/app/controller/core.dart';
+import 'package:naiyouwl/app/controller/protocol.dart';
+import 'package:naiyouwl/app/controller/service.dart';
+import 'package:naiyouwl/app/controller/tray.dart';
+import 'package:naiyouwl/app/controller/window.dart';
+
+
+
+
+class Controllers {
+  late final TrayController tray;
+  late final WindowController window;
+ // late final ProtocolController protocol;
+
+  late final CoreController core;
+  late final ConfigController config;
+  late final ServiceController service;
+  late final GlobalController  global;
+  // late final PageLogController pageLog;
+  // late final PageMainController pageMain;
+  // late final PageHomeController pageHome;
+  // late final PageRuleController pageRule;
+  // late final PageProxieController pageProxie;
+  // late final PageProfileController pageProfile;
+  // late final PageSettingController pageSetting;
+  // late final PageConnectionController pageConnection;
+
+  void init() {
+    tray = Get.find();
+    window = Get.find();
+    //protocol = Get.find();
+
+    core = Get.find();
+    config = Get.find();
+    service = Get.find();
+    global = Get.find();
+    // pageLog = Get.find();
+    // pageMain = Get.find();
+    // pageHome = Get.find();
+    // pageRule = Get.find();
+    // pageProxie = Get.find();
+    // pageProfile = Get.find();
+    // pageSetting = Get.find();
+    // pageConnection = Get.find();
+  }
+}
+
+final controllers = Controllers();

+ 143 - 0
lib/app/controller/core.dart

@@ -0,0 +1,143 @@
+import 'package:dio/dio.dart';
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/bean/clash_core.dart';
+import 'package:naiyouwl/app/bean/connect.dart';
+import 'package:naiyouwl/app/bean/proxie.dart';
+import 'package:naiyouwl/app/bean/rule.dart';
+import 'package:naiyouwl/app/utils/system_proxy.dart';
+import 'package:web_socket_channel/io.dart';
+
+
+class CoreController extends GetxController {
+  final dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9090'));
+  var version = ClashCoreVersion(premium: true, version: '').obs;
+  var address = ''.obs;
+  var secret = ''.obs;
+  var config = ClashCoreConfig(
+    port: 0,
+    socksPort: 0,
+    redirPort: 0,
+    tproxyPort: 0,
+    mixedPort: 0,
+    authentication: [],
+    allowLan: false,
+    bindAddress: '',
+    mode: '',
+    logLevel: '',
+    ipv6: false,
+  ).obs;
+
+  var ruleProvider = RuleProvider(providers: {}).obs;
+  var rule = Rule(rules: []).obs;
+
+  CoreController() {
+    //dio.addSentry();
+  }
+
+  SystemProxyConfig get proxyConfig {
+    final mixedPort = config.value.mixedPort == 0 ? null : config.value.mixedPort;
+    final httpPort = mixedPort ?? config.value.port;
+    final httpsPort = mixedPort ?? config.value.port;
+    final socksPort = mixedPort ?? config.value.socksPort;
+    return SystemProxyConfig(
+      http: httpPort == 0 ? null : '127.0.0.1:$httpPort',
+      https: httpsPort == 0 ? null : '127.0.0.1:$httpsPort',
+      socks: socksPort == 0 ? null : '127.0.0.1:$socksPort',
+    );
+  }
+
+  setApi(String apiAddress, String apiSecret) {
+    address.value = apiAddress;
+    secret.value = apiSecret;
+    dio.options.baseUrl = 'http://${address.value}';
+    dio.options.headers['Authorization'] = 'Bearer ${secret.value}';
+  }
+
+  Future<dynamic> fetchHello() async {
+    return await dio.get('/');
+  }
+
+  Future<void> updateVersion() async {
+    final res = await dio.get('/version');
+    version.value = ClashCoreVersion.fromJson(res.data);
+  }
+
+  Future<void> updateConfig() async {
+    final res = await dio.get('/configs');
+    config.value = ClashCoreConfig.fromJson(res.data);
+  }
+
+  Future<void> fetchConfigUpdate(Map<String, dynamic> config) async {
+    await dio.patch('/configs', data: config);
+    await updateConfig();
+  }
+
+  // type updateConfigRequest struct {
+  // 	Path    string `json:"path"`
+  // 	Payload string `json:"payload"`
+  // }
+  // https://github.com/Dreamacro/clash/blob/c231fd14666d6ea05d6a75eaba6db69f9eee5ae9/hub/route/configs.go#L95
+  Future<void> fetchReloadConfig(Map<String, String> config) async {
+    await dio.put('/configs', data: config);
+  }
+
+  Future<void> fetchCloseConnections(String id) async {
+    await dio.delete('/connections/${Uri.encodeComponent(id)}');
+  }
+
+  IOWebSocketChannel fetchConnectionsWs() {
+    return IOWebSocketChannel.connect(
+      Uri.parse('ws://${address.value}/connections'),
+      headers: {"Authorization": dio.options.headers["Authorization"]},
+    );
+  }
+
+  Future updateRuleProvider() async {
+    final res = await dio.get('/providers/rules');
+    ruleProvider.value = RuleProvider.fromJson(res.data);
+    ruleProvider.refresh();
+  }
+
+  Future updateRule() async {
+    final res = await dio.get('/rules');
+    rule.value = Rule.fromJson(res.data);
+    rule.refresh();
+  }
+
+  Future<void> fetchRuleProviderUpdate(String name) async {
+    await dio.put('/providers/rules/${Uri.encodeComponent(name)}');
+  }
+
+  Future<Proxie> fetchProxie() async {
+    final res = await dio.get('/proxies');
+    return Proxie.fromJson(res.data);
+  }
+
+  Future<ProxieProvider> fetchProxieProvider() async {
+    final res = await dio.get('/providers/proxies');
+    return ProxieProvider.fromJson(res.data);
+  }
+
+  Future<void> fetchProxieProviderHealthCheck(String provider) async {
+    await dio.get('/providers/proxies/${Uri.encodeComponent(provider)}/healthcheck');
+  }
+
+  Future<void> fetchSetProxieGroup(String group, String value) async {
+    await dio.put('/proxies/${Uri.encodeComponent(group)}', data: {'name': value});
+  }
+
+  Future<void> fetchProxieProviderUpdate(String name) async {
+    await dio.put('/providers/proxies/${Uri.encodeComponent(name)}');
+  }
+
+  Future<int> fetchProxieDelay(String name) async {
+    final query = {'timeout': 5000, 'url': 'http://www.gstatic.com/generate_204'};
+    final res = await dio.get('/proxies/${Uri.encodeComponent(name)}/delay', queryParameters: query);
+    return res.data['delay'] ?? 0;
+  }
+
+  Future<Connect> fetchConnection() async {
+    final res = await dio.get('/connections');
+    return Connect.fromJson(res.data);
+  }
+}

+ 36 - 0
lib/app/controller/protocol.dart

@@ -0,0 +1,36 @@
+// import 'package:day/day.dart';
+// import 'package:get/get.dart';
+// import 'package:naiyouwl/app/bean/config.dart';
+// // import 'package:protocol_handler/protocol_handler.dart';
+//
+//
+// import 'package:naiyouwl/app/controller//controllers.dart';
+//
+// //class ProtocolController extends GetxController with ProtocolListener {
+//   // void initProtocol() {
+//   //   protocolHandler.addListener(this);
+//   // }
+//   //
+//   // @override
+//   // void onProtocolUrlReceived(String url) async {
+//   //   // ref https://github.com/biyidev/biyi/blob/37aa84ec063fcbac717ace26acd361764ab9a2c5/lib/pages/desktop_popup/desktop_popup.dart#L829
+//   //   // clash://install-config?url=xxxx
+//   //   final uri = Uri.parse(url);
+//   //   if (uri.scheme != 'clash') return;
+//   //   if (uri.authority == 'install-config') {
+//   //     final paths = Uri.parse(uri.queryParameters['url']!).pathSegments;
+//   //     String name = paths.isNotEmpty ? paths.last : Day().format('YYYYMMDD_HHmmss');
+//   //     name = name.replaceFirst(RegExp(r'(\.\w*)?$'), '.yaml');
+//   //     controllers.pageProfile.showAddSubPopup(controllers.pageMain.context, ConfigSub(name: name, url: uri.queryParameters['url']));
+//   //   } else {
+//   //     return;
+//   //   }
+//   //   await controllers.window.showWindow();
+//   // }
+//   //
+//   // @override
+//   // void dispose() {
+//   //   protocolHandler.removeListener(this);
+//   //   super.dispose();
+//   // }
+// //}

+ 242 - 0
lib/app/controller/service.dart

@@ -0,0 +1,242 @@
+import 'dart:io';
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:dio/dio.dart';
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/bean/ClashServiceInfo.dart';
+import 'package:naiyouwl/app/const/const.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:naiyouwl/app/utils/logger.dart';
+import 'package:naiyouwl/app/utils/shell.dart';
+import 'package:naiyouwl/app/utils/system_dns.dart';
+import 'package:naiyouwl/app/utils/system_proxy.dart';
+import 'package:naiyouwl/app/utils/utils.dart';
+
+import 'package:path/path.dart' as path;
+import 'package:flutter/foundation.dart';
+import 'package:bot_toast/bot_toast.dart';
+
+import 'package:web_socket_channel/io.dart';
+
+
+
+final headers = {"User-Agent": "naiyou-for-flutter/0.0.1"};
+
+class ServiceController extends GetxController {
+  final dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9899', headers: headers));
+
+  var serviceMode = false.obs;
+
+  var coreStatus = RunningState.stoped.obs;
+  var serviceStatus = RunningState.stoped.obs;
+
+  Process? clashServiceProcess;
+
+  bool get isRunning => serviceStatus.value == RunningState.running && coreStatus.value == RunningState.running;
+  bool get isCanOperationService =>
+      ![RunningState.starting, RunningState.stopping].contains(serviceStatus.value) &&
+      ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
+  bool get isCanOperationCore =>
+      serviceStatus.value == RunningState.running && ![RunningState.starting, RunningState.stopping].contains(coreStatus.value);
+
+  ServiceController() {
+    //dio.addSentry();
+  }
+
+  Future<void> startService() async {
+    serviceStatus.value = RunningState.starting;
+    if (Platform.isLinux) {
+      await fixBinaryExecutePermissions(Files.assetsClashService);
+      await fixBinaryExecutePermissions(Files.assetsClashCore);
+    }
+    try {
+      final data = await fetchInfo();
+      serviceMode.value = data.mode == 'service-mode';
+    } catch (e) {
+      await startUserModeService();
+      if (serviceStatus.value == RunningState.error) return;
+    }
+    serviceStatus.value = RunningState.running;
+  }
+
+  Future<void> fixBinaryExecutePermissions(File file) async {
+    final stat = await file.stat();
+    // 0b001000000
+    final has = (stat.mode & 64) == 64;
+    if (has) return;
+    await Process.run('chmod', ['+x', file.path]);
+  }
+
+  Future<void> startUserModeService() async {
+    serviceMode.value = false;
+    try {
+      int? exitCode;
+      clashServiceProcess = await Process.start(Files.assetsClashService.path, ['user-mode'], mode: ProcessStartMode.inheritStdio);
+      clashServiceProcess!.exitCode.then((code) => exitCode = code);
+
+      while (true) {
+        await Future.delayed(const Duration(milliseconds: 200));
+        if (exitCode == 101) {
+          BotToast.showText(text: 'clash-service exit with code: $exitCode,After 10 seconds, try to restart');
+          log.error('After 10 seconds, try to restart');
+          await Future.delayed(const Duration(seconds: 10));
+          await startUserModeService();
+          break;
+        } else if (exitCode != null) {
+          serviceStatus.value = RunningState.error;
+          break;
+        }
+        try {
+          await dio.post('/info');
+          break;
+        } catch (_) {}
+      }
+    } catch (e) {
+      serviceStatus.value = RunningState.error;
+      BotToast.showText(text: e.toString());
+    }
+  }
+
+  Future<void> stopService() async {
+    serviceStatus.value = RunningState.stopping;
+    if (coreStatus.value == RunningState.running) await stopClashCore();
+    if (!serviceMode.value) {
+      if (clashServiceProcess != null) {
+        clashServiceProcess!.kill();
+        clashServiceProcess = null;
+      } else if (kDebugMode) {
+        await killProcess(path.basename(Files.assetsClashService.path));
+      }
+    }
+    serviceStatus.value = RunningState.stoped;
+  }
+
+    // for macos
+    Future<void> waitServiceStart() async {
+      while (true) {
+        await Future.delayed(const Duration(milliseconds: 100));
+        try {
+          await dio.post('/info');
+          break;
+        } catch (_) {}
+      }
+    }
+
+    // for windows
+    Future<void> waitServiceStop() async {
+      while (true) {
+        await Future.delayed(const Duration(milliseconds: 100));
+        try {
+          await dio.post('/info');
+        } catch (e) {
+          break;
+        }
+      }
+    }
+
+    Future<ClashServiceInfo> fetchInfo() async {
+      final res = await dio.post('/info');
+      return ClashServiceInfo.fromJson(res.data);
+    }
+
+    IOWebSocketChannel fetchLogWs() {
+      return IOWebSocketChannel.connect(Uri.parse('ws://127.0.0.1:9089/logs'), headers: headers);
+    }
+
+    Future<void> fetchStart(String name) async {
+      await fetchStop();
+      final res = await dio.post<String>('/start', data: {
+        "args": ['-d', Paths.config.path, '-f', path.join(Paths.config.path, name)]
+      });
+      if (json.decode(res.data!)["code"] != 0) throw json.decode(res.data!)["msg"];
+    }
+
+    Future<void> fetchStop() async {
+      await dio.post('/stop');
+    }
+
+    Future<void> install() async {
+      final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall", "install", "start"]);
+      log.debug('install', res.stdout, res.stderr);
+      if (res.exitCode != 0) throw res.stderr;
+      await waitServiceStart();
+    }
+
+    Future<void> uninstall() async {
+      final res = await runAsAdmin(Files.assetsClashService.path, ["stop", "uninstall"]);
+      log.debug('uninstall', res.stdout, res.stderr);
+      if (res.exitCode != 0) throw res.stderr;
+      await waitServiceStop();
+    }
+
+    Future<void> serviceModeSwitch(bool open) async {
+      if (serviceStatus.value == RunningState.running) await stopService();
+      try {
+        open ? await install() : await uninstall();
+      } catch (e) {
+        BotToast.showText(text: e.toString());
+      }
+      await startService();
+      await startClashCore();
+    }
+
+    Future<void> startClashCore() async {
+      try {
+        coreStatus.value = RunningState.starting;
+        await fetchStart(controllers.config.config.value.selected);
+        controllers.core.setApi(controllers.config.clashCoreApiAddress.value, controllers.config.clashCoreApiSecret.value);
+        while (true) {
+          await Future.delayed(const Duration(milliseconds: 200));
+          final info = await fetchInfo();
+          if (info.status == 'running') {
+            try {
+              await controllers.core.fetchHello();
+              break;
+            } catch (_) {}
+          } else {
+            throw 'clash-core start error';
+          }
+        }
+        await controllers.core.updateConfig();
+        if (Platform.isMacOS &&
+            controllers.service.serviceMode.value &&
+            controllers.config.clashCoreTunEnable.value &&
+            controllers.config.clashCoreDns.isNotEmpty) {
+          await MacSystemDns.instance.set([controllers.config.clashCoreDns.value]);
+        }
+        if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(controllers.core.proxyConfig);
+        coreStatus.value = RunningState.running;
+      } catch (e) {
+        log.error(e);
+        BotToast.showText(text: e.toString());
+        coreStatus.value = RunningState.error;
+      }
+    }
+
+    Future<void> stopClashCore() async {
+      coreStatus.value = RunningState.stopping;
+      if (Platform.isMacOS &&
+          controllers.service.serviceMode.value &&
+          controllers.config.clashCoreTunEnable.value &&
+          controllers.config.clashCoreDns.isNotEmpty) {
+        await MacSystemDns.instance.set([]);
+      }
+      if (controllers.config.config.value.setSystemProxy) await SystemProxy.instance.set(SystemProxyConfig());
+      await fetchStop();
+      coreStatus.value = RunningState.stoped;
+    }
+
+    Future<void> reloadClashCore() async {
+      BotToast.showText(text: '正在重启 Clash Core ……');
+      await stopClashCore();
+      await controllers.config.readClashCoreApi();
+      await startClashCore();
+      if (coreStatus.value == RunningState.error) {
+        BotToast.showText(text: '重启失败');
+      } else {
+        await controllers.core.updateVersion();
+        BotToast.showText(text: '重启成功');
+      }
+    }
+}

+ 138 - 0
lib/app/controller/tray.dart

@@ -0,0 +1,138 @@
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/bean/proxie.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:naiyouwl/app/utils/utils.dart';
+import 'package:tray_manager/tray_manager.dart';
+import 'package:url_launcher/url_launcher.dart';
+import 'package:window_manager/window_manager.dart';
+
+
+class TrayController extends GetxController with TrayListener {
+  late Menu trayMenu;
+
+  var show = false.obs;
+
+  Future<void> initTray() async {
+    await trayManager.setIcon('assets/images/logo/logo.ico');
+    // await trayManager.setTitle('Clash For Flutter');
+    updateTray();
+    trayManager.addListener(this);
+  }
+
+  Future<void> updateTray() async {
+    final visible = await windowManager.isVisible();
+    final disabled = !controllers.service.isRunning;
+
+    trayMenu = Menu(items: [
+      MenuItem.checkbox(label: 'tray_show'.tr, checked: visible, onClick: handleClickShow),
+      MenuItem.separator(),
+      // MenuItem.submenu(
+      //   label: 'proxie_group_title'.tr,
+      //   disabled: disabled,
+      //   submenu: Menu(
+      //       items: controllers.pageProxie.proxieGroups
+      //           .map((it) => MenuItem.submenu(
+      //                 label: it.name,
+      //                 submenu: Menu(
+      //                   items: (it.all ?? [])
+      //                       .map((t) => MenuItem.checkbox(
+      //                             label: t,
+      //                             checked: t == it.now,
+      //                             disabled: it.type != 'Selector',
+      //                             onClick: (m) => handleClickProxieItem(it, m),
+      //                           ))
+      //                       .toList(),
+      //                 ),
+      //               ))
+      //           .toList()),
+      // ),
+      MenuItem(
+        label: 'tray_restart_clash_core'.tr,
+        disabled: !controllers.service.isCanOperationCore,
+        onClick: handleClickRestartClashCore,
+      ),
+      MenuItem.checkbox(
+        label: 'setting_set_as_system_proxy'.tr,
+        checked: controllers.config.config.value.setSystemProxy,
+        disabled: disabled || controllers.global.systemProxySwitchIng.value,
+        onClick: handleClickSetAsSystemProxy,
+      ),
+      MenuItem.checkbox(
+        label: 'setting_service_open'.tr,
+        checked: controllers.service.serviceMode.value,
+        disabled: !controllers.service.isCanOperationService,
+        onClick: handleClickServiceModeSwitch,
+      ),
+      MenuItem.submenu(
+          label: 'tray_copy_command_line_proxy'.tr,
+          disabled: disabled,
+          submenu: Menu(items: [
+            MenuItem(label: 'bash', onClick: handleClickCopyCommandLineProxy),
+            MenuItem(label: 'cmd', onClick: handleClickCopyCommandLineProxy),
+            MenuItem(label: 'powershell', onClick: handleClickCopyCommandLineProxy),
+          ])),
+      MenuItem.separator(),
+      MenuItem(label: 'tray_about'.tr, onClick: handleClickAbout),
+      MenuItem(label: 'tray_exit'.tr, onClick: handleClickExit),
+    ]);
+    await trayManager.setContextMenu(trayMenu);
+  }
+
+  @override
+  void onTrayIconMouseDown() async {
+    await controllers.window.showWindow();
+  }
+
+  @override
+  void onTrayIconRightMouseDown() async {
+    show.value = true;
+    await updateTray();
+    await trayManager.popUpContextMenu();
+    show.value = false;
+  }
+
+  Future<void> handleClickShow(MenuItem menuItem) async {
+    show.value = false;
+    if (menuItem.checked == true) {
+      await controllers.window.closeWindow();
+    } else {
+      await controllers.window.showWindow();
+    }
+  }
+
+  Future<void> handleClickProxieItem(ProxieProxiesItem proxie, MenuItem menuItem) async {
+    await controllers.global.handleSetProxieGroup(proxie, menuItem.label!);
+  }
+
+  Future<void> handleClickSetAsSystemProxy(MenuItem menuItem) async {
+    await controllers.global.systemProxySwitch(menuItem.checked != true);
+  }
+
+  Future<void> handleClickCopyCommandLineProxy(MenuItem menuItem) async {
+    final title = menuItem.label!;
+    final proxyConfig = controllers.core.proxyConfig;
+    await copyCommandLineProxy(title, http: proxyConfig.http, https: proxyConfig.https);
+  }
+
+  Future<void> handleClickAbout(MenuItem menuItem) async {
+    await launchUrl(Uri.parse('https://github.com/csj8520/clash_for_flutter'));
+  }
+
+  Future<void> handleClickExit(MenuItem menuItem) async {
+    await controllers.global.handleExit();
+  }
+
+  Future<void> handleClickRestartClashCore(MenuItem menuItem) async {
+    await controllers.service.reloadClashCore();
+  }
+
+  Future<void> handleClickServiceModeSwitch(MenuItem menuItem) async {
+    await controllers.service.serviceModeSwitch(menuItem.checked != true);
+  }
+
+  @override
+  void dispose() {
+    trayManager.removeListener(this);
+    super.dispose();
+  }
+}

+ 45 - 0
lib/app/controller/window.dart

@@ -0,0 +1,45 @@
+import 'package:get/get.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:window_manager/window_manager.dart';
+
+class WindowController extends GetxController with WindowListener {
+  /// focus blur minimize maximize unmaximize restore resized close move moved...
+  var event = ''.obs;
+  var isVisible = false.obs;
+
+  void initWindow() async {
+    windowManager.addListener(this);
+    isVisible.value = await windowManager.isVisible();
+  }
+
+  @override
+  void onWindowClose() async {
+    await windowManager.hide();
+  }
+
+  @override
+  void onWindowEvent(String eventName) async {
+    if (controllers.tray.show.value) return;
+    event.value = eventName;
+    if (['focus', 'restore'].contains(eventName)) {
+      isVisible.value = true;
+    } else if (['close', 'minimize'].contains(eventName)) {
+      isVisible.value = false;
+    }
+  }
+
+  Future<void> closeWindow() async {
+    await windowManager.close();
+  }
+
+  Future<void> showWindow() async {
+    await windowManager.show();
+    isVisible.value = true;
+  }
+
+  @override
+  void dispose() {
+    windowManager.removeListener(this);
+    super.dispose();
+  }
+}

+ 0 - 18
lib/app/global_controller/GlobalController.dart

@@ -1,18 +0,0 @@
-import 'package:get/get.dart';
-
-import '../bean/clash_config_generator.dart';
-import '../common/LogHelper.dart';
-import '../data/model/NodeMode.dart';
-import '../network/api_service.dart';
-
-class GlobalController extends GetxController {
-  var nodeModes = <NodeMode>[].obs;
-  var isLoading = false.obs;
-  var errorMsg = ''.obs;
-  Future<void> fetchNodes() async {
-    nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
-  }
-
-
-
-}

+ 17 - 17
lib/app/modules/home/controllers/home_controller.dart

@@ -3,6 +3,8 @@ import 'dart:io';
 
 import 'package:dart_json_mapper/dart_json_mapper.dart';
 import 'package:get/get.dart';
+import 'package:naiyouwl/app/controller/GlobalController.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
 import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
 import '../../../common/LogHelper.dart';
@@ -12,10 +14,8 @@ import '../../../data/model/LocalUser.dart';
 import '../../../data/model/NodeMode.dart';
 import '../../../data/model/SysConfig.dart';
 import '../../../data/model/UserMode.dart';
-import '../../../global_controller/GlobalController.dart';
 import '../../../network/api_service.dart';
 import '../../../routes/app_pages.dart';
-import '../../../service/clash_service.dart';
 
 enum ImageType {
   CUSTOMER,
@@ -35,7 +35,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
   var selectNode = '选择节点'.obs;
   var connectStatus = Rx<ConnectionStatus>(ConnectionStatus.disconnected);
   var nodeModes = <NodeMode>[];
-  late final GlobalController globalController ;
+  late final GlobalController globalController = controllers.global;
 
 
   final Map<ImageType, String> imageMap = {
@@ -65,22 +65,22 @@ class HomeController extends GetxController with TrayListener,WindowListener {
 
     if(connectStatus.value == ConnectionStatus.stopped){
       updateStatus(ConnectionStatus.disconnected);
-      await Get.find<ClashService>().clearSystemProxy();
+   //   await Get.find<ClashService>().clearSystemProxy();
       return;
     }
 
     updateStatus(ConnectionStatus.connecting);
-    await Get.find<ClashService>().makeClash(globalController.nodeModes);
-    await Get.find<ClashService>().chageProxyConfig();
-
-    Future.delayed(const Duration(seconds: 3), () async {
-      if(!Get.find<ClashService>().isSystemProxy()){
-        await Get.find<ClashService>().setSystemProxy();
-      } else {
-        await Get.find<ClashService>().clearSystemProxy();
-      }
-      updateStatus(ConnectionStatus.stopped);
-    });
+    // await Get.find<ClashService>().makeClash(globalController.nodeModes);
+    // await Get.find<ClashService>().chageProxyConfig();
+    //
+    // Future.delayed(const Duration(seconds: 3), () async {
+    //   if(!Get.find<ClashService>().isSystemProxy()){
+    //     await Get.find<ClashService>().setSystemProxy();
+    //   } else {
+    //     await Get.find<ClashService>().clearSystemProxy();
+    //   }
+    //   updateStatus(ConnectionStatus.stopped);
+    // });
   }
 
 
@@ -142,7 +142,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
   @override
   void onInit() {
     super.onInit();
-    globalController = Get.put(GlobalController());
+    //globalController = Get.put(GlobalController());
     fetchSysConfig();
     fetchLocalUser();
     fetchUserinfo();
@@ -184,7 +184,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
     switch (menuItem.key) {
       case 'exit':
         windowManager.close().then((value) async {
-          await Get.find<ClashService>().closeClashDaemon();
+          //await Get.find<ClashService>().closeClashDaemon();
           exit(0);
         });
         break;

+ 0 - 3
lib/app/modules/home/views/home_view.dart

@@ -9,9 +9,6 @@ import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
 import '../../../component/connection_status.dart';
 import '../../../component/connection_widget.dart';
-
-import '../../../global_controller/GlobalController.dart';
-import '../../../service/clash_service.dart';
 import '../controllers/home_controller.dart';
 
 

+ 1 - 1
lib/app/modules/node/controllers/node_controller.dart

@@ -1,11 +1,11 @@
 import 'dart:io';
 
 import 'package:get/get.dart';
+import 'package:naiyouwl/app/controller/GlobalController.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 import '../../../common/LogHelper.dart';
 import '../../../data/model/NodeMode.dart';
-import '../../../global_controller/GlobalController.dart';
 import '../../../network/api_service.dart';
 enum NodeDisplayStrategy {
   All,

+ 0 - 698
lib/app/service/clash_service.dart

@@ -1,698 +0,0 @@
-import 'dart:async';
-import 'dart:convert';
-import 'dart:ffi' as ffi;
-import 'dart:io';
-import 'dart:isolate';
-import 'package:dio/dio.dart';
-import 'package:naiyouwl/main.dart';
-import 'package:dart_json_mapper/dart_json_mapper.dart';
-import 'package:ffi/ffi.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
-import 'package:path/path.dart';
-import 'package:path/path.dart' as p;
-import 'package:get/get.dart';
-import 'package:kommon/request/request.dart';
-import 'package:kommon/tool/sp_util.dart';
-import 'package:naiyouwl/clash_generated_bindings.dart';
-import 'package:path_provider/path_provider.dart';
-import 'package:proxy_manager/proxy_manager.dart';
-import 'package:tray_manager/tray_manager.dart';
-import '../bean/clash_config_entity.dart';
-import '../data/model/NodeMode.dart';
-late NativeLibrary clashFFI;
-
-//android 或者ios
-//const mobileChannel = MethodChannel("xxxPlugin");
-
-class ClashService extends GetxService with TrayListener {
-  // 需要一起改端口
-  static const clashBaseUrl = "http://127.0.0.1:";
-  var  clashExtPort = 22346;
-
-  // 运行时
-  late Directory _clashDirectory;
-  RandomAccessFile? _clashLock;
-
-  // 流量
-  final uploadRate = 0.0.obs;
-  final downRate = 0.0.obs;
-  final yamlConfigs = RxSet<FileSystemEntity>();
-  final currentYaml = 'config.yaml'.obs;
-  final proxyStatus = RxMap<String, int>();
-  final proxyYamlCurrent = "".obs;
-  final proxyYaml = 'proxy.yaml'.obs;
-
-
-  // action
-  static const ACTION_SET_SYSTEM_PROXY = "assr";
-  static const ACTION_UNSET_SYSTEM_PROXY = "ausr";
-  static const MAX_ENTRIES = 5;
-
-  // default port
-  static var initializedHttpPort = 0;
-  static var initializedSockPort = 0;
-  static var initializedMixedPort = 0;
-
-  // config
-  Rx<ClashConfigEntity?> configEntity = Rx(null);
-
-  // log
-  Stream<dynamic>? logStream;
-  RxMap<String, dynamic> proxies = RxMap();
-  RxBool isSystemProxyObs = RxBool(false);
-
-
-
-  ClashService() {
-    // load lib
-    var fullPath = "";
-    if (Platform.isWindows) {
-      fullPath = "libclash.dll";
-    } else if (Platform.isMacOS) {
-      fullPath = "libclash.dylib";
-    } else {
-      fullPath = "libclash.so";
-    }
-    final lib = ffi.DynamicLibrary.open(fullPath);
-    clashFFI = NativeLibrary(lib);
-    clashFFI.init_native_api_bridge(ffi.NativeApi.initializeApiDLData);
-  }
-
-  Future<ClashService> init() async {
-    _clashDirectory = await getApplicationSupportDirectory();
-    final httpPort = await getUnusedPort();
-    final socksPort = await getUnusedPort();
-    final mixedPort = await getUnusedPort();
-    final export = await getUnusedPort();
-    final _ = SpUtil.getData('yaml', defValue: currentYaml.value);
-    initializedHttpPort = SpUtil.getData('http-port', defValue: httpPort);
-    initializedSockPort = SpUtil.getData('socks-port', defValue: socksPort);
-    initializedMixedPort = SpUtil.getData('mixed-port', defValue: mixedPort);
-    currentYaml.value = _;
-    clashExtPort = export;
-    Request.setBaseUrl(clashBaseUrl +"$clashExtPort");
-    final clashConfigPath = p.join(_clashDirectory.path, "clash");
-    _clashDirectory = Directory(clashConfigPath);
-    if (kDebugMode) {
-      print("fclash work directory: ${_clashDirectory.path}");
-    }
-    final clashConf = p.join(_clashDirectory.path, currentYaml.value);
-    final countryMMdb = p.join(_clashDirectory.path, 'Country.mmdb');
-    if (!await _clashDirectory.exists()) {
-      await _clashDirectory.create(recursive: true);
-    }
-    // copy executable to directory
-    final mmdb = await rootBundle.load('assets/tp/clash/Country.mmdb');
-    // write to clash dir
-    final mmdbF = File(countryMMdb);
-    if (!mmdbF.existsSync()) {
-      await mmdbF.writeAsBytes(mmdb.buffer.asInt8List());
-    }
-    final countryGeoIP= p.join(_clashDirectory.path, 'geoip.dat');
-
-    final geoip = await rootBundle.load('assets/tp/clash/geoip.dat');
-    // write to clash dir
-    final geoipF = File(countryGeoIP);
-    if (!geoipF.existsSync()) {
-      await geoipF.writeAsBytes(geoip.buffer.asInt8List());
-    }
-    final countryGeoSite= p.join(_clashDirectory.path, 'geosite.dat');
-
-    final geoSite = await rootBundle.load('assets/tp/clash/geoip.dat');
-    // write to clash dir
-    final geoSiteF = File(countryGeoSite);
-    if (!geoSiteF.existsSync()) {
-      await geoSiteF.writeAsBytes(geoSite.buffer.asInt8List());
-    }
-
-    final config = await rootBundle.load('assets/tp/clash/config.yaml');
-    // write to clash dir
-    final configF = File(clashConf);
-    if (!configF.existsSync()) {
-      await configF.writeAsBytes(config.buffer.asInt8List());
-    }
-    // create or detect lock file
-    await _acquireLock(_clashDirectory);
-    // ffi
-    clashFFI.set_home_dir(_clashDirectory.path.toNativeUtf8().cast());
-    clashFFI.clash_init(_clashDirectory.path.toNativeUtf8().cast());
-    clashFFI.set_config(clashConf.toNativeUtf8().cast());
-    clashFFI.set_ext_controller(clashExtPort);
-    if (clashFFI.parse_options() == 0) {
-      Get.printInfo(info: "parse ok");
-    }
-    Future.delayed(Duration.zero, () {
-     initDaemon();
-    });
-    // tray show issue
-    if (isDesktop) {
-      trayManager.addListener(this);
-    }
-    // wait getx initialize
-    // Future.delayed(const Duration(seconds: 3), () {
-    //   if (!Platform.isWindows) {
-    //     Get.find<NotificationService>()
-    //         .showNotification("Fclash", "Is running".tr);
-    //   }
-    // });
-    return this;
-  }
-  Future<int> getUnusedPort() async {
-    var server = await ServerSocket.bind(InternetAddress.loopbackIPv4, 0);
-    int port = server.port;
-    await server.close();
-    return port;
-  }
-
-  void getConfigs() {
-    yamlConfigs.clear();
-    final entities = _clashDirectory.listSync();
-    for (final entity in entities) {
-      if (entity.path.toLowerCase().endsWith('.yaml') &&
-          !yamlConfigs.contains(entity)) {
-        yamlConfigs.add(entity);
-        Get.printInfo(info: 'detected: ${entity.path}');
-      }
-    }
-  }
-
-  Map<String, dynamic> getConnections() {
-    final connsPtr = clashFFI.get_all_connections().cast<Utf8>();
-    String connections = connsPtr.toDartString();
-    // malloc.free(connsPtr);
-    return json.decode(connections);
-  }
-
-  void closeAllConnections() {
-    clashFFI.close_all_connections();
-  }
-
-  bool closeConnection(String connectionId) {
-    final id = connectionId.toNativeUtf8().cast<ffi.Char>();
-    return clashFFI.close_connection(id) == 1;
-  }
-
-  void getCurrentClashConfig() {
-    final configPtr = clashFFI.get_configs().cast<Utf8>();
-    final jsondata = json.decode(configPtr.toDartString());
-    final data =  JsonMapper.deserialize<ClashConfigEntity>(jsondata);
-    configEntity.value = data;
-    // malloc.free(configPtr);
-  }
-
-  Future<void> chageProxyConfig() async {
-    final clashConf = p.join(_clashDirectory.path, proxyYaml.value);
-    final f = File(clashConf);
-    if (f.existsSync() && await changeYaml(f)) {
-      // set subscription
-      // await SpUtil.setData('profile_$name', url);
-     // await reload();
-    }
-
-  }
-  String generateRules() {
-    return '''
-rules:
-  - GEOSITE,cn,DIRECT
-  - GEOIP,CN,DIRECT
-  - MATCH,proxy
-  ''';
-  }
-
-  Future<void> makeClash(List<NodeMode> nodeModes) async {
-    var proxies = nodeModes.map(nodeToYaml).toList();
-
-    var config = '''
-port: 7891
-socks-port: 7890
-redir-port: 7893
-allow-lan: true
-mode: rule
-log-level: info
-ipv6: false
-unified-delay: false
-geodata-mode: true
-tcp-concurrent: false
-find-process-mode: strict
-global-client-fingerprint: chrome
-external-controller: 0.0.0.0:9090
-proxies:
-\n${proxies.join('\n')}
-proxy-groups:
-  - name: proxy
-    type: select
-    proxies: 
-      ${nodeModes.map((node) => '- ${node.name}').join('\n      ')}
-${generateRules()}
-  ''';
-    proxyYamlCurrent.value = config;
-    final clashConf = p.join(_clashDirectory.path, proxyYaml.value);
-    final configF = File(clashConf);
-    await configF.writeAsBytes(utf8.encode(config));
-  }
-
-
-
-  String nodeToYaml(NodeMode node) {
-    const prefix = '  '; // 两个空格的缩进
-    switch (node.type) {
-      case 'trojan':
-        return '''$prefix- { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }''';
-      case 'shadowsocks':
-        return '''$prefix- { name: ${node.name}, type: ss, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }''';
-      case 'v2ray':
-        final type = (node.vless == 1) ? 'vless' : 'vmess';
-        if (type == 'vless') {
-          return '''$prefix- { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, udp: 1, flow: xtls-rprx-vision, servername: www.amazon.com, tls: true, reality-opts: { public-key: ${node.vlessPulkey} } }''';
-        } else {
-          return '''$prefix- { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, cipher: ${node.method}, udp: 1 }''';
-        }
-      default:
-        return '';
-    }
-  }
-
-  Future<void> reload() async {
-    // get configs
-    getConfigs();
-    getCurrentClashConfig();
-    // proxies
-    getProxies();
-    updateTray();
-  }
-  void initDaemon() async {
-    printInfo(info: 'init clash service');
-    // wait for online
-    // while (!await isRunning()) {
-    //   printInfo(info: 'waiting online status');
-    //   await Future.delayed(const Duration(milliseconds: 500));
-    // }
-    // get traffic
-    Timer.periodic(const Duration(seconds: 1), (t) {
-      final trafficPtr = clashFFI.get_traffic().cast<Utf8>();
-      final traffic = trafficPtr.toDartString();
-      // if (kDebugMode) {
-      //   debugPrint(traffic);
-      // }
-      try {
-        final trafficJson = jsonDecode(traffic);
-        uploadRate.value = trafficJson['Up'].toDouble() / 1024; // KB
-        downRate.value = trafficJson['Down'].toDouble() / 1024; // KB
-        // fix: 只有KDE不会导致Tray自动消失
-        // final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
-        // updateTray();
-      } catch (e) {
-        Get.printError(info: '$e');
-      }
-      // malloc.free(trafficPtr);
-    });
-    // // system proxy
-    // // listen port
-    await reload();
-    checkPort();
-    if (isSystemProxy()) {
-      setSystemProxy();
-    }
-  }
-
-  @override
-  void onClose() {
-    closeClashDaemon();
-    super.onClose();
-  }
-
-  Future<void> closeClashDaemon() async {
-    Get.printInfo(info: 'fclash: closing daemon');
-    // double check
-    // stopClashSubP();
-    if (isSystemProxy()) {
-      // just clear system proxy
-      await clearSystemProxy(permanent: false);
-    }
-    await _clashLock?.unlock();
-  }
-  Future<void> setSystemProxy() async {
-    if (isDesktop) {
-      if (configEntity.value != null) {
-        final entity = configEntity.value!;
-        if (entity.port != 0) {
-          await Future.wait([
-            proxyManager.setAsSystemProxy(
-                ProxyTypes.http, '127.0.0.1', entity.port!),
-            proxyManager.setAsSystemProxy(
-                ProxyTypes.https, '127.0.0.1', entity.port!)
-          ]);
-          debugPrint("set http");
-        }
-        if (entity.socksPort != 0 && !Platform.isWindows) {
-          debugPrint("set socks");
-          await proxyManager.setAsSystemProxy(
-              ProxyTypes.socks, '127.0.0.1', entity.socksPort!);
-        }
-        await setIsSystemProxy(true);
-      }
-    } else {
-      if (configEntity.value != null) {
-        final entity = configEntity.value!;
-        if (entity.port != 0) {
-          // await mobileChannel
-          //     .invokeMethod("SetHttpPort", {"port": entity.port});
-        }
-       // mobileChannel.invokeMethod("StartProxy");
-        await setIsSystemProxy(true);
-      }
-
-      // await Clipboard.setData(
-      //     ClipboardData(text: "${configEntity.value?.port}"));
-      // final dialog = BrnDialog(
-      //   titleText: "请手动设置代理",
-      //   messageText:
-      //       "端口号已复制。请进入已连接WiFi的详情设置,将代理设置为手动,主机名填写127.0.0.1,端口填写${configEntity.value?.port},然后返回点击已完成即可",
-      //   actionsText: ["取消", "已完成", "去设置填写"],
-      //   indexedActionCallback: (index) async {
-      //     if (index == 0) {
-      //       if (Get.isOverlaysOpen) {
-      //         Get.back();
-      //       }
-      //     } else if (index == 1) {
-      //       final proxy = await SystemProxy.getProxySettings();
-      //       if (proxy != null) {
-      //         if (proxy["host"] == "127.0.0.1" &&
-      //             int.parse(proxy["port"].toString()) ==
-      //                 configEntity.value?.port) {
-      //           Future.delayed(Duration.zero, () {
-      //             if (Get.overlayContext != null) {
-      //               BrnToast.show("设置成功", Get.overlayContext!);
-      //               setIsSystemProxy(true);
-      //             }
-      //           });
-      //           if (Get.isOverlaysOpen) {
-      //             Get.back();
-      //           }
-      //         }
-      //       } else {
-      //         Future.delayed(Duration.zero, () {
-      //           if (Get.overlayContext != null) {
-      //             BrnToast.show("好像未完成设置哦", Get.overlayContext!);
-      //           }
-      //         });
-      //       }
-      //     } else {
-      //       Future.delayed(Duration.zero, () {
-      //         BrnToast.show("端口号已复制", Get.context!);
-      //       });
-      //       await OpenSettings.openWIFISetting();
-      //     }
-      //   },
-      // );
-      // Get.dialog(dialog);
-    }
-  }
-
-  Future<void> clearSystemProxy({bool permanent = true}) async {
-    if (isDesktop) {
-      await proxyManager.cleanSystemProxy();
-      if (permanent) {
-        await setIsSystemProxy(false);
-      }
-    } else {
-      //mobileChannel.invokeMethod("StopProxy");
-      await setIsSystemProxy(false);
-      // final dialog = BrnDialog(
-      //   titleText: "请手动设置代理",
-      //   messageText: "请进入已连接WiFi的详情设置,将代理设置为无",
-      //   actionsText: ["取消", "已完成", "去设置清除"],
-      //   indexedActionCallback: (index) async {
-      //     if (index == 0) {
-      //       if (Get.isOverlaysOpen) {
-      //         Get.back();
-      //       }
-      //     } else if (index == 1) {
-      //       final proxy = await SystemProxy.getProxySettings();
-      //       if (proxy != null) {
-      //         Future.delayed(Duration.zero, () {
-      //           if (Get.overlayContext != null) {
-      //             BrnToast.show("好像没有清除成功哦,当前代理${proxy}", Get.overlayContext!);
-      //           }
-      //         });
-      //       } else {
-      //         Future.delayed(Duration.zero, () {
-      //           if (Get.overlayContext != null) {
-      //             BrnToast.show("清除成功", Get.overlayContext!);
-      //           }
-      //           setIsSystemProxy(false);
-      //           if (Get.isOverlaysOpen) {
-      //             Get.back();
-      //           }
-      //         });
-      //       }
-      //     } else {
-      //       OpenSettings.openWIFISetting().then((_) async {
-      //         final proxy = await SystemProxy.getProxySettings();
-      //         debugPrint("$proxy");
-      //       });
-      //     }
-      //   },
-      // );
-      // Get.dialog(dialog);
-    }
-  }
-  void getProxies() {
-    final proxiesPtr = clashFFI.get_proxies().cast<Utf8>();
-    proxies.value = json.decode(proxiesPtr.toDartString());
-
-  }
-
-  bool isSystemProxy() {
-    return SpUtil.getData('system_proxy', defValue: false);
-  }
-
-  Future<bool> setIsSystemProxy(bool proxy) {
-    isSystemProxyObs.value = proxy;
-    return SpUtil.setData('system_proxy', proxy);
-  }
-
-
-
-  void checkPort() {
-    if (configEntity.value != null) {
-      if (configEntity.value!.port == 0) {
-        changeConfigField('port', initializedHttpPort);
-      }
-      if (configEntity.value!.mixedPort == 0) {
-        changeConfigField('mixed-port', initializedMixedPort);
-      }
-      if (configEntity.value!.socksPort == 0) {
-        changeConfigField('socks-port', initializedSockPort);
-      }
-      updateTray();
-    }
-  }
-  //切换配置
-  bool changeConfigField(String field, dynamic value) {
-    try {
-      int ret = clashFFI.change_config_field(
-          json.encode(<String, dynamic>{field: value}).toNativeUtf8().cast());
-      return ret == 0;
-    } finally {
-      getCurrentClashConfig();
-      if (field.endsWith("port") && isSystemProxy()) {
-        setSystemProxy();
-      }
-    }
-  }
-
-  Future<void> _acquireLock(Directory clashDirectory) async {
-    final path = p.join(clashDirectory.path, "fclash.lock");
-    final lockFile = File(path);
-    if (!lockFile.existsSync()) {
-      lockFile.createSync(recursive: true);
-    }
-    try {
-      _clashLock = await lockFile.open(mode: FileMode.write);
-      await _clashLock?.lock();
-    } catch (e) {
-      // if (!Platform.isWindows) {
-      //   await Get.find<NotificationService>()
-      //       .showNotification("Fclash", "Already running, Now exit.".tr);
-      // }
-      exit(0);
-    }
-  }
-  ReceivePort? _logReceivePort;
-
-  void startLogging() {
-    _logReceivePort?.close();
-    _logReceivePort = ReceivePort();
-    logStream = _logReceivePort!.asBroadcastStream();
-    if (kDebugMode) {
-      logStream?.listen((event) {
-        debugPrint("LOG: ${event}");
-      });
-    }
-    final nativePort = _logReceivePort!.sendPort.nativePort;
-    debugPrint("port: $nativePort");
-    clashFFI.start_log(nativePort);
-  }
-  void stopLog() {
-    logStream = null;
-    clashFFI.stop_log();
-  }
-  void updateTray() {
-    if (!isDesktop) {
-      return;
-    }
-    final stringList = List<MenuItem>.empty(growable: true);
-    // yaml
-    stringList
-        .add(MenuItem(label: "profile: ${currentYaml.value}", disabled: true));
-    if (proxies['proxies'] != null) {
-      Map<String, dynamic> m = proxies['proxies'];
-      m.removeWhere((key, value) => value['type'] != "Selector");
-      var cnt = 0;
-      for (final k in m.keys) {
-        if (cnt >= ClashService.MAX_ENTRIES) {
-          stringList.add(MenuItem(label: "...", disabled: true));
-          break;
-        }
-        stringList.add(
-            MenuItem(label: "${m[k]['name']}: ${m[k]['now']}", disabled: true));
-        cnt += 1;
-      }
-    }
-    // port
-    if (configEntity.value != null) {
-      stringList.add(
-          MenuItem(label: 'http: ${configEntity.value?.port}', disabled: true));
-      stringList.add(MenuItem(
-          label: 'socks: ${configEntity.value?.socksPort}', disabled: true));
-    }
-    // system proxy
-    stringList.add(MenuItem.separator());
-    if (!isSystemProxy()) {
-      stringList
-          .add(MenuItem(label: "Not system proxy yet.".tr, disabled: true));
-      stringList.add(MenuItem(
-          label: "Set as system proxy".tr,
-          toolTip: "click to set fclash as system proxy".tr,
-          key: ACTION_SET_SYSTEM_PROXY));
-    } else {
-      stringList.add(MenuItem(label: "System proxy now.".tr, disabled: true));
-      stringList.add(MenuItem(
-          label: "Unset system proxy".tr,
-          toolTip: "click to reset system proxy",
-          key: ACTION_UNSET_SYSTEM_PROXY));
-      stringList.add(MenuItem.separator());
-    }
-
-    initAppTray(details: stringList, isUpdate: true);
-  }
-
-  Future<bool> _changeConfig(FileSystemEntity config) async {
-    // check if it has `rule-set`, and try to convert it
-    final content = await convertConfig(await File(config.path).readAsString())
-        .catchError((e) {
-      printError(info: e);
-    });
-    if (content.isNotEmpty) {
-      await File(config.path).writeAsString(content);
-    }
-    // judge valid
-    if (clashFFI.is_config_valid(config.path.toNativeUtf8().cast()) == 0) {
-      final resp = await Request.dioClient.put('/configs',
-          queryParameters: {"force": false}, data: {"path": config.path});
-      Get.printInfo(info: 'config changed ret: ${resp.statusCode}');
-      currentYaml.value = basename(config.path);
-      SpUtil.setData('yaml', currentYaml.value);
-      return resp.statusCode == 204;
-    } else {
-      Future.delayed(Duration.zero, () {
-        Get.defaultDialog(
-            middleText: 'not a valid config file'.tr,
-            onConfirm: () {
-              Get.back();
-            });
-      });
-      config.delete();
-      return false;
-    }
-  }
-  Future<bool> addProfile(String name, String url) async {
-    final configName = '$name.yaml';
-    final newProfilePath = join(_clashDirectory.path, configName);
-    try {
-      final uri = Uri.tryParse(url);
-      if (uri == null) {
-        return false;
-      }
-      final resp = await Dio(BaseOptions(
-          headers: {'User-Agent': 'clash.meta'},
-          sendTimeout: 15000,
-          receiveTimeout: 15000))
-          .downloadUri(uri, newProfilePath, onReceiveProgress: (i, t) {
-        Get.printInfo(info: "$i/$t");
-      });
-      return resp.statusCode == 200;
-    } catch (e) {
-      //BrnToast.show("Error: ${e}", Get.context!);
-    } finally {
-      final f = File(newProfilePath);
-      if (f.existsSync() && await changeYaml(f)) {
-        // set subscription
-        await SpUtil.setData('profile_$name', url);
-        return true;
-      }
-      return false;
-    }
-  }
-
-  Future<bool> deleteProfile(FileSystemEntity config) async {
-    if (config.existsSync()) {
-      config.deleteSync();
-      await SpUtil.remove('profile_${basename(config.path)}');
-      reload();
-      return true;
-    } else {
-      return false;
-    }
-  }
-  //切换配置文件
-  Future<bool> changeYaml(FileSystemEntity config) async {
-    try {
-      if (await config.exists()) {
-        return await _changeConfig(config);
-      } else {
-        return false;
-      }
-    } finally {
-      reload();
-    }
-  }
-
-  bool changeProxy(String selectName, String proxyName) {
-    final ret = clashFFI.change_proxy(
-        selectName.toNativeUtf8().cast(), proxyName.toNativeUtf8().cast());
-    if (ret == 0) {
-      reload();
-    }
-    return ret == 0;
-  }
-
-  bool isHideWindowWhenStart() {
-    return SpUtil.getData('boot_window_hide', defValue: false);
-  }
-
-  void handleSignal() {
-    StreamSubscription? subTerm;
-    subTerm = ProcessSignal.sigterm.watch().listen((event) {
-      subTerm?.cancel();
-      // _clashProcess?.kill();
-    });
-  }
-
-}
-
-Future<String> convertConfig(String content) async {
-  return "";
-}

+ 23 - 0
lib/app/utils/event.dart

@@ -0,0 +1,23 @@
+class Event {
+  final Map<String, List<Function>> _events = {};
+
+  Event on(String key, Function handle) {
+    final handles = _events[key] ??= [];
+    if (!handles.contains(handle)) handles.add(handle);
+    return this;
+  }
+
+  Event emit(String key, dynamic arg) {
+    final handles = _events[key];
+    handles?.forEach((it) => it(arg));
+    return this;
+  }
+
+  Event off(String key, Function handle) {
+    final handles = _events[key];
+    if (handles == null) return this;
+    handles.remove(handle);
+    if (handles.isEmpty) _events.remove(key);
+    return this;
+  }
+}

+ 56 - 0
lib/app/utils/logger.dart

@@ -0,0 +1,56 @@
+import 'dart:io';
+
+import 'package:naiyouwl/app/utils/event.dart';
+
+class Logger {
+  final Map<String, int> _timeMap = {};
+  final Event _event = Event();
+
+  _join([Object? text1, Object? text2, Object? text3, Object? text4, Object? text5, Object? text6, Object? text7, Object? text8]) {
+    return [text1, text2, text3, text4, text5, text6, text7, text8].whereType<Object>().join(' ');
+  }
+
+  log(Object text, {String level = 'info'}) {
+    String log = 'time="${DateTime.now().toString()}" level=$level msg="$text"';
+    stdout.writeln(log);
+    _event.emit('log', log);
+  }
+
+  info(Object? text, [Object? text2, Object? text3, Object? text4, Object? text5, Object? text6, Object? text7, Object? text8]) {
+    log(_join(text, text2, text3, text4, text5, text6, text7, text8), level: 'info');
+  }
+
+  warning(Object? text, [Object? text2, Object? text3, Object? text4, Object? text5, Object? text6, Object? text7, Object? text8]) {
+    log(_join(text, text2, text3, text4, text5, text6, text7, text8), level: 'warning');
+  }
+
+  error(Object? text, [Object? text2, Object? text3, Object? text4, Object? text5, Object? text6, Object? text7, Object? text8]) {
+    log(_join(text, text2, text3, text4, text5, text6, text7, text8), level: 'error');
+  }
+
+  debug(Object? text, [Object? text2, Object? text3, Object? text4, Object? text5, Object? text6, Object? text7, Object? text8]) {
+    log(_join(text, text2, text3, text4, text5, text6, text7, text8), level: 'debug');
+  }
+
+  time(String text) {
+    _timeMap[text] = DateTime.now().microsecondsSinceEpoch;
+  }
+
+  timeEnd(String text) {
+    final startTime = _timeMap[text];
+    if (startTime == null) return warning("Timer '$text' does not exist");
+    int now = DateTime.now().microsecondsSinceEpoch;
+    _timeMap.remove(text);
+    debug('$text: ${(now - startTime) / 1000} ms');
+  }
+
+  on({void Function(String event)? onLog}) {
+    if (onLog != null) _event.on('log', onLog);
+  }
+
+  off({void Function(String event)? onLog}) {
+    if (onLog != null) _event.off('log', onLog);
+  }
+}
+
+Logger log = Logger();

+ 39 - 0
lib/app/utils/shell.dart

@@ -0,0 +1,39 @@
+import 'dart:io';
+
+import 'package:process_run/shell.dart';
+import 'package:path/path.dart' as path;
+
+import 'package:naiyouwl/app/const/const.dart';
+import 'package:naiyouwl/app/utils/logger.dart';
+
+Future<void> killProcess(String name) async {
+  log.debug("kill: ", name);
+  if (Platform.isWindows) {
+    await Process.run('taskkill', ["/F", "/FI", "IMAGENAME eq $name"]);
+  } else {
+    await Process.run('bash', ["-c", "ps -ef | grep $name | grep -v grep | awk '{print \$2}' | xargs kill -9"]);
+  }
+}
+
+Future<ProcessResult> runAsAdmin(String executable, List<String> arguments) async {
+  String executablePath = shellArgument(executable).replaceAll(' ', r'\\ ');
+  executablePath = executablePath.substring(1, executablePath.length - 1);
+  if (Platform.isMacOS) {
+    return await Process.run(
+      'osascript',
+      [
+        '-e',
+        shellArguments(['do', 'shell', 'script', '$executablePath ${shellArguments(arguments)}', 'with', 'administrator', 'privileges']),
+      ],
+    );
+  } else if (Platform.isWindows) {
+    return await Process.run(
+      path.join(Paths.assetsBin.path, "run-as-admin.bat"),
+      [executable, ...arguments],
+    );
+  } else {
+    // https://blog.csdn.net/weixin_49867936/article/details/109612918
+    // https://askubuntu.com/questions/287845/how-to-configure-pkexec
+    return await Process.run("pkexec", [executable, ...arguments]);
+  }
+}

+ 44 - 0
lib/app/utils/system_dns.dart

@@ -0,0 +1,44 @@
+import 'dart:io';
+
+import 'package:naiyouwl/app/utils/logger.dart';
+
+class SystemDnsPlatform {
+  Future<void> set(List<String> dns) async {
+    throw UnimplementedError();
+  }
+
+  Future<List<String>> get() async {
+    throw UnimplementedError();
+  }
+}
+
+class MacSystemDns extends SystemDnsPlatform {
+  static MacSystemDns instance = MacSystemDns();
+
+  Future<List<String>> getNetworks() async {
+    final result = await Process.run('networksetup', ['-listallnetworkservices']);
+    return result.stdout.toString().trim().split('\n').sublist(1);
+  }
+
+  @override
+  Future<void> set(List<String> dns) async {
+    final networks = await getNetworks();
+    final List<String> commands = [];
+    for (var network in networks) {
+      if (dns.isEmpty) {
+        commands.add('networksetup -setdnsservers "$network" "empty"');
+      } else {
+        commands.add('networksetup -setdnsservers "$network" "${dns.join('" "')}"');
+      }
+    }
+    log.debug('MacSystemDns.set:', commands);
+    await Process.run('bash', ['-c', commands.join(' && ')]);
+  }
+
+  @override
+  Future<List<String>> get() async {
+    final out = (await Process.run('scutil', ['--dns'])).stdout.toString().trim().split('\n\n').last;
+    final res = RegExp(r'nameserver\[\d\]\s*:\s*(.+)').allMatches(out);
+    return res.map((e) => e.group(1)).whereType<String>().toList();
+  }
+}

+ 188 - 0
lib/app/utils/system_proxy.dart

@@ -0,0 +1,188 @@
+import 'dart:io';
+
+import 'package:naiyouwl/app/utils/logger.dart';
+
+class SystemProxyServer {
+  String server;
+
+  String get host {
+    return server.split(':')[0];
+  }
+
+  String get port {
+    return server.split(':')[1];
+  }
+
+  SystemProxyServer(this.server);
+}
+
+class SystemProxyConfig {
+  String? http;
+  String? https;
+  String? socks;
+  SystemProxyConfig({this.http, this.https, this.socks});
+}
+
+class SystemProxyPlatform {
+  Future<void> set(SystemProxyConfig conf) async {
+    throw UnimplementedError();
+  }
+
+  Future<SystemProxyConfig> get() async {
+    throw UnimplementedError();
+  }
+}
+
+class MacSystemProxy extends SystemProxyPlatform {
+  static MacSystemProxy instance = MacSystemProxy();
+
+  Future<List<String>> getNetworks() async {
+    final result = await Process.run('networksetup', ['-listallnetworkservices']);
+    return result.stdout.toString().trim().split('\n').sublist(1);
+  }
+
+  @override
+  Future<void> set(SystemProxyConfig conf) async {
+    final networks = await getNetworks();
+    List<String> commands = [];
+    for (var network in networks) {
+      if (conf.http != null) {
+        final http = SystemProxyServer(conf.http!);
+        commands.add('networksetup -setwebproxy "$network" "${http.host}" "${http.port}"');
+      } else {
+        commands.add('networksetup -setwebproxy "$network" "" ""');
+        commands.add('networksetup -setwebproxystate "$network" off');
+      }
+      if (conf.https != null) {
+        final https = SystemProxyServer(conf.https!);
+        commands.add('networksetup -setsecurewebproxy "$network" "${https.host}" "${https.port}"');
+      } else {
+        commands.add('networksetup -setsecurewebproxy "$network" "" ""');
+        commands.add('networksetup -setsecurewebproxystate "$network" off');
+      }
+      if (conf.socks != null) {
+        final socks = SystemProxyServer(conf.socks!);
+        commands.add('networksetup -setsocksfirewallproxy "$network" "${socks.host}" "${socks.port}"');
+      } else {
+        commands.add('networksetup -setsocksfirewallproxy "$network" "" ""');
+        commands.add('networksetup -setsocksfirewallproxystate "$network" off');
+      }
+    }
+
+    log.debug('MacSystemProxy.set:', commands);
+    await Process.run('bash', ['-c', commands.join(' && ')]);
+  }
+
+  @override
+  Future<SystemProxyConfig> get() async {
+    final out = (await Process.run('scutil', ['--proxy'])).stdout.toString().trim();
+    final states = ['HTTP', 'HTTPS', 'SOCKS'].map((it) {
+      final enableReg = RegExp('(?<=${it}Enable\\s*:\\s*)([^\\s]+)').firstMatch(out);
+      final hostReg = RegExp('(?<=${it}Proxy\\s*:\\s*)([^\\s]+)').firstMatch(out);
+      final portReg = RegExp('(?<=${it}Port\\s*:\\s*)([^\\s]+)').firstMatch(out);
+      final enable = enableReg?.group(1);
+      final host = hostReg?.group(1);
+      final port = portReg?.group(1);
+      if (enable == null || enable == '0' || host == null || port == null) return null;
+      return '$host:$port';
+    }).toList();
+    return SystemProxyConfig(http: states[0], https: states[1], socks: states[2]);
+  }
+}
+
+class WinSystemProxy extends SystemProxyPlatform {
+  static String regPath = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
+  static WinSystemProxy instance = WinSystemProxy();
+
+  @override
+  Future<void> set(SystemProxyConfig conf) async {
+    // socks 会丢失域名信息 导致规则失效
+    // http=127.0.0.1:7893;https=127.0.0.1:7890;socks=127.0.0.1:7891;   chrome, firefox use https
+    // http://127.0.0.1:7893;https=127.0.0.1:7890;socks=127.0.0.1:7891; chrome use http; firefox use https
+    // http://127.0.0.1:7893;https=127.0.0.1:7890;                      chrome use http; firefox use https
+    // https=127.0.0.1:7890;socks=127.0.0.1:7891;                       chrome, firefox use https
+    // http://127.0.0.1:7893;socks=127.0.0.1:7891;                      chrome, firefox use http
+    // http=127.0.0.1:7893;socks=127.0.0.1:7891;                        chrome use socks, firefox not use
+    // socks=127.0.0.1:7891;                                            chrome, firefox use socks4
+    // http=127.0.0.1:7893;                                             chrome, firefox not use
+    // http=xxx 不建议使用
+    String servers = "";
+    if (conf.http != null) servers += "http://${conf.http};";
+    if (conf.https != null) servers += "https=${conf.https};";
+    if (conf.socks != null) servers += "socks=${conf.socks};";
+    if (servers.isNotEmpty) {
+      await Process.run('reg', ['add', regPath, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '1', '/f']);
+      await Process.run('reg', ['add', regPath, '/v', 'ProxyServer', '/t', 'REG_SZ', '/d', servers, '/f']);
+    } else {
+      await Process.run('reg', ['add', regPath, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '0', '/f']);
+      await Process.run('reg', ['add', regPath, '/v', 'ProxyServer', '/t', 'REG_SZ', '/d', '', '/f']);
+    }
+  }
+
+  @override
+  Future<SystemProxyConfig> get() async {
+    final enable = (await Process.run('reg', ['query', regPath, '/v', 'ProxyEnable'])).stdout.toString().contains('0x1');
+    final result = SystemProxyConfig();
+    if (!enable) return result;
+    final out = await Process.run('reg', ['query', regPath, '/v', 'ProxyServer']);
+    final outStr = out.stdout.toString().trim();
+    final serversStr = RegExp(r'(?<=ProxyServer\s+REG_SZ\s+)(?!\s+).+').firstMatch(outStr)?.group(0);
+    if (serversStr != null) {
+      final serverReg = RegExp(r"((?:https?)|(?:socks))(?:(?:\:\/\/)|(?:=))(\d+\.\d+\.\d+\.\d+\:\d+)");
+      final results = serversStr.split(";").where((it) => it.isNotEmpty).map((it) => serverReg.firstMatch(it)?.groups([1, 2])).whereType<List>();
+      for (var it in results) {
+        final state = it[1];
+        switch (it[0]) {
+          case 'http':
+            result.http = state;
+            break;
+          case 'https':
+            result.https = state;
+            break;
+          case 'socks':
+            result.socks = state;
+            break;
+        }
+      }
+    }
+    return result;
+  }
+}
+
+class SystemProxy extends SystemProxyPlatform {
+  static SystemProxy instance = SystemProxy();
+
+  @override
+  Future<void> set(SystemProxyConfig conf) async {
+    try {
+      log.time('setProxy');
+      if (Platform.isWindows) {
+        return WinSystemProxy.instance.set(conf);
+      } else if (Platform.isMacOS) {
+        return MacSystemProxy.instance.set(conf);
+      } else {
+        throw UnimplementedError();
+      }
+    } catch (e) {
+      rethrow;
+    } finally {
+      log.timeEnd('setProxy');
+    }
+  }
+
+  @override
+  Future<SystemProxyConfig> get() async {
+    try {
+      log.time('getProxyState');
+      if (Platform.isWindows) {
+        return WinSystemProxy.instance.get();
+      } else if (Platform.isMacOS) {
+        return MacSystemProxy.instance.get();
+      } else {
+        throw UnimplementedError();
+      }
+    } finally {
+      log.timeEnd('getProxyState');
+    }
+  }
+}

+ 66 - 0
lib/app/utils/utils.dart

@@ -0,0 +1,66 @@
+import 'dart:math' as math;
+
+import 'package:flutter/services.dart';
+
+String bytesToSize(int bytes) {
+  if (bytes == 0) return '0 B';
+  const k = 1024;
+  const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+  final i = (math.log(bytes) / math.log(k)).floor();
+  return '${(bytes / math.pow(k, i)).toStringAsFixed(2)} ${sizes[i]}';
+}
+
+const Map<String, Map<String, String>> _copyCommandLineProxyTypes = {
+  'cmd': {'prefix': 'set ', 'quot': '', 'join': '&&'},
+  'bash': {'prefix': 'export ', 'quot': '"', 'join': ' && '},
+  'powershell': {'prefix': '\$env:', 'quot': '"', 'join': ';'},
+};
+
+Future<void> copyCommandLineProxy(String type, {String? http, String? https}) async {
+  final types = _copyCommandLineProxyTypes[type];
+  if (types == null) return;
+  final prefix = types['prefix']!;
+  final quot = types['quot']!;
+  final join = types['join']!;
+  List<String> commands = [];
+  if (http != null) commands.add('${prefix}http_proxy=${quot}http://$http$quot');
+  if (https != null) commands.add('${prefix}https_proxy=${quot}http://$https$quot');
+
+  if (commands.isNotEmpty) await Clipboard.setData(ClipboardData(text: commands.join(join)));
+}
+
+extension OrNull<T> on T {
+  T? orNull(bool a) {
+    return a ? this : null;
+  }
+}
+
+extension BindOne<T, R> on R Function(T a) {
+  R Function() bindOne(T a) {
+    return () => this(a);
+  }
+}
+
+extension BindFirst<T, T2, R> on R Function(T a, T2 b) {
+  R Function(T2 b) bindFirst(T a) {
+    return (T2 b) => this(a, b);
+  }
+}
+
+extension MapIndex<T> on List<T> {
+  List<R> mapIndex<R>(R Function(int index, T item) fn) {
+    final List<R> list = [];
+    for (int idx = 0; idx < length; idx++) {
+      list.add(fn(idx, this[idx]));
+    }
+    return list;
+  }
+}
+
+enum RunningState {
+  starting,
+  running,
+  stopping,
+  stoped,
+  error,
+}

+ 87 - 28
lib/main.dart

@@ -1,20 +1,29 @@
+import 'dart:async';
 import 'dart:io';
 import 'dart:ui';
 
 
+import 'package:bot_toast/bot_toast.dart';
 import 'package:dart_json_mapper/dart_json_mapper.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
 import 'package:get/get.dart';
 import 'package:kommon/tool/sp_util.dart';
+import 'package:naiyouwl/app/controller/GlobalController.dart';
+import 'package:naiyouwl/app/controller/config.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:naiyouwl/app/controller/core.dart';
+import 'package:naiyouwl/app/controller/service.dart';
+import 'package:naiyouwl/app/controller/tray.dart';
+import 'package:naiyouwl/app/controller/window.dart';
 import 'package:naiyouwl/app/main_screen.dart';
+import 'package:naiyouwl/app/modules/welcome/views/welcome_view.dart';
 import 'package:proxy_manager/proxy_manager.dart';
 import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
 
 import 'app/routes/app_pages.dart';
-import 'app/service/clash_service.dart';
 import 'main.mapper.g.dart' show initializeJsonMapper;
 
 final proxyManager = ProxyManager();
@@ -37,26 +46,21 @@ void main() async {
     },
   }));
 
-  const Set<PointerDeviceKind> kTouchLikeDeviceTypes = <PointerDeviceKind>{
-    PointerDeviceKind.touch,
-    PointerDeviceKind.mouse,
-    PointerDeviceKind.stylus,
-    PointerDeviceKind.invertedStylus,
-    PointerDeviceKind.unknown
-  };
+
+
+
+
+  Get.put(TrayController());
+  Get.put(WindowController());
+  Get.put(CoreController());
+  Get.put(ConfigController());
+  Get.put(ServiceController());
+  Get.put(GlobalController());
 
   await initAppService();
 
   runApp(
-    GetMaterialApp(
-      scrollBehavior: const MaterialScrollBehavior().copyWith(
-          scrollbars: true,
-          dragDevices: kTouchLikeDeviceTypes
-      ),
-      title: "Application",
-      initialRoute: AppPages.INITIAL,
-      getPages: AppPages.routes,
-    ),
+      const MyApp()
   );
 
   if (isDesktop) {
@@ -80,12 +84,15 @@ Future<void> initWindow() async {
   );
   windowManager.waitUntilReadyToShow(opts, () {
     // hide window when start
-    if (Get.find<ClashService>().isHideWindowWhenStart() && kReleaseMode) {
-      windowManager.hide();
-    } else {
-      windowManager.show();
-      windowManager.focus();
-    }
+    // if (Get.find<ClashService>().isHideWindowWhenStart() && kReleaseMode) {
+    //   windowManager.hide();
+    // } else {
+    //   windowManager.show();
+    //   windowManager.focus();
+    // }
+
+    windowManager.show();
+    windowManager.focus();
     //windowManager.show();
     // windowManager.focus();
   });
@@ -94,11 +101,7 @@ Future<void> initWindow() async {
 
 Future<void> initAppService() async {
   await SpUtil.getInstance();
-  await Get.putAsync(() => ClashService().init());
-  if (isDesktop) {
-    // await Get.putAsync(() => AutostartService().init());
-  }
-  //Get.put(ThemeController());
+
 }
 
 Future<void> initAppTray(
@@ -122,4 +125,60 @@ Future<void> initAppTray(
     items.insertAll(0, details);
   }
   await trayManager.setContextMenu(Menu(items: items));
+}
+
+
+class MyApp extends StatefulWidget {
+  const MyApp({Key? key}) : super(key: key);
+
+  @override
+  State<MyApp> createState() => _MyAppState();
+}
+
+class _MyAppState extends State<MyApp> {
+  late StreamSubscription<bool> listenShow;
+
+  @override
+  void initState() {
+    controllers.init();
+    controllers.global.init(context);
+    listenShow = controllers.window.isVisible.stream.listen((event) async {
+      if (!event) return;
+      await Future.delayed(const Duration(milliseconds: 100));
+      await Get.forceAppUpdate();
+    });
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+
+    const Set<PointerDeviceKind> kTouchLikeDeviceTypes = <PointerDeviceKind>{
+      PointerDeviceKind.touch,
+      PointerDeviceKind.mouse,
+      PointerDeviceKind.stylus,
+      PointerDeviceKind.invertedStylus,
+      PointerDeviceKind.unknown
+    };
+
+
+    return GetMaterialApp(
+      scrollBehavior: const MaterialScrollBehavior().copyWith(
+          scrollbars: true,
+          dragDevices: kTouchLikeDeviceTypes
+      ),
+      builder: BotToastInit(),
+      title: "Application",
+      initialRoute: AppPages.INITIAL,
+      getPages: AppPages.routes,
+
+    );
+  }
+
+  @override
+  void dispose() {
+    controllers.global.dispose();
+    listenShow.cancel();
+    super.dispose();
+  }
 }

+ 1 - 1
macos/Podfile.lock

@@ -94,4 +94,4 @@ SPEC CHECKSUMS:
 
 PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367
 
-COCOAPODS: 1.13.0
+COCOAPODS: 1.12.1

+ 0 - 8
macos/Runner.xcodeproj/project.pbxproj

@@ -27,8 +27,6 @@
 		33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; };
 		33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; };
 		33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; };
-		3B5A16BB2ADD14A2002F295B /* libclash.dylib in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 3B5A16BA2ADD14A2002F295B /* libclash.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
-		3B5A16BC2ADD1937002F295B /* libclash.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B5A16B82ADD1496002F295B /* libclash.dylib */; settings = {ATTRIBUTES = (Weak, ); }; };
 		8444AE800248F37BB8ABF637 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F397D2CB6412D720D523E30C /* Pods_Runner.framework */; };
 		CC6EB3AF67BFAAA84BB25D80 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AC0DF046605705B6C85E2E2 /* Pods_RunnerTests.framework */; };
 /* End PBXBuildFile section */
@@ -57,7 +55,6 @@
 			dstPath = "";
 			dstSubfolderSpec = 10;
 			files = (
-				3B5A16BB2ADD14A2002F295B /* libclash.dylib in Bundle Framework */,
 			);
 			name = "Bundle Framework";
 			runOnlyForDeploymentPostprocessing = 0;
@@ -84,8 +81,6 @@
 		33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = "<group>"; };
 		33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = "<group>"; };
 		33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = "<group>"; };
-		3B5A16B82ADD1496002F295B /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../core/libclash.dylib; sourceTree = "<group>"; };
-		3B5A16BA2ADD14A2002F295B /* libclash.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libclash.dylib; path = ../core/libclash.dylib; sourceTree = "<group>"; };
 		75F24307BAD49042829F2A60 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
 		7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = "<group>"; };
 		9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = "<group>"; };
@@ -108,7 +103,6 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
-				3B5A16BC2ADD1937002F295B /* libclash.dylib in Frameworks */,
 				8444AE800248F37BB8ABF637 /* Pods_Runner.framework in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
@@ -138,7 +132,6 @@
 		33CC10E42044A3C60003C045 = {
 			isa = PBXGroup;
 			children = (
-				3B5A16BA2ADD14A2002F295B /* libclash.dylib */,
 				33FAB671232836740065AC1E /* Runner */,
 				33CEB47122A05771004F2AC0 /* Flutter */,
 				331C80D6294CF71000263BE5 /* RunnerTests */,
@@ -208,7 +201,6 @@
 		D73912EC22F37F3D000D13A0 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
-				3B5A16B82ADD1496002F295B /* libclash.dylib */,
 				F397D2CB6412D720D523E30C /* Pods_Runner.framework */,
 				0AC0DF046605705B6C85E2E2 /* Pods_RunnerTests.framework */,
 			);

+ 8 - 1
pubspec.yaml

@@ -6,6 +6,10 @@ environment:
   sdk: '>=3.1.1 <4.0.0'
 
 dependencies:
+  web_socket_channel: ^2.1.0
+  yaml: ^3.1.0
+  flutter_emoji: ^2.4.0
+  process_run: ^0.13.0
   tray_manager: ^0.2.0
   proxy_manager: ^0.0.3
   shared_preferences: ^2.0.10
@@ -26,6 +30,7 @@ dependencies:
   url_launcher: ^6.1.11
   path_provider: ^2.0.11
   kommon: ^0.4.1
+  bot_toast: ^4.1.3
 
 dev_dependencies:
 
@@ -37,7 +42,9 @@ dev_dependencies:
   test: ^1.24.3
 flutter:
   assets:
-    - assets/tp/clash/
+    - assets/
+    - assets/bin/
+    - assets/dep/
     - assets/images/login/
     - assets/images/main/
     - assets/images/node/

+ 100 - 0
scripts/init.dart

@@ -0,0 +1,100 @@
+// ignore_for_file: avoid_print
+
+import 'dart:io';
+import 'package:dio/dio.dart';
+import 'package:archive/archive.dart';
+import 'package:naiyouwl/app/const/const.dart';
+import 'package:path/path.dart' as path;
+
+final dio = Dio();
+
+// https://github.com/dart-lang/sdk/issues/31610
+final assetsPath = path.normalize(path.join(Platform.script.toFilePath(), '../../assets'));
+final binDir = Directory(path.join(assetsPath, 'bin'));
+final depDir = Directory(path.join(assetsPath, 'dep'));
+
+Future downloadLatestClashCore() async {
+  final String clashCoreName = 'clash-${ClashName.platform}-${ClashName.arch}';
+
+  final info = await dio.get('https://api.github.com/repos/MetaCubeX/Clash.Meta/releases');
+  final Map<String, dynamic> latest = (info.data['assets'] as List<dynamic>).firstWhere((it) => (it['name'] as String).contains(clashCoreName));
+
+  final String name = latest['name'];
+  final tempFile = File(path.join(binDir.path, '$name.temp'));
+
+  print('Downloading $name');
+  await dio.download(latest['browser_download_url'], tempFile.path);
+  print('Download Success');
+
+  print('Unarchiving $name');
+  final tempBetys = await tempFile.readAsBytes();
+  if (name.contains('.gz')) {
+    final bytes = GZipDecoder().decodeBytes(tempBetys);
+    final String filePath = path.join(binDir.path, clashCoreName);
+    await File(filePath).writeAsBytes(bytes);
+    await Process.run('chmod', ['+x', filePath]);
+  } else {
+    final file = ZipDecoder().decodeBytes(tempBetys).first;
+    await File(path.join(binDir.path, file.name)).writeAsBytes(file.content);
+  }
+  await tempFile.delete();
+  print('Unarchiv Success');
+}
+
+Future downloadLatestClashService() async {
+  final String serviceName = 'clash-for-flutter-service-${ClashName.platform}-${ClashName.arch}';
+  final info = await dio.get('https://api.github.com/repos/csj8520/clash-for-flutter-service/releases/latest');
+  final Map<String, dynamic> latest = (info.data['assets'] as List<dynamic>).firstWhere((it) => (it['name'] as String).contains(serviceName));
+
+  final String name = latest['name'];
+  final tempFile = File(path.join(binDir.path, '$name.temp'));
+
+  print('Downloading $name');
+  await dio.download(latest['browser_download_url'], tempFile.path);
+  print('Download Success');
+
+  print('Unarchiving $name');
+  final tempBetys = await tempFile.readAsBytes();
+  if (name.contains('.gz')) {
+    final bytes = GZipDecoder().decodeBytes(tempBetys);
+    final String filePath = path.join(binDir.path, serviceName);
+    await File(filePath).writeAsBytes(bytes);
+    await Process.run('chmod', ['+x', filePath]);
+  } else {
+    final file = ZipDecoder().decodeBytes(tempBetys).first;
+    await File(path.join(binDir.path, file.name)).writeAsBytes(file.content);
+  }
+  await tempFile.delete();
+  print('Unarchiv Success');
+}
+
+Future downloadCountryMmdb() async {
+  print('Downloading Country.mmdb');
+  final String geoipFilePath = path.join(depDir.path, 'Country.mmdb');
+  await dio.download('https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb', geoipFilePath);
+  print('Download Success');
+}
+
+Future downloadWintun() async {
+  print('Downloading Wintun');
+  final File wintunFile = File(path.join(depDir.path, 'wintun.zip.temp'));
+  await dio.download('https://www.wintun.net/builds/wintun-0.14.1.zip', wintunFile.path);
+  print('Download Success');
+  print('Unarchiving wintun.zip');
+  final files = ZipDecoder().decodeBytes(await wintunFile.readAsBytes());
+  final file = files.firstWhere((it) => it.name == 'wintun/bin/${ClashName.arch}/wintun.dll');
+  await File(path.join(depDir.path, 'wintun.dll')).writeAsBytes(file.content);
+  await wintunFile.delete();
+  print('Unarchiv Success');
+}
+
+void main() async {
+  if (!(await binDir.exists())) await binDir.create();
+  if (!(await depDir.exists())) await depDir.create();
+
+  // await downloadLatestClashCore();
+  // await downloadLatestClashService();
+
+  await downloadCountryMmdb();
+  if (Platform.isWindows) await downloadWintun();
+}

Деякі файли не було показано, через те що забагато файлів було змінено