alroyso 1 year ago
parent
commit
98e694c157

+ 2 - 2
lib/app/data/model/NodeMode.dart

@@ -20,7 +20,6 @@ class nodeMode {
   String? ip;
   int? onlineUsers;
   String? countryCode;
-
   nodeMode(
       {this.id,
         this.name,
@@ -42,7 +41,8 @@ class nodeMode {
         this.vlessPulkey,
         this.ip,
         this.onlineUsers,
-        this.countryCode});
+        this.countryCode,
+      });
 
   nodeMode.fromJson(Map<String, dynamic> json) {
     id = json['id'];

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

@@ -0,0 +1,25 @@
+import 'package:get/get.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 {
+    try {
+      isLoading.value = true;
+      nodeModes.clear();
+      nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
+      LogHelper().d(nodeModes.toList());
+      //loadSelectedNode();
+    } catch (e) {
+      errorMsg.value = e.toString();
+    } finally {
+      isLoading.value = false;
+    }
+  }
+
+}

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

@@ -3,8 +3,10 @@ import 'package:get/get.dart';
 import '../../../common/LogHelper.dart';
 import '../../../common/SharedPreferencesUtil.dart';
 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';
 
@@ -24,6 +26,9 @@ class HomeController extends GetxController {
   var userMode = User().obs;
   var errorMsg = ''.obs;
   var selectNode = '选择节点'.obs;
+  var nodeModes = <nodeMode>[];
+  late final GlobalController globalController ;
+
 
   final Map<ImageType, String> imageMap = {
     ImageType.CUSTOMER: "images/main/customer.png",
@@ -45,6 +50,24 @@ class HomeController extends GetxController {
     LogHelper().d("${imageMap[type]} tapped as ${type.toString().split('.').last}");
   }
 
+  // Future<void> fetchNodes() async {
+  //   try {
+  //     isLoading.value = true;
+  //     nodeModes.clear();
+  //     nodeModes = await ApiService().getNode("/api/client/v4/nodes?vless=1");
+  //     final globalController = Get.put(GlobalController());
+  //
+  //     globalController.nodeModes.value = nodeModes;
+  //
+  //     LogHelper().d(nodeModes.toList());
+  //     //loadSelectedNode();
+  //   } catch (e) {
+  //     errorMsg.value = e.toString();
+  //   } finally {
+  //     isLoading.value = false;
+  //   }
+  // }
+
   Future<void> fetchSysConfig() async {
     try {
       Map<String, dynamic>? data  = await SharedPreferencesUtil().getObject("sysconfig");
@@ -52,6 +75,7 @@ class HomeController extends GetxController {
         sysConfig.value = SysConfig.fromJson(data);
         LogHelper().d(sysConfig.value.toJson());
       }
+      globalController.fetchNodes();
     } catch (e) {
       errorMsg.value = e.toString();
     } finally {
@@ -88,6 +112,7 @@ class HomeController extends GetxController {
   @override
   void onInit() {
     super.onInit();
+    globalController = Get.put(GlobalController());
     fetchSysConfig();
     fetchLocalUser();
     fetchUserinfo();

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

@@ -9,6 +9,7 @@ import 'package:window_manager/window_manager.dart';
 import '../../../component/connection_status.dart';
 import '../../../component/connection_widget.dart';
 
+import '../../../global_controller/GlobalController.dart';
 import '../controllers/home_controller.dart';
 
 
@@ -30,7 +31,8 @@ class HomeView extends GetView<HomeController> {
           appBar: const SysAppBar(title: Text("首页"),),
 
           body: Obx(() {
-            return Column(
+
+            return controller.globalController.isLoading.value ? const CircularProgressIndicator() : Column(
               children: [
                 UserStatusWidget(isActive: controller.GetEnable(),isLoading: controller.isLoading.value,username:controller.GetUserName(),expiryDate: controller.GetExpiredAt(),userTraffic:controller.GetTraffic(),onRefresh: () async {
                   // 这里插入刷新操作代码

+ 167 - 12
lib/app/modules/node/controllers/node_controller.dart

@@ -1,36 +1,191 @@
+import 'dart:io';
+
 import 'package:get/get.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,
+  LeastUsers,
+  SelectedFirst,
+}
 
 class NodeController extends GetxController {
   //TODO: Implement NodeController
 
   final count = 0.obs;
   var isLoading = false.obs;
-  var nodeModes = <nodeMode>[].obs;
+  var nodesToShow = <nodeMode>[].obs;
+  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();       // 通知观察者
+    }
+  }
+  void pingAllNodes() async {
+    for (var node in nodeModes) {
+       tcpPing(node);  // 这里用了 await 使其串行 ping,若希望并行可移除 await
+    }
+  }
 
-  Future<void> fetchNodes() async {
-    try {
-      isLoading.value = true;
-      nodeModes.clear();
-      nodeModes.value = await ApiService().getNode("/api/client/v4/nodes?vless=1");
-      LogHelper().d(nodeModes.toList());
-    } catch (e) {
-      errorMsg.value = e.toString();
-    } finally {
-      isLoading.value = false;
+  Future<void> pingSingleNode(nodeMode node) async {
+
+    await tcpPing(node);
+
+  }
+
+
+  void selectNode(nodeMode node) {
+    selectedNode.value = node;
+    _storeSelectedNode(node);
+    //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!);
+  }
+
+  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); //选择人数最小的
+  }
+
+  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);
+      }
+    }
+  }
+
+  void filterNodesWithLeastUsersInHK() {
+    displayStrategy.value = NodeDisplayStrategy.LeastUsers;
+    updateNodesToDisplay();
+  }
+
+
+
+  void filterNodesByRegion(String region) {
+    nodesToShow.value = nodeModes.where((node) => node.countryCode == region).toList();
+    updateNodesToDisplay();
+  }
+
+  void showAllNodes() {
+    displayStrategy.value = NodeDisplayStrategy.All;
+    updateNodesToDisplay();
+   // nodesToShow.value = List.from(nodeModes);
+  }
+  void showSelectedFirst() {
+    displayStrategy.value = NodeDisplayStrategy.SelectedFirst;
+    updateNodesToDisplay();
+    // 如果需要,可以添加其他逻辑。
+  }
+
+  void updateNodesToDisplay() {
+    List<nodeMode> nodes = List.from(nodeModes);
+
+    switch (displayStrategy.value) {
+      case NodeDisplayStrategy.LeastUsers:
+        List<nodeMode> filteredNodes = nodeModes.where((node) => node.countryCode == "hk" && node.onlineUsers != null).toList();
+
+        if (filteredNodes.isNotEmpty) {
+          int? minUsers = filteredNodes.map((node) => node.onlineUsers!).reduce((curr, next) => curr < next ? curr : next);
+          nodes = nodes.where((node) => node.onlineUsers == minUsers && node.countryCode == "hk").toList();
+          selectNode(nodes.first);
+        } else {
+          // Handle the case where no nodes match the criteria. Maybe display a message or use a fallback.
+          nodes = []; // Empty list or some fallback
+        }
+
+        break;
+      case NodeDisplayStrategy.SelectedFirst:
+
+        break;
+      case NodeDisplayStrategy.All:
+      // // 将选中的节点移动到前面
+      //   if (selectedNode.value != null) {
+      //     nodes.removeWhere((node) => node.id == selectedNode.value?.id);
+      //     nodes.insert(0, selectedNode.value!);
+      //   }
+        break;
+      default:
+      // 不需要额外的处理,因为nodes已经包含所有节点。
+        break;
+    }
+
+    nodesToShow.value = nodes; // 使用 assignAll() 方法触发更新
+  }
+
 
   @override
   void onInit() {
     super.onInit();
-    fetchNodes();
+    //fetchNodes();
+
+    globalController = Get.put(GlobalController());
+    nodeModes =  globalController.nodeModes;
+    nodesToShow.value = nodeModes;
   }
 
   @override

+ 83 - 24
lib/app/modules/node/views/node_view.dart

@@ -10,7 +10,6 @@ class NodeView extends GetView<NodeController> {
 
   @override
   Widget build(BuildContext context) {
-
     final _refreshIndicatorKey = GlobalKey<RefreshIndicatorState>();
 
 
@@ -23,8 +22,22 @@ class NodeView extends GetView<NodeController> {
       ),
       child: Scaffold(
           backgroundColor: Colors.transparent,
-          appBar: const SysAppBar(
-            title: Text("节点列表"), toolbarHeight: kToolbarHeight + 40,),
+          appBar: SysAppBar(
+            title: Text("节点列表"),
+            toolbarHeight: kToolbarHeight + 40,
+            actions: [
+              Row(
+                children: [
+                  IconButton(
+                    icon: Icon(Icons.refresh),
+                    onPressed: () {
+                      controller.pingAllNodes();
+                    },
+                  ),
+                  Text('Ping All'), // 这里是您的标题
+                ],
+              ),
+            ],),
           body: Container(
             width: 376,
             height: 600,
@@ -54,34 +67,80 @@ class NodeView extends GetView<NodeController> {
                     }
                     return const SizedBox.shrink();
                   }),
-
+                  Padding(
+                    padding: const EdgeInsets.fromLTRB(0, 0, 0, 10),
+                    child: Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceAround,
+                      children: [
+                        ElevatedButton(
+                          onPressed: () => controller.filterNodesWithLeastUsersInHK(),
+                          child: Text("人数最少"),
+                        ),
+                        ElevatedButton(
+                          onPressed: () => controller.showAllNodes(),
+                          child: Text("显示所有节点"),
+                        ),
+                        // ElevatedButton(
+                        //   onPressed: () => controller.showSelectedFirst(),
+                        //   child: Text("选中的在前"),
+                        // ),
+                        // 你可以在这里添加更多的按钮
+                      ],
+                    ),
+                  ),
                   Expanded(
                     child: Obx(() {
                       return RefreshIndicator(
                         key: _refreshIndicatorKey,
-                        onRefresh: controller.fetchNodes,
+                        onRefresh: controller.globalController.fetchNodes,
                         child: ListView.builder(
-                          itemCount: controller.nodeModes.length,
+                          itemCount: controller.nodesToShow.length,
                           itemBuilder: (BuildContext context, int index) {
-                            final node = controller.nodeModes[index];
+                            final node = controller.nodesToShow[index];
+
+                            return Obx(() {
+                              // print(controller.selectedNode.value?.id);
+                              // print(
+                              //     controller.selectedNode.value?.id == node.id);
+                              // //  controller.nodeModes[controller.selectedIndex.value]
+                              // print("node ---- ${node.id} index ---- $index");
+                              bool isNodeLoading = controller.isLoadingMap[node
+                                  .id!] ?? false;
+                              var pingResult = controller.pingResults[node
+                                  .id] ??
+                                  '';
+                              return Container(
+                                color: controller.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}'),
+                                  trailing: Row(
+                                    mainAxisSize: MainAxisSize.min,
+                                    children: [
+                                      if (isNodeLoading)
+                                        const CircularProgressIndicator(),
+                                      if (!isNodeLoading) ...[
+                                        Text(pingResult),
+                                        ElevatedButton(
+                                          onPressed: () {
+                                            controller.pingSingleNode(node);
+                                          },
+                                          child: Text('测速'),
+                                        ),
+                                      ],
 
-                            return InkWell(
-                                onTap: () {
-                                  print("Item at index $index was tapped.");
-                                  controller.selectedIndex.value = index;
-                                },
-                                child: Obx(() {
-                                  final color = controller.selectedIndex.value == index
-                                      ? Colors.blueAccent
-                                      : null;
-                                  return Container(
-                                    color: color,
-                                    child: ListTile(
-                                      title: Text(node.name.toString()),
-                                    ),
-                                  );
-                                })
-                            );
+                                    ],
+                                  ),
+                                  onTap: () {
+                                    controller.selectNode(node);
+                                  },
+                                ),
+                              );
+                            });
                           },
                         ),
                       );