diff --git a/lib/api/api.dart b/lib/api/api.dart index 9cebb15..bcc7a78 100644 --- a/lib/api/api.dart +++ b/lib/api/api.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:hive/hive.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/data/app_placard_data.dart'; @@ -82,7 +83,13 @@ class Api { } static Future getRepoData(String dir, String name) async { - final r = await RSHttp.getText("${URLConf.apiRepoPath}/$dir/$name"); + final r = await RSHttp.getText("${URLConf.apiRepoPath}/$dir/$name",withCustomDns: await isUseInternalDNS()); return r; } + + static Future isUseInternalDNS() async { + final userBox = await Hive.openBox("app_conf"); + final isUseInternalDNS = userBox.get("isUseInternalDNS", defaultValue: false); + return isUseInternalDNS; + } } diff --git a/lib/common/conf/const_conf.dart b/lib/common/conf/const_conf.dart index 70a06ca..e1f5ec9 100644 --- a/lib/common/conf/const_conf.dart +++ b/lib/common/conf/const_conf.dart @@ -5,4 +5,5 @@ class ConstConf { static const gameChannels = ["LIVE", "PTU", "EPTU", "TECH-PREVIEW", "HOTFIX"]; static const isMSE = String.fromEnvironment("MSE", defaultValue: "false") == "true"; + static const dohAddress = "https://223.6.6.6/resolve"; } diff --git a/lib/common/conf/url_conf.dart b/lib/common/conf/url_conf.dart index b2e9a38..c9f38d6 100644 --- a/lib/common/conf/url_conf.dart +++ b/lib/common/conf/url_conf.dart @@ -1,3 +1,5 @@ +import 'package:starcitizen_doctor/api/api.dart'; +import 'package:starcitizen_doctor/common/io/doh_client.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/rust/http_package.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; @@ -38,16 +40,14 @@ class URLConf { static Future checkHost() async { // 使用 DNS 获取可用列表 - final gitApiList = - _genFinalList(await RSHttp.dnsLookupTxt("git.dns.scbox.org")); + final gitApiList = _genFinalList(await dnsLookupTxt("git.dns.scbox.org")); dPrint("DNS gitApiList ==== $gitApiList"); final fasterGit = await getFasterUrl(gitApiList); dPrint("gitApiList.Faster ==== $fasterGit"); if (fasterGit != null) { gitApiHome = fasterGit; } - final rssApiList = - _genFinalList(await RSHttp.dnsLookupTxt("rss.dns.scbox.org")); + final rssApiList = _genFinalList(await dnsLookupTxt("rss.dns.scbox.org")); final fasterRss = await getFasterUrl(rssApiList); dPrint("DNS rssApiList ==== $rssApiList"); dPrint("rssApiList.Faster ==== $fasterRss"); @@ -58,6 +58,15 @@ class URLConf { return isUrlCheckPass; } + static Future> dnsLookupTxt(String host) async { + if (await Api.isUseInternalDNS()) { + dPrint("[URLConf] use internal DNS LookupTxt $host"); + return RSHttp.dnsLookupTxt(host); + } + dPrint("[URLConf] use DOH LookupTxt $host"); + return (await DohClient.resolveTXT(host)) ?? []; + } + static Future getFasterUrl(List urls) async { String firstUrl = ""; int callLen = 0; diff --git a/lib/common/io/doh_client.dart b/lib/common/io/doh_client.dart new file mode 100644 index 0000000..0c54b2d --- /dev/null +++ b/lib/common/io/doh_client.dart @@ -0,0 +1,46 @@ +import 'dart:convert'; + +import 'package:starcitizen_doctor/common/conf/const_conf.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/data/doh_client_response_data.dart'; + +class DohClient { + static Future resolve( + String domain, String type) async { + try { + final r = await RSHttp.getText( + "${ConstConf.dohAddress}?name=$domain&type=$type"); + final data = DohClientResponseData.fromJson(json.decode(r)); + return data; + } catch (e) { + dPrint("DohClient.resolve error: $e"); + return null; + } + } + + static Future?> resolveIP(String domain, String type) async { + final data = await resolve(domain, type); + if (data == null) return []; + return data.answer?.map((e) => _removeDataPadding(e.data)).toList(); + } + + static Future?> resolveTXT(String domain) async { + final data = await resolve(domain, "TXT"); + if (data == null) return []; + return data.answer?.map((e) => _removeDataPadding(e.data)).toList(); + } + + static String _removeDataPadding(String? data) { + // data demo: {"data":"\"https://git.scbox.xkeyc.cn,https://gitapi.scbox.org\""} + if (data == null) return ""; + data = data.trim(); + if (data.startsWith("\"")){ + data = data.substring(1); + } + if (data.endsWith("\"")){ + data = data.substring(0, data.length - 1); + } + return data; + } +} diff --git a/lib/data/doh_client_response_data.dart b/lib/data/doh_client_response_data.dart new file mode 100644 index 0000000..d804125 --- /dev/null +++ b/lib/data/doh_client_response_data.dart @@ -0,0 +1,109 @@ +class DohClientResponseData { + DohClientResponseData({ + this.status, + this.tc, + this.rd, + this.ra, + this.ad, + this.cd, + this.question, + this.answer, + }); + + DohClientResponseData.fromJson(dynamic json) { + status = json['Status']; + tc = json['TC']; + rd = json['RD']; + ra = json['RA']; + ad = json['AD']; + cd = json['CD']; + question = json['Question'] != null + ? DohClientResponseQuestionData.fromJson(json['Question']) + : null; + if (json['Answer'] != null) { + answer = []; + json['Answer'].forEach((v) { + answer?.add(DohClientResponseAnswerData.fromJson(v)); + }); + } + } + + num? status; + bool? tc; + bool? rd; + bool? ra; + bool? ad; + bool? cd; + DohClientResponseQuestionData? question; + List? answer; + + Map toJson() { + final map = {}; + map['Status'] = status; + map['TC'] = tc; + map['RD'] = rd; + map['RA'] = ra; + map['AD'] = ad; + map['CD'] = cd; + if (question != null) { + map['Question'] = question?.toJson(); + } + if (answer != null) { + map['Answer'] = answer?.map((v) => v.toJson()).toList(); + } + return map; + } +} + +class DohClientResponseAnswerData { + DohClientResponseAnswerData({ + this.name, + this.ttl, + this.type, + this.data, + }); + + DohClientResponseAnswerData.fromJson(dynamic json) { + name = json['name']; + ttl = json['TTL']; + type = json['type']; + data = json['data']; + } + + String? name; + num? ttl; + num? type; + String? data; + + Map toJson() { + final map = {}; + map['name'] = name; + map['TTL'] = ttl; + map['type'] = type; + map['data'] = data; + return map; + } +} + + +class DohClientResponseQuestionData { + DohClientResponseQuestionData({ + this.name, + this.type, + }); + + DohClientResponseQuestionData.fromJson(dynamic json) { + name = json['name']; + type = json['type']; + } + + String? name; + num? type; + + Map toJson() { + final map = {}; + map['name'] = name; + map['type'] = type; + return map; + } +} diff --git a/lib/ui/settings/settings_ui.dart b/lib/ui/settings/settings_ui.dart index 1ebc4fd..4683e06 100644 --- a/lib/ui/settings/settings_ui.dart +++ b/lib/ui/settings/settings_ui.dart @@ -15,6 +15,12 @@ class SettingsUI extends HookConsumerWidget { final appGlobalState = ref.watch(appGlobalModelProvider); final appGlobalModel = ref.read(appGlobalModelProvider.notifier); return ListView(padding: const EdgeInsets.all(16), children: [ + makeTitle("应用"), + makeSettingsItem(const Icon(FluentIcons.link, size: 20), + S.current.setting_action_create_settings_shortcut, + subTitle: S.current.setting_action_create_desktop_shortcut, + onTap: () => model.addShortCut(context)), + const SizedBox(height: 12), makeSettingsItem( const Icon(FontAwesomeIcons.language, size: 20), S.current.settings_app_language, @@ -26,11 +32,32 @@ class SettingsUI extends HookConsumerWidget { showGoIcon: false, ), const SizedBox(height: 12), - makeSettingsItem(const Icon(FluentIcons.link, size: 20), - S.current.setting_action_create_settings_shortcut, - subTitle: S.current.setting_action_create_desktop_shortcut, - onTap: () => model.addShortCut(context)), + makeSettingsItem( + const Icon(FontAwesomeIcons.networkWired, size: 20), "使用内置 DNS", + subTitle: "开启后可能解决部分地区 DNS 污染的问题", + switchStatus: sate.isUseInternalDNS, + onSwitch: model.onChangeUseInternalDNS, + onTap: () => model.onChangeUseInternalDNS(!sate.isUseInternalDNS)), const SizedBox(height: 12), + makeSettingsItem(const Icon(FluentIcons.delete, size: 20), + S.current.setting_action_clear_translation_file_cache, + subTitle: S.current.setting_action_info_cache_clearing_info( + (sate.locationCacheSize / 1024 / 1024).toStringAsFixed(2)), + onTap: () => model.cleanLocationCache(context)), + const SizedBox(height: 12), + makeSettingsItem(const Icon(FluentIcons.speed_high, size: 20), + S.current.setting_action_tool_site_access_acceleration, + onTap: () => + model.onChangeToolSiteMirror(!sate.isEnableToolSiteMirrors), + subTitle: S.current.setting_action_info_mirror_server_info, + onSwitch: model.onChangeToolSiteMirror, + switchStatus: sate.isEnableToolSiteMirrors), + const SizedBox(height: 12), + makeSettingsItem(const Icon(FluentIcons.document_set, size: 20), + S.current.setting_action_view_log, + onTap: () => model.showLogs(), + subTitle: S.current.setting_action_info_view_log_file), + makeTitle("功能"), makeSettingsItem(const Icon(FontAwesomeIcons.microchip, size: 20), S.current.setting_action_ignore_efficiency_cores_on_launch, subTitle: S.current @@ -57,27 +84,19 @@ class SettingsUI extends HookConsumerWidget { model.delName("custom_game_path"); }), const SizedBox(height: 12), - makeSettingsItem(const Icon(FluentIcons.delete, size: 20), - S.current.setting_action_clear_translation_file_cache, - subTitle: S.current.setting_action_info_cache_clearing_info( - (sate.locationCacheSize / 1024 / 1024).toStringAsFixed(2)), - onTap: () => model.cleanLocationCache(context)), - const SizedBox(height: 12), - makeSettingsItem(const Icon(FluentIcons.speed_high, size: 20), - S.current.setting_action_tool_site_access_acceleration, - onTap: () => - model.onChangeToolSiteMirror(!sate.isEnableToolSiteMirrors), - subTitle: S.current.setting_action_info_mirror_server_info, - onSwitch: model.onChangeToolSiteMirror, - switchStatus: sate.isEnableToolSiteMirrors), - const SizedBox(height: 12), - makeSettingsItem(const Icon(FluentIcons.document_set, size: 20), - S.current.setting_action_view_log, - onTap: () => model.showLogs(), - subTitle: S.current.setting_action_info_view_log_file), ]); } + Widget makeTitle(String title) { + return Padding( + padding: const EdgeInsets.only(top: 12, bottom: 12), + child: Text( + title, + style: TextStyle(fontSize: 24), + ), + ); + } + Widget makeSettingsItem( Widget icon, String title, { diff --git a/lib/ui/settings/settings_ui_model.dart b/lib/ui/settings/settings_ui_model.dart index 49693eb..67e4786 100644 --- a/lib/ui/settings/settings_ui_model.dart +++ b/lib/ui/settings/settings_ui_model.dart @@ -25,6 +25,7 @@ class SettingsUIState with _$SettingsUIState { String? customLauncherPath, String? customGamePath, @Default(0) int locationCacheSize, + @Default(false) bool isUseInternalDNS, }) = _SettingsUIState; } @@ -38,10 +39,11 @@ class SettingsUIModel extends _$SettingsUIModel { } void _initState() async { - _updateGameLaunchECore(); - _loadCustomPath(); - _loadLocationCacheSize(); - _loadToolSiteMirrorState(); + await _updateGameLaunchECore(); + await _loadCustomPath(); + await _loadLocationCacheSize(); + await _loadToolSiteMirrorState(); + await _loadUseInternalDNS(); } Future setGameLaunchECore(BuildContext context) async { @@ -113,7 +115,7 @@ class SettingsUIModel extends _$SettingsUIModel { await confBox.put(pathKey, dir); } - _loadCustomPath() async { + Future _loadCustomPath() async { final confBox = await Hive.openBox("app_conf"); final customLauncherPath = confBox.get("custom_launcher_path"); final customGamePath = confBox.get("custom_game_path"); @@ -127,7 +129,7 @@ class SettingsUIModel extends _$SettingsUIModel { _initState(); } - _loadLocationCacheSize() async { + Future _loadLocationCacheSize() async { final len1 = await SystemHelper.getDirLen( "${appGlobalState.applicationSupportDir}/Localizations"); final len2 = await SystemHelper.getDirLen( @@ -182,7 +184,7 @@ class SettingsUIModel extends _$SettingsUIModel { showToast(context, S.current.setting_action_info_shortcut_created); } - _loadToolSiteMirrorState() async { + Future _loadToolSiteMirrorState() async { final userBox = await Hive.openBox("app_conf"); final isEnableToolSiteMirrors = userBox.get("isEnableToolSiteMirrors", defaultValue: false); @@ -200,4 +202,16 @@ class SettingsUIModel extends _$SettingsUIModel { SystemHelper.openDir(getDPrintFile()?.absolute.path.replaceAll("/", "\\"), isFile: true); } + + void onChangeUseInternalDNS(bool? b) { + final userBox = Hive.box("app_conf"); + userBox.put("isUseInternalDNS", b ?? false); + _initState(); + } + + Future _loadUseInternalDNS() async { + final userBox = await Hive.openBox("app_conf"); + final isUseInternalDNS = userBox.get("isUseInternalDNS", defaultValue: false); + state = state.copyWith(isUseInternalDNS: isUseInternalDNS); + } } diff --git a/lib/ui/settings/settings_ui_model.freezed.dart b/lib/ui/settings/settings_ui_model.freezed.dart index 50ecdd5..f053451 100644 --- a/lib/ui/settings/settings_ui_model.freezed.dart +++ b/lib/ui/settings/settings_ui_model.freezed.dart @@ -21,6 +21,7 @@ mixin _$SettingsUIState { String? get customLauncherPath => throw _privateConstructorUsedError; String? get customGamePath => throw _privateConstructorUsedError; int get locationCacheSize => throw _privateConstructorUsedError; + bool get isUseInternalDNS => throw _privateConstructorUsedError; /// Create a copy of SettingsUIState /// with the given fields replaced by the non-null parameter values. @@ -40,7 +41,8 @@ abstract class $SettingsUIStateCopyWith<$Res> { String inputGameLaunchECore, String? customLauncherPath, String? customGamePath, - int locationCacheSize}); + int locationCacheSize, + bool isUseInternalDNS}); } /// @nodoc @@ -63,6 +65,7 @@ class _$SettingsUIStateCopyWithImpl<$Res, $Val extends SettingsUIState> Object? customLauncherPath = freezed, Object? customGamePath = freezed, Object? locationCacheSize = null, + Object? isUseInternalDNS = null, }) { return _then(_value.copyWith( isEnableToolSiteMirrors: null == isEnableToolSiteMirrors @@ -85,6 +88,10 @@ class _$SettingsUIStateCopyWithImpl<$Res, $Val extends SettingsUIState> ? _value.locationCacheSize : locationCacheSize // ignore: cast_nullable_to_non_nullable as int, + isUseInternalDNS: null == isUseInternalDNS + ? _value.isUseInternalDNS + : isUseInternalDNS // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -102,7 +109,8 @@ abstract class _$$SettingsUIStateImplCopyWith<$Res> String inputGameLaunchECore, String? customLauncherPath, String? customGamePath, - int locationCacheSize}); + int locationCacheSize, + bool isUseInternalDNS}); } /// @nodoc @@ -123,6 +131,7 @@ class __$$SettingsUIStateImplCopyWithImpl<$Res> Object? customLauncherPath = freezed, Object? customGamePath = freezed, Object? locationCacheSize = null, + Object? isUseInternalDNS = null, }) { return _then(_$SettingsUIStateImpl( isEnableToolSiteMirrors: null == isEnableToolSiteMirrors @@ -145,6 +154,10 @@ class __$$SettingsUIStateImplCopyWithImpl<$Res> ? _value.locationCacheSize : locationCacheSize // ignore: cast_nullable_to_non_nullable as int, + isUseInternalDNS: null == isUseInternalDNS + ? _value.isUseInternalDNS + : isUseInternalDNS // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -157,7 +170,8 @@ class _$SettingsUIStateImpl implements _SettingsUIState { this.inputGameLaunchECore = "0", this.customLauncherPath, this.customGamePath, - this.locationCacheSize = 0}); + this.locationCacheSize = 0, + this.isUseInternalDNS = false}); @override @JsonKey() @@ -172,10 +186,13 @@ class _$SettingsUIStateImpl implements _SettingsUIState { @override @JsonKey() final int locationCacheSize; + @override + @JsonKey() + final bool isUseInternalDNS; @override String toString() { - return 'SettingsUIState(isEnableToolSiteMirrors: $isEnableToolSiteMirrors, inputGameLaunchECore: $inputGameLaunchECore, customLauncherPath: $customLauncherPath, customGamePath: $customGamePath, locationCacheSize: $locationCacheSize)'; + return 'SettingsUIState(isEnableToolSiteMirrors: $isEnableToolSiteMirrors, inputGameLaunchECore: $inputGameLaunchECore, customLauncherPath: $customLauncherPath, customGamePath: $customGamePath, locationCacheSize: $locationCacheSize, isUseInternalDNS: $isUseInternalDNS)'; } @override @@ -193,7 +210,9 @@ class _$SettingsUIStateImpl implements _SettingsUIState { (identical(other.customGamePath, customGamePath) || other.customGamePath == customGamePath) && (identical(other.locationCacheSize, locationCacheSize) || - other.locationCacheSize == locationCacheSize)); + other.locationCacheSize == locationCacheSize) && + (identical(other.isUseInternalDNS, isUseInternalDNS) || + other.isUseInternalDNS == isUseInternalDNS)); } @override @@ -203,7 +222,8 @@ class _$SettingsUIStateImpl implements _SettingsUIState { inputGameLaunchECore, customLauncherPath, customGamePath, - locationCacheSize); + locationCacheSize, + isUseInternalDNS); /// Create a copy of SettingsUIState /// with the given fields replaced by the non-null parameter values. @@ -221,7 +241,8 @@ abstract class _SettingsUIState implements SettingsUIState { final String inputGameLaunchECore, final String? customLauncherPath, final String? customGamePath, - final int locationCacheSize}) = _$SettingsUIStateImpl; + final int locationCacheSize, + final bool isUseInternalDNS}) = _$SettingsUIStateImpl; @override bool get isEnableToolSiteMirrors; @@ -233,6 +254,8 @@ abstract class _SettingsUIState implements SettingsUIState { String? get customGamePath; @override int get locationCacheSize; + @override + bool get isUseInternalDNS; /// Create a copy of SettingsUIState /// with the given fields replaced by the non-null parameter values. diff --git a/lib/ui/settings/settings_ui_model.g.dart b/lib/ui/settings/settings_ui_model.g.dart index 77d964c..f8e7804 100644 --- a/lib/ui/settings/settings_ui_model.g.dart +++ b/lib/ui/settings/settings_ui_model.g.dart @@ -6,7 +6,7 @@ part of 'settings_ui_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$settingsUIModelHash() => r'c625c9c743ba160bedd62a5c9f3c435422c94a42'; +String _$settingsUIModelHash() => r'de58885742e29aae6b1226c16c03655a6a6b018d'; /// See also [SettingsUIModel]. @ProviderFor(SettingsUIModel)