diff --git a/assets/binary/unp4kc.zip b/assets/binary/unp4kc.zip new file mode 100644 index 0000000..efaf4ee Binary files /dev/null and b/assets/binary/unp4kc.zip differ diff --git a/lib/app.dart b/lib/app.dart index fac870f..0a35093 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -29,6 +29,7 @@ import 'ui/home/downloader/home_downloader_ui.dart'; import 'ui/home/game_doctor/game_doctor_ui.dart'; import 'ui/index_ui.dart'; import 'ui/settings/upgrade_dialog.dart'; +import 'ui/tools/unp4kc/unp4kc_ui.dart'; part 'app.g.dart'; @@ -77,6 +78,13 @@ GoRouter router(RouterRef ref) { ), ], ), + GoRoute(path: '/tools', builder: (_, __) => const SizedBox(), routes: [ + GoRoute( + path: 'unp4kc', + pageBuilder: (context, state) => + myPageBuilder(context, state, const UnP4kcUI()), + ), + ]), ], ); } diff --git a/lib/app.g.dart b/lib/app.g.dart index 9fdfa42..f73d8b9 100644 --- a/lib/app.g.dart +++ b/lib/app.g.dart @@ -6,7 +6,7 @@ part of 'app.dart'; // RiverpodGenerator // ************************************************************************** -String _$routerHash() => r'e7b1e3a9fd74b4f00e3d71017615d7fb82bd649d'; +String _$routerHash() => r'7ce5ef6a7a4f6f604a457dd050e04ee594c4760a'; /// See also [router]. @ProviderFor(router) @@ -20,7 +20,7 @@ final routerProvider = AutoDisposeProvider.internal( ); typedef RouterRef = AutoDisposeProviderRef; -String _$appGlobalModelHash() => r'ae7f6704a80297ac3e0f70412c676a1829046831'; +String _$appGlobalModelHash() => r'a604c415d2d855ede8727dd75ef991ab2afcf234'; /// See also [AppGlobalModel]. @ProviderFor(AppGlobalModel) diff --git a/lib/common/conf/binary_conf.dart b/lib/common/conf/binary_conf.dart index 9d34558..13bb7a0 100644 --- a/lib/common/conf/binary_conf.dart +++ b/lib/common/conf/binary_conf.dart @@ -1,12 +1,14 @@ import 'dart:io'; import 'package:archive/archive.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; class BinaryModuleConf { static const _modules = { "aria2c": "0", + "unp4kc": "0", }; static Future extractModule(List modules, String workingDir) async { @@ -16,7 +18,8 @@ class BinaryModuleConf { final version = m.value; final dir = "$workingDir\\$name"; final versionFile = File("$dir\\version"); - if (await versionFile.exists() && + if (kReleaseMode && + await versionFile.exists() && (await versionFile.readAsString()).trim() == version) { dPrint( "BinaryModuleConf.extractModule skip $name version == $version"); diff --git a/lib/data/app_unp4k_p4k_item_data.dart b/lib/data/app_unp4k_p4k_item_data.dart new file mode 100644 index 0000000..465eb86 --- /dev/null +++ b/lib/data/app_unp4k_p4k_item_data.dart @@ -0,0 +1,60 @@ +/// name : "Data\\Textures\\planets\\surface\\ground\\architecture\\city\\city_suburbs_02_displ.dds.6" +/// size : 524288 +/// compressedSize : 169812 +/// isDirectory : false +/// isFile : true +/// isEncrypted : false +/// isUnicodeText : false +/// dateTime : "2019-12-16T15:11:18" +/// version : 45 + +class AppUnp4kP4kItemData { + AppUnp4kP4kItemData({ + this.name, + this.size, + this.compressedSize, + this.isDirectory, + this.isFile, + this.isEncrypted, + this.isUnicodeText, + this.dateTime, + this.version, + }); + + AppUnp4kP4kItemData.fromJson(dynamic json) { + name = json['name']; + size = json['size']; + compressedSize = json['compressedSize']; + isDirectory = json['isDirectory']; + isFile = json['isFile']; + isEncrypted = json['isEncrypted']; + isUnicodeText = json['isUnicodeText']; + dateTime = json['dateTime']; + version = json['version']; + } + + String? name; + num? size; + num? compressedSize; + bool? isDirectory; + bool? isFile; + bool? isEncrypted; + bool? isUnicodeText; + String? dateTime; + num? version; + List children = []; + + Map toJson() { + final map = {}; + map['name'] = name; + map['size'] = size; + map['compressedSize'] = compressedSize; + map['isDirectory'] = isDirectory; + map['isFile'] = isFile; + map['isEncrypted'] = isEncrypted; + map['isUnicodeText'] = isUnicodeText; + map['dateTime'] = dateTime; + map['version'] = version; + return map; + } +} diff --git a/lib/provider/aria2c.g.dart b/lib/provider/aria2c.g.dart index f9db6d6..0c79d2b 100644 --- a/lib/provider/aria2c.g.dart +++ b/lib/provider/aria2c.g.dart @@ -6,7 +6,7 @@ part of 'aria2c.dart'; // RiverpodGenerator // ************************************************************************** -String _$aria2cModelHash() => r'6685f6a716016113487de190a61f71196094526e'; +String _$aria2cModelHash() => r'5431c2d9667f17ff03d0794711af22b015feda0d'; /// See also [Aria2cModel]. @ProviderFor(Aria2cModel) diff --git a/lib/provider/unp4kc.dart b/lib/provider/unp4kc.dart new file mode 100644 index 0000000..30b1dd9 --- /dev/null +++ b/lib/provider/unp4kc.dart @@ -0,0 +1,191 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/memory.dart'; +import 'package:flutter/foundation.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:starcitizen_doctor/common/conf/binary_conf.dart'; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.dart'; +import 'package:starcitizen_doctor/data/app_unp4k_p4k_item_data.dart'; +import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart'; + +part 'unp4kc.freezed.dart'; + +part 'unp4kc.g.dart'; + +@freezed +class Unp4kcState with _$Unp4kcState { + const factory Unp4kcState({ + required bool startUp, + Map? files, + MemoryFileSystem? fs, + required String curPath, + String? endMessage, + MapEntry? tempOpenFile, + }) = _Unp4kcState; +} + +@riverpod +class Unp4kCModel extends _$Unp4kCModel { + Process? _process; + + @override + Unp4kcState build() { + state = + const Unp4kcState(startUp: false, curPath: '\\', endMessage: "初始化中..."); + _init(); + return state; + } + + ToolsUIState get _toolsState => ref.read(toolsUIModelProvider); + + String getGamePath() => _toolsState.scInstalledPath; + + void _init() async { + final execDir = "${appGlobalState.applicationBinaryModuleDir}\\unp4kc"; + await BinaryModuleConf.extractModule( + ["unp4kc"], appGlobalState.applicationBinaryModuleDir!); + final exec = "$execDir\\unp4kc.exe"; + final ps = await Process.start(exec, []); + StringBuffer stringBuffer = StringBuffer(); + _process = ps; + ps.stdout.listen((event) async { + final eventStr = String.fromCharCodes(event); + stringBuffer.write(eventStr); + if (!eventStr.endsWith("\n")) return; + final str = stringBuffer.toString().trim(); + stringBuffer.clear(); + try { + final eventJson = await compute(json.decode, str); + _handleMessage(eventJson, ps); + } catch (e) { + dPrint("[unp4kc] json error: $e"); + } + }); + ps.stderr.listen((event) { + final eventStr = String.fromCharCodes(event); + dPrint("[unp4kc] stderr: $eventStr"); + }); + state = state.copyWith(startUp: true); + ref.onDispose(() { + ps.kill(); + dPrint("[unp4kc] kill ..."); + }); + } + + void _handleMessage(Map eventJson, Process ps) async { + final action = eventJson["action"]; + final data = eventJson["data"]; + final gamePath = getGamePath(); + final gameP4kPath = "$gamePath\\Data.p4k"; + switch (action.toString().trim()) { + case "info: startup": + ps.stdin.writeln(gameP4kPath); + state = state.copyWith(endMessage: "正在读取P4K 文件 ..."); + break; + case "data: P4K_Files": + final p4kFiles = (data as List); + final files = {}; + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + state = state.copyWith(endMessage: "正在处理文件 ..."); + for (var i = 0; i < p4kFiles.length; i++) { + final item = AppUnp4kP4kItemData.fromJson(p4kFiles[i]); + item.name = "${item.name}"; + files["\\${item.name}"] = item; + await fs + .file(item.name?.replaceAll("\\", "/") ?? "") + .create(recursive: true); + } + state = state.copyWith( + files: files, fs: fs, endMessage: "加载完毕:${files.length} 个文件"); + break; + case "info: Extracted_Open": + final filePath = data.toString(); + dPrint("[unp4kc] Extracted_Open file: $filePath"); + const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini"]; + const imgExt = [".png"]; + String openType = "unknown"; + for (var element in textExt) { + if (filePath.endsWith(element)) { + openType = "text"; + } + } + for (var element in imgExt) { + if (filePath.endsWith(element)) { + openType = "image"; + } + } + + state = state.copyWith( + tempOpenFile: MapEntry(openType, filePath), + endMessage: "打开文件:$filePath"); + break; + default: + dPrint("[unp4kc] unknown action: $action"); + break; + } + } + + List? getFiles() { + final path = state.curPath.replaceAll("\\", "/"); + final fs = state.fs; + if (fs == null) return null; + final dir = fs.directory(path); + if (!dir.existsSync()) return []; + final files = dir.listSync(recursive: false, followLinks: false); + files.sort((a, b) { + if (a is Directory && b is File) { + return -1; + } else if (a is File && b is Directory) { + return 1; + } else { + return a.path.compareTo(b.path); + } + }); + final result = []; + for (var file in files) { + if (file is File) { + final f = state.files?[file.path.replaceAll("/", "\\")]; + if (f != null) { + if (!(f.name?.startsWith("\\") ?? true)) { + f.name = "\\${f.name}"; + } + result.add(f); + } + } else { + result.add(AppUnp4kP4kItemData( + name: file.path.replaceAll("/", "\\"), isDirectory: true)); + } + } + return result; + } + + void changeDir(String name, {bool fullPath = false}) { + if (fullPath) { + state = state.copyWith(curPath: name); + } else { + state = state.copyWith(curPath: "${state.curPath}$name\\"); + } + } + + openFile(String filePath) async { + final tempPath = "${appGlobalState.applicationSupportDir}\\temp\\unp4k\\"; + state = state.copyWith( + tempOpenFile: const MapEntry("loading", ""), + endMessage: "读取文件:$filePath ..."); + extractFile(filePath, tempPath, mode: "extract_open"); + } + + extractFile(String filePath, String outputPath, + {String mode = "extract"}) async { + // remove first \\ + if (filePath.startsWith("\\")) { + filePath = filePath.substring(1); + } + outputPath = "$outputPath$filePath"; + dPrint("extractFile .... $filePath"); + _process?.stdin.writeln("$mode<:,:>$filePath<:,:>$outputPath"); + } +} diff --git a/lib/provider/unp4kc.freezed.dart b/lib/provider/unp4kc.freezed.dart new file mode 100644 index 0000000..e4d1aa4 --- /dev/null +++ b/lib/provider/unp4kc.freezed.dart @@ -0,0 +1,269 @@ +// 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 'unp4kc.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 _$Unp4kcState { + bool get startUp => throw _privateConstructorUsedError; + Map? get files => + throw _privateConstructorUsedError; + MemoryFileSystem? get fs => throw _privateConstructorUsedError; + String get curPath => throw _privateConstructorUsedError; + String? get endMessage => throw _privateConstructorUsedError; + MapEntry? get tempOpenFile => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $Unp4kcStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $Unp4kcStateCopyWith<$Res> { + factory $Unp4kcStateCopyWith( + Unp4kcState value, $Res Function(Unp4kcState) then) = + _$Unp4kcStateCopyWithImpl<$Res, Unp4kcState>; + @useResult + $Res call( + {bool startUp, + Map? files, + MemoryFileSystem? fs, + String curPath, + String? endMessage, + MapEntry? tempOpenFile}); +} + +/// @nodoc +class _$Unp4kcStateCopyWithImpl<$Res, $Val extends Unp4kcState> + implements $Unp4kcStateCopyWith<$Res> { + _$Unp4kcStateCopyWithImpl(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? startUp = null, + Object? files = freezed, + Object? fs = freezed, + Object? curPath = null, + Object? endMessage = freezed, + Object? tempOpenFile = freezed, + }) { + return _then(_value.copyWith( + startUp: null == startUp + ? _value.startUp + : startUp // ignore: cast_nullable_to_non_nullable + as bool, + files: freezed == files + ? _value.files + : files // ignore: cast_nullable_to_non_nullable + as Map?, + fs: freezed == fs + ? _value.fs + : fs // ignore: cast_nullable_to_non_nullable + as MemoryFileSystem?, + curPath: null == curPath + ? _value.curPath + : curPath // ignore: cast_nullable_to_non_nullable + as String, + endMessage: freezed == endMessage + ? _value.endMessage + : endMessage // ignore: cast_nullable_to_non_nullable + as String?, + tempOpenFile: freezed == tempOpenFile + ? _value.tempOpenFile + : tempOpenFile // ignore: cast_nullable_to_non_nullable + as MapEntry?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$Unp4kcStateImplCopyWith<$Res> + implements $Unp4kcStateCopyWith<$Res> { + factory _$$Unp4kcStateImplCopyWith( + _$Unp4kcStateImpl value, $Res Function(_$Unp4kcStateImpl) then) = + __$$Unp4kcStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool startUp, + Map? files, + MemoryFileSystem? fs, + String curPath, + String? endMessage, + MapEntry? tempOpenFile}); +} + +/// @nodoc +class __$$Unp4kcStateImplCopyWithImpl<$Res> + extends _$Unp4kcStateCopyWithImpl<$Res, _$Unp4kcStateImpl> + implements _$$Unp4kcStateImplCopyWith<$Res> { + __$$Unp4kcStateImplCopyWithImpl( + _$Unp4kcStateImpl _value, $Res Function(_$Unp4kcStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? startUp = null, + Object? files = freezed, + Object? fs = freezed, + Object? curPath = null, + Object? endMessage = freezed, + Object? tempOpenFile = freezed, + }) { + return _then(_$Unp4kcStateImpl( + startUp: null == startUp + ? _value.startUp + : startUp // ignore: cast_nullable_to_non_nullable + as bool, + files: freezed == files + ? _value._files + : files // ignore: cast_nullable_to_non_nullable + as Map?, + fs: freezed == fs + ? _value.fs + : fs // ignore: cast_nullable_to_non_nullable + as MemoryFileSystem?, + curPath: null == curPath + ? _value.curPath + : curPath // ignore: cast_nullable_to_non_nullable + as String, + endMessage: freezed == endMessage + ? _value.endMessage + : endMessage // ignore: cast_nullable_to_non_nullable + as String?, + tempOpenFile: freezed == tempOpenFile + ? _value.tempOpenFile + : tempOpenFile // ignore: cast_nullable_to_non_nullable + as MapEntry?, + )); + } +} + +/// @nodoc + +class _$Unp4kcStateImpl with DiagnosticableTreeMixin implements _Unp4kcState { + const _$Unp4kcStateImpl( + {required this.startUp, + final Map? files, + this.fs, + required this.curPath, + this.endMessage, + this.tempOpenFile}) + : _files = files; + + @override + final bool startUp; + final Map? _files; + @override + Map? get files { + final value = _files; + if (value == null) return null; + if (_files is EqualUnmodifiableMapView) return _files; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + final MemoryFileSystem? fs; + @override + final String curPath; + @override + final String? endMessage; + @override + final MapEntry? tempOpenFile; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'Unp4kcState')) + ..add(DiagnosticsProperty('startUp', startUp)) + ..add(DiagnosticsProperty('files', files)) + ..add(DiagnosticsProperty('fs', fs)) + ..add(DiagnosticsProperty('curPath', curPath)) + ..add(DiagnosticsProperty('endMessage', endMessage)) + ..add(DiagnosticsProperty('tempOpenFile', tempOpenFile)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$Unp4kcStateImpl && + (identical(other.startUp, startUp) || other.startUp == startUp) && + const DeepCollectionEquality().equals(other._files, _files) && + (identical(other.fs, fs) || other.fs == fs) && + (identical(other.curPath, curPath) || other.curPath == curPath) && + (identical(other.endMessage, endMessage) || + other.endMessage == endMessage) && + (identical(other.tempOpenFile, tempOpenFile) || + other.tempOpenFile == tempOpenFile)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + startUp, + const DeepCollectionEquality().hash(_files), + fs, + curPath, + endMessage, + tempOpenFile); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$Unp4kcStateImplCopyWith<_$Unp4kcStateImpl> get copyWith => + __$$Unp4kcStateImplCopyWithImpl<_$Unp4kcStateImpl>(this, _$identity); +} + +abstract class _Unp4kcState implements Unp4kcState { + const factory _Unp4kcState( + {required final bool startUp, + final Map? files, + final MemoryFileSystem? fs, + required final String curPath, + final String? endMessage, + final MapEntry? tempOpenFile}) = _$Unp4kcStateImpl; + + @override + bool get startUp; + @override + Map? get files; + @override + MemoryFileSystem? get fs; + @override + String get curPath; + @override + String? get endMessage; + @override + MapEntry? get tempOpenFile; + @override + @JsonKey(ignore: true) + _$$Unp4kcStateImplCopyWith<_$Unp4kcStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/provider/unp4kc.g.dart b/lib/provider/unp4kc.g.dart new file mode 100644 index 0000000..f0ef991 --- /dev/null +++ b/lib/provider/unp4kc.g.dart @@ -0,0 +1,25 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'unp4kc.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$unp4kCModelHash() => r'46b6ac12670a6ff6ffb9d5bc8a8d04c07c570a8c'; + +/// See also [Unp4kCModel]. +@ProviderFor(Unp4kCModel) +final unp4kCModelProvider = + AutoDisposeNotifierProvider.internal( + Unp4kCModel.new, + name: r'unp4kCModelProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$unp4kCModelHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$Unp4kCModel = 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/ui/home/game_doctor/game_doctor_ui_model.g.dart b/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart index c401157..bb763b1 100644 --- a/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart +++ b/lib/ui/home/game_doctor/game_doctor_ui_model.g.dart @@ -7,7 +7,7 @@ part of 'game_doctor_ui_model.dart'; // ************************************************************************** String _$homeGameDoctorUIModelHash() => - r'b4132559510e3e59b1e2e330d9327ff8790df461'; + r'137f6393bbbd76f3af0f7d0dd27d44d8473e42cc'; /// See also [HomeGameDoctorUIModel]. @ProviderFor(HomeGameDoctorUIModel) diff --git a/lib/ui/home/localization/localization_ui_model.g.dart b/lib/ui/home/localization/localization_ui_model.g.dart index 3945a6f..dddaaff 100644 --- a/lib/ui/home/localization/localization_ui_model.g.dart +++ b/lib/ui/home/localization/localization_ui_model.g.dart @@ -7,7 +7,7 @@ part of 'localization_ui_model.dart'; // ************************************************************************** String _$localizationUIModelHash() => - r'87152654734d322cd20d62baf050918adc9fd11b'; + r'da9d0a3ae28825fd9331dd2b6db3d094cf3c0eb9'; /// See also [LocalizationUIModel]. @ProviderFor(LocalizationUIModel) diff --git a/lib/ui/settings/settings_ui_model.g.dart b/lib/ui/settings/settings_ui_model.g.dart index 84b69ea..a9b4154 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'acc2a90f5bbfc6ba82b17454e73881ac32b30b6a'; +String _$settingsUIModelHash() => r'aab08176293b380f09c89e006f373fbfd7a7ba16'; /// See also [SettingsUIModel]. @ProviderFor(SettingsUIModel) diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index a9da6f4..e903263 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -104,6 +104,13 @@ class ToolsUIModel extends _$ToolsUIModel { const Icon(FluentIcons.admin, size: 24), onTap: () => _adminRSILauncher(context), ), + ToolsItemData( + "unp4kc", + "P4K 查看器", + "解包星际公民 p4k 文件", + const Icon(FontAwesomeIcons.fileZipper, size: 24), + onTap: () => _unp4kc(context), + ), ]; state = state.copyWith(items: items); @@ -573,4 +580,8 @@ class ToolsUIModel extends _$ToolsUIModel { context: context, builder: (BuildContext context) => const HostsBoosterDialogUI()); } + + _unp4kc(BuildContext context) async { + context.push("/tools/unp4kc"); + } } diff --git a/lib/ui/tools/tools_ui_model.g.dart b/lib/ui/tools/tools_ui_model.g.dart index 17347fc..e0ae79d 100644 --- a/lib/ui/tools/tools_ui_model.g.dart +++ b/lib/ui/tools/tools_ui_model.g.dart @@ -6,7 +6,7 @@ part of 'tools_ui_model.dart'; // RiverpodGenerator // ************************************************************************** -String _$toolsUIModelHash() => r'e96ded635df8f59fb93d92f8dac337c44482dc1c'; +String _$toolsUIModelHash() => r'5753016dc2cbcf3fafd2fa561d5b91a3295ca04b'; /// See also [ToolsUIModel]. @ProviderFor(ToolsUIModel) diff --git a/lib/ui/tools/unp4kc/unp4kc_ui.dart b/lib/ui/tools/unp4kc/unp4kc_ui.dart new file mode 100644 index 0000000..d3f4306 --- /dev/null +++ b/lib/ui/tools/unp4kc/unp4kc_ui.dart @@ -0,0 +1,272 @@ +import 'dart:io'; + +import 'package:file_sizes/file_sizes.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:re_editor/re_editor.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/provider/unp4kc.dart'; +import 'package:starcitizen_doctor/widgets/widgets.dart'; +import 'package:super_sliver_list/super_sliver_list.dart'; + +class UnP4kcUI extends HookConsumerWidget { + const UnP4kcUI({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(unp4kCModelProvider); + final model = ref.read(unp4kCModelProvider.notifier); + final files = model.getFiles(); + final paths = state.curPath.trim().split("\\"); + return makeDefaultPage(context, + title: "P4K 查看器 -> ${model.getGamePath()}", + useBodyContainer: false, + content: state.files == null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: makeLoading(context)), + if (state.endMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "${state.endMessage}", + style: const TextStyle(fontSize: 12), + ), + ), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + decoration: BoxDecoration( + color: + FluentTheme.of(context).cardColor.withOpacity(.06)), + height: 36, + padding: const EdgeInsets.only(left: 12, right: 12), + child: SuperListView.builder( + itemCount: paths.length - 1, + scrollDirection: Axis.horizontal, + itemBuilder: (BuildContext context, int index) { + var path = paths[index]; + if (path.isEmpty) { + path = "\\"; + } + final fullPath = + "${paths.sublist(0, index + 1).join("\\")}\\"; + return Row( + children: [ + IconButton( + icon: Text(path), + onPressed: () { + model.changeDir(fullPath, fullPath: true); + }, + ), + const Icon( + FluentIcons.chevron_right, + size: 12, + ), + ], + ); + }, + ), + ), + Expanded( + child: Row( + children: [ + Container( + width: MediaQuery.of(context).size.width * .3, + decoration: BoxDecoration( + color: FluentTheme.of(context) + .cardColor + .withOpacity(.01), + ), + child: SuperListView.builder( + padding: const EdgeInsets.only( + top: 6, bottom: 6, left: 3, right: 12), + itemBuilder: (BuildContext context, int index) { + final item = files![index]; + final fileName = item.name + ?.replaceAll(state.curPath.trim(), "") ?? + "?"; + return Container( + margin: const EdgeInsets.only(top: 4, bottom: 4), + decoration: BoxDecoration( + color: FluentTheme.of(context) + .cardColor + .withOpacity(.05), + ), + child: IconButton( + onPressed: () { + if (item.isDirectory ?? false) { + model.changeDir(fileName); + } else { + model.openFile(item.name ?? ""); + } + }, + icon: Padding( + padding: + const EdgeInsets.only(left: 4, right: 4), + child: Row( + children: [ + if (item.isDirectory ?? false) + const Icon( + FluentIcons.folder_fill, + color: + Color.fromRGBO(255, 224, 138, 1), + ) + else if (fileName.endsWith(".xml")) + const Icon( + FluentIcons.file_code, + ) + else + const Icon( + FluentIcons.open_file, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + fileName, + style: const TextStyle( + fontSize: 13), + textAlign: TextAlign.start, + ), + ), + ], + ), + if (!(item.isDirectory ?? + true)) ...[ + const SizedBox(height: 1), + Row( + children: [ + Text( + FileSize.getSize(item.size), + style: TextStyle( + fontSize: 10, + color: Colors.white + .withOpacity(.6)), + ), + const SizedBox(width: 12), + Text( + "${item.dateTime}", + style: TextStyle( + fontSize: 10, + color: Colors.white + .withOpacity(.6)), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(width: 3), + Icon( + FluentIcons.chevron_right, + size: 14, + color: Colors.white.withOpacity(.6), + ) + ], + ), + ), + ), + ); + }, + itemCount: files?.length ?? 0, + ), + ), + Expanded( + child: Container( + child: state.tempOpenFile == null + ? const Center( + child: Text("单击文件以预览"), + ) + : state.tempOpenFile?.key == "loading" + ? makeLoading(context) + : Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + if (state.tempOpenFile?.key == "text") + Expanded( + child: _TextTempWidget( + state.tempOpenFile?.value ?? + "")) + else + Expanded( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + "未知文件类型\n${state.tempOpenFile?.value}"), + const SizedBox(height: 32), + FilledButton( + child: const Padding( + padding: + EdgeInsets.all(4), + child: Text("打开文件夹"), + ), + onPressed: () { + SystemHelper.openDir( + state.tempOpenFile + ?.value ?? + ""); + }) + ], + ), + ), + ) + ], + ), + ), + )) + ], + )), + if (state.endMessage != null) + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "${state.endMessage}", + style: const TextStyle(fontSize: 12), + ), + ), + ], + )); + } +} + +class _TextTempWidget extends HookConsumerWidget { + final String filePath; + + const _TextTempWidget(this.filePath); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final textData = useState(null); + + useEffect(() { + File(filePath).readAsString().then((value) { + textData.value = value; + }).catchError((err) { + textData.value = "Error: $err"; + }); + return null; + }, const []); + + if (textData.value == null) return makeLoading(context); + + return CodeEditor( + controller: CodeLineEditingController.fromText('${textData.value}'), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 59e6a96..cbef5ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -84,6 +84,9 @@ dependencies: # path: ../../xkeyC/dart_aria2_rpc intl: ^0.18.0 synchronized: ^3.1.0+1 + super_sliver_list: ^0.4.1 + file: ^7.0.0 + re_editor: ^0.1.0 dependency_overrides: http: ^1.1.2 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 0bebbaf..b3b3d4f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -21,4 +21,4 @@ hickory-resolver = { version = "0.24" } anyhow = "1.0" win32job = "2" lazy_static = "1.4" -scopeguard = "1.2" +scopeguard = "1.2" \ No newline at end of file diff --git a/rust/src/api/process_api.rs b/rust/src/api/process_api.rs index 7ac562f..5fc88a0 100644 --- a/rust/src/api/process_api.rs +++ b/rust/src/api/process_api.rs @@ -1,7 +1,8 @@ use std::sync::Arc; -use tokio::io::{AsyncBufReadExt, BufReader}; -use crate::frb_generated::StreamSink; +use tokio::io::{AsyncBufReadExt, BufReader}; + +use crate::frb_generated::StreamSink; pub async fn start_process( executable: String, @@ -37,12 +38,16 @@ pub async fn start_process( } let stdout = child.stdout.take().expect("Failed to open stdout"); let stderr = child.stderr.take().expect("Failed to open stderr"); + // let stdin = child.stdin.take().expect("Failed to open stderr"); + let output_task = tokio::spawn(process_output(stdout, stream_sink_arc.clone())); let error_task = tokio::spawn(process_error(stderr, stream_sink_arc.clone())); + // let input_task = tokio::spawn(process_input(stdin)); tokio::select! { _ = output_task => (), _ = error_task => (), + // _ = input_task => (), } let exit_status = child.wait().await.expect("Failed to wait for child process"); @@ -82,5 +87,4 @@ async fn process_error(stderr: R, stream_sink: Arc>) println!("{}", line.trim()); stream_sink.add("error:".to_string() + &*line.trim().to_string()).unwrap(); } -} - +} \ No newline at end of file