diff --git a/lib/common/helper/system_helper.dart b/lib/common/helper/system_helper.dart index c3c7509..c23cad8 100644 --- a/lib/common/helper/system_helper.dart +++ b/lib/common/helper/system_helper.dart @@ -257,4 +257,10 @@ foreach ($adapter in $adapterMemory) { await Process.run( SystemHelper.powershellPath, ["explorer.exe", "/select,\"$path\""]); } + + static String getHostsFilePath() { + final envVars = Platform.environment; + final systemRoot = envVars["SYSTEMROOT"]; + return "$systemRoot\\System32\\drivers\\etc\\hosts"; + } } diff --git a/lib/common/io/rs_http.dart b/lib/common/io/rs_http.dart index 271946b..662a861 100644 --- a/lib/common/io/rs_http.dart +++ b/lib/common/io/rs_http.dart @@ -51,13 +51,20 @@ class RSHttp { } static Future head(String url, - {Map? headers}) async { + {Map? headers, String? withIpAddress}) async { final r = await rust_http.fetch( - method: MyMethod.head, url: url, headers: headers); + method: MyMethod.head, + url: url, + headers: headers, + withIpAddress: withIpAddress); return r; } static Future> dnsLookupTxt(String host) async { return await rust_http.dnsLookupTxt(host: host); } + + static Future> dnsLookupIps(String host) async { + return await rust_http.dnsLookupIps(host: host); + } } diff --git a/lib/common/utils/async.dart b/lib/common/utils/async.dart index b6a4bd3..30b0b13 100644 --- a/lib/common/utils/async.dart +++ b/lib/common/utils/async.dart @@ -8,7 +8,7 @@ extension AsyncError on Future { return await this; } catch (e) { dPrint("unwrap error:$e"); - if (context != null) { + if (context != null && context.mounted) { showToast(context, "出现错误: $e"); } return null; diff --git a/lib/ui/tools/dialogs/hosts_booster_dialog_ui.dart b/lib/ui/tools/dialogs/hosts_booster_dialog_ui.dart new file mode 100644 index 0000000..fc408f0 --- /dev/null +++ b/lib/ui/tools/dialogs/hosts_booster_dialog_ui.dart @@ -0,0 +1,290 @@ +import 'dart:io'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/common/utils/async.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; + +class HostsBoosterDialogUI extends HookConsumerWidget { + const HostsBoosterDialogUI({super.key}); + + static const _hostsMap = { + "Recaptcha": ["www.recaptcha.net", "recaptcha.net"], + "RSI 官网": ["robertsspaceindustries.com"], + "RSI Zendesk 客服站": ["cloudimperiumservicesllc.zendesk.com"], + "RSI 客服站": ["support.robertsspaceindustries.com"], + }; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final checkedMap = useState>({}); + final workingMap = useState>({}); + final workingText = useState(""); + + doHost(BuildContext context) async { + if (workingMap.value.isEmpty) { + final hasTrue = + checkedMap.value.values.where((element) => element).firstOrNull != + null; + if (!hasTrue) { + for (var k in _hostsMap.keys) { + checkedMap.value[k] = true; + } + checkedMap.value = Map.from(checkedMap.value); + } + } + workingText.value = "正在查询 DNS 并测试可访问性 请耐心等待..."; + final ipsMap = await _doCheckDns(workingMap, checkedMap); + workingText.value = "正在写入 Hosts ..."; + if (!context.mounted) return; + await _doWriteHosts(ipsMap).unwrap(context: context); + workingText.value = "读取配置 ..."; + await _readHostsState(workingMap, checkedMap); + workingText.value = ""; + } + + useEffect(() { + // 监听 Hosts 文件变更 + _readHostsState(workingMap, checkedMap); + return null; + }, const []); + + return ContentDialog( + constraints: + BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .55), + title: Row( + children: [ + IconButton( + icon: const Icon( + FluentIcons.back, + size: 22, + ), + onPressed: + workingText.value.isEmpty ? Navigator.of(context).pop : null), + const SizedBox(width: 12), + const Text("Hosts 加速"), + const Spacer(), + Button( + onPressed: () => _openHostsFile(context), + child: const Padding( + padding: EdgeInsets.all(3), + child: Row( + children: [ + Icon(FluentIcons.open_file), + SizedBox(width: 6), + Text("打开 Hosts 文件"), + ], + ), + )) + ], + ), + content: AnimatedSize( + duration: const Duration(milliseconds: 200), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 12), + const Row( + children: [ + SizedBox(width: 12), + Text("状态"), + SizedBox(width: 38), + Text("站点"), + Spacer(), + Text("是否启用"), + SizedBox(width: 12), + ], + ), + const SizedBox(height: 12), + ListView.builder( + itemCount: _hostsMap.length, + shrinkWrap: true, + padding: const EdgeInsets.all(6), + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (BuildContext context, int index) { + final isEnable = + checkedMap.value[_hostsMap.keys.elementAt(index)] ?? false; + final workingState = + workingMap.value[_hostsMap.keys.elementAt(index)]; + return Container( + padding: const EdgeInsets.all(12), + margin: const EdgeInsets.only(bottom: 12), + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + if (workingState == null) + Icon(FontAwesomeIcons.xmark, + size: 24, color: Colors.red), + if (workingState == 0) + const SizedBox( + width: 24, height: 24, child: ProgressRing()), + if (workingState == 1) + Icon(FontAwesomeIcons.check, + size: 24, color: Colors.green), + const SizedBox(width: 24), + const SizedBox(width: 12), + Text(_hostsMap.keys.elementAt(index)), + const Spacer(), + ToggleSwitch( + onChanged: (value) { + checkedMap.value[_hostsMap.keys.elementAt(index)] = + value; + checkedMap.value = Map.from(checkedMap.value); + }, + checked: isEnable, + ), + ], + ), + ); + }, + ), + const SizedBox(height: 12), + if (workingText.value.isNotEmpty) + SizedBox( + height: 86, + child: Column( + children: [ + const SizedBox( + height: 42, width: 42, child: ProgressRing()), + const SizedBox(height: 12), + Text(workingText.value), + ], + ), + ) + else + Padding( + padding: const EdgeInsets.all(12), + child: FilledButton( + onPressed: () => doHost(context), + child: const Padding( + padding: + EdgeInsets.only(top: 3, bottom: 3, left: 12, right: 12), + child: Text("一键加速"), + ), + ), + ), + ], + ), + ), + ); + } + + Future _openHostsFile(BuildContext context) async { + // 使用管理员权限调用记事本打开 Hosts 文件 + Process.run(SystemHelper.powershellPath, [ + "-Command", + "Start-Process notepad.exe -Verb runAs -ArgumentList ${SystemHelper.getHostsFilePath()}" + // ignore: use_build_context_synchronously + ]).unwrap(context: context); + } + + Future> _doCheckDns( + ValueNotifier> workingMap, + ValueNotifier> checkedMap) async { + Map result = {}; + final trueLen = checkedMap.value.values.where((element) => element).length; + if (trueLen == 0) { + return result; + } + for (var kv in _hostsMap.entries) { + final siteName = kv.key; + final siteHost = kv.value.first; + if (!(checkedMap.value[siteName] ?? false)) { + continue; + } + workingMap.value[siteName] = 0; + workingMap.value = Map.from(workingMap.value); + RSHttp.dnsLookupIps(siteHost).then((ips) async { + int tryCount = ips.length; + try { + for (var ip in ips) { + final resp = + await RSHttp.head("https://$siteHost", withIpAddress: ip); + dPrint( + "[HostsBooster] host== $siteHost ip== $ip resp== ${resp.headers}"); + if (resp.headers.isNotEmpty) { + if (result[siteName] == null) { + result[siteName] = ip; + workingMap.value[siteName] = 1; + workingMap.value = Map.from(workingMap.value); + break; + } + } + } + } catch (e) { + tryCount--; + if (tryCount == 0) { + workingMap.value[siteName] = null; + workingMap.value = Map.from(workingMap.value); + result[siteName] = ""; + } + } + }, onError: (e) { + workingMap.value[siteName] = null; + workingMap.value = Map.from(workingMap.value); + result[siteName] = ""; + }); + } + while (true) { + await Future.delayed(const Duration(milliseconds: 100)); + if (result.length == trueLen) { + return result; + } + } + } + + Future _doWriteHosts(Map ipsMap) async { + // 读取 hosts 文件 + final hostsFile = File(SystemHelper.getHostsFilePath()); + final hostsFileString = await hostsFile.readAsString(); + final hostsFileLines = hostsFileString.split("\n"); + final newHostsFileLines = []; + + // copy Lines + for (var line in hostsFileLines) { + if (line.contains("#StarCitizenToolBox")) { + break; + } + newHostsFileLines.add(line); + } + dPrint("userHostsFile == $hostsFileString"); + for (var kv in ipsMap.entries) { + final domains = _hostsMap[kv.key] ?? []; + for (var domain in domains) { + if (kv.value != "") { + newHostsFileLines + .add("${kv.value} $domain #StarCitizenToolBox"); + } + } + } + await hostsFile.writeAsString(newHostsFileLines.join("\n"), flush: true); + } + + Future _readHostsState(ValueNotifier> workingMap, + ValueNotifier> checkedMap) async { + workingMap.value.clear(); + final hostsFile = File(SystemHelper.getHostsFilePath()); + final hostsFileString = await hostsFile.readAsString(); + final hostsFileLines = hostsFileString.split("\n"); + dPrint("userHostsFile == $hostsFileString"); + for (var line in hostsFileLines) { + if (line.contains("#StarCitizenToolBox")) { + for (var host in _hostsMap.entries) { + if (line.contains(host.value.first)) { + workingMap.value[host.key] = 1; + workingMap.value = Map.from(workingMap.value); + checkedMap.value[host.key] = true; + checkedMap.value = Map.from(checkedMap.value); + } + } + } + } + } +} diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index 780cbd4..d3985d8 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -22,6 +22,8 @@ import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:xml/xml.dart'; +import 'dialogs/hosts_booster_dialog_ui.dart'; + part 'tools_ui_model.g.dart'; part 'tools_ui_model.freezed.dart'; @@ -81,6 +83,13 @@ class ToolsUIModel extends _$ToolsUIModel { const Icon(FontAwesomeIcons.download, size: 28), onTap: () => _downloadP4k(context), ), + ToolsItemData( + "hosts_booster", + "Hosts 加速", + "将 IP 信息写入 Hosts 文件,解决部分地区的 DNS 污染导致无法登录官网等问题。", + const Icon(FluentIcons.virtual_network, size: 28), + onTap: () => _doHostsBooster(context), + ), ToolsItemData( "reinstall_eac", "重装 EasyAntiCheat 反作弊", @@ -549,4 +558,10 @@ class ToolsUIModel extends _$ToolsUIModel { void onChangeLauncherPath(String s) { state = state.copyWith(rsiLauncherInstalledPath: s); } + + _doHostsBooster(BuildContext context) async { + showDialog( + context: context, + builder: (BuildContext context) => const HostsBoosterDialogUI()); + } }