From 5c45e23d23812a8c906ea0fe3cb4faf65bb35a9f Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sun, 10 Mar 2024 16:26:04 +0800 Subject: [PATCH] =?UTF-8?q?feat:riverpod=20=E8=BF=81=E7=A7=BB=20Localizati?= =?UTF-8?q?onUIModel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/utils/async.dart | 12 + lib/ui/home/home_ui.dart | 23 +- .../localization/localization_dialog_ui.dart | 444 ++++++++++++++++++ .../localization/localization_ui_model.dart | 374 +++++++++++++++ .../localization_ui_model.freezed.dart | 272 +++++++++++ .../localization/localization_ui_model.g.dart | 27 ++ lib/widgets/widgets.dart | 7 + 7 files changed, 1156 insertions(+), 3 deletions(-) create mode 100644 lib/common/utils/async.dart create mode 100644 lib/ui/home/localization/localization_dialog_ui.dart create mode 100644 lib/ui/home/localization/localization_ui_model.dart create mode 100644 lib/ui/home/localization/localization_ui_model.freezed.dart create mode 100644 lib/ui/home/localization/localization_ui_model.g.dart diff --git a/lib/common/utils/async.dart b/lib/common/utils/async.dart new file mode 100644 index 0000000..eccb0f6 --- /dev/null +++ b/lib/common/utils/async.dart @@ -0,0 +1,12 @@ +import 'package:starcitizen_doctor/common/utils/log.dart'; + +extension AsyncError on Future { + Future unwrap() async { + try { + return await this; + } catch (e) { + dPrint("unwrap error:$e"); + return null; + } + } +} diff --git a/lib/ui/home/home_ui.dart b/lib/ui/home/home_ui.dart index 3a46111..f55b4b1 100644 --- a/lib/ui/home/home_ui.dart +++ b/lib/ui/home/home_ui.dart @@ -14,6 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart'; import 'dialogs/home_countdown_dialog_ui.dart'; import 'dialogs/home_md_content_dialog_ui.dart'; import 'home_ui_model.dart'; +import 'localization/localization_dialog_ui.dart'; class HomeUI extends HookConsumerWidget { const HomeUI({super.key}); @@ -446,7 +447,7 @@ class HomeUI extends HookConsumerWidget { itemBuilder: (context, index) { final item = items.elementAt(index); return HoverButton( - onPressed: () => _onMenuTap(context, item.key), + onPressed: () => _onMenuTap(context, item.key, homeState), builder: (BuildContext context, Set states) { return Container( width: 300, @@ -748,8 +749,24 @@ class HomeUI extends HookConsumerWidget { context: context, builder: (context) => const HomeCountdownDialogUI()); } - _onMenuTap(BuildContext context, String key) { - context.push("/index/$key"); + _onMenuTap( + BuildContext context, String key, HomeUIModelState homeState) async { + const String gameInstallReqInfo = + "该功能需要一个有效的安装位置\n\n如果您的游戏未下载完成,请等待下载完毕后使用此功能。\n\n如果您的游戏已下载完毕但未识别,请启动一次游戏后重新打开盒子 或 在设置选项中手动设置安装位置。"; + switch (key) { + case "localization": + if (homeState.scInstalledPath == "not_install") { + showToast(context, gameInstallReqInfo); + break; + } + await showDialog( + context: context, + dismissWithEsc: false, + builder: (BuildContext context) => const LocalizationDialogUI()); + break; + default: + context.push("/index/$key"); + } } } diff --git a/lib/ui/home/localization/localization_dialog_ui.dart b/lib/ui/home/localization/localization_dialog_ui.dart new file mode 100644 index 0000000..aba0d12 --- /dev/null +++ b/lib/ui/home/localization/localization_dialog_ui.dart @@ -0,0 +1,444 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:starcitizen_doctor/data/sc_localization_data.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; + +import 'localization_ui_model.dart'; + +class LocalizationDialogUI extends HookConsumerWidget { + const LocalizationDialogUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(localizationUIModelProvider); + final model = ref.read(localizationUIModelProvider.notifier); + final curInstallInfo = state.apiLocalizationData?[state.patchStatus?.value]; + + useEffect(() { + addPostFrameCallback(() { + model.checkUserCfg(context); + }); + return null; + }, []); + + return ContentDialog( + title: makeTitle(context, model, state), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .7, + minHeight: MediaQuery.of(context).size.height * .9), + content: Padding( + padding: const EdgeInsets.only(left: 12, right: 12, top: 12), + child: SingleChildScrollView( + child: Column( + children: [ + AnimatedSize( + duration: const Duration(milliseconds: 130), + child: state.patchStatus?.key == true && + state.patchStatus?.value == "游戏内置" + ? Padding( + padding: const EdgeInsets.only(bottom: 12), + child: InfoBar( + title: const Text("警告"), + content: const Text( + "您正在使用游戏内置文本,官方文本目前为机器翻译(截至3.21.0),建议您在下方安装社区汉化。"), + severity: InfoBarSeverity.info, + style: InfoBarThemeData(decoration: (severity) { + return const BoxDecoration( + color: Color.fromRGBO(155, 7, 7, 1.0)); + }, iconColor: (severity) { + return Colors.white; + }), + ), + ) + : SizedBox( + width: MediaQuery.of(context).size.width, + ), + ), + makeListContainer( + "汉化状态", + [ + if (state.patchStatus == null) + makeLoading(context) + else ...[ + const SizedBox(height: 6), + Row( + children: [ + Center( + child: Text( + "启用(${LocalizationUIModel.languageSupport[state.selectedLanguage]}):"), + ), + const Spacer(), + ToggleSwitch( + checked: state.patchStatus?.key == true, + onChanged: model.updateLangCfg, + ) + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Text("已安装版本:${state.patchStatus?.value}"), + const Spacer(), + if (state.patchStatus?.value != "游戏内置") + Row( + children: [ + Button( + onPressed: model.goFeedback, + child: const Padding( + padding: EdgeInsets.all(4), + child: Row( + children: [ + Icon(FluentIcons.feedback), + SizedBox(width: 6), + Text("汉化反馈"), + ], + ), + )), + const SizedBox(width: 16), + Button( + onPressed: model.doDelIniFile(), + child: const Padding( + padding: EdgeInsets.all(4), + child: Row( + children: [ + Icon(FluentIcons.delete), + SizedBox(width: 6), + Text("卸载汉化"), + ], + ), + )), + ], + ), + ], + ), + AnimatedSize( + duration: const Duration(milliseconds: 130), + child: (curInstallInfo != null && + curInstallInfo.note != null && + curInstallInfo.note!.isNotEmpty) + ? Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + width: MediaQuery.of(context).size.width, + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + borderRadius: BorderRadius.circular(7)), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + const Text( + "备注:", + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 6), + Text( + "${curInstallInfo.note}", + style: TextStyle( + color: + Colors.white.withOpacity(.8)), + ) + ], + ), + ), + ), + ) + : SizedBox( + width: MediaQuery.of(context).size.width, + ), + ), + ], + ], + context), + makeListContainer( + "社区汉化", + [ + if (state.apiLocalizationData == null) + makeLoading(context) + else if (state.apiLocalizationData!.isEmpty) + Center( + child: Text( + "该语言/版本 暂无可用汉化,敬请期待!", + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(.8)), + ), + ) + else + for (final item in state.apiLocalizationData!.entries) + makeRemoteList(context, model, item, state), + ], + context), + const SizedBox(height: 12), + IconButton( + icon: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(state.enableCustomize + ? FluentIcons.chevron_up + : FluentIcons.chevron_down), + const SizedBox(width: 12), + const Text("高级功能"), + ], + ), + onPressed: model.toggleCustomize), + AnimatedSize( + duration: const Duration(milliseconds: 130), + child: Column( + children: [ + const SizedBox(height: 12), + state.enableCustomize + ? makeListContainer( + "自定义文本", + [ + if (state.customizeList == null) + makeLoading(context) + else if (state.customizeList!.isEmpty) + Center( + child: Text( + "暂无自定义文本", + style: TextStyle( + fontSize: 13, + color: Colors.white.withOpacity(.8)), + ), + ) + else ...[ + for (final file in state.customizeList!) + Row( + children: [ + Text( + model.getCustomizeFileName(file), + ), + const Spacer(), + if (state.workingVersion == file) + const Padding( + padding: EdgeInsets.only(right: 12), + child: ProgressRing(), + ) + else + Button( + onPressed: + model.doLocalInstall(file), + child: const Padding( + padding: EdgeInsets.only( + left: 8, + right: 8, + top: 4, + bottom: 4), + child: Text("安装"), + )) + ], + ) + ], + ], + context, + actions: [ + Button( + onPressed: () => model.openDir(context), + child: const Padding( + padding: EdgeInsets.all(4), + child: Row( + children: [ + Icon(FluentIcons.folder_open), + SizedBox(width: 6), + Text("打开文件夹"), + ], + ), + )), + ]) + : SizedBox( + width: MediaQuery.of(context).size.width, + ) + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget makeRemoteList(BuildContext context, LocalizationUIModel model, + MapEntry item, LocalizationUIState state) { + final isWorking = state.workingVersion.isNotEmpty; + final isMineWorking = state.workingVersion == item.key; + final isInstalled = state.patchStatus?.value == item.key; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Column( + children: [ + Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "${item.value.info}", + style: const TextStyle(fontSize: 19), + ), + const SizedBox(height: 4), + Text( + "版本号:${item.value.versionName}", + style: TextStyle(color: Colors.white.withOpacity(.6)), + ), + const SizedBox(height: 4), + Text( + "通道:${item.value.gameChannel}", + style: TextStyle(color: Colors.white.withOpacity(.6)), + ), + const SizedBox(height: 4), + Text( + "更新时间:${item.value.updateAt}", + style: TextStyle(color: Colors.white.withOpacity(.6)), + ), + ], + ), + const Spacer(), + if (isMineWorking) + const Padding( + padding: EdgeInsets.only(right: 12), + child: ProgressRing(), + ) + else + Button( + onPressed: ((item.value.enable == true && + !isWorking && + !isInstalled) + ? model.doRemoteInstall(context, item.value) + : null), + child: Padding( + padding: const EdgeInsets.only( + left: 8, right: 8, top: 4, bottom: 4), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 6), + child: Icon(isInstalled + ? FluentIcons.check_mark + : (item.value.enable ?? false) + ? FluentIcons.download + : FluentIcons.disable_updates), + ), + Text(isInstalled + ? "已安装" + : ((item.value.enable ?? false) ? "安装" : "不可用")), + ], + ), + )), + ], + ), + const SizedBox(height: 6), + Container( + color: Colors.white.withOpacity(.05), + height: 1, + ), + ], + ), + ); + } + + Widget makeListContainer( + String title, List children, BuildContext context, + {List actions = const []}) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: AnimatedSize( + duration: const Duration(milliseconds: 130), + child: Container( + decoration: BoxDecoration( + color: FluentTheme.of(context).cardColor, + borderRadius: BorderRadius.circular(7)), + child: Padding( + padding: + const EdgeInsets.only(top: 12, bottom: 12, left: 24, right: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + title, + style: const TextStyle(fontSize: 22), + ), + const Spacer(), + if (actions.isNotEmpty) ...actions, + ], + ), + const SizedBox( + height: 6, + ), + Container( + color: Colors.white.withOpacity(.1), + height: 1, + ), + const SizedBox(height: 12), + ...children + ], + ), + ), + ), + ), + ); + } + + Widget makeTitle(BuildContext context, LocalizationUIModel model, + LocalizationUIState state) { + return Row( + children: [ + IconButton( + icon: const Icon( + FluentIcons.back, + size: 22, + ), + onPressed: model.onBack(context)), + const SizedBox(width: 12), + const Text("汉化管理"), + const SizedBox(width: 24), + Text( + "${model.getScInstallPath()}", + style: const TextStyle(fontSize: 13), + ), + const Spacer(), + SizedBox( + height: 36, + child: Row( + children: [ + const Text( + "语言: ", + style: TextStyle(fontSize: 16), + ), + ComboBox( + value: state.selectedLanguage, + items: [ + for (final lang + in LocalizationUIModel.languageSupport.entries) + ComboBoxItem( + value: lang.key, + child: Text(lang.value), + ) + ], + onChanged: state.workingVersion.isNotEmpty + ? null + : (v) { + if (v == null) return; + model.selectLang(v); + }, + ) + ], + ), + ), + const SizedBox(width: 12), + Button( + onPressed: model.doRefresh(), + child: const Padding( + padding: EdgeInsets.all(6), + child: Icon(FluentIcons.refresh), + )), + ], + ); + } +} diff --git a/lib/ui/home/localization/localization_ui_model.dart b/lib/ui/home/localization/localization_ui_model.dart new file mode 100644 index 0000000..be9689b --- /dev/null +++ b/lib/ui/home/localization/localization_ui_model.dart @@ -0,0 +1,374 @@ +// ignore_for_file: avoid_build_context_in_providers +import 'dart:async'; +import 'dart:io'; + +import 'package:archive/archive_io.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/api/analytics.dart'; +import 'package:starcitizen_doctor/api/api.dart'; +import 'package:starcitizen_doctor/common/conf/url_conf.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/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.dart'; +import 'package:starcitizen_doctor/data/sc_localization_data.dart'; +import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +part 'localization_ui_model.g.dart'; + +part 'localization_ui_model.freezed.dart'; + +@freezed +class LocalizationUIState with _$LocalizationUIState { + const factory LocalizationUIState({ + String? selectedLanguage, + Map? apiLocalizationData, + @Default("") String workingVersion, + MapEntry? patchStatus, + List? customizeList, + @Default(false) bool enableCustomize, + }) = _LocalizationUIState; +} + +@riverpod +class LocalizationUIModel extends _$LocalizationUIModel { + static const languageSupport = { + "chinese_(simplified)": "简体中文", + "chinese_(traditional)": "繁體中文", + }; + + late final _downloadDir = + Directory("${appGlobalState.applicationSupportDir}\\Localizations"); + + late final customizeDir = + Directory("${_downloadDir.absolute.path}\\Customize_ini"); + + late final scDataDir = + Directory("${ref.read(homeUIModelProvider).scInstalledPath}\\data"); + + late final cfgFile = File("${scDataDir.absolute.path}\\system.cfg"); + + StreamSubscription? _customizeDirListenSub; + + String get _scInstallPath => ref.read(homeUIModelProvider).scInstalledPath!; + + @override + LocalizationUIState build() { + state = LocalizationUIState(selectedLanguage: languageSupport.keys.first); + _init(); + return state; + } + + _init() async { + if (!customizeDir.existsSync()) { + await customizeDir.create(recursive: true); + } + _customizeDirListenSub = customizeDir.watch().listen((event) { + _scanCustomizeDir(); + }); + ref.onDispose(() { + _customizeDirListenSub?.cancel(); + _customizeDirListenSub = null; + }); + _loadData(); + } + + _loadData() async { + await _updateStatus(); + _scanCustomizeDir(); + final l = await Api.getScLocalizationData(state.selectedLanguage!).unwrap(); + if (l != null) { + final apiLocalizationData = {}; + for (var element in l) { + final isPTU = !_scInstallPath.contains("LIVE"); + if (isPTU && element.gameChannel == "PTU") { + apiLocalizationData[element.versionName ?? ""] = element; + } else if (!isPTU && element.gameChannel == "PU") { + apiLocalizationData[element.versionName ?? ""] = element; + } + } + state = state.copyWith(apiLocalizationData: apiLocalizationData); + } + } + + void checkUserCfg(BuildContext context) async { + final userCfgFile = File("$_scInstallPath\\USER.cfg"); + if (await userCfgFile.exists()) { + final cfgString = await userCfgFile.readAsString(); + if (cfgString.contains("g_language") && + !cfgString.contains("g_language=${state.selectedLanguage}")) { + if (!context.mounted) return; + final ok = await showConfirmDialogs( + context, + "是否移除不兼容的汉化参数", + const Text( + "USER.cfg 包含不兼容的汉化参数,这可能是以前的汉化文件的残留信息。\n\n这将可能导致汉化无效或乱码,点击确认为您一键移除(不会影响其他配置)。"), + constraints: BoxConstraints( + maxWidth: MediaQuery.of(context).size.width * .35)); + if (ok == true) { + var finalString = ""; + for (var item in cfgString.split("\n")) { + if (!item.trim().startsWith("g_language")) { + finalString = "$finalString$item\n"; + } + } + await userCfgFile.delete(); + await userCfgFile.create(); + await userCfgFile.writeAsString(finalString, flush: true); + _loadData(); + } + } + } + } + + Future updateLangCfg(bool enable) async { + final selectedLanguage = state.selectedLanguage!; + final status = await _getLangCfgEnableLang(lang: selectedLanguage); + final exists = await cfgFile.exists(); + if (status == enable) { + await _updateStatus(); + return; + } + StringBuffer newStr = StringBuffer(); + var str = []; + if (exists) { + str = (await cfgFile.readAsString()).replaceAll(" ", "").split("\n"); + } + if (enable) { + if (exists) { + for (var value in str) { + if (value.contains("sys_languages")) { + value = "sys_languages=$selectedLanguage"; + } else if (value.contains("g_language")) { + value = "g_language=$selectedLanguage"; + } else if (value.contains("g_languageAudio")) { + value = "g_language=english"; + } + if (value.trim().isNotEmpty) newStr.writeln(value); + } + } + if (!newStr.toString().contains("sys_languages=$selectedLanguage")) { + newStr.writeln("sys_languages=$selectedLanguage"); + } + if (!newStr.toString().contains("g_language=$selectedLanguage")) { + newStr.writeln("g_language=$selectedLanguage"); + } + if (!newStr.toString().contains("g_languageAudio")) { + newStr.writeln("g_languageAudio=english"); + } + } else { + if (exists) { + for (var value in str) { + if (value.contains("sys_languages=")) { + continue; + } else if (value.contains("g_language")) { + continue; + } + newStr.writeln(value); + } + } + } + if (exists) await cfgFile.delete(recursive: true); + await cfgFile.create(recursive: true); + await cfgFile.writeAsString(newStr.toString()); + await _updateStatus(); + } + + void goFeedback() { + launchUrlString(URLConf.feedbackUrl); + } + + VoidCallback? doDelIniFile() { + return () async { + final iniFile = File( + "${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini"); + if (await iniFile.exists()) await iniFile.delete(); + await updateLangCfg(false); + await _updateStatus(); + }; + } + + void toggleCustomize() { + state = state.copyWith(enableCustomize: !state.enableCustomize); + } + + String getCustomizeFileName(String path) { + return path.split("\\").last; + } + + VoidCallback? doLocalInstall(String filePath) { + if (state.workingVersion.isNotEmpty) return null; + return () async { + final f = File(filePath); + if (!await f.exists()) return; + state = state.copyWith(workingVersion: filePath); + final str = await f.readAsString(); + await _installFormString( + StringBuffer(str), "自定义_${getCustomizeFileName(filePath)}"); + state = state.copyWith(workingVersion: ""); + }; + } + + _installFormString(StringBuffer globalIni, String versionName) async { + final iniFile = File( + "${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini"); + if (versionName.isNotEmpty) { + if (!globalIni.toString().endsWith("\n")) { + globalIni.write("\n"); + } + globalIni.write("_starcitizen_doctor_localization_version=$versionName"); + } + + /// write cfg + if (await cfgFile.exists()) {} + + /// write ini + if (await iniFile.exists()) { + await iniFile.delete(); + } + await iniFile.create(recursive: true); + await iniFile.writeAsString("\uFEFF${globalIni.toString().trim()}", + flush: true); + await updateLangCfg(true); + await _updateStatus(); + } + + openDir(BuildContext context) async { + showToast(context, + "即将打开本地化文件夹,请将自定义的 任意名称.ini 文件放入 Customize_ini 文件夹。\n\n添加新文件后未显示请使用右上角刷新按钮。\n\n安装时请确保选择了正确的语言。"); + await Process.run(SystemHelper.powershellPath, + ["explorer.exe", "/select,\"${customizeDir.absolute.path}\"\\"]); + } + + VoidCallback? doRemoteInstall( + BuildContext context, ScLocalizationData value) { + return () async { + AnalyticsApi.touch("install_localization"); + final downloadUrl = + "${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz"; + final savePath = + File("${_downloadDir.absolute.path}\\${value.versionName}.sclang"); + try { + state = state.copyWith(workingVersion: value.versionName!); + if (!await savePath.exists()) { + // download + dPrint("downloading file to $savePath"); + final r = await RSHttp.get(downloadUrl); + if (r.statusCode == 200 && r.data != null) { + await savePath.writeAsBytes(r.data!); + } else { + throw "statusCode Error : ${r.statusCode}"; + } + } else { + dPrint("use cache $savePath"); + } + await Future.delayed(const Duration(milliseconds: 300)); + // check file + final globalIni = await compute(_readArchive, savePath.absolute.path); + if (globalIni.isEmpty) { + throw "文件受损,请重新下载"; + } + await _installFormString(globalIni, value.versionName ?? ""); + } catch (e) { + if (!context.mounted) return; + await showToast(context, "安装出错!\n\n $e"); + if (await savePath.exists()) await savePath.delete(); + } + state = state.copyWith(workingVersion: ""); + }; + } + + static StringBuffer _readArchive(String savePath) { + final inputStream = InputFileStream(savePath); + final archive = + TarDecoder().decodeBytes(GZipDecoder().decodeBuffer(inputStream)); + StringBuffer globalIni = StringBuffer(""); + for (var element in archive.files) { + if (element.name.contains("global.ini")) { + for (var value + in (element.rawContent?.readString() ?? "").split("\n")) { + final tv = value.trim(); + if (tv.isNotEmpty) globalIni.writeln(tv); + } + } + } + archive.clear(); + return globalIni; + } + + String? getScInstallPath() { + return ref.read(homeUIModelProvider).scInstalledPath; + } + + void selectLang(String v) { + state = state.copyWith(selectedLanguage: v); + _loadData(); + } + + VoidCallback? onBack(BuildContext context) { + if (state.workingVersion.isNotEmpty) return null; + return () { + Navigator.pop(context); + }; + } + + VoidCallback? doRefresh() { + if (state.workingVersion.isNotEmpty) return null; + return () { + state = state.copyWith(apiLocalizationData: null); + _loadData(); + }; + } + + void _scanCustomizeDir() { + final fileList = customizeDir.listSync(); + final customizeList = []; + for (var value in fileList) { + if (value is File && value.path.endsWith(".ini")) { + customizeList.add(value.absolute.path); + } + } + state = state.copyWith(customizeList: customizeList); + } + + _updateStatus() async { + final patchStatus = MapEntry( + await _getLangCfgEnableLang(lang: state.selectedLanguage!), + await _getInstalledIniVersion( + "${scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini")); + state = state.copyWith(patchStatus: patchStatus); + } + + Future _getLangCfgEnableLang({String lang = ""}) async { + if (!await cfgFile.exists()) return false; + final str = (await cfgFile.readAsString()).replaceAll(" ", ""); + return str.contains("sys_languages=$lang") && + str.contains("g_language=$lang") && + str.contains("g_languageAudio=english"); + } + + static Future _getInstalledIniVersion(String iniPath) async { + final iniFile = File(iniPath); + if (!await iniFile.exists()) return "游戏内置"; + final iniStringSplit = (await iniFile.readAsString()).split("\n"); + for (var i = iniStringSplit.length - 1; i > 0; i--) { + if (iniStringSplit[i] + .contains("_starcitizen_doctor_localization_version=")) { + final v = iniStringSplit[i] + .trim() + .split("_starcitizen_doctor_localization_version=")[1]; + return v; + } + } + return "自定义文件"; + } + + Future checkLangUpdate() async { + // TODO 检查更新 + } +} diff --git a/lib/ui/home/localization/localization_ui_model.freezed.dart b/lib/ui/home/localization/localization_ui_model.freezed.dart new file mode 100644 index 0000000..9f1bf50 --- /dev/null +++ b/lib/ui/home/localization/localization_ui_model.freezed.dart @@ -0,0 +1,272 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'localization_ui_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LocalizationUIState { + String? get selectedLanguage => throw _privateConstructorUsedError; + Map? get apiLocalizationData => + throw _privateConstructorUsedError; + String get workingVersion => throw _privateConstructorUsedError; + MapEntry? get patchStatus => throw _privateConstructorUsedError; + List? get customizeList => throw _privateConstructorUsedError; + bool get enableCustomize => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LocalizationUIStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LocalizationUIStateCopyWith<$Res> { + factory $LocalizationUIStateCopyWith( + LocalizationUIState value, $Res Function(LocalizationUIState) then) = + _$LocalizationUIStateCopyWithImpl<$Res, LocalizationUIState>; + @useResult + $Res call( + {String? selectedLanguage, + Map? apiLocalizationData, + String workingVersion, + MapEntry? patchStatus, + List? customizeList, + bool enableCustomize}); +} + +/// @nodoc +class _$LocalizationUIStateCopyWithImpl<$Res, $Val extends LocalizationUIState> + implements $LocalizationUIStateCopyWith<$Res> { + _$LocalizationUIStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? selectedLanguage = freezed, + Object? apiLocalizationData = freezed, + Object? workingVersion = null, + Object? patchStatus = freezed, + Object? customizeList = freezed, + Object? enableCustomize = null, + }) { + return _then(_value.copyWith( + selectedLanguage: freezed == selectedLanguage + ? _value.selectedLanguage + : selectedLanguage // ignore: cast_nullable_to_non_nullable + as String?, + apiLocalizationData: freezed == apiLocalizationData + ? _value.apiLocalizationData + : apiLocalizationData // ignore: cast_nullable_to_non_nullable + as Map?, + workingVersion: null == workingVersion + ? _value.workingVersion + : workingVersion // ignore: cast_nullable_to_non_nullable + as String, + patchStatus: freezed == patchStatus + ? _value.patchStatus + : patchStatus // ignore: cast_nullable_to_non_nullable + as MapEntry?, + customizeList: freezed == customizeList + ? _value.customizeList + : customizeList // ignore: cast_nullable_to_non_nullable + as List?, + enableCustomize: null == enableCustomize + ? _value.enableCustomize + : enableCustomize // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LocalizationUIStateImplCopyWith<$Res> + implements $LocalizationUIStateCopyWith<$Res> { + factory _$$LocalizationUIStateImplCopyWith(_$LocalizationUIStateImpl value, + $Res Function(_$LocalizationUIStateImpl) then) = + __$$LocalizationUIStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String? selectedLanguage, + Map? apiLocalizationData, + String workingVersion, + MapEntry? patchStatus, + List? customizeList, + bool enableCustomize}); +} + +/// @nodoc +class __$$LocalizationUIStateImplCopyWithImpl<$Res> + extends _$LocalizationUIStateCopyWithImpl<$Res, _$LocalizationUIStateImpl> + implements _$$LocalizationUIStateImplCopyWith<$Res> { + __$$LocalizationUIStateImplCopyWithImpl(_$LocalizationUIStateImpl _value, + $Res Function(_$LocalizationUIStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? selectedLanguage = freezed, + Object? apiLocalizationData = freezed, + Object? workingVersion = null, + Object? patchStatus = freezed, + Object? customizeList = freezed, + Object? enableCustomize = null, + }) { + return _then(_$LocalizationUIStateImpl( + selectedLanguage: freezed == selectedLanguage + ? _value.selectedLanguage + : selectedLanguage // ignore: cast_nullable_to_non_nullable + as String?, + apiLocalizationData: freezed == apiLocalizationData + ? _value._apiLocalizationData + : apiLocalizationData // ignore: cast_nullable_to_non_nullable + as Map?, + workingVersion: null == workingVersion + ? _value.workingVersion + : workingVersion // ignore: cast_nullable_to_non_nullable + as String, + patchStatus: freezed == patchStatus + ? _value.patchStatus + : patchStatus // ignore: cast_nullable_to_non_nullable + as MapEntry?, + customizeList: freezed == customizeList + ? _value._customizeList + : customizeList // ignore: cast_nullable_to_non_nullable + as List?, + enableCustomize: null == enableCustomize + ? _value.enableCustomize + : enableCustomize // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$LocalizationUIStateImpl implements _LocalizationUIState { + const _$LocalizationUIStateImpl( + {this.selectedLanguage, + final Map? apiLocalizationData, + this.workingVersion = "", + this.patchStatus, + final List? customizeList, + this.enableCustomize = false}) + : _apiLocalizationData = apiLocalizationData, + _customizeList = customizeList; + + @override + final String? selectedLanguage; + final Map? _apiLocalizationData; + @override + Map? get apiLocalizationData { + final value = _apiLocalizationData; + if (value == null) return null; + if (_apiLocalizationData is EqualUnmodifiableMapView) + return _apiLocalizationData; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + @JsonKey() + final String workingVersion; + @override + final MapEntry? patchStatus; + final List? _customizeList; + @override + List? get customizeList { + final value = _customizeList; + if (value == null) return null; + if (_customizeList is EqualUnmodifiableListView) return _customizeList; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + @JsonKey() + final bool enableCustomize; + + @override + String toString() { + return 'LocalizationUIState(selectedLanguage: $selectedLanguage, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, customizeList: $customizeList, enableCustomize: $enableCustomize)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LocalizationUIStateImpl && + (identical(other.selectedLanguage, selectedLanguage) || + other.selectedLanguage == selectedLanguage) && + const DeepCollectionEquality() + .equals(other._apiLocalizationData, _apiLocalizationData) && + (identical(other.workingVersion, workingVersion) || + other.workingVersion == workingVersion) && + (identical(other.patchStatus, patchStatus) || + other.patchStatus == patchStatus) && + const DeepCollectionEquality() + .equals(other._customizeList, _customizeList) && + (identical(other.enableCustomize, enableCustomize) || + other.enableCustomize == enableCustomize)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + selectedLanguage, + const DeepCollectionEquality().hash(_apiLocalizationData), + workingVersion, + patchStatus, + const DeepCollectionEquality().hash(_customizeList), + enableCustomize); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LocalizationUIStateImplCopyWith<_$LocalizationUIStateImpl> get copyWith => + __$$LocalizationUIStateImplCopyWithImpl<_$LocalizationUIStateImpl>( + this, _$identity); +} + +abstract class _LocalizationUIState implements LocalizationUIState { + const factory _LocalizationUIState( + {final String? selectedLanguage, + final Map? apiLocalizationData, + final String workingVersion, + final MapEntry? patchStatus, + final List? customizeList, + final bool enableCustomize}) = _$LocalizationUIStateImpl; + + @override + String? get selectedLanguage; + @override + Map? get apiLocalizationData; + @override + String get workingVersion; + @override + MapEntry? get patchStatus; + @override + List? get customizeList; + @override + bool get enableCustomize; + @override + @JsonKey(ignore: true) + _$$LocalizationUIStateImplCopyWith<_$LocalizationUIStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/home/localization/localization_ui_model.g.dart b/lib/ui/home/localization/localization_ui_model.g.dart new file mode 100644 index 0000000..cfdaf16 --- /dev/null +++ b/lib/ui/home/localization/localization_ui_model.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'localization_ui_model.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$localizationUIModelHash() => + r'654fd38b5f38bee5fd2cab69ab003846a311a4ff'; + +/// See also [LocalizationUIModel]. +@ProviderFor(LocalizationUIModel) +final localizationUIModelProvider = AutoDisposeNotifierProvider< + LocalizationUIModel, LocalizationUIState>.internal( + LocalizationUIModel.new, + name: r'localizationUIModelProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$localizationUIModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LocalizationUIModel = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 31e0455..624e5ef 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -11,6 +11,7 @@ import 'dart:ui' as ui; export 'src/cache_image.dart'; export 'src/countdown_time_text.dart'; +export '../common/utils/async.dart'; export '../common/utils/base_utils.dart'; Widget makeLoading( @@ -199,3 +200,9 @@ class LoadingWidget extends HookConsumerWidget { } } } + +addPostFrameCallback(Function() callback) { + WidgetsBinding.instance.addPostFrameCallback((_) { + callback(); + }); +} \ No newline at end of file