feat: download Localization

This commit is contained in:
xkeyC 2024-09-04 20:50:23 +08:00
parent 308480095b
commit 07f2ab03cb
12 changed files with 185 additions and 422 deletions

View File

@ -215,7 +215,7 @@ class _$AppGlobalStateImpl implements _AppGlobalState {
this.appLocale, this.appLocale,
this.appConfBox, this.appConfBox,
this.backgroundImageAssetsPath = this.backgroundImageAssetsPath =
"assets/backgrounds/SC_01_Wallpaper_3840x2160.jpg"}); "assets/backgrounds/SC_01_Wallpaper_3840x2160.webp"});
@override @override
final String? deviceUUID; final String? deviceUUID;

View File

@ -167,7 +167,7 @@ class HomeUI extends HookConsumerWidget {
)), )),
const SizedBox(width: 12), const SizedBox(width: 12),
Button( Button(
onPressed: () {}, onPressed: () {},
child: const Padding( child: const Padding(
padding: EdgeInsets.all(6), padding: EdgeInsets.all(6),
child: Icon(FluentIcons.folder_open), child: Icon(FluentIcons.folder_open),
@ -788,15 +788,10 @@ class HomeUI extends HookConsumerWidget {
_onMenuTap(BuildContext context, String key, HomeUIModelState homeState, _onMenuTap(BuildContext context, String key, HomeUIModelState homeState,
WidgetRef ref) async { WidgetRef ref) async {
String gameInstallReqInfo = // String gameInstallReqInfo =
S.current.home_action_info_valid_install_location_required; // S.current.home_action_info_valid_install_location_required;
switch (key) { switch (key) {
case "localization": case "localization":
if (homeState.scInstalledPath == "not_install") {
// TODO
// ToolsUIModel.English(context, showNotGameInstallMsg: true);
break;
}
final model = ref.watch(homeUIModelProvider.notifier); final model = ref.watch(homeUIModelProvider.notifier);
model.checkLocalizationUpdate(); model.checkLocalizationUpdate();
await showDialog( await showDialog(
@ -807,14 +802,18 @@ class HomeUI extends HookConsumerWidget {
model.checkLocalizationUpdate(skipReload: true); model.checkLocalizationUpdate(skipReload: true);
break; break;
case "performance": case "performance":
if (homeState.scInstalledPath == "not_install") { return;
showToast(context, gameInstallReqInfo); // if (homeState.scInstalledPath == "not_install") {
break; // showToast(context, gameInstallReqInfo);
} // break;
context.push("/index/$key"); // }
break; // context.push("/index/$key");
// break;
case "game_doctor":
return;
default: default:
context.push("/index/$key"); return;
// context.push("/index/$key");
} }
} }

View File

@ -6,7 +6,7 @@ part of 'home_ui_model.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$homeUIModelHash() => r'6a768281606856766737a63aaeebb392c4613d2b'; String _$homeUIModelHash() => r'422565027563e9bfd3ebddb08e518cade1c967d0';
/// See also [HomeUIModel]. /// See also [HomeUIModel].
@ProviderFor(HomeUIModel) @ProviderFor(HomeUIModel)

View File

@ -1,5 +1,4 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
@ -9,7 +8,6 @@ import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/app_advanced_localization_data.dart'; import 'package:starcitizen_doctor/data/app_advanced_localization_data.dart';
import 'package:starcitizen_doctor/data/sc_localization_data.dart'; import 'package:starcitizen_doctor/data/sc_localization_data.dart';
import '../home_ui_model.dart';
import 'advanced_localization_ui.json.dart'; import 'advanced_localization_ui.json.dart';
import 'localization_ui_model.dart'; import 'localization_ui_model.dart';
@ -178,40 +176,7 @@ class AdvancedLocalizationUIModel extends _$AdvancedLocalizationUIModel {
Future<(String, String)> _readIni(LocalizationUIState localizationUIState, Future<(String, String)> _readIni(LocalizationUIState localizationUIState,
LocalizationUIModel localizationUIModel) async { LocalizationUIModel localizationUIModel) async {
final homeUIState = ref.read(homeUIModelProvider); return ("", "");
final gameDir = homeUIState.scInstalledPath;
if (gameDir == null) return ("", "");
state = state.copyWith(
workingText: S.current.home_localization_advanced_msg_reading_p4k);
final p4kGlobalIni = await readEnglishInI(gameDir);
dPrint("read p4kGlobalIni => ${p4kGlobalIni.length}");
state = state.copyWith(
workingText: S.current
.home_localization_advanced_msg_reading_server_localization_text);
if (state.customizeGlobalIni != null) {
final apiLocalizationData = ScLocalizationData(
versionName: S.current.localization_info_custom_files,
info: "Customize");
state = state.copyWith(apiLocalizationData: apiLocalizationData);
return (p4kGlobalIni, state.customizeGlobalIni!);
} else {
final apiLocalizationData =
localizationUIState.apiLocalizationData?.values.firstOrNull;
if (apiLocalizationData == null) return ("", "");
final file = File(
"${localizationUIModel.getDownloadDir().absolute.path}\\${apiLocalizationData.versionName}.sclang");
if (!await file.exists()) {
await localizationUIModel.downloadLocalizationFile(
file, apiLocalizationData);
}
state = state.copyWith(apiLocalizationData: apiLocalizationData);
final serverGlobalIni =
(await compute(LocalizationUIModel.readArchive, file.absolute.path))
.toString();
dPrint("read serverGlobalIni => ${serverGlobalIni.length}");
return (p4kGlobalIni, serverGlobalIni);
}
} }
Future<String> readEnglishInI(String gameDir) async { Future<String> readEnglishInI(String gameDir) async {

View File

@ -7,7 +7,7 @@ part of 'advanced_localization_ui_model.dart';
// ************************************************************************** // **************************************************************************
String _$advancedLocalizationUIModelHash() => String _$advancedLocalizationUIModelHash() =>
r'60ccd50f54b948d16be001f5ea07972a0fd9ed3f'; r'5aa7e690f1db04febf5a747f4c8cd2eb79a0ed44';
/// See also [AdvancedLocalizationUIModel]. /// See also [AdvancedLocalizationUIModel].
@ProviderFor(AdvancedLocalizationUIModel) @ProviderFor(AdvancedLocalizationUIModel)

View File

@ -21,127 +21,19 @@ class LocalizationDialogUI extends HookConsumerWidget {
useEffect(() { useEffect(() {
addPostFrameCallback(() { addPostFrameCallback(() {
model.checkUserCfg(context); // model.checkUserCfg(context);
}); });
return null; return null;
}, []); }, []);
return ContentDialog( return ContentDialog(
title: makeTitle(context, model, state), title: makeTitle(context, model, state),
constraints: BoxConstraints( constraints: const BoxConstraints(maxWidth: 1080, minHeight: 920),
maxWidth: MediaQuery.of(context).size.width * .7,
minHeight: MediaQuery.of(context).size.height * .9),
content: Padding( content: Padding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 12), padding: const EdgeInsets.only(left: 12, right: 12, top: 12),
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
AnimatedSize(
duration: const Duration(milliseconds: 130),
child: state.patchStatus?.key == true &&
state.patchStatus?.value ==
S.current.home_action_info_game_built_in
? Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current.home_action_info_warning),
content: Text(S.current
.localization_info_machine_translation_warning),
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,
),
),
if (!(model.getScInstallPath() ?? "").contains("LIVE"))
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InfoBar(
title: Text(S.current
.home_localization_ptu_advanced_localization_tip_title),
content: Text(S.current
.home_localization_ptu_advanced_localization_tip_title_info),
severity: InfoBarSeverity.info,
style: InfoBarThemeData(decoration: (severity) {
return BoxDecoration(color: Colors.orange);
}, iconColor: (severity) {
return Colors.white;
}),
),
),
makeListContainer(
S.current.localization_info_translation_status,
[
if (state.patchStatus == null)
makeLoading(context)
else ...[
const SizedBox(height: 6),
Row(
children: [
Center(
child: Text(S.current.localization_info_enabled(
LocalizationUIModel.languageSupport[
state.selectedLanguage] ??
"")),
),
const Spacer(),
ToggleSwitch(
checked: state.patchStatus?.key == true,
onChanged: model.updateLangCfg,
)
],
),
const SizedBox(height: 12),
Row(
children: [
Text(S.current.localization_info_installed_version(
"${state.patchStatus?.value ?? ""} ${(state.isInstalledAdvanced ?? false) ? S.current.home_localization_msg_version_advanced : ""}")),
const Spacer(),
if (state.patchStatus?.value !=
S.current.home_action_info_game_built_in)
Row(
children: [
Button(
onPressed: model.goFeedback,
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.feedback),
const SizedBox(width: 6),
Text(S.current
.localization_action_translation_feedback),
],
),
)),
const SizedBox(width: 16),
Button(
onPressed: model.doDelIniFile(),
child: Padding(
padding: const EdgeInsets.all(4),
child: Row(
children: [
const Icon(FluentIcons.delete),
const SizedBox(width: 6),
Text(S.current
.localization_action_uninstall_translation),
],
),
)),
],
),
],
),
],
],
context),
makeListContainer( makeListContainer(
S.current.localization_info_community_translation, S.current.localization_info_community_translation,
[ [
@ -352,15 +244,37 @@ class LocalizationDialogUI extends HookConsumerWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Text(S.current.home_action_localization_management), Text(S.current.home_action_localization_management),
const SizedBox(width: 24), const SizedBox(width: 24),
Text( // Text(
"${model.getScInstallPath()}", // "${model.getScInstallPath()}",
style: const TextStyle(fontSize: 13), // style: const TextStyle(fontSize: 13),
), // ),
const Spacer(), const Spacer(),
SizedBox( SizedBox(
height: 36, height: 36,
child: Row( child: Row(
children: [ children: [
Text(
S.current.localization_info_channel(""),
style: const TextStyle(fontSize: 16),
),
const SizedBox(width: 12),
ComboBox<String>(
value: state.selectedChannel,
items: [
for (final channel in ["LIVE", "PTU"])
ComboBoxItem(
value: channel,
child: Text(channel),
)
],
onChanged: state.workingVersion.isNotEmpty
? null
: (v) {
if (v == null) return;
model.selectChannel(v);
},
),
const SizedBox(width: 24),
Text( Text(
S.current.localization_info_language, S.current.localization_info_language,
style: const TextStyle(fontSize: 16), style: const TextStyle(fontSize: 16),
@ -453,8 +367,7 @@ class LocalizationDialogUI extends HookConsumerWidget {
), ),
confirm: S.current.localization_action_install, confirm: S.current.localization_action_install,
cancel: S.current.home_action_cancel, cancel: S.current.home_action_cancel,
constraints: constraints: const BoxConstraints(maxWidth: 720),
BoxConstraints(maxWidth: MediaQuery.of(context).size.width * .45),
); );
if (userOK) { if (userOK) {
if (!context.mounted) return; if (!context.mounted) return;

View File

@ -1,26 +1,24 @@
// ignore_for_file: avoid_build_context_in_providers // ignore_for_file: avoid_build_context_in_providers
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'package:archive/archive.dart';
import 'dart:js_interop';
import 'package:archive/archive_io.dart';
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:riverpod_annotation/riverpod_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/api/api.dart';
import 'package:starcitizen_doctor/common/conf/const_conf.dart';
import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/io/rs_http.dart';
import 'package:starcitizen_doctor/common/utils/log.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/data/sc_localization_data.dart';
import 'package:starcitizen_doctor/generated/no_l10n_strings.dart'; import 'package:starcitizen_doctor/generated/no_l10n_strings.dart';
import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/home_ui_model.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:web/web.dart' as web;
part 'localization_ui_model.g.dart'; part 'localization_ui_model.g.dart';
part 'localization_ui_model.freezed.dart'; part 'localization_ui_model.freezed.dart';
@ -29,6 +27,7 @@ part 'localization_ui_model.freezed.dart';
class LocalizationUIState with _$LocalizationUIState { class LocalizationUIState with _$LocalizationUIState {
factory LocalizationUIState({ factory LocalizationUIState({
String? selectedLanguage, String? selectedLanguage,
@Default("LIVE") String selectedChannel,
Map<String, ScLocalizationData>? apiLocalizationData, Map<String, ScLocalizationData>? apiLocalizationData,
@Default("") String workingVersion, @Default("") String workingVersion,
MapEntry<bool, String>? patchStatus, MapEntry<bool, String>? patchStatus,
@ -44,16 +43,6 @@ class LocalizationUIModel extends _$LocalizationUIModel {
"chinese_(traditional)": NoL10n.langZHT, "chinese_(traditional)": NoL10n.langZHT,
}; };
Directory get _downloadDir =>
Directory("${appGlobalState.applicationSupportDir}\\Localizations");
Directory getDownloadDir() => _downloadDir;
Directory get _scDataDir =>
Directory("${ref.read(homeUIModelProvider).scInstalledPath}\\data");
File get _cfgFile => File("${_scDataDir.absolute.path}\\system.cfg");
StreamSubscription? _customizeDirListenSub; StreamSubscription? _customizeDirListenSub;
String get _scInstallPath => ref.read(homeUIModelProvider).scInstalledPath!; String get _scInstallPath => ref.read(homeUIModelProvider).scInstalledPath!;
@ -66,9 +55,6 @@ class LocalizationUIModel extends _$LocalizationUIModel {
} }
Future<void> _init() async { Future<void> _init() async {
if (_scInstallPath == "not_install") {
return;
}
ref.onDispose(() { ref.onDispose(() {
_customizeDirListenSub?.cancel(); _customizeDirListenSub?.cancel();
_customizeDirListenSub = null; _customizeDirListenSub = null;
@ -85,14 +71,13 @@ class LocalizationUIModel extends _$LocalizationUIModel {
Future<void> _loadData() async { Future<void> _loadData() async {
_allVersionLocalizationData.clear(); _allVersionLocalizationData.clear();
await _updateStatus();
for (var lang in languageSupport.keys) { for (var lang in languageSupport.keys) {
final l = await Api.getScLocalizationData(lang).unwrap(); final l = await Api.getScLocalizationData(lang).unwrap();
if (l != null) { if (l != null) {
if (lang == state.selectedLanguage) { if (lang == state.selectedLanguage) {
final apiLocalizationData = <String, ScLocalizationData>{}; final apiLocalizationData = <String, ScLocalizationData>{};
for (var element in l) { for (var element in l) {
final isPTU = !_scInstallPath.contains("LIVE"); final isPTU = !state.selectedChannel.contains("LIVE");
if (isPTU && element.gameChannel == "PTU") { if (isPTU && element.gameChannel == "PTU") {
apiLocalizationData[element.versionName ?? ""] = element; apiLocalizationData[element.versionName ?? ""] = element;
} else if (!isPTU && element.gameChannel == "PU") { } else if (!isPTU && element.gameChannel == "PU") {
@ -110,87 +95,19 @@ class LocalizationUIModel extends _$LocalizationUIModel {
} }
} }
void checkUserCfg(BuildContext context) async { Future<String> genLangCfg() 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,
S.current.localization_info_remove_incompatible_translation_params,
Text(S.current
.localization_info_incompatible_translation_params_warning),
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<void> updateLangCfg(bool enable) async {
final selectedLanguage = state.selectedLanguage!; final selectedLanguage = state.selectedLanguage!;
final status = await _getLangCfgEnableLang(lang: selectedLanguage);
final exists = await _cfgFile.exists();
if (status == enable) {
await _updateStatus();
return;
}
StringBuffer newStr = StringBuffer(); StringBuffer newStr = StringBuffer();
var str = <String>[]; if (!newStr.toString().contains("sys_languages=$selectedLanguage")) {
if (exists) { newStr.writeln("sys_languages=$selectedLanguage");
str = (await _cfgFile.readAsString()).replaceAll(" ", "").split("\n");
} }
if (enable) { if (!newStr.toString().contains("g_language=$selectedLanguage")) {
if (exists) { newStr.writeln("g_language=$selectedLanguage");
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); if (!newStr.toString().contains("g_languageAudio")) {
await _cfgFile.create(recursive: true); newStr.writeln("g_languageAudio=english");
await _cfgFile.writeAsString(newStr.toString()); }
await _updateStatus(); return newStr.toString();
} }
void goFeedback() { void goFeedback() {
@ -198,13 +115,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
} }
VoidCallback? doDelIniFile() { VoidCallback? doDelIniFile() {
return () async { 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();
};
} }
String getCustomizeFileName(String path) { String getCustomizeFileName(String path) {
@ -214,8 +125,7 @@ class LocalizationUIModel extends _$LocalizationUIModel {
installFormString(StringBuffer globalIni, String versionName, installFormString(StringBuffer globalIni, String versionName,
{bool? advanced}) async { {bool? advanced}) async {
dPrint("LocalizationUIModel -> installFormString $versionName"); dPrint("LocalizationUIModel -> installFormString $versionName");
final iniFile = File(
"${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
if (versionName.isNotEmpty) { if (versionName.isNotEmpty) {
if (!globalIni.toString().endsWith("\n")) { if (!globalIni.toString().endsWith("\n")) {
globalIni.write("\n"); globalIni.write("\n");
@ -226,39 +136,40 @@ class LocalizationUIModel extends _$LocalizationUIModel {
globalIni globalIni
.write("_starcitizen_doctor_localization_version=$versionName\n"); .write("_starcitizen_doctor_localization_version=$versionName\n");
} }
final selectedLanguage = state.selectedLanguage!;
final iniFileString = "\uFEFF${globalIni.toString().trim()}";
final cfg = await genLangCfg();
final archive = Archive();
archive.addFile(ArchiveFile(
"data/Localization/$selectedLanguage/global.ini", 0, iniFileString));
archive.addFile(ArchiveFile("data/system.cfg", 0, cfg));
final zip = await compute(_encodeZipFile, archive);
if (zip == null) return;
final blob = Blob.fromBytes(zip, opt: {
"type": "application/zip",
});
final url = web.URL.createObjectURL(blob);
jsDownloadBlobFile(url, "Localization_$versionName.zip");
}
/// write cfg List<int>? _encodeZipFile(Archive archive) {
if (await _cfgFile.exists()) {} final zip = ZipEncoder().encode(archive);
return zip;
/// 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();
} }
VoidCallback? doRemoteInstall( VoidCallback? doRemoteInstall(
BuildContext context, ScLocalizationData value) { BuildContext context, ScLocalizationData value) {
return () async { return () async {
AnalyticsApi.touch("install_localization"); // AnalyticsApi.touch("install_localization");
final savePath = // final savePath =
File("${_downloadDir.absolute.path}\\${value.versionName}.sclang"); // File("${_downloadDir.absolute.path}\\${value.versionName}.sclang");
try { try {
state = state.copyWith(workingVersion: value.versionName!); state = state.copyWith(workingVersion: value.versionName!);
if (!await savePath.exists()) { final data = await downloadLocalizationFile(value);
// download
await downloadLocalizationFile(savePath, value);
} else {
dPrint("use cache $savePath");
}
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
// check file // check file
final globalIni = await compute(readArchive, savePath.absolute.path); final globalIni = await compute(readArchive, data);
if (globalIni.isEmpty) { if (globalIni.isEmpty) {
throw S.current.localization_info_corrupted_file; throw S.current.localization_info_corrupted_file;
} }
@ -267,30 +178,25 @@ class LocalizationUIModel extends _$LocalizationUIModel {
if (!context.mounted) return; if (!context.mounted) return;
await showToast( await showToast(
context, S.current.localization_info_installation_error(e)); context, S.current.localization_info_installation_error(e));
if (await savePath.exists()) await savePath.delete();
} }
state = state.copyWith(workingVersion: ""); state = state.copyWith(workingVersion: "");
}; };
} }
Future<void> downloadLocalizationFile( Future<Uint8List> downloadLocalizationFile(ScLocalizationData value) async {
File savePath, ScLocalizationData value) async { dPrint("downloading downloadLocalizationFile ...");
dPrint("downloading file to $savePath");
final downloadUrl = final downloadUrl =
"${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz"; "${URLConf.gitlabLocalizationUrl}/archive/${value.versionName}.tar.gz";
final r = await RSHttp.get(downloadUrl); final r = await RSHttp.get(downloadUrl);
if (r.statusCode == 200 && r.data != null) { if (r.statusCode == 200 && r.data != null) {
await savePath.create(recursive: true); return r.data!;
await savePath.writeAsBytes(r.data!, flush: true);
} else { } else {
throw "statusCode Error : ${r.statusCode}"; throw "statusCode Error : ${r.statusCode}";
} }
} }
static StringBuffer readArchive(String savePath) { static StringBuffer readArchive(Uint8List data) {
final inputStream = InputFileStream(savePath); final archive = TarDecoder().decodeBytes(GZipDecoder().decodeBytes(data));
final archive =
TarDecoder().decodeBytes(GZipDecoder().decodeBuffer(inputStream));
StringBuffer globalIni = StringBuffer(""); StringBuffer globalIni = StringBuffer("");
for (var element in archive.files) { for (var element in archive.files) {
if (element.name.contains("global.ini")) { if (element.name.contains("global.ini")) {
@ -331,57 +237,6 @@ class LocalizationUIModel extends _$LocalizationUIModel {
}; };
} }
_updateStatus() async {
final patchStatus = MapEntry(
await _getLangCfgEnableLang(lang: state.selectedLanguage!),
await _getInstalledIniVersion(
"${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini"));
final isInstalledAdvanced = await _checkAdvancedStatus(
"${_scDataDir.absolute.path}\\Localization\\${state.selectedLanguage}\\global.ini");
state = state.copyWith(
patchStatus: patchStatus, isInstalledAdvanced: isInstalledAdvanced);
}
Future<bool> _checkAdvancedStatus(String path) async {
final iniFile = File(path);
if (!await iniFile.exists()) {
return false;
}
final iniString = (await iniFile.readAsString());
return iniString.contains("_starcitizen_doctor_localization_advanced=true");
}
Future<bool> _getLangCfgEnableLang(
{String lang = "", String gamePath = ""}) async {
if (gamePath.isEmpty) {
gamePath = _scInstallPath;
}
final cfgFile = File("${_scDataDir.absolute.path}\\system.cfg");
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<String> _getInstalledIniVersion(String iniPath) async {
final iniFile = File(iniPath);
if (!await iniFile.exists()) {
return S.current.home_action_info_game_built_in;
}
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 S.current.localization_info_custom_files;
}
Future<List<String>> checkLangUpdate({bool skipReload = false}) async { Future<List<String>> checkLangUpdate({bool skipReload = false}) async {
if (_scInstallPath == "not_install") { if (_scInstallPath == "not_install") {
return []; return [];
@ -394,40 +249,29 @@ class LocalizationUIModel extends _$LocalizationUIModel {
if (homeState.scInstallPaths.isEmpty) return []; if (homeState.scInstallPaths.isEmpty) return [];
List<String> updates = []; List<String> updates = [];
for (var scInstallPath in homeState.scInstallPaths) {
//
final scDataDir = Directory("$scInstallPath\\data\\Localization");
//
final dirList = await scDataDir.list().toList();
for (var element in dirList) {
for (var lang in languageSupport.keys) {
if (element.path.contains(lang) &&
await _getLangCfgEnableLang(
lang: lang, gamePath: scInstallPath)) {
final installedVersion =
await _getInstalledIniVersion("${element.path}\\global.ini");
if (installedVersion == S.current.home_action_info_game_built_in ||
installedVersion == S.current.localization_info_custom_files) {
continue;
}
final curData = _allVersionLocalizationData[lang];
dPrint("check Localization update $scInstallPath");
if (!(curData?.keys.contains(installedVersion) ?? false)) {
// has update
for (var channel in ConstConf.gameChannels) {
if (scInstallPath.contains(channel)) {
dPrint("check Localization update: has update -> $channel");
updates.add(channel);
}
}
} else {
dPrint("check Localization update: up to date");
}
}
}
}
}
return updates; return updates;
} }
void selectChannel(String v) {
state = state.copyWith(selectedChannel: v);
_loadData();
}
} }
@JS("Blob")
extension type Blob._(JSObject _) implements JSObject {
external factory Blob(JSArray<JSArrayBuffer> blobParts, JSAny? options);
factory Blob.fromBytes(List<int> bytes, {Map? opt}) {
final data = Uint8List.fromList(bytes).buffer.toJS;
return Blob([data].toJS, opt?.jsify());
}
external JSArrayBuffer? get blobParts;
external JSObject? get options;
}
@JS()
external void jsDownloadBlobFile(String blobUrl, String filename);

View File

@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc /// @nodoc
mixin _$LocalizationUIState { mixin _$LocalizationUIState {
String? get selectedLanguage => throw _privateConstructorUsedError; String? get selectedLanguage => throw _privateConstructorUsedError;
String get selectedChannel => throw _privateConstructorUsedError;
Map<String, ScLocalizationData>? get apiLocalizationData => Map<String, ScLocalizationData>? get apiLocalizationData =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
String get workingVersion => throw _privateConstructorUsedError; String get workingVersion => throw _privateConstructorUsedError;
@ -39,6 +40,7 @@ abstract class $LocalizationUIStateCopyWith<$Res> {
@useResult @useResult
$Res call( $Res call(
{String? selectedLanguage, {String? selectedLanguage,
String selectedChannel,
Map<String, ScLocalizationData>? apiLocalizationData, Map<String, ScLocalizationData>? apiLocalizationData,
String workingVersion, String workingVersion,
MapEntry<bool, String>? patchStatus, MapEntry<bool, String>? patchStatus,
@ -62,6 +64,7 @@ class _$LocalizationUIStateCopyWithImpl<$Res, $Val extends LocalizationUIState>
@override @override
$Res call({ $Res call({
Object? selectedLanguage = freezed, Object? selectedLanguage = freezed,
Object? selectedChannel = null,
Object? apiLocalizationData = freezed, Object? apiLocalizationData = freezed,
Object? workingVersion = null, Object? workingVersion = null,
Object? patchStatus = freezed, Object? patchStatus = freezed,
@ -73,6 +76,10 @@ class _$LocalizationUIStateCopyWithImpl<$Res, $Val extends LocalizationUIState>
? _value.selectedLanguage ? _value.selectedLanguage
: selectedLanguage // ignore: cast_nullable_to_non_nullable : selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?, as String?,
selectedChannel: null == selectedChannel
? _value.selectedChannel
: selectedChannel // ignore: cast_nullable_to_non_nullable
as String,
apiLocalizationData: freezed == apiLocalizationData apiLocalizationData: freezed == apiLocalizationData
? _value.apiLocalizationData ? _value.apiLocalizationData
: apiLocalizationData // ignore: cast_nullable_to_non_nullable : apiLocalizationData // ignore: cast_nullable_to_non_nullable
@ -107,6 +114,7 @@ abstract class _$$LocalizationUIStateImplCopyWith<$Res>
@useResult @useResult
$Res call( $Res call(
{String? selectedLanguage, {String? selectedLanguage,
String selectedChannel,
Map<String, ScLocalizationData>? apiLocalizationData, Map<String, ScLocalizationData>? apiLocalizationData,
String workingVersion, String workingVersion,
MapEntry<bool, String>? patchStatus, MapEntry<bool, String>? patchStatus,
@ -128,6 +136,7 @@ class __$$LocalizationUIStateImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? selectedLanguage = freezed, Object? selectedLanguage = freezed,
Object? selectedChannel = null,
Object? apiLocalizationData = freezed, Object? apiLocalizationData = freezed,
Object? workingVersion = null, Object? workingVersion = null,
Object? patchStatus = freezed, Object? patchStatus = freezed,
@ -139,6 +148,10 @@ class __$$LocalizationUIStateImplCopyWithImpl<$Res>
? _value.selectedLanguage ? _value.selectedLanguage
: selectedLanguage // ignore: cast_nullable_to_non_nullable : selectedLanguage // ignore: cast_nullable_to_non_nullable
as String?, as String?,
selectedChannel: null == selectedChannel
? _value.selectedChannel
: selectedChannel // ignore: cast_nullable_to_non_nullable
as String,
apiLocalizationData: freezed == apiLocalizationData apiLocalizationData: freezed == apiLocalizationData
? _value._apiLocalizationData ? _value._apiLocalizationData
: apiLocalizationData // ignore: cast_nullable_to_non_nullable : apiLocalizationData // ignore: cast_nullable_to_non_nullable
@ -168,6 +181,7 @@ class __$$LocalizationUIStateImplCopyWithImpl<$Res>
class _$LocalizationUIStateImpl implements _LocalizationUIState { class _$LocalizationUIStateImpl implements _LocalizationUIState {
_$LocalizationUIStateImpl( _$LocalizationUIStateImpl(
{this.selectedLanguage, {this.selectedLanguage,
this.selectedChannel = "LIVE",
final Map<String, ScLocalizationData>? apiLocalizationData, final Map<String, ScLocalizationData>? apiLocalizationData,
this.workingVersion = "", this.workingVersion = "",
this.patchStatus, this.patchStatus,
@ -178,6 +192,9 @@ class _$LocalizationUIStateImpl implements _LocalizationUIState {
@override @override
final String? selectedLanguage; final String? selectedLanguage;
@override
@JsonKey()
final String selectedChannel;
final Map<String, ScLocalizationData>? _apiLocalizationData; final Map<String, ScLocalizationData>? _apiLocalizationData;
@override @override
Map<String, ScLocalizationData>? get apiLocalizationData { Map<String, ScLocalizationData>? get apiLocalizationData {
@ -208,7 +225,7 @@ class _$LocalizationUIStateImpl implements _LocalizationUIState {
@override @override
String toString() { String toString() {
return 'LocalizationUIState(selectedLanguage: $selectedLanguage, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)'; return 'LocalizationUIState(selectedLanguage: $selectedLanguage, selectedChannel: $selectedChannel, apiLocalizationData: $apiLocalizationData, workingVersion: $workingVersion, patchStatus: $patchStatus, isInstalledAdvanced: $isInstalledAdvanced, customizeList: $customizeList)';
} }
@override @override
@ -218,6 +235,8 @@ class _$LocalizationUIStateImpl implements _LocalizationUIState {
other is _$LocalizationUIStateImpl && other is _$LocalizationUIStateImpl &&
(identical(other.selectedLanguage, selectedLanguage) || (identical(other.selectedLanguage, selectedLanguage) ||
other.selectedLanguage == selectedLanguage) && other.selectedLanguage == selectedLanguage) &&
(identical(other.selectedChannel, selectedChannel) ||
other.selectedChannel == selectedChannel) &&
const DeepCollectionEquality() const DeepCollectionEquality()
.equals(other._apiLocalizationData, _apiLocalizationData) && .equals(other._apiLocalizationData, _apiLocalizationData) &&
(identical(other.workingVersion, workingVersion) || (identical(other.workingVersion, workingVersion) ||
@ -234,6 +253,7 @@ class _$LocalizationUIStateImpl implements _LocalizationUIState {
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
selectedLanguage, selectedLanguage,
selectedChannel,
const DeepCollectionEquality().hash(_apiLocalizationData), const DeepCollectionEquality().hash(_apiLocalizationData),
workingVersion, workingVersion,
patchStatus, patchStatus,
@ -253,6 +273,7 @@ class _$LocalizationUIStateImpl implements _LocalizationUIState {
abstract class _LocalizationUIState implements LocalizationUIState { abstract class _LocalizationUIState implements LocalizationUIState {
factory _LocalizationUIState( factory _LocalizationUIState(
{final String? selectedLanguage, {final String? selectedLanguage,
final String selectedChannel,
final Map<String, ScLocalizationData>? apiLocalizationData, final Map<String, ScLocalizationData>? apiLocalizationData,
final String workingVersion, final String workingVersion,
final MapEntry<bool, String>? patchStatus, final MapEntry<bool, String>? patchStatus,
@ -262,6 +283,8 @@ abstract class _LocalizationUIState implements LocalizationUIState {
@override @override
String? get selectedLanguage; String? get selectedLanguage;
@override @override
String get selectedChannel;
@override
Map<String, ScLocalizationData>? get apiLocalizationData; Map<String, ScLocalizationData>? get apiLocalizationData;
@override @override
String get workingVersion; String get workingVersion;

View File

@ -7,7 +7,7 @@ part of 'localization_ui_model.dart';
// ************************************************************************** // **************************************************************************
String _$localizationUIModelHash() => String _$localizationUIModelHash() =>
r'd08a3d100c72b3f1f4a6c96944d4a73bb3c21808'; r'404f40f1f2c04494ce570173262ee0d8579b30b4';
/// See also [LocalizationUIModel]. /// See also [LocalizationUIModel].
@ProviderFor(LocalizationUIModel) @ProviderFor(LocalizationUIModel)

View File

@ -14,6 +14,8 @@ import 'package:starcitizen_doctor/common/conf/url_conf.dart';
import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart';
import 'package:web/web.dart' as web;
class SplashUI extends HookConsumerWidget { class SplashUI extends HookConsumerWidget {
const SplashUI({super.key}); const SplashUI({super.key});
@ -102,7 +104,7 @@ class SplashUI extends HookConsumerWidget {
if (userOk) { if (userOk) {
await appConf.put("splash_alert_info_version", _alertInfoVersion); await appConf.put("splash_alert_info_version", _alertInfoVersion);
} else { } else {
exit(0); web.window.close();
} }
} }
} }

View File

@ -53,6 +53,8 @@ dependencies:
super_sliver_list: ^0.4.1 super_sliver_list: ^0.4.1
re_editor: ^0.3.0 re_editor: ^0.3.0
re_highlight: ^0.0.3 re_highlight: ^0.0.3
cross_file: ^0.3.4+2
web: ^1.0.0
dependency_overrides: dependency_overrides:
http: ^1.1.2 http: ^1.1.2

View File

@ -1,40 +1,55 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<!-- <!--
If you are serving your web app in a path other than the root, change the If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from. href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for The path provided below has to start and end with a slash "/" in order for
it to work correctly. it to work correctly.
For more details: For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base * https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`. the `--base-href` argument provided to `flutter build`.
--> -->
<base href="$FLUTTER_BASE_HREF"> <base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible"> <meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project."> <meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons --> <!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black"> <meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="starcitizentoolbox_web"> <meta name="apple-mobile-web-app-title" content="starcitizentoolbox_web">
<link rel="apple-touch-icon" href="icons/Icon-192.png"> <link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/> <link rel="icon" type="image/png" href="favicon.png"/>
<title>starcitizentoolbox_web</title> <title>starcitizentoolbox_web</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<script type="application/javascript" src="/assets/packages/flutter_inappwebview_web/assets/web/web_support.js" defer></script>
<script>
function jsDownloadBlobFile(blobUrl, filename) {
const a = document.createElement("a");
document.body.appendChild(a);
a.href = blobUrl;
a.download = filename;
a.click();
setTimeout(() => {
window.URL.revokeObjectURL(blobUrl);
document.body.removeChild(a);
}, 0);
}
</script>
</head> </head>
<body> <body>
<script src="flutter_bootstrap.js" async></script> <script src="flutter_bootstrap.js" async></script>
</body> </body>
</html> </html>