alroyso 1 year ago
parent
commit
1e446a775d

BIN
assets/tp/clash/geoip.dat


File diff suppressed because it is too large
+ 779 - 0
assets/tp/clash/geosite.dat


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

@@ -95,6 +95,9 @@ ${proxyGroups.map((group) => group.toYamlString()).join('\n')}
 rules:
 - ${rules.join('\n- ')}''';
   }
+
+
+
 }
 
 // void main() {

+ 21 - 46
lib/app/component/connection_widget.dart

@@ -1,21 +1,22 @@
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
-
 import 'connection_status.dart';
 
 class ConnectionWidget extends StatefulWidget {
-
   final ConnectionStatus status;
-  final Function(ConnectionStatus) onStatusChange;
-  const ConnectionWidget({super.key, required this.status, required this.onStatusChange});
+  final Function() onTap;  // 只通知外部发生了点击事件
+
+  const ConnectionWidget({
+    Key? key,
+    required this.status,
+    required this.onTap,
+  }) : super(key: key);
 
   @override
   _ConnectionWidgetState createState() => _ConnectionWidgetState();
 }
 
 class _ConnectionWidgetState extends State<ConnectionWidget> {
-
-  ConnectionStatus _currentStatus = ConnectionStatus.disconnected;
   late String currentImage;
 
   @override
@@ -24,41 +25,16 @@ class _ConnectionWidgetState extends State<ConnectionWidget> {
     _updateImage();
   }
 
-  void _updateStatus() {
-    setState(() {
-      switch (_currentStatus) {
-        case ConnectionStatus.disconnected:
-          _currentStatus = ConnectionStatus.connecting;
-          Future.delayed(const Duration(seconds: 5), () {
-            if (mounted) { // 确保Widget仍然在Widget树中
-              setState(() {
-                _currentStatus = ConnectionStatus.stopped;
-                _updateImage();
-              });
-            }
-          });
-          break;
-        case ConnectionStatus.connecting:
-        // 在"connecting"状态时加入延迟
-
-        //_currentStatus = ConnectionStatus.stopped;
-          break;
-        case ConnectionStatus.stopped:
-          _currentStatus = ConnectionStatus.disconnected;
-          break;
-      }
-      _updateImage();
-    });
-  }
-
   @override
   void didUpdateWidget(ConnectionWidget oldWidget) {
     super.didUpdateWidget(oldWidget);
-    _updateImage();
+    if (oldWidget.status != widget.status) {
+      _updateImage();
+    }
   }
 
   void _updateImage() {
-    switch (_currentStatus) {
+    switch (widget.status) {
       case ConnectionStatus.disconnected:
         currentImage = 'assets/images/main/disconnected.gif';
         break;
@@ -76,18 +52,20 @@ class _ConnectionWidgetState extends State<ConnectionWidget> {
     return Stack(
       alignment: Alignment.center,
       children: <Widget>[
-        // Gif作为背景
-        Image.asset(currentImage, fit: BoxFit.cover,width: 250 ,height: 250,),
-
-        // 圆形透明按钮
+        Image.asset(
+          currentImage,
+          fit: BoxFit.cover,
+          width: 250,
+          height: 250,
+        ),
         ClipOval(
           child: Material(
             color: Colors.transparent,
             child: GestureDetector(
-              onTap: _updateStatus,
+              onTap: widget.onTap,  // 使用外部传入的点击函数
               child: Container(
-                width: 100,  // 按钮宽度
-                height: 100, // 按钮高度
+                width: 100,
+                height: 100,
                 decoration: const BoxDecoration(
                   shape: BoxShape.circle,
                   color: Colors.transparent,
@@ -98,8 +76,5 @@ class _ConnectionWidgetState extends State<ConnectionWidget> {
         ),
       ],
     );
-
-
-
   }
-}
+}

+ 82 - 46
lib/app/data/model/NodeMode.dart

@@ -1,51 +1,87 @@
 import 'package:dart_json_mapper/dart_json_mapper.dart';
 
+import 'package:dart_json_mapper/dart_json_mapper.dart';
+
 @jsonSerializable
 class NodeMode {
-  int? id;
-  String? name;
-  String? host;
-  String? group;
-  String? type;
-  int? port;
-  String? uuid;
-  String? method;
-  int? v2AlterId;
-  String? v2Net;
-  String? v2Type;
-  String? v2Host;
-  String? v2Path;
-  String? v2Tls;
-  String? v2Sni;
-  int? udp;
-  int? vless;
-  String? vlessPulkey;
-  String? ip;
-  @JsonProperty(name: "online_users")
-  int? onlineUsers;
-  @JsonProperty(name: "country_code")
-  String? countryCode;
-  NodeMode(
-      {this.id,
-        this.name,
-        this.host,
-        this.group,
-        this.type,
-        this.port,
-        this.uuid,
-        this.method,
-        this.v2AlterId,
-        this.v2Net,
-        this.v2Type,
-        this.v2Host,
-        this.v2Path,
-        this.v2Tls,
-        this.v2Sni,
-        this.udp,
-        this.vless,
-        this.vlessPulkey,
-        this.ip,
-        this.onlineUsers,
-        this.countryCode,
-      });
+  final int id;
+  final String name;
+  final String host;
+  final String group;
+  final String type;
+  final int port;
+  final String? passwd;
+  final String? sni;
+  final int udp;
+  final String? ip;
+
+  @JsonProperty(name: 'online_users')
+  final int onlineUsers;
+  @JsonProperty(name: 'country_code')
+  final String countryCode;
+  @JsonProperty(name: 'uuid')
+  final String? uuid;
+  final String? method;
+  @JsonProperty(name: 'v2_alter_id')
+  final int? v2AlterId;
+  @JsonProperty(name: 'v2_net')
+  final String? v2Net;
+  @JsonProperty(name: 'v2_type')
+  final String? v2Type;
+  @JsonProperty(name: 'v2_host')
+  final String? v2Host;
+  @JsonProperty(name: 'v2_path')
+  final String? v2Path;
+  @JsonProperty(name: 'v2_tls')
+  final String? v2Tls;
+  @JsonProperty(name: 'v2_sni')
+  final String? v2Sni;
+  final int? vless;
+  @JsonProperty(name: 'vless_pulkey')
+  final String? vlessPulkey;
+
+  NodeMode({
+    required this.id,
+    required this.name,
+    required this.host,
+    required this.group,
+    required this.type,
+    required this.port,
+    this.passwd,
+    this.sni,
+    required this.udp,
+    this.ip,
+    required this.onlineUsers,
+    required this.countryCode,
+    this.uuid,
+    this.method,
+    this.v2AlterId,
+    this.v2Net,
+    this.v2Type,
+    this.v2Host,
+    this.v2Path,
+    this.v2Tls,
+    this.v2Sni,
+    this.vless,
+    this.vlessPulkey,
+  });
 }
+
+
+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 'ss':
+      return '''- { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }''';
+    case 'v2ray':
+      final type = (node.v2Type == 'v2ray' && 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 '';
+  }
+}

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

@@ -1,5 +1,6 @@
 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';
@@ -11,4 +12,7 @@ class GlobalController extends GetxController {
   Future<void> fetchNodes() async {
     nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
   }
+
+
+
 }

+ 25 - 0
lib/app/main_screen.dart

@@ -0,0 +1,25 @@
+import 'package:flutter/cupertino.dart';
+import 'package:tray_manager/tray_manager.dart';
+import 'package:window_manager/window_manager.dart';
+
+class MainScreen extends StatefulWidget {
+  const MainScreen({Key? key}) : super(key: key);
+
+  @override
+  State<MainScreen> createState() => _MainScreenState();
+}
+
+class _MainScreenState extends State<MainScreen>
+    with
+        WindowListener,
+        TrayListener{
+
+    @override
+  Widget build(BuildContext context) {
+    // TODO: implement build
+    return const Text("text");
+  }
+
+
+
+}

+ 31 - 0
lib/app/modules/home/controllers/home_controller.dart

@@ -7,6 +7,7 @@ import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
 import '../../../common/LogHelper.dart';
 import '../../../common/SharedPreferencesUtil.dart';
+import '../../../component/connection_status.dart';
 import '../../../data/model/LocalUser.dart';
 import '../../../data/model/NodeMode.dart';
 import '../../../data/model/SysConfig.dart';
@@ -32,6 +33,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
   var userMode = User().obs;
   var errorMsg = ''.obs;
   var selectNode = '选择节点'.obs;
+  var connectStatus = Rx<ConnectionStatus>(ConnectionStatus.disconnected);
   var nodeModes = <NodeMode>[];
   late final GlobalController globalController ;
 
@@ -55,6 +57,33 @@ class HomeController extends GetxController with TrayListener,WindowListener {
     }
     LogHelper().d("${imageMap[type]} tapped as ${type.toString().split('.').last}");
   }
+  void updateStatus(ConnectionStatus newStatus) {
+    connectStatus.value = newStatus;
+  }
+
+  void SetSysProxy() async{
+
+    if(connectStatus.value == ConnectionStatus.stopped){
+      updateStatus(ConnectionStatus.disconnected);
+      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);
+    });
+  }
+
+
 
   Future<void> fetchSysConfig() async {
     try {
@@ -74,6 +103,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
       isLoading.value = true;
       userMode.value = await ApiService().userinfo("/api/client/v4/userinfo");
       await globalController.fetchNodes();
+
     } catch (e) {
       errorMsg.value = e.toString();
     } finally {
@@ -85,6 +115,7 @@ class HomeController extends GetxController with TrayListener,WindowListener {
     try {
       isLoading.value = true;
       await globalController.fetchNodes();
+
     } catch (e) {
       errorMsg.value = e.toString();
     } finally {

+ 33 - 19
lib/app/modules/home/views/home_view.dart

@@ -31,7 +31,7 @@ class HomeView extends GetView<HomeController> {
       ),
       child: Scaffold(
           backgroundColor: Colors.transparent,
-          appBar: SysAppBar(title: Text("首页"),actions: [
+          appBar: SysAppBar(title: Text("首页"), actions: [
             Row(
               children: [
                 IconButton(
@@ -46,25 +46,32 @@ class HomeView extends GetView<HomeController> {
           ],),
 
           body: Obx(() {
-            if(controller.errorMsg.isNotEmpty){
+            if (controller.errorMsg.isNotEmpty) {
               WidgetsBinding.instance.addPostFrameCallback((_) {
                 ScaffoldMessenger.of(context).showSnackBar(
                     SnackBar(content: Text(controller.errorMsg.value))
                 );
               });
             }
-            return controller.isLoading.value ? const Center(child: CircularProgressIndicator()) : Column(
+            return controller.isLoading.value ? const Center(
+                child: CircularProgressIndicator()) : Column(
               children: [
                 // 错误消息展示
 
 
-                UserStatusWidget(isActive: controller.GetEnable(),isLoading: controller.isLoading.value,username:controller.GetUserName(),expiryDate: controller.GetExpiredAt(),userTraffic:controller.GetTraffic(),onRefresh: () async {
-                  // 这里插入刷新操作代码
-                  //await Future.delayed(Duration(seconds: 2));
-                  if(controller.isLoading.value != true){
-                    controller.fetchUserinfo();
-                  }
-                },),
+                UserStatusWidget(
+                  isActive: controller.GetEnable(),
+                  isLoading: controller.isLoading.value,
+                  username: controller.GetUserName(),
+                  expiryDate: controller.GetExpiredAt(),
+                  userTraffic: controller.GetTraffic(),
+                  onRefresh: () async {
+                    // 这里插入刷新操作代码
+                    //await Future.delayed(Duration(seconds: 2));
+                    if (controller.isLoading.value != true) {
+                      controller.fetchUserinfo();
+                    }
+                  },),
                 Padding(
                     padding: const EdgeInsets.fromLTRB(20, 20, 0, 0),
                     child: Row(
@@ -98,10 +105,12 @@ class HomeView extends GetView<HomeController> {
                     ],
                   ),
                 ),
-                ConnectionWidget(
-                  status: ConnectionStatus.disconnected, onStatusChange: (con) {
-
-                },),
+                Obx(() {
+                  return ConnectionWidget(
+                    status: controller.connectStatus.value, onTap: () {
+                          controller.SetSysProxy();
+                  },);
+                }),
                 Padding(
                   padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
                   child: SizedBox(
@@ -109,7 +118,7 @@ class HomeView extends GetView<HomeController> {
                     height: 40,
                     child: ElevatedButton(
                       onPressed: () {
-                           controller.RouteNode();
+                        controller.RouteNode();
                       },
                       style: ElevatedButton.styleFrom(
                         primary: Colors.white, // 设置背景颜色为白色
@@ -139,7 +148,10 @@ class UserStatusWidget extends StatefulWidget {
   final String username;
   final String userTraffic;
   final String expiryDate;
-  const UserStatusWidget({Key? key, required this.isActive, required this.isLoading,required this.username, required this.userTraffic, required this.expiryDate, required this.onRefresh}) : super(key: key);
+
+  const UserStatusWidget(
+      {Key? key, required this.isActive, required this.isLoading, required this.username, required this.userTraffic, required this.expiryDate, required this.onRefresh})
+      : super(key: key);
 
   @override
   _UserStatusWidgetState createState() => _UserStatusWidgetState();
@@ -178,17 +190,19 @@ class _UserStatusWidgetState extends State<UserStatusWidget> {
                     ),
                     const SizedBox(width: 10,),
                     IconButton(
-                      icon: widget.isLoading ? const CircularProgressIndicator() : Image.asset("assets/images/main/refresh.png"),
+                      icon: widget.isLoading
+                          ? const CircularProgressIndicator()
+                          : Image.asset("assets/images/main/refresh.png"),
                       onPressed: () {
                         // 刷新操作
-                        if(!widget.isLoading){
+                        if (!widget.isLoading) {
                           widget.onRefresh();
                         }
                       },
                     )
                   ],
                 ),
-                 Text(
+                Text(
                   widget.expiryDate,
                   style: const TextStyle(
                     fontSize: 8.0, // 设置字体大小为20像素

+ 173 - 28
lib/app/service/clash_service.dart

@@ -3,6 +3,7 @@ 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';
@@ -18,6 +19,7 @@ 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
@@ -38,6 +40,9 @@ class ClashService extends GetxService with TrayListener {
   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";
@@ -78,9 +83,9 @@ class ClashService extends GetxService with TrayListener {
     _clashDirectory = await getApplicationSupportDirectory();
 
     final _ = SpUtil.getData('yaml', defValue: currentYaml.value);
-    initializedHttpPort = SpUtil.getData('http-port', defValue: 12346);
-    initializedSockPort = SpUtil.getData('socks-port', defValue: 12347);
-    initializedMixedPort = SpUtil.getData('mixed-port', defValue: 12348);
+    initializedHttpPort = SpUtil.getData('http-port', defValue: 7899);
+    initializedSockPort = SpUtil.getData('socks-port', defValue: 7877);
+    initializedMixedPort = SpUtil.getData('mixed-port', defValue: 7811);
     currentYaml.value = _;
     Request.setBaseUrl(clashBaseUrl);
     final clashConfigPath = p.join(_clashDirectory.path, "clash");
@@ -100,6 +105,23 @@ class ClashService extends GetxService with TrayListener {
     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);
@@ -169,6 +191,78 @@ class ClashService extends GetxService with TrayListener {
     // 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();
@@ -185,31 +279,31 @@ class ClashService extends GetxService with TrayListener {
     //   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
+    // 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();
-    }
+    // checkPort();
+    // if (isSystemProxy()) {
+    //   setSystemProxy();
+    // }
   }
 
   @override
@@ -357,7 +451,7 @@ class ClashService extends GetxService with TrayListener {
   void getProxies() {
     final proxiesPtr = clashFFI.get_proxies().cast<Utf8>();
     proxies.value = json.decode(proxiesPtr.toDartString());
-    // malloc.free(proxiesPtr);
+
   }
 
   bool isSystemProxy() {
@@ -514,7 +608,45 @@ class ClashService extends GetxService with TrayListener {
       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 {
@@ -536,6 +668,19 @@ class ClashService extends GetxService with TrayListener {
     }
     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 {

+ 8 - 4
lib/main.dart

@@ -3,10 +3,12 @@ import 'dart:ui';
 
 
 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/main_screen.dart';
 import 'package:proxy_manager/proxy_manager.dart';
 import 'package:tray_manager/tray_manager.dart';
 import 'package:window_manager/window_manager.dart';
@@ -54,7 +56,6 @@ void main() async {
       title: "Application",
       initialRoute: AppPages.INITIAL,
       getPages: AppPages.routes,
-
     ),
   );
 
@@ -79,9 +80,12 @@ Future<void> initWindow() async {
   );
   windowManager.waitUntilReadyToShow(opts, () {
     // hide window when start
-    // if (Get.find<ClashService>().isHideWindowWhenStart() && kReleaseMode) {
-    //   windowManager.hide();
-    // }
+    if (Get.find<ClashService>().isHideWindowWhenStart() && kReleaseMode) {
+      windowManager.hide();
+    } else {
+      windowManager.show();
+      windowManager.focus();
+    }
     //windowManager.show();
     // windowManager.focus();
   });

+ 14 - 14
macos/Runner/AppDelegate.swift

@@ -7,19 +7,19 @@ class AppDelegate: FlutterAppDelegate {
     return false
   }
 
-    override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool)
-      -> Bool
-    {
-      if !flag {
-        for window in NSApp.windows {
-          if !window.isVisible {
-            window.setIsVisible(true)
-          }
-          window.makeKeyAndOrderFront(self)
-          NSApp.activate(ignoringOtherApps: true)
-        }
-      }
-      return true
-    }
+//     override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool)
+//       -> Bool
+//     {
+//       if !flag {
+//         for window in NSApp.windows {
+//           if !window.isVisible {
+//             window.setIsVisible(true)
+//           }
+//           window.makeKeyAndOrderFront(self)
+//           NSApp.activate(ignoringOtherApps: true)
+//         }
+//       }
+//       return true
+//     }
 
 }

+ 3 - 1
pubspec.yaml

@@ -27,12 +27,14 @@ dependencies:
   path_provider: ^2.0.11
   kommon: ^0.4.1
 
-dev_dependencies: 
+dev_dependencies:
+
   flutter_lints: ^2.0.0
   flutter_test: 
     sdk: flutter
   ffigen: any
   build_runner: ^2.4.4
+  test: ^1.24.3
 flutter:
   assets:
     - assets/tp/clash/

+ 72 - 0
test/node_mode_test.dart

@@ -0,0 +1,72 @@
+
+import 'package:naiyouwl/app/data/model/NodeMode.dart';
+import 'package:test/test.dart';
+import 'package:dart_json_mapper/dart_json_mapper.dart';
+import 'package:naiyouwl/main.dart';
+void main() {
+  group('NodeMode tests', () {
+    test('Convert JSON to YAML', () {
+      var jsonData = '''[
+        {
+            "id": 908,
+            "name": "香港原生61D",
+            "host": "soca01.top",
+            "group": "用户中心",
+            "type": "trojan",
+            "port": 443,
+            "passwd": "VMhGp5wEIyCDf90T",
+            "sni": "",
+            "udp": 1,
+            "ip": null,
+            "online_users": 174,
+            "country_code": "hk"
+        },
+        {
+            "id": 871,
+            "name": "马来西亚02",
+            "host": "ncyidong.ip8000.top",
+            "group": "用户中心",
+            "type": "v2ray",
+            "port": 29694,
+            "uuid": "459b4a80-bd61-4ecd-a26b-e9c1809d9e45",
+            "method": "auto",
+            "v2_alter_id": 0,
+            "v2_net": "tcp",
+            "v2_type": "none",
+            "v2_host": "",
+            "v2_path": "/xej8pandp2augugy",
+            "v2_tls": "",
+            "v2_sni": "king-new04.xyz",
+            "udp": 1,
+            "vless": 1,
+            "vless_pulkey": "qhTzYYIgBzDLNYR79oxftqdo1kzL-1_hGJKfqrOliCY",
+            "ip": "38.60.194.62",
+            "online_users": 10,
+            "country_code": "fr"
+        },
+
+        {
+            "id": 714,
+            "name": "阿根廷02",
+            "host": "ncyidong.ip8000.top",
+            "group": "用户中心",
+            "type": "shadowsocks",
+            "method": "aes-128-gcm",
+            "udp": 1,
+            "port": 15464,
+            "passwd": "VMhGp5wEIyCDf90T",
+            "ip": "38.54.45.152",
+            "online_users": 18,
+            "country_code": "au"
+        }
+        ...
+      ]'''; // 这里简化为省略号。请用你提供的完整JSON替换。
+
+      var nodes = JsonMapper.deserialize<List<NodeMode>>(jsonData)!;
+      var proxies = nodes.map(nodeToYaml).toList();
+
+      // 这里我们只检查转换后的YAML是否为空,你可以根据需要添加更多具体的测试条件。
+      expect(proxies, isNotEmpty);
+    });
+  });
+}

+ 0 - 30
test/widget_test.dart

@@ -1,30 +0,0 @@
-// This is a basic Flutter widget test.
-//
-// To perform an interaction with a widget in your test, use the WidgetTester
-// utility in the flutter_test package. For example, you can send tap and scroll
-// gestures. You can also use WidgetTester to find child widgets in the widget
-// tree, read text, and verify that the values of widget properties are correct.
-
-import 'package:flutter/material.dart';
-import 'package:flutter_test/flutter_test.dart';
-
-import 'package:naiyouwl/main.dart';
-
-void main() {
-  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
-    // Build our app and trigger a frame.
-    //await tester.pumpWidget(const MyApp());
-
-    // Verify that our counter starts at 0.
-    expect(find.text('0'), findsOneWidget);
-    expect(find.text('1'), findsNothing);
-
-    // Tap the '+' icon and trigger a frame.
-    await tester.tap(find.byIcon(Icons.add));
-    await tester.pump();
-
-    // Verify that our counter has incremented.
-    expect(find.text('0'), findsNothing);
-    expect(find.text('1'), findsOneWidget);
-  });
-}

Some files were not shown because too many files changed in this diff