import 'dart:io'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:naiyouwl/app/bean/config.dart'; import 'package:naiyouwl/app/const/const.dart'; import 'package:yaml/yaml.dart'; import 'package:path/path.dart' as path; import 'package:flutter_emoji/flutter_emoji.dart'; import '../data/model/NodeMode.dart'; final Map _defaultConfig = { 'selected': 'example.yaml', 'updateInterval': 86400, 'updateSubsAtStart': false, 'setSystemProxy': false, 'startAtLogin': false, 'breakConnections': false, 'language': 'zh_CN', 'port': 9899, 'subs': [], }; class ConfigController extends GetxController { late final dio; var config = Config.fromJson(_defaultConfig).obs; var clashCoreApiAddress = '127.0.0.1:9090'.obs; var clashCoreApiSecret = ''.obs; var clashCoreDns = ''.obs; var clashCoreTunEnable = false.obs; var servicePort = 0.obs; Future initConfig() async { // var port = await getFreePort(); //dio.addSentry(); dio = Dio(BaseOptions(baseUrl: clashCoreApiAddress.value)); if (!await Paths.config.exists()) await Paths.config.create(recursive: true); if (!await Files.configCountryMmdb.exists()) await Files.assetsCountryMmdb.copy(Files.configCountryMmdb.path); if (Platform.isWindows && !await Files.configWintun.exists()) await Files.assetsWintun.copy(Files.configWintun.path); final locale = Get.deviceLocale!; _defaultConfig['language'] = '${locale.languageCode}_${locale.countryCode}'; if (await Files.configConfig.exists()) { final local = json.decode(await Files.configConfig.readAsString()); config.value = Config.fromJson({..._defaultConfig, ...local}); } else { config.value = Config.fromJson(_defaultConfig); } //config.value.port = port; if (config.value.subs.isEmpty) { if (!await Files.configExample.exists()) await Files.assetsExample.copy(Files.configExample.path); config.value.subs.add(ConfigSub(name: 'example.yaml', url: '', updateTime: 0)); config.value.selected = 'example.yaml'; } await save(); await readClashCoreApi(); } String nodeToYaml(NodeMode node) { switch (node.type) { case 'trojan': return ''' - { name: ${node.name}, type: ${node.type}, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, udp: 1 }'''; case 'shadowsocks': return ''' - { name: ${node.name}, type: ss, server: ${node.host}, port: ${node.port}, password: ${node.passwd}, cipher: ${node.method}, udp: 1 }'''; case 'v2ray': final type = (node.vless == 1) ? 'vless' : 'vmess'; if (type == 'vless') { return ''' - { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, udp: 1, flow: xtls-rprx-vision, servername: www.amazon.com, tls: true, reality-opts: { public-key: ${node.vlessPulkey} } }'''; } else { return ''' - { name: ${node.name}, type: $type, server: ${node.host}, port: ${node.port}, uuid: ${node.uuid}, alterId: ${node.v2AlterId}, cipher: ${node.method}, udp: 1 }'''; } default: return ''; } } Future makeClashConfig(List nodes) async{ var mixedport = 9888; bool bg = await isPortOccupied(mixedport); if(bg) { mixedport = await getFreePort(); } var extePort = 9777; final ac = await isPortOccupied(extePort); if(ac) { extePort = await getFreePort(); } var proxies = nodes.map(nodeToYaml).toList(); var proxyGroups = ''' proxy-groups: - name: proxy type: select proxies: - ${nodes.map((node) => node.name).join('\n - ')} '''; var rules = ''' rules: - GEOIP,CN,DIRECT - MATCH,proxy '''; var initconfig = ''' mixed-port: $mixedport allow-lan: true bind-address: '*' mode: Rule log-level: info external-controller: '127.0.0.1:$extePort' proxies: ${proxies.join('\n')} $proxyGroups $rules '''; await Files.makeProxyConfig.writeAsString(initconfig); config.value.selected = 'proxy.yaml'; } Future save() async { await Files.configConfig.writeAsString(json.encode(config.toJson())); } Future readClashCoreApi() async { final configStr = await File(path.join(Paths.config.path, config.value.selected)).readAsString(); // final emoji = EmojiParser(); // final b = emoji.unemojify(_config); final configJson = loadYaml(configStr.replaceAll(EmojiParser.REGEX_EMOJI, 'emoji')); // print(_json["external-controller"]); // https://github.com/dart-lang/yaml/issues/53 // final _extControl = RegExp(r'''(? setLanguage(String language) async { config.value.language = language; await save(); config.refresh(); } Future setSystemProxy(bool open) async { config.value.setSystemProxy = open; await save(); config.refresh(); } Future setUpdateInterval(int value) async { config.value.updateInterval = value; await save(); config.refresh(); } Future setUpdateSubsAtStart(bool value) async { config.value.updateSubsAtStart = value; await save(); config.refresh(); } Future setSelectd(String selected) async { config.value.selected = selected; await save(); config.refresh(); } Future updateSub(ConfigSub sub) async { if ((sub.url ?? '').isEmpty) return false; final res = await dio.get(sub.url!); final subInfo = res.headers['subscription-userinfo']; final file = File(path.join(Paths.config.path, sub.name)); final oldConfig = await file.exists() ? await file.readAsString() : ''; final changed = oldConfig != res.data; if (changed) await file.writeAsString(res.data); sub.updateTime = DateTime.now().millisecondsSinceEpoch ~/ 1000; sub.info = null; if (subInfo != null) { // final info = Map.fromEntries( // subInfo.first.split(RegExp(r';\s*')).where((s) => s.isNotEmpty).map((e) => e.split('=')).map((e) => MapEntry(e[0], int.parse(e[1])))); final entries = subInfo.first.split(RegExp(r';\s*')) .where((s) => s.isNotEmpty) .map((e) => e.split('=')) .toList(); final info = {}; for (var entry in entries) { info[entry[0]] = int.parse(entry[1]); } sub.info = ConfigSubInfo.fromJson(info); } await setSub(sub.name, sub); return changed; } Future setSub(String subName, ConfigSub sub) async { final idx = config.value.subs.indexWhere((it) => it.name == subName); config.value.subs[idx] = sub; if (subName != sub.name) { final file = File(path.join(Paths.config.path, subName)); if (await file.exists()) await file.rename(path.join(Paths.config.path, sub.name)); } await save(); config.refresh(); } Future addSub(ConfigSub sub) async { config.value.subs.add(sub); final file = File(path.join(Paths.config.path, sub.name)); if (!await file.exists()) await file.create(); await save(); config.refresh(); } Future deleteSub(String subName) async { final file = File(path.join(Paths.config.path, subName)); if (await file.exists()) await file.delete(); config.value.subs.removeWhere((it) => it.name == subName); await save(); config.refresh(); } Future setBreakConnections(bool value) async { config.value.breakConnections = value; await save(); config.refresh(); } Future getFreePort() async { var server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0); int port = server.port; await server.close(); return port; } Future isPortOccupied(int port) async { bool isOccupied = false; ServerSocket? server; try { server = await ServerSocket.bind(InternetAddress.loopbackIPv4, port); } catch (e) { isOccupied = true; } finally { await server?.close(); } return isOccupied; } }