clash_service.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688
  1. import 'dart:async';
  2. import 'dart:convert';
  3. import 'dart:ffi' as ffi;
  4. import 'dart:io';
  5. import 'dart:isolate';
  6. import 'package:dio/dio.dart';
  7. import 'package:naiyouwl/main.dart';
  8. import 'package:dart_json_mapper/dart_json_mapper.dart';
  9. import 'package:ffi/ffi.dart';
  10. import 'package:flutter/foundation.dart';
  11. import 'package:flutter/services.dart';
  12. import 'package:path/path.dart';
  13. import 'package:path/path.dart' as p;
  14. import 'package:get/get.dart';
  15. import 'package:kommon/request/request.dart';
  16. import 'package:kommon/tool/sp_util.dart';
  17. import 'package:naiyouwl/clash_generated_bindings.dart';
  18. import 'package:path_provider/path_provider.dart';
  19. import 'package:proxy_manager/proxy_manager.dart';
  20. import 'package:tray_manager/tray_manager.dart';
  21. import '../bean/clash_config_entity.dart';
  22. import '../data/model/NodeMode.dart';
  23. late NativeLibrary clashFFI;
  24. //android 或者ios
  25. //const mobileChannel = MethodChannel("xxxPlugin");
  26. class ClashService extends GetxService with TrayListener {
  27. // 需要一起改端口
  28. static const clashBaseUrl = "http://127.0.0.1:$clashExtPort";
  29. static const clashExtPort = 22346;
  30. // 运行时
  31. late Directory _clashDirectory;
  32. RandomAccessFile? _clashLock;
  33. // 流量
  34. final uploadRate = 0.0.obs;
  35. final downRate = 0.0.obs;
  36. final yamlConfigs = RxSet<FileSystemEntity>();
  37. final currentYaml = 'config.yaml'.obs;
  38. final proxyStatus = RxMap<String, int>();
  39. final proxyYamlCurrent = "".obs;
  40. final proxyYaml = 'proxy.yaml'.obs;
  41. // action
  42. static const ACTION_SET_SYSTEM_PROXY = "assr";
  43. static const ACTION_UNSET_SYSTEM_PROXY = "ausr";
  44. static const MAX_ENTRIES = 5;
  45. // default port
  46. static var initializedHttpPort = 0;
  47. static var initializedSockPort = 0;
  48. static var initializedMixedPort = 0;
  49. // config
  50. Rx<ClashConfigEntity?> configEntity = Rx(null);
  51. // log
  52. Stream<dynamic>? logStream;
  53. RxMap<String, dynamic> proxies = RxMap();
  54. RxBool isSystemProxyObs = RxBool(false);
  55. ClashService() {
  56. // load lib
  57. var fullPath = "";
  58. if (Platform.isWindows) {
  59. fullPath = "libclash.dll";
  60. } else if (Platform.isMacOS) {
  61. fullPath = "libclash.dylib";
  62. } else {
  63. fullPath = "libclash.so";
  64. }
  65. final lib = ffi.DynamicLibrary.open(fullPath);
  66. clashFFI = NativeLibrary(lib);
  67. clashFFI.init_native_api_bridge(ffi.NativeApi.initializeApiDLData);
  68. }
  69. Future<ClashService> init() async {
  70. _clashDirectory = await getApplicationSupportDirectory();
  71. final _ = SpUtil.getData('yaml', defValue: currentYaml.value);
  72. initializedHttpPort = SpUtil.getData('http-port', defValue: 7899);
  73. initializedSockPort = SpUtil.getData('socks-port', defValue: 7877);
  74. initializedMixedPort = SpUtil.getData('mixed-port', defValue: 7811);
  75. currentYaml.value = _;
  76. Request.setBaseUrl(clashBaseUrl);
  77. final clashConfigPath = p.join(_clashDirectory.path, "clash");
  78. _clashDirectory = Directory(clashConfigPath);
  79. if (kDebugMode) {
  80. print("fclash work directory: ${_clashDirectory.path}");
  81. }
  82. final clashConf = p.join(_clashDirectory.path, currentYaml.value);
  83. final countryMMdb = p.join(_clashDirectory.path, 'Country.mmdb');
  84. if (!await _clashDirectory.exists()) {
  85. await _clashDirectory.create(recursive: true);
  86. }
  87. // copy executable to directory
  88. final mmdb = await rootBundle.load('assets/tp/clash/Country.mmdb');
  89. // write to clash dir
  90. final mmdbF = File(countryMMdb);
  91. if (!mmdbF.existsSync()) {
  92. await mmdbF.writeAsBytes(mmdb.buffer.asInt8List());
  93. }
  94. final countryGeoIP= p.join(_clashDirectory.path, 'geoip.dat');
  95. final geoip = await rootBundle.load('assets/tp/clash/geoip.dat');
  96. // write to clash dir
  97. final geoipF = File(countryGeoIP);
  98. if (!geoipF.existsSync()) {
  99. await geoipF.writeAsBytes(geoip.buffer.asInt8List());
  100. }
  101. final countryGeoSite= p.join(_clashDirectory.path, 'geosite.dat');
  102. final geoSite = await rootBundle.load('assets/tp/clash/geoip.dat');
  103. // write to clash dir
  104. final geoSiteF = File(countryGeoSite);
  105. if (!geoSiteF.existsSync()) {
  106. await geoSiteF.writeAsBytes(geoSite.buffer.asInt8List());
  107. }
  108. final config = await rootBundle.load('assets/tp/clash/config.yaml');
  109. // write to clash dir
  110. final configF = File(clashConf);
  111. if (!configF.existsSync()) {
  112. await configF.writeAsBytes(config.buffer.asInt8List());
  113. }
  114. // create or detect lock file
  115. await _acquireLock(_clashDirectory);
  116. // ffi
  117. clashFFI.set_home_dir(_clashDirectory.path.toNativeUtf8().cast());
  118. clashFFI.clash_init(_clashDirectory.path.toNativeUtf8().cast());
  119. clashFFI.set_config(clashConf.toNativeUtf8().cast());
  120. clashFFI.set_ext_controller(clashExtPort);
  121. if (clashFFI.parse_options() == 0) {
  122. Get.printInfo(info: "parse ok");
  123. }
  124. Future.delayed(Duration.zero, () {
  125. initDaemon();
  126. });
  127. // tray show issue
  128. if (isDesktop) {
  129. trayManager.addListener(this);
  130. }
  131. // wait getx initialize
  132. // Future.delayed(const Duration(seconds: 3), () {
  133. // if (!Platform.isWindows) {
  134. // Get.find<NotificationService>()
  135. // .showNotification("Fclash", "Is running".tr);
  136. // }
  137. // });
  138. return this;
  139. }
  140. void getConfigs() {
  141. yamlConfigs.clear();
  142. final entities = _clashDirectory.listSync();
  143. for (final entity in entities) {
  144. if (entity.path.toLowerCase().endsWith('.yaml') &&
  145. !yamlConfigs.contains(entity)) {
  146. yamlConfigs.add(entity);
  147. Get.printInfo(info: 'detected: ${entity.path}');
  148. }
  149. }
  150. }
  151. Map<String, dynamic> getConnections() {
  152. final connsPtr = clashFFI.get_all_connections().cast<Utf8>();
  153. String connections = connsPtr.toDartString();
  154. // malloc.free(connsPtr);
  155. return json.decode(connections);
  156. }
  157. void closeAllConnections() {
  158. clashFFI.close_all_connections();
  159. }
  160. bool closeConnection(String connectionId) {
  161. final id = connectionId.toNativeUtf8().cast<ffi.Char>();
  162. return clashFFI.close_connection(id) == 1;
  163. }
  164. void getCurrentClashConfig() {
  165. final configPtr = clashFFI.get_configs().cast<Utf8>();
  166. final jsondata = json.decode(configPtr.toDartString());
  167. final data = JsonMapper.deserialize<ClashConfigEntity>(jsondata);
  168. configEntity.value = data;
  169. // malloc.free(configPtr);
  170. }
  171. Future<void> chageProxyConfig() async {
  172. final clashConf = p.join(_clashDirectory.path, proxyYaml.value);
  173. final f = File(clashConf);
  174. if (f.existsSync() && await changeYaml(f)) {
  175. // set subscription
  176. // await SpUtil.setData('profile_$name', url);
  177. // await reload();
  178. }
  179. }
  180. String generateRules() {
  181. return '''
  182. rules:
  183. - GEOSITE,cn,DIRECT
  184. - GEOIP,CN,DIRECT
  185. - MATCH,proxy
  186. ''';
  187. }
  188. Future<void> makeClash(List<NodeMode> nodeModes) async {
  189. var proxies = nodeModes.map(nodeToYaml).toList();
  190. var config = '''
  191. port: 7891
  192. socks-port: 7890
  193. redir-port: 7893
  194. allow-lan: true
  195. mode: rule
  196. log-level: info
  197. ipv6: false
  198. unified-delay: false
  199. geodata-mode: true
  200. tcp-concurrent: false
  201. find-process-mode: strict
  202. global-client-fingerprint: chrome
  203. external-controller: 0.0.0.0:9090
  204. proxies:
  205. \n${proxies.join('\n')}
  206. proxy-groups:
  207. - name: proxy
  208. type: select
  209. proxies:
  210. ${nodeModes.map((node) => '- ${node.name}').join('\n ')}
  211. ${generateRules()}
  212. ''';
  213. proxyYamlCurrent.value = config;
  214. final clashConf = p.join(_clashDirectory.path, proxyYaml.value);
  215. final configF = File(clashConf);
  216. await configF.writeAsBytes(utf8.encode(config));
  217. }
  218. String nodeToYaml(NodeMode node) {
  219. const prefix = ' '; // 两个空格的缩进
  220. switch (node.type) {
  221. case 'trojan':
  222. return '''$prefix- { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }''';
  223. case 'shadowsocks':
  224. return '''$prefix- { name: ${node.name}, type: ss, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }''';
  225. case 'v2ray':
  226. final type = (node.vless == 1) ? 'vless' : 'vmess';
  227. if (type == 'vless') {
  228. 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} } }''';
  229. } else {
  230. return '''$prefix- { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, cipher: ${node.method}, udp: 1 }''';
  231. }
  232. default:
  233. return '';
  234. }
  235. }
  236. Future<void> reload() async {
  237. // get configs
  238. getConfigs();
  239. getCurrentClashConfig();
  240. // proxies
  241. getProxies();
  242. updateTray();
  243. }
  244. void initDaemon() async {
  245. printInfo(info: 'init clash service');
  246. // wait for online
  247. // while (!await isRunning()) {
  248. // printInfo(info: 'waiting online status');
  249. // await Future.delayed(const Duration(milliseconds: 500));
  250. // }
  251. // get traffic
  252. // Timer.periodic(const Duration(seconds: 1), (t) {
  253. // final trafficPtr = clashFFI.get_traffic().cast<Utf8>();
  254. // final traffic = trafficPtr.toDartString();
  255. // if (kDebugMode) {
  256. // debugPrint(traffic);
  257. // }
  258. // try {
  259. // final trafficJson = jsonDecode(traffic);
  260. // uploadRate.value = trafficJson['Up'].toDouble() / 1024; // KB
  261. // downRate.value = trafficJson['Down'].toDouble() / 1024; // KB
  262. // // fix: 只有KDE不会导致Tray自动消失
  263. // // final desktop = Platform.environment['XDG_CURRENT_DESKTOP'];
  264. // // updateTray();
  265. // } catch (e) {
  266. // Get.printError(info: '$e');
  267. // }
  268. // // malloc.free(trafficPtr);
  269. // });
  270. // // system proxy
  271. // // listen port
  272. await reload();
  273. // checkPort();
  274. // if (isSystemProxy()) {
  275. // setSystemProxy();
  276. // }
  277. }
  278. @override
  279. void onClose() {
  280. closeClashDaemon();
  281. super.onClose();
  282. }
  283. Future<void> closeClashDaemon() async {
  284. Get.printInfo(info: 'fclash: closing daemon');
  285. // double check
  286. // stopClashSubP();
  287. if (isSystemProxy()) {
  288. // just clear system proxy
  289. await clearSystemProxy(permanent: false);
  290. }
  291. await _clashLock?.unlock();
  292. }
  293. Future<void> setSystemProxy() async {
  294. if (isDesktop) {
  295. if (configEntity.value != null) {
  296. final entity = configEntity.value!;
  297. if (entity.port != 0) {
  298. await Future.wait([
  299. proxyManager.setAsSystemProxy(
  300. ProxyTypes.http, '127.0.0.1', entity.port!),
  301. proxyManager.setAsSystemProxy(
  302. ProxyTypes.https, '127.0.0.1', entity.port!)
  303. ]);
  304. debugPrint("set http");
  305. }
  306. if (entity.socksPort != 0 && !Platform.isWindows) {
  307. debugPrint("set socks");
  308. await proxyManager.setAsSystemProxy(
  309. ProxyTypes.socks, '127.0.0.1', entity.socksPort!);
  310. }
  311. await setIsSystemProxy(true);
  312. }
  313. } else {
  314. if (configEntity.value != null) {
  315. final entity = configEntity.value!;
  316. if (entity.port != 0) {
  317. // await mobileChannel
  318. // .invokeMethod("SetHttpPort", {"port": entity.port});
  319. }
  320. // mobileChannel.invokeMethod("StartProxy");
  321. await setIsSystemProxy(true);
  322. }
  323. // await Clipboard.setData(
  324. // ClipboardData(text: "${configEntity.value?.port}"));
  325. // final dialog = BrnDialog(
  326. // titleText: "请手动设置代理",
  327. // messageText:
  328. // "端口号已复制。请进入已连接WiFi的详情设置,将代理设置为手动,主机名填写127.0.0.1,端口填写${configEntity.value?.port},然后返回点击已完成即可",
  329. // actionsText: ["取消", "已完成", "去设置填写"],
  330. // indexedActionCallback: (index) async {
  331. // if (index == 0) {
  332. // if (Get.isOverlaysOpen) {
  333. // Get.back();
  334. // }
  335. // } else if (index == 1) {
  336. // final proxy = await SystemProxy.getProxySettings();
  337. // if (proxy != null) {
  338. // if (proxy["host"] == "127.0.0.1" &&
  339. // int.parse(proxy["port"].toString()) ==
  340. // configEntity.value?.port) {
  341. // Future.delayed(Duration.zero, () {
  342. // if (Get.overlayContext != null) {
  343. // BrnToast.show("设置成功", Get.overlayContext!);
  344. // setIsSystemProxy(true);
  345. // }
  346. // });
  347. // if (Get.isOverlaysOpen) {
  348. // Get.back();
  349. // }
  350. // }
  351. // } else {
  352. // Future.delayed(Duration.zero, () {
  353. // if (Get.overlayContext != null) {
  354. // BrnToast.show("好像未完成设置哦", Get.overlayContext!);
  355. // }
  356. // });
  357. // }
  358. // } else {
  359. // Future.delayed(Duration.zero, () {
  360. // BrnToast.show("端口号已复制", Get.context!);
  361. // });
  362. // await OpenSettings.openWIFISetting();
  363. // }
  364. // },
  365. // );
  366. // Get.dialog(dialog);
  367. }
  368. }
  369. Future<void> clearSystemProxy({bool permanent = true}) async {
  370. if (isDesktop) {
  371. await proxyManager.cleanSystemProxy();
  372. if (permanent) {
  373. await setIsSystemProxy(false);
  374. }
  375. } else {
  376. //mobileChannel.invokeMethod("StopProxy");
  377. await setIsSystemProxy(false);
  378. // final dialog = BrnDialog(
  379. // titleText: "请手动设置代理",
  380. // messageText: "请进入已连接WiFi的详情设置,将代理设置为无",
  381. // actionsText: ["取消", "已完成", "去设置清除"],
  382. // indexedActionCallback: (index) async {
  383. // if (index == 0) {
  384. // if (Get.isOverlaysOpen) {
  385. // Get.back();
  386. // }
  387. // } else if (index == 1) {
  388. // final proxy = await SystemProxy.getProxySettings();
  389. // if (proxy != null) {
  390. // Future.delayed(Duration.zero, () {
  391. // if (Get.overlayContext != null) {
  392. // BrnToast.show("好像没有清除成功哦,当前代理${proxy}", Get.overlayContext!);
  393. // }
  394. // });
  395. // } else {
  396. // Future.delayed(Duration.zero, () {
  397. // if (Get.overlayContext != null) {
  398. // BrnToast.show("清除成功", Get.overlayContext!);
  399. // }
  400. // setIsSystemProxy(false);
  401. // if (Get.isOverlaysOpen) {
  402. // Get.back();
  403. // }
  404. // });
  405. // }
  406. // } else {
  407. // OpenSettings.openWIFISetting().then((_) async {
  408. // final proxy = await SystemProxy.getProxySettings();
  409. // debugPrint("$proxy");
  410. // });
  411. // }
  412. // },
  413. // );
  414. // Get.dialog(dialog);
  415. }
  416. }
  417. void getProxies() {
  418. final proxiesPtr = clashFFI.get_proxies().cast<Utf8>();
  419. proxies.value = json.decode(proxiesPtr.toDartString());
  420. }
  421. bool isSystemProxy() {
  422. return SpUtil.getData('system_proxy', defValue: false);
  423. }
  424. Future<bool> setIsSystemProxy(bool proxy) {
  425. isSystemProxyObs.value = proxy;
  426. return SpUtil.setData('system_proxy', proxy);
  427. }
  428. void checkPort() {
  429. if (configEntity.value != null) {
  430. if (configEntity.value!.port == 0) {
  431. changeConfigField('port', initializedHttpPort);
  432. }
  433. if (configEntity.value!.mixedPort == 0) {
  434. changeConfigField('mixed-port', initializedMixedPort);
  435. }
  436. if (configEntity.value!.socksPort == 0) {
  437. changeConfigField('socks-port', initializedSockPort);
  438. }
  439. updateTray();
  440. }
  441. }
  442. //切换配置
  443. bool changeConfigField(String field, dynamic value) {
  444. try {
  445. int ret = clashFFI.change_config_field(
  446. json.encode(<String, dynamic>{field: value}).toNativeUtf8().cast());
  447. return ret == 0;
  448. } finally {
  449. getCurrentClashConfig();
  450. if (field.endsWith("port") && isSystemProxy()) {
  451. setSystemProxy();
  452. }
  453. }
  454. }
  455. Future<void> _acquireLock(Directory clashDirectory) async {
  456. final path = p.join(clashDirectory.path, "fclash.lock");
  457. final lockFile = File(path);
  458. if (!lockFile.existsSync()) {
  459. lockFile.createSync(recursive: true);
  460. }
  461. try {
  462. _clashLock = await lockFile.open(mode: FileMode.write);
  463. await _clashLock?.lock();
  464. } catch (e) {
  465. // if (!Platform.isWindows) {
  466. // await Get.find<NotificationService>()
  467. // .showNotification("Fclash", "Already running, Now exit.".tr);
  468. // }
  469. exit(0);
  470. }
  471. }
  472. ReceivePort? _logReceivePort;
  473. void startLogging() {
  474. _logReceivePort?.close();
  475. _logReceivePort = ReceivePort();
  476. logStream = _logReceivePort!.asBroadcastStream();
  477. if (kDebugMode) {
  478. logStream?.listen((event) {
  479. debugPrint("LOG: ${event}");
  480. });
  481. }
  482. final nativePort = _logReceivePort!.sendPort.nativePort;
  483. debugPrint("port: $nativePort");
  484. clashFFI.start_log(nativePort);
  485. }
  486. void stopLog() {
  487. logStream = null;
  488. clashFFI.stop_log();
  489. }
  490. void updateTray() {
  491. if (!isDesktop) {
  492. return;
  493. }
  494. final stringList = List<MenuItem>.empty(growable: true);
  495. // yaml
  496. stringList
  497. .add(MenuItem(label: "profile: ${currentYaml.value}", disabled: true));
  498. if (proxies['proxies'] != null) {
  499. Map<String, dynamic> m = proxies['proxies'];
  500. m.removeWhere((key, value) => value['type'] != "Selector");
  501. var cnt = 0;
  502. for (final k in m.keys) {
  503. if (cnt >= ClashService.MAX_ENTRIES) {
  504. stringList.add(MenuItem(label: "...", disabled: true));
  505. break;
  506. }
  507. stringList.add(
  508. MenuItem(label: "${m[k]['name']}: ${m[k]['now']}", disabled: true));
  509. cnt += 1;
  510. }
  511. }
  512. // port
  513. if (configEntity.value != null) {
  514. stringList.add(
  515. MenuItem(label: 'http: ${configEntity.value?.port}', disabled: true));
  516. stringList.add(MenuItem(
  517. label: 'socks: ${configEntity.value?.socksPort}', disabled: true));
  518. }
  519. // system proxy
  520. stringList.add(MenuItem.separator());
  521. if (!isSystemProxy()) {
  522. stringList
  523. .add(MenuItem(label: "Not system proxy yet.".tr, disabled: true));
  524. stringList.add(MenuItem(
  525. label: "Set as system proxy".tr,
  526. toolTip: "click to set fclash as system proxy".tr,
  527. key: ACTION_SET_SYSTEM_PROXY));
  528. } else {
  529. stringList.add(MenuItem(label: "System proxy now.".tr, disabled: true));
  530. stringList.add(MenuItem(
  531. label: "Unset system proxy".tr,
  532. toolTip: "click to reset system proxy",
  533. key: ACTION_UNSET_SYSTEM_PROXY));
  534. stringList.add(MenuItem.separator());
  535. }
  536. initAppTray(details: stringList, isUpdate: true);
  537. }
  538. Future<bool> _changeConfig(FileSystemEntity config) async {
  539. // check if it has `rule-set`, and try to convert it
  540. final content = await convertConfig(await File(config.path).readAsString())
  541. .catchError((e) {
  542. printError(info: e);
  543. });
  544. if (content.isNotEmpty) {
  545. await File(config.path).writeAsString(content);
  546. }
  547. // judge valid
  548. if (clashFFI.is_config_valid(config.path.toNativeUtf8().cast()) == 0) {
  549. final resp = await Request.dioClient.put('/configs',
  550. queryParameters: {"force": false}, data: {"path": config.path});
  551. Get.printInfo(info: 'config changed ret: ${resp.statusCode}');
  552. currentYaml.value = basename(config.path);
  553. SpUtil.setData('yaml', currentYaml.value);
  554. return resp.statusCode == 204;
  555. } else {
  556. Future.delayed(Duration.zero, () {
  557. Get.defaultDialog(
  558. middleText: 'not a valid config file'.tr,
  559. onConfirm: () {
  560. Get.back();
  561. });
  562. });
  563. config.delete();
  564. return false;
  565. }
  566. }
  567. Future<bool> addProfile(String name, String url) async {
  568. final configName = '$name.yaml';
  569. final newProfilePath = join(_clashDirectory.path, configName);
  570. try {
  571. final uri = Uri.tryParse(url);
  572. if (uri == null) {
  573. return false;
  574. }
  575. final resp = await Dio(BaseOptions(
  576. headers: {'User-Agent': 'clash.meta'},
  577. sendTimeout: 15000,
  578. receiveTimeout: 15000))
  579. .downloadUri(uri, newProfilePath, onReceiveProgress: (i, t) {
  580. Get.printInfo(info: "$i/$t");
  581. });
  582. return resp.statusCode == 200;
  583. } catch (e) {
  584. //BrnToast.show("Error: ${e}", Get.context!);
  585. } finally {
  586. final f = File(newProfilePath);
  587. if (f.existsSync() && await changeYaml(f)) {
  588. // set subscription
  589. await SpUtil.setData('profile_$name', url);
  590. return true;
  591. }
  592. return false;
  593. }
  594. }
  595. Future<bool> deleteProfile(FileSystemEntity config) async {
  596. if (config.existsSync()) {
  597. config.deleteSync();
  598. await SpUtil.remove('profile_${basename(config.path)}');
  599. reload();
  600. return true;
  601. } else {
  602. return false;
  603. }
  604. }
  605. //切换配置文件
  606. Future<bool> changeYaml(FileSystemEntity config) async {
  607. try {
  608. if (await config.exists()) {
  609. return await _changeConfig(config);
  610. } else {
  611. return false;
  612. }
  613. } finally {
  614. reload();
  615. }
  616. }
  617. bool changeProxy(String selectName, String proxyName) {
  618. final ret = clashFFI.change_proxy(
  619. selectName.toNativeUtf8().cast(), proxyName.toNativeUtf8().cast());
  620. if (ret == 0) {
  621. reload();
  622. }
  623. return ret == 0;
  624. }
  625. bool isHideWindowWhenStart() {
  626. return SpUtil.getData('boot_window_hide', defValue: false);
  627. }
  628. void handleSignal() {
  629. StreamSubscription? subTerm;
  630. subTerm = ProcessSignal.sigterm.watch().listen((event) {
  631. subTerm?.cancel();
  632. // _clashProcess?.kill();
  633. });
  634. }
  635. }
  636. Future<String> convertConfig(String content) async {
  637. return "";
  638. }