|
@@ -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 "";
|
|
|
-}
|