alroyso před 1 rokem
rodič
revize
660ce5eb38

+ 30 - 0
lib/app/component/button_select.dart

@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:naiyouwl/app/utils/utils.dart';
+import 'package:styled_widget/styled_widget.dart';
+class ButtonSelect extends StatelessWidget {
+  const ButtonSelect({Key? key, required this.labels, this.value = 0, this.onSelect}) : super(key: key);
+  final List<String> labels;
+  final int value;
+  final void Function(int idx)? onSelect;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.center,
+      children: List.generate(
+        labels.length,
+        ((idx) => TextButton(
+              onPressed: onSelect?.bindOne(idx),
+              child: Text(labels[idx]).textColor(value == idx ? Colors.white : const Color(0xff54759a)).fontSize(12),
+            ).decorated(
+              color: idx == value ? Theme.of(context).primaryColor : Colors.white,
+              border: idx == value ? null : Border.all(color: const Color(0xffe4eaef), width: 1),
+              borderRadius: BorderRadius.horizontal(
+                left: idx == 0 ? const Radius.circular(4) : Radius.zero,
+                right: idx == labels.length - 1 ? const Radius.circular(4) : Radius.zero,
+              ),
+            )),
+      ),
+    );
+  }
+}

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

@@ -70,6 +70,10 @@ class Files {
     return File(path.join(Paths.config.path, '.config.json'));
   }
 
+  static File get makeProxyConfig {
+    return File(path.join(Paths.config.path, 'proxy.yaml'));
+  }
+
   static File get configCountryMmdb {
     return File(path.join(Paths.config.path, 'Country.mmdb'));
   }

+ 31 - 8
lib/app/controller/GlobalController.dart

@@ -8,18 +8,20 @@ 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:shared_preferences/shared_preferences.dart';
 import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
 
 class GlobalController extends GetxController {
 
   late BuildContext context;
-
+  final List<String> modes = ['rule', 'global'];
   var nodeModes = <NodeMode>[].obs;
   var isLoading = false.obs;
   var errorMsg = ''.obs;
   var systemProxySwitchIng = false.obs;
-
+  bool allowStatusUpdate = false;
+  final selectedNode = Rx<NodeMode?>(null);
   // 策略组
   var proxieGroups = <ProxieProxiesItem>[].obs;
   // 代理集
@@ -47,9 +49,9 @@ class GlobalController extends GetxController {
 
     // init config
     await controllers.config.initConfig();
-    final language = controllers.config.config.value.language.split('_');
-
-    //await controllers.pageSetting.applyLanguage(Locale(language[0], language[1]));
+    // final language = controllers.config.config.value.language.split('_');
+    //
+    // await applyLanguage(Locale(language[0], language[1]));
 
     await controllers.service.initConfig();
     // init service
@@ -57,9 +59,10 @@ class GlobalController extends GetxController {
     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.service.startClashCore();
+    // if (controllers.service.coreStatus.value != RunningState.running) return;
+    // await controllers.core.updateVersion();
     // await controllers.pageProxie.updateDate();
 
     initRegularlyUpdate();
@@ -69,6 +72,16 @@ class GlobalController extends GetxController {
     nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
   }
 
+  Future<void> makeProxy() async {
+     await controllers.config.makeClashConfig(nodeModes);
+     await controllers.service.reloadClashCore();
+
+  }
+
+  Future<SystemProxyConfig> getSysProxy() async{
+     return  await SystemProxy.instance.get();
+  }
+
   Future<void> systemProxySwitch(bool open) async {
     systemProxySwitchIng.value = true;
 
@@ -157,6 +170,16 @@ class GlobalController extends GetxController {
     });
   }
 
+  Future<void> loadSelectedNode() async {
+    final prefs = await SharedPreferences.getInstance();
+    final selectedNodeId = prefs.getInt('selectedNodeId');
+    if (selectedNodeId != null) {
+      //selectedIndex.value = nodeModes.indexWhere((item) => item.id == selectedNodeId);
+      selectedNode.value = nodeModes.firstWhere((node) => node.id == selectedNodeId);
+    }
+  }
+
+
   void initRegularlyUpdate() {
     Future.delayed(const Duration(minutes: 5)).then((_) async {
       for (final it in controllers.config.config.value.subs) {

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

@@ -8,6 +8,8 @@ import 'package:yaml/yaml.dart';
 import 'package:path/path.dart' as path;
 import 'package:flutter_emoji/flutter_emoji.dart';
 
+import '../data/model/NodeMode.dart';
+
 final Map<String, dynamic> _defaultConfig = {
   'selected': 'example.yaml',
   'updateInterval': 86400,
@@ -58,6 +60,68 @@ class ConfigController extends GetxController {
     await readClashCoreApi();
   }
 
+  String nodeToYaml(NodeMode node) {
+    switch (node.type) {
+      case 'trojan':
+        return '''  - { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }''';
+      case 'shadowsocks':
+        return '''  - { 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 '''  - { 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 '''  - { 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> makeClashConfig(List<NodeMode> nodes) async{
+    var mixedport  = 5988;
+    bool bg = await isPortOccupied(mixedport);
+    if(!bg) {
+      mixedport = await getFreePort();
+    }
+    var extePort  = 5987;
+    bg = await isPortOccupied(extePort);
+    if(!bg) {
+      mixedport = await getFreePort();
+    }
+    var proxies = nodes.map(nodeToYaml).toList();
+
+    var proxyGroups = '''
+proxy-groups:
+  - name: proxy
+    type: select
+    proxies:
+      - ${nodes.map((node) => node.name).join('\n      - ')}
+  ''';
+
+    var rules = '''
+rules:
+  - GEOIP,CN,DIRECT
+  - MATCH,proxy
+  ''';
+
+    var initconfig = '''
+mixed-port: $mixedport
+allow-lan: true
+bind-address: '*'
+mode: Rule
+log-level: info
+external-controller: '127.0.0.1:$extePort'
+proxies:
+${proxies.join('\n')}
+$proxyGroups
+$rules
+  ''';
+
+    await Files.makeProxyConfig.writeAsString(initconfig);
+    config.value.selected = 'proxy.yaml';
+
+  }
+
   Future<void> save() async {
     await Files.configConfig.writeAsString(json.encode(config.toJson()));
   }
@@ -184,5 +248,17 @@ class ConfigController extends GetxController {
     await server.close();
     return port;
   }
+  Future<bool> isPortOccupied(int port) async {
+    bool isOccupied = false;
+    ServerSocket? server;
+    try {
+      server = await ServerSocket.bind(InternetAddress.loopbackIPv4, port);
+    } catch (e) {
+      isOccupied = true;
+    } finally {
+      await server?.close();
+    }
+    return isOccupied;
+  }
 
 }

+ 6 - 6
lib/app/controller/core.dart

@@ -4,12 +4,15 @@ 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/controller/controllers.dart';
 import 'package:naiyouwl/app/utils/system_proxy.dart';
 import 'package:web_socket_channel/io.dart';
 
 
 class CoreController extends GetxController {
-  late final dio ;
+  late final dio  = Dio(BaseOptions(
+       baseUrl: 'http://127.0.0.1:9090',
+  ));
   var version = ClashCoreVersion(premium: true, version: '').obs;
   var address = ''.obs;
   var secret = ''.obs;
@@ -21,7 +24,7 @@ class CoreController extends GetxController {
     mixedPort: 0,
     allowLan: false,
     bindAddress: '',
-    mode: '',
+    mode: 'rule',
     logLevel: '',
     ipv6: false,
   ).obs;
@@ -29,10 +32,7 @@ class CoreController extends GetxController {
   var ruleProvider = RuleProvider(providers: {}).obs;
   var rule = Rule(rules: []).obs;
 
-  CoreController() {
-    //dio.addSentry();
-    dio = Dio(BaseOptions(baseUrl: 'http://127.0.0.1:9090'));
-  }
+  CoreController();
 
   SystemProxyConfig get proxyConfig {
     final mixedPort = config.value.mixedPort == 0 ? null : config.value.mixedPort;

+ 1 - 1
lib/app/controller/service.dart

@@ -241,7 +241,7 @@ class ServiceController extends GetxController {
     }
 
     Future<void> reloadClashCore() async {
-      BotToast.showText(text: '正在重启 Clash Core ……');
+      BotToast.showText(text: '正在重启 Core ……');
       await stopClashCore();
       await controllers.config.readClashCoreApi();
       await startClashCore();

+ 26 - 25
lib/app/controller/tray.dart

@@ -24,33 +24,33 @@ class TrayController extends GetxController with TrayListener {
     final disabled = !controllers.service.isRunning;
 
     trayMenu = Menu(items: [
-      //MenuItem.checkbox(label: 'tray_show'.tr, checked: !visible, onClick: handleClickShow),
+      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.submenu(
+        label: 'proxie_group_title'.tr,
+        disabled: disabled,
+        submenu: Menu(
+            items: controllers.global.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,
@@ -123,6 +123,7 @@ class TrayController extends GetxController with TrayListener {
   }
 
   Future<void> handleClickRestartClashCore(MenuItem menuItem) async {
+    controllers.global.allowStatusUpdate = true;
     await controllers.service.reloadClashCore();
   }
 

+ 246 - 0
lib/app/i18n/i18n.dart

@@ -0,0 +1,246 @@
+import 'package:get/get.dart';
+import 'package:flutter/cupertino.dart';
+
+// 大部分翻译来自:
+// https://github.com/Dreamacro/clash-dashboard/blob/master/src/i18n/en_US.ts
+
+class I18n extends Translations {
+  static const List<Locale> locales = [
+    Locale('zh', 'CN'),
+    Locale('en', 'US'),
+  ];
+
+  static const List<String> localeSwitchs = [
+    "中文",
+    "English",
+  ];
+
+  @override
+  Map<String, Map<String, String>> get keys => {
+        'en_US': {
+          "clash_core_version": " Core Version",
+          // tray
+          "tray_restart_clash_core": "Restart  Core",
+          "tray_show": "Show",
+          "tray_copy_command_line_proxy": "Copy command line proxy",
+          "tray_about": "About",
+          "tray_exit": "Exit",
+          // modal
+          "model_ok": "Ok",
+          "model_cancel": "Cancel",
+          "model_delete": "Delete",
+          // sidebar
+          "sidebar_proxies": "Proxies",
+          "sidebar_profiles": "Profiles",
+          "sidebar_logs": "Logs",
+          "sidebar_rules": "Rules",
+          "sidebar_settings": "Setting",
+          "sidebar_connections": "Connections",
+          "sidebar_version": "Version",
+          // setting
+          "setting_title": "Settings",
+          "setting_start_at_login": "Start at login",
+          "setting_language": "Language",
+          "setting_set_as_system_proxy": "Set as system proxy",
+          "setting_allow_connect_from_lan": "Allow connect from Lan",
+          "setting_proxy_mode": "Mode",
+          "setting_socks5_proxy_port": "Socks5 proxy port",
+          "setting_http_proxy_port": "HTTP proxy port",
+          "setting_mixed_proxy_port": "Mixed proxy port",
+          "setting_external_controller": "External controller",
+          "setting_service_open": "Open Service",
+          "setting_mode_global": "Global",
+          "setting_mode_rules": "Rules",
+          "setting_mode_direct": "Direct",
+          "setting_mode_script": "Script",
+          // proxie
+          "proxie_title": 'Proxies',
+          "proxie_group_title": "Policy Group",
+          "proxie_provider_title": "Providers",
+          "proxie_provider_update_time": "Last updated at",
+          "proxie_expand": "Expand",
+          "proxie_collapse": "Collapse",
+          "proxie_speed_test": "Speed Test",
+          "proxie_break_connections": "Close connections which include the group",
+          // rule
+          "rule_title": "Rules",
+          "rule_provider_title": "Providers",
+          "rule_provider_update_time": "Last updated at",
+          "rule_rule_count": "Rule count",
+          // connection
+          "connection_title": "Connections",
+          "connection_keep_closed": "Keep closed connections",
+          "connection_total": "(total: upload @upload download @download)",
+          "connection_filter": "filter",
+          "connection_close_all_title": "Warning",
+          "connection_close_all_content": "This would close all connections",
+          "connection_columns_host": "Host",
+          "connection_columns_network": "Network",
+          "connection_columns_process": "Type",
+          "connection_columns_type": "Chains",
+          "connection_columns_chains": "Process",
+          "connection_columns_rule": "Rule",
+          "connection_columns_time": "Time",
+          "connection_columns_speed": "Speed",
+          "connection_columns_upload": "Upload",
+          "connection_columns_download": "Download",
+          "connection_columns_source_ip": "Source IP",
+          "connection_info_title": "Connection",
+          "connection_info_id": "ID",
+          "connection_info_host": "Host",
+          "connection_info_empty": "Empty",
+          "connection_info_dst_ip": "IP",
+          "connection_info_src_ip": "Source",
+          "connection_info_upload": "Upload",
+          "connection_info_download": "Download",
+          "connection_info_network": "Network",
+          "connection_info_process": "Process",
+          "connection_info_process_path": "Path",
+          "connection_info_inbound": "Inbound",
+          "connection_info_rule": "Rule",
+          "connection_info_chains": "Chains",
+          "connection_info_status": "Status",
+          "connection_info_opening": "Open",
+          "connection_info_closed": "Closed",
+          "connection_info_close_connection": "Close",
+          // profile
+          "profile_title": "Profiles",
+          "profile_update_interval": "Update interval",
+          "profile_update_interval_min": "Not less than one minute!",
+          "profile_update_interval_error": "Please enter the correct time!",
+          "profile_hour": "Hour",
+          "profile_columens_config_name": "Config name",
+          "profile_columens_url": "URL",
+          "profile_columens_update_time": "Update time",
+          "profile_columens_traffic": "Used/Total",
+          "profile_columens_expire": "Expiration",
+          "profile_columens_open_config_folder": "Open config folder",
+          "profile_columens_add_config": "Add config",
+          "profile_config_add": "Add",
+          "profile_config_edit": "Edit",
+          "profile_config_ext_error": "Make sure the file suffix is .yaml",
+          "profile_config_already_exists": "Config: @name already exists",
+          "profile_config_no_change": "Config no change",
+          "profile_config_update_error": "Update config: @name Error\nMsg: @msg",
+          "profile_config_keep_one": "Keep at least one config file",
+          "profile_config_mode_title": "Warning",
+          "profile_config_mode_content": "Delete: @name config, disk files will be deleted!",
+          "profile_config_mode_file_name": "name",
+          "profile_config_mode_file_name_hint": "config.yaml",
+          "profile_config_mode_url": "url",
+          "profile_config_mode_url_hint": "The local config can be left blank",
+        },
+        'zh_CN': {
+          "clash_core_version": "内核版本",
+          // tray
+          "tray_restart_clash_core": "重启Core",
+          "tray_show": "显示",
+          "tray_copy_command_line_proxy": "复制命令行代理",
+          "tray_about": "关于",
+          "tray_exit": "退出",
+          // modal
+          "model_ok": "确 定",
+          "model_cancel": "取 消",
+          "model_delete": "删 除",
+          // sidebar
+          "sidebar_proxies": "代理",
+          "sidebar_profiles": "配置",
+          "sidebar_logs": "日志",
+          "sidebar_rules": "规则",
+          "sidebar_settings": "设置",
+          "sidebar_connections": "连接",
+          // setting
+          "setting_title": "设置",
+          "setting_start_at_login": "开机时启动",
+          "setting_language": "语言",
+          "setting_set_as_system_proxy": "设置为系统代理",
+          "setting_allow_connect_from_lan": "允许来自局域网的连接",
+          "setting_proxy_mode": "代理模式",
+          "setting_socks5_proxy_port": "Socks5 代理端口",
+          "setting_http_proxy_port": "HTTP 代理端口",
+          "setting_mixed_proxy_port": "混合代理端口",
+          "setting_external_controller": "外部控制设置",
+          "setting_service_open": "开启服务",
+          "setting_mode_global": "全局",
+          "setting_mode_rules": "局部",
+          "setting_mode_direct": "直连",
+          "setting_mode_script": "脚本",
+          // proxie
+          "proxie_title": '代理',
+          "proxie_group_title": "策略组",
+          "proxie_provider_title": "代理集",
+          "proxie_provider_update_time": "最后更新于",
+          "proxie_expand": "展开",
+          "proxie_collapse": "收起",
+          "proxie_speed_test": "测速",
+          "proxie_break_connections": "切换时打断包含策略组的连接",
+          // rule
+          "rule_title": "规则",
+          "rule_provider_title": "规则集",
+          "rule_provider_update_time": "最后更新于",
+          "rule_rule_count": "规则条数",
+          // connection
+          "connection_title": "连接",
+          "connection_keep_closed": "保留关闭连接",
+          "connection_total": "(总量:上传 @upload 下载 @download)",
+          "connection_filter": "过滤",
+          "connection_close_all_title": "警告",
+          "connection_close_all_content": "将会关闭所有连接",
+          "connection_columns_host": "域名",
+          "connection_columns_network": "网络",
+          "connection_columns_process": "进程",
+          "connection_columns_type": "类型",
+          "connection_columns_chains": "节点链",
+          "connection_columns_rule": "规则",
+          "connection_columns_time": "连接时间",
+          "connection_columns_speed": "速率",
+          "connection_columns_upload": "上传",
+          "connection_columns_download": "下载",
+          "connection_columns_source_ip": "来源 IP",
+          "connection_info_title": "连接信息",
+          "connection_info_id": "ID",
+          "connection_info_host": "域名",
+          "connection_info_empty": "空",
+          "connection_info_dst_ip": "IP",
+          "connection_info_src_ip": "来源",
+          "connection_info_upload": "上传",
+          "connection_info_download": "下载",
+          "connection_info_network": "网络",
+          "connection_info_process": "进程",
+          "connection_info_process_path": "路径",
+          "connection_info_inbound": "入口",
+          "connection_info_rule": "规则",
+          "connection_info_chains": "代理",
+          "connection_info_status": "状态",
+          "connection_info_opening": "连接中",
+          "connection_info_closed": "已关闭",
+          "connection_info_close_connection": "关闭连接",
+          // profile
+          "profile_title": "配置",
+          "profile_update_interval": "更新间隔",
+          "profile_update_interval_min": "时间不可小于一分钟!",
+          "profile_update_interval_error": "请输入正确的时间!",
+          "profile_hour": "小时",
+          "profile_columens_config_name": "配置文件名称",
+          "profile_columens_url": "链接",
+          "profile_columens_update_time": "更新时间",
+          "profile_columens_traffic": "已用/总量",
+          "profile_columens_expire": "过期时间",
+          "profile_columens_open_config_folder": "打开配置文件夹",
+          "profile_columens_add_config": "添加配置",
+          "profile_config_add": "添加",
+          "profile_config_edit": "编辑",
+          "profile_config_ext_error": "请确保文件后缀名为.yaml",
+          "profile_config_already_exists": "配置文件:@name 已存在",
+          "profile_config_no_change": "配置无变化",
+          "profile_config_update_error": "更新配置:@name 失败\nMsg: @msg",
+          "profile_config_keep_one": "请至少保留一个配置文件",
+          "profile_config_mode_title": "警告",
+          "profile_config_mode_content": "删除:@name 配置,磁盘内文件会同时删除!",
+          "profile_config_mode_file_name": "文件名",
+          "profile_config_mode_file_name_hint": "config.yaml",
+          "profile_config_mode_url": "地址",
+          "profile_config_mode_url_hint": "本地配置可留空",
+        }
+      };
+}

+ 47 - 58
lib/app/modules/home/controllers/home_controller.dart

@@ -1,10 +1,14 @@
 
+import 'dart:async';
 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:naiyouwl/app/controller/service.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';
 import '../../../common/LogHelper.dart';
@@ -24,7 +28,7 @@ enum ImageType {
   RENEWAL,
 }
 
-class HomeController extends GetxController with TrayListener,WindowListener {
+class HomeController extends GetxController {
   //TODO: Implement HomeController
 
   var isLoading = false.obs;
@@ -32,12 +36,11 @@ class HomeController extends GetxController with TrayListener,WindowListener {
   var localUsers = LocalUser().obs;
   var userMode = User().obs;
   var errorMsg = ''.obs;
-  var selectNode = '选择节点'.obs;
+
   var connectStatus = Rx<ConnectionStatus>(ConnectionStatus.disconnected);
   var nodeModes = <NodeMode>[];
   late final GlobalController globalController = controllers.global;
-
-
+  StreamSubscription<RunningState>? _statusSubscription;
   final Map<ImageType, String> imageMap = {
     ImageType.CUSTOMER: "assets/images/main/customer.png",
     ImageType.PROMOTION: "assets/images/main/promotion.png",
@@ -61,26 +64,43 @@ class HomeController extends GetxController with TrayListener,WindowListener {
     connectStatus.value = newStatus;
   }
 
-  void SetSysProxy() async{
+  void _handleStateChange([dynamic _]) async {
+    final coreStatus = controllers.service.coreStatus.value;
+    final isVisible = controllers.window.isVisible.value;
 
-    if(connectStatus.value == ConnectionStatus.stopped){
+    if (coreStatus == RunningState.running && isVisible &&  controllers.global.allowStatusUpdate) {
+      updateStatus(ConnectionStatus.connecting);
+      await Future.delayed(const Duration(seconds: 5));
+      updateStatus(ConnectionStatus.stopped);
+      await controllers.global.updateDate();
+    } else {
       updateStatus(ConnectionStatus.disconnected);
-   //   await Get.find<ClashService>().clearSystemProxy();
+    }
+  }
+
+  Future<void> handleButtonClick() async {
+
+    // 如果当前状态是已连接或正在连接,则停止服务
+    if (connectStatus.value == ConnectionStatus.connecting ||
+        connectStatus.value == ConnectionStatus.stopped) {
+      connectStatus.value = ConnectionStatus.disconnected;
+      controllers.global.allowStatusUpdate = false;
+      // 停止服务
+      await controllers.service.stopClashCore();
       return;
+    } else {
+
+      controllers.global.allowStatusUpdate = true;
+      //生成配置
+      await controllers.global.makeProxy();
+      // 开始重启服务
+      await controllers.service.reloadClashCore();
     }
+  }
+
+
+  void SetSysProxy() async{
 
-    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);
-    // });
   }
 
 
@@ -138,16 +158,17 @@ class HomeController extends GetxController with TrayListener,WindowListener {
   }
 
 
+
   final count = 0.obs;
   @override
   void onInit() {
     super.onInit();
-    //globalController = Get.put(GlobalController());
+    _statusSubscription = controllers.service.coreStatus.stream.listen(_handleStateChange);
+
     fetchSysConfig();
     fetchLocalUser();
     fetchUserinfo();
-    windowManager.addListener(this);
-    trayManager.addListener(this);
+
   }
 
   @override
@@ -157,50 +178,18 @@ class HomeController extends GetxController with TrayListener,WindowListener {
 
   @override
   void onClose() {
+    _statusSubscription?.cancel();
     super.onClose();
-  }
-
-
-  @override
-  void onWindowClose() {
-    super.onWindowClose();
-    windowManager.hide();
-  }
 
-  @override
-  void onTrayIconMouseDown() {
-    // windowManager.focus();
-    windowManager.show();
   }
 
-  @override
-  void onTrayIconRightMouseDown() {
-    super.onTrayIconRightMouseDown();
-    trayManager.popUpContextMenu();
-  }
-
-  @override
-  void onTrayMenuItemClick(MenuItem menuItem) {
-    switch (menuItem.key) {
-      case 'exit':
-        windowManager.close().then((value) async {
-          //await Get.find<ClashService>().closeClashDaemon();
-          exit(0);
-        });
-        break;
-      case 'show':
-        windowManager.focus();
-        windowManager.show();
-    }
-  }
-
-
   bool   GetEnable()  => userMode.value.enable == 1;
   String GetUserName() => localUsers.value.email.toString();
   String GetExpiredAt() => "到期时间:${userMode.value.expiredAt}";
   String GetTraffic() => "用户流量:${userMode.value.unusedTraffic}";
-  String GetNode() => selectNode.value;
-
+  String GetNode() => controllers.global.selectedNode.value?.name ?? "未选择节点";
+  String getHttp() => "https://127.0.0.1:${controllers.core.config.value.mixedPort}";
+  String getSocket() => "socks5://127.0.0.1:${controllers.core.config.value.mixedPort}";
 
   void RouteNode() =>  Get.toNamed(Routes.NODE);
 }

+ 23 - 14
lib/app/modules/home/views/home_view.dart

@@ -4,11 +4,11 @@ import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 
 import 'package:get/get.dart';
+import 'package:naiyouwl/app/component/button_select.dart';
+import 'package:naiyouwl/app/component/connection_widget.dart';
 import 'package:naiyouwl/app/component/sys_app_bar.dart';
-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 'package:naiyouwl/app/controller/controllers.dart';
+import 'package:naiyouwl/app/utils/system_proxy.dart';
 import '../controllers/home_controller.dart';
 
 
@@ -50,6 +50,8 @@ class HomeView extends GetView<HomeController> {
                 );
               });
             }
+
+            final disabled = !controllers.service.isRunning;
             return controller.isLoading.value ? const Center(
                 child: CircularProgressIndicator()) : Column(
               children: [
@@ -90,22 +92,21 @@ class HomeView extends GetView<HomeController> {
                     )
 
                 ),
-                const SizedBox(height: 35,),
-                const Padding(
-                  padding: EdgeInsets.fromLTRB(30, 0, 30, 0),
+                const SizedBox(height: 30,),
+                Padding(
+                  padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                   child: Row(
                     children: [
-
-                      Text("http://127.0.0.1:5336"),
-                      Spacer(),
-                      Text("socks://127.0.0.1:5337"),
+                      Text(controller.getHttp().toString()),
+                      const Spacer(),
+                      Text(controller.getSocket().toString()),
                     ],
                   ),
                 ),
                 Obx(() {
                   return ConnectionWidget(
                     status: controller.connectStatus.value, onTap: () {
-                          controller.SetSysProxy();
+                          controller.handleButtonClick();
                   },);
                 }),
                 Padding(
@@ -128,7 +129,15 @@ class HomeView extends GetView<HomeController> {
                     ),
                   ),
                 ),
-
+                const SizedBox(height: 20,),
+                Align(
+                  alignment: Alignment.center,
+                  child:ButtonSelect(
+                    labels: ['setting_mode_rules'.tr,'setting_mode_global'.tr,],
+                    value: controllers.global.modes.indexOf(controllers.core.config.value.mode),
+                    onSelect: disabled ? null : (idx) => controllers.core.fetchConfigUpdate({'mode': controllers.global.modes[idx]}),
+                  ),
+                )
               ],
             );
           })
@@ -164,7 +173,7 @@ class _UserStatusWidgetState extends State<UserStatusWidget> {
     return Align(
       alignment: Alignment.topCenter,
       child: Padding(
-        padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 10.0),
+        padding: const EdgeInsets.fromLTRB(5.0, 0, 5.0, 10.0),
         child:
         Row(
           children: [

+ 41 - 55
lib/app/modules/node/controllers/node_controller.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 
 import 'package:get/get.dart';
 import 'package:naiyouwl/app/controller/GlobalController.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
 import 'package:shared_preferences/shared_preferences.dart';
 
 import '../../../common/LogHelper.dart';
@@ -22,52 +23,36 @@ class NodeController extends GetxController {
   var nodeModes = <NodeMode>[];
   var selectedIndex = (-1).obs;
   var errorMsg = ''.obs;
-  final selectedNode = Rx<NodeMode?>(null);
+
   var pingResults = <int, String>{}.obs;
   //final selectedNodeId = (-1).obs;
   final isLoadingMap = <int, bool>{}.obs;
   var displayStrategy = Rx<NodeDisplayStrategy>(NodeDisplayStrategy.All);
-  late final GlobalController globalController ;
-  // Future<void> fetchNodes() async {
-  //   try {
-  //     isLoading.value = true;
-  //     nodeModes.clear();
-  //     nodeModes = await ApiService().getNode("/api/client/v4/nodes?vless=1");
-  //     nodesToShow.value = nodeModes;
-  //     LogHelper().d(nodeModes.toList());
-  //     loadSelectedNode();
-  //   } catch (e) {
-  //     errorMsg.value = e.toString();
-  //   } finally {
-  //     isLoading.value = false;
-  //   }
-  // }
+
 
 
   Future<void> tcpPing(NodeMode node) async {
     int? nodeId = node.id;
 
-    if (nodeId != null) {
-      isLoadingMap[nodeId] = true;  // 开始ping时设置为true
-      isLoadingMap.refresh();      // 通知观察者
-
-      final stopwatch = Stopwatch()..start();
-      Socket? socket;
-      try {
-        socket = await Socket.connect(node.host!, node.port!, timeout: const Duration(seconds: 3));
-        final elapsed = stopwatch.elapsedMilliseconds;
-        pingResults[nodeId] = '${elapsed}ms';  // 使用普通的映射赋值
-        pingResults.refresh();                // 通知观察者
-        socket.destroy();
-      } catch (e) {
-        pingResults[nodeId] = 'Error';        // 使用普通的映射赋值
-        pingResults.refresh();                // 通知观察者
-      }
-
-      isLoadingMap[nodeId] = false; // ping结束后设置为false
-      isLoadingMap.refresh();       // 通知观察者
+    isLoadingMap[nodeId] = true;  // 开始ping时设置为true
+    isLoadingMap.refresh();      // 通知观察者
+
+    final stopwatch = Stopwatch()..start();
+    Socket? socket;
+    try {
+      socket = await Socket.connect(node.host!, node.port!, timeout: const Duration(seconds: 3));
+      final elapsed = stopwatch.elapsedMilliseconds;
+      pingResults[nodeId] = '${elapsed}ms';  // 使用普通的映射赋值
+      pingResults.refresh();                // 通知观察者
+      socket.destroy();
+    } catch (e) {
+      pingResults[nodeId] = 'Error';        // 使用普通的映射赋值
+      pingResults.refresh();                // 通知观察者
+    }
+
+    isLoadingMap[nodeId] = false; // ping结束后设置为false
+    isLoadingMap.refresh();       // 通知观察者
     }
-  }
   void pingAllNodes() async {
     for (var node in nodeModes) {
        tcpPing(node);  // 这里用了 await 使其串行 ping,若希望并行可移除 await
@@ -82,41 +67,45 @@ class NodeController extends GetxController {
 
 
   void selectNode(NodeMode node) {
-    selectedNode.value = node;
+    controllers.global.selectedNode.value = node;
     _storeSelectedNode(node);
+
+    Get.back();
     //selectedIndex.value = nodeModes.indexWhere((item) => item.id == node.id);
   }
 
   Future<void> _storeSelectedNode(NodeMode node) async {
     final prefs = await SharedPreferences.getInstance();
     // 为简化起见,我们只存储node的ID,但您可以根据需要存储更多信息
-    prefs.setInt('selectedNodeId', node.id!);
-  }
+    prefs.setInt('selectedNodeId', node.id);
+
+    await controllers.global.loadSelectedNode();
+
 
-  Future<void> loadSelectedNode() async {
-    final prefs = await SharedPreferences.getInstance();
-    final selectedNodeId = prefs.getInt('selectedNodeId');
-    if (selectedNodeId != null) {
-      //selectedIndex.value = nodeModes.indexWhere((item) => item.id == selectedNodeId);
-      selectedNode.value = nodeModes.firstWhere((node) => node.id == selectedNodeId);
-    }
   }
 
+  // Future<void> loadSelectedNode() async {
+  //   final prefs = await SharedPreferences.getInstance();
+  //   final selectedNodeId = prefs.getInt('selectedNodeId');
+  //   if (selectedNodeId != null) {
+  //     //selectedIndex.value = nodeModes.indexWhere((item) => item.id == selectedNodeId);
+  //     selectedNode.value = nodeModes.firstWhere((node) => node.id == selectedNodeId);
+  //   }
+  // }
+
   //自动选择人数最小的线路
   NodeMode selectBestNode(List<NodeMode> nodes) {
     return nodes.where((node) => node.countryCode == 'HK') //筛选地区是HK的节点
         .reduce((value, element) =>
-    value.onlineUsers! < element.onlineUsers! ? value : element); //选择人数最小的
+    value.onlineUsers< element.onlineUsers? value : element); //选择人数最小的
   }
 
   void selectMinOnlineUsersNodeInRegion(String region) {
     List<NodeMode> nodesInRegion = nodeModes.where((node) => node.countryCode == region).toList();
     if (nodesInRegion.isNotEmpty) {
       NodeMode? minNode = nodesInRegion.reduce((curr, next) => curr.onlineUsers! <= next.onlineUsers! ? curr : next);
-      if (minNode != null) {
-        selectNode(minNode);
-      }
-    }
+      selectNode(minNode);
+        }
   }
 
   void filterNodesWithLeastUsersInHK() {
@@ -181,10 +170,7 @@ class NodeController extends GetxController {
   @override
   void onInit() {
     super.onInit();
-    //fetchNodes();
-
-    globalController = Get.put(GlobalController());
-    nodeModes =  globalController.nodeModes;
+    nodeModes =  controllers.global.nodeModes;
     nodesToShow.value = nodeModes;
   }
 

+ 6 - 5
lib/app/modules/node/views/node_view.dart

@@ -1,6 +1,7 @@
 import 'package:flutter/material.dart';
 
 import 'package:get/get.dart';
+import 'package:naiyouwl/app/controller/controllers.dart';
 
 import '../../../component/sys_app_bar.dart';
 import '../controllers/node_controller.dart';
@@ -92,7 +93,7 @@ class NodeView extends GetView<NodeController> {
                     child: Obx(() {
                       return RefreshIndicator(
                         key: _refreshIndicatorKey,
-                        onRefresh: controller.globalController.fetchNodes,
+                        onRefresh: controllers.global.fetchNodes,
                         child: ListView.builder(
                           itemCount: controller.nodesToShow.length,
                           itemBuilder: (BuildContext context, int index) {
@@ -105,19 +106,19 @@ class NodeView extends GetView<NodeController> {
                               // //  controller.nodeModes[controller.selectedIndex.value]
                               // print("node ---- ${node.id} index ---- $index");
                               bool isNodeLoading = controller.isLoadingMap[node
-                                  .id!] ?? false;
+                                  .id] ?? false;
                               var pingResult = controller.pingResults[node
                                   .id] ??
                                   '';
                               return Container(
-                                color: controller.selectedNode.value?.id ==
+                                color: controllers.global.selectedNode.value?.id ==
                                     node.id ? Colors.black12 : null,
                                 child: ListTile(
                                   key: ValueKey(node.id),
                                   title: Text(node.name.toString()),
                                   //tileColor: controller.selectedNode.value?.id == node.id ? Colors.blueAccent : null,
                                   // 如果选中则更改背景颜色
-                                  subtitle: Text('${node.type}'),
+                                  subtitle: Text(node.type),
                                   trailing: Row(
                                     mainAxisSize: MainAxisSize.min,
                                     children: [
@@ -129,7 +130,7 @@ class NodeView extends GetView<NodeController> {
                                           onPressed: () {
                                             controller.pingSingleNode(node);
                                           },
-                                          child: Text('测速'),
+                                          child: const Text('测速'),
                                         ),
                                       ],
 

+ 1 - 1
lib/app/utils/shell.dart

@@ -17,7 +17,7 @@ Future<void> killProcess(String name) async {
 
 Future<ProcessResult> runAsAdmin(String executable, List<String> arguments) async {
   String executablePath = shellArgument(executable).replaceAll(' ', r'\\ ');
-  executablePath = executablePath.substring(1, executablePath.length - 1);
+  executablePath = executablePath.substring(1, executablePath.length);
   if (Platform.isMacOS) {
     return await Process.run(
       'osascript',

+ 11 - 4
lib/main.dart

@@ -5,12 +5,11 @@ 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:flutter/services.dart';
-
+import 'package:flutter_localizations/flutter_localizations.dart';
 import 'package:get/get.dart';
 import 'package:kommon/tool/sp_util.dart';
+import 'package:naiyouwl/app/i18n/i18n.dart';
 import 'package:naiyouwl/app/controller/GlobalController.dart';
 import 'package:naiyouwl/app/controller/config.dart';
 import 'package:naiyouwl/app/controller/controllers.dart';
@@ -19,9 +18,9 @@ import 'package:naiyouwl/app/controller/service.dart';
 import 'package:naiyouwl/app/controller/tray.dart';
 import 'package:naiyouwl/app/controller/window.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 'main.mapper.g.dart' show initializeJsonMapper;
 
@@ -146,6 +145,14 @@ class _MyAppState extends State<MyApp> {
           scrollbars: true,
           dragDevices: kTouchLikeDeviceTypes
       ),
+      translations: I18n(),
+      locale: Get.deviceLocale,
+      localizationsDelegates: const [
+        GlobalMaterialLocalizations.delegate,
+        GlobalCupertinoLocalizations.delegate,
+        GlobalWidgetsLocalizations.delegate,
+      ],
+      supportedLocales: I18n.locales,
       builder: BotToastInit(),
       title: "Application",
       initialRoute: AppPages.INITIAL,

+ 1 - 1
macos/Podfile.lock

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

+ 3 - 0
pubspec.yaml

@@ -6,6 +6,7 @@ environment:
   sdk: '>=3.1.1 <4.0.0'
 
 dependencies:
+  styled_widget: ^0.4.0+3
   web_socket_channel: ^2.1.0
   yaml: ^3.1.0
   flutter_emoji: ^2.4.0
@@ -22,6 +23,8 @@ dependencies:
   get: 4.6.6
   flutter: 
     sdk: flutter
+  flutter_localizations:
+    sdk: flutter
   dio: ^4.0.6
   connectivity_plus: ^2.3.6
   flutter_secure_storage: ^5.0.2