diff --git a/assets/binary/unp4kc.zip b/assets/binary/unp4kc.zip deleted file mode 100644 index 4069956..0000000 Binary files a/assets/binary/unp4kc.zip and /dev/null differ diff --git a/lib/common/conf/binary_conf.dart b/lib/common/conf/binary_conf.dart index 41a4aae..39246ac 100644 --- a/lib/common/conf/binary_conf.dart +++ b/lib/common/conf/binary_conf.dart @@ -6,10 +6,7 @@ import 'package:flutter/services.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; class BinaryModuleConf { - static const _modules = { - "aria2c": "0", - "unp4kc": "1", - }; + static const _modules = {"aria2c": "0"}; static Future extractModule(List modules, String workingDir) async { for (var m in _modules.entries) { @@ -18,11 +15,8 @@ class BinaryModuleConf { final version = m.value; final dir = "$workingDir\\$name"; final versionFile = File("$dir\\version"); - if (kReleaseMode && - await versionFile.exists() && - (await versionFile.readAsString()).trim() == version) { - dPrint( - "BinaryModuleConf.extractModule skip $name version == $version"); + if (kReleaseMode && await versionFile.exists() && (await versionFile.readAsString()).trim() == version) { + dPrint("BinaryModuleConf.extractModule skip $name version == $version"); continue; } // write model file diff --git a/lib/common/rust/api/unp4k_api.dart b/lib/common/rust/api/unp4k_api.dart new file mode 100644 index 0000000..a362e5d --- /dev/null +++ b/lib/common/rust/api/unp4k_api.dart @@ -0,0 +1,51 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; +import 'package:freezed_annotation/freezed_annotation.dart' hide protected; +part 'unp4k_api.freezed.dart'; + +// These functions are ignored because they are not marked as `pub`: `dos_datetime_to_millis`, `ensure_files_loaded` + +/// 打开 P4K 文件(仅打开,不读取文件列表) +Future p4KOpen({required String p4KPath}) => + RustLib.instance.api.crateApiUnp4KApiP4KOpen(p4KPath: p4KPath); + +/// 获取文件数量(会触发文件列表加载) +Future p4KGetFileCount() => + RustLib.instance.api.crateApiUnp4KApiP4KGetFileCount(); + +/// 获取所有文件列表 +Future> p4KGetAllFiles() => + RustLib.instance.api.crateApiUnp4KApiP4KGetAllFiles(); + +/// 提取文件到内存 +Future p4KExtractToMemory({required String filePath}) => + RustLib.instance.api.crateApiUnp4KApiP4KExtractToMemory(filePath: filePath); + +/// 提取文件到磁盘 +Future p4KExtractToDisk({ + required String filePath, + required String outputPath, +}) => RustLib.instance.api.crateApiUnp4KApiP4KExtractToDisk( + filePath: filePath, + outputPath: outputPath, +); + +/// 关闭 P4K 读取器 +Future p4KClose() => RustLib.instance.api.crateApiUnp4KApiP4KClose(); + +/// P4K 文件项信息 +@freezed +sealed class P4kFileItem with _$P4kFileItem { + const factory P4kFileItem({ + required String name, + required bool isDirectory, + required BigInt size, + required BigInt compressedSize, + required PlatformInt64 dateModified, + }) = _P4kFileItem; +} diff --git a/lib/common/rust/api/unp4k_api.freezed.dart b/lib/common/rust/api/unp4k_api.freezed.dart new file mode 100644 index 0000000..d37457c --- /dev/null +++ b/lib/common/rust/api/unp4k_api.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// 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 'unp4k_api.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$P4kFileItem { + + String get name; bool get isDirectory; BigInt get size; BigInt get compressedSize; PlatformInt64 get dateModified; +/// Create a copy of P4kFileItem +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$P4kFileItemCopyWith get copyWith => _$P4kFileItemCopyWithImpl(this as P4kFileItem, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is P4kFileItem&&(identical(other.name, name) || other.name == name)&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.size, size) || other.size == size)&&(identical(other.compressedSize, compressedSize) || other.compressedSize == compressedSize)&&(identical(other.dateModified, dateModified) || other.dateModified == dateModified)); +} + + +@override +int get hashCode => Object.hash(runtimeType,name,isDirectory,size,compressedSize,dateModified); + +@override +String toString() { + return 'P4kFileItem(name: $name, isDirectory: $isDirectory, size: $size, compressedSize: $compressedSize, dateModified: $dateModified)'; +} + + +} + +/// @nodoc +abstract mixin class $P4kFileItemCopyWith<$Res> { + factory $P4kFileItemCopyWith(P4kFileItem value, $Res Function(P4kFileItem) _then) = _$P4kFileItemCopyWithImpl; +@useResult +$Res call({ + String name, bool isDirectory, BigInt size, BigInt compressedSize, PlatformInt64 dateModified +}); + + + + +} +/// @nodoc +class _$P4kFileItemCopyWithImpl<$Res> + implements $P4kFileItemCopyWith<$Res> { + _$P4kFileItemCopyWithImpl(this._self, this._then); + + final P4kFileItem _self; + final $Res Function(P4kFileItem) _then; + +/// Create a copy of P4kFileItem +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? name = null,Object? isDirectory = null,Object? size = null,Object? compressedSize = null,Object? dateModified = null,}) { + return _then(_self.copyWith( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable +as bool,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as BigInt,compressedSize: null == compressedSize ? _self.compressedSize : compressedSize // ignore: cast_nullable_to_non_nullable +as BigInt,dateModified: null == dateModified ? _self.dateModified : dateModified // ignore: cast_nullable_to_non_nullable +as PlatformInt64, + )); +} + +} + + +/// Adds pattern-matching-related methods to [P4kFileItem]. +extension P4kFileItemPatterns on P4kFileItem { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _P4kFileItem value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _P4kFileItem() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _P4kFileItem value) $default,){ +final _that = this; +switch (_that) { +case _P4kFileItem(): +return $default(_that);} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _P4kFileItem value)? $default,){ +final _that = this; +switch (_that) { +case _P4kFileItem() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String name, bool isDirectory, BigInt size, BigInt compressedSize, PlatformInt64 dateModified)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _P4kFileItem() when $default != null: +return $default(_that.name,_that.isDirectory,_that.size,_that.compressedSize,_that.dateModified);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String name, bool isDirectory, BigInt size, BigInt compressedSize, PlatformInt64 dateModified) $default,) {final _that = this; +switch (_that) { +case _P4kFileItem(): +return $default(_that.name,_that.isDirectory,_that.size,_that.compressedSize,_that.dateModified);} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String name, bool isDirectory, BigInt size, BigInt compressedSize, PlatformInt64 dateModified)? $default,) {final _that = this; +switch (_that) { +case _P4kFileItem() when $default != null: +return $default(_that.name,_that.isDirectory,_that.size,_that.compressedSize,_that.dateModified);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _P4kFileItem implements P4kFileItem { + const _P4kFileItem({required this.name, required this.isDirectory, required this.size, required this.compressedSize, required this.dateModified}); + + +@override final String name; +@override final bool isDirectory; +@override final BigInt size; +@override final BigInt compressedSize; +@override final PlatformInt64 dateModified; + +/// Create a copy of P4kFileItem +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$P4kFileItemCopyWith<_P4kFileItem> get copyWith => __$P4kFileItemCopyWithImpl<_P4kFileItem>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _P4kFileItem&&(identical(other.name, name) || other.name == name)&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.size, size) || other.size == size)&&(identical(other.compressedSize, compressedSize) || other.compressedSize == compressedSize)&&(identical(other.dateModified, dateModified) || other.dateModified == dateModified)); +} + + +@override +int get hashCode => Object.hash(runtimeType,name,isDirectory,size,compressedSize,dateModified); + +@override +String toString() { + return 'P4kFileItem(name: $name, isDirectory: $isDirectory, size: $size, compressedSize: $compressedSize, dateModified: $dateModified)'; +} + + +} + +/// @nodoc +abstract mixin class _$P4kFileItemCopyWith<$Res> implements $P4kFileItemCopyWith<$Res> { + factory _$P4kFileItemCopyWith(_P4kFileItem value, $Res Function(_P4kFileItem) _then) = __$P4kFileItemCopyWithImpl; +@override @useResult +$Res call({ + String name, bool isDirectory, BigInt size, BigInt compressedSize, PlatformInt64 dateModified +}); + + + + +} +/// @nodoc +class __$P4kFileItemCopyWithImpl<$Res> + implements _$P4kFileItemCopyWith<$Res> { + __$P4kFileItemCopyWithImpl(this._self, this._then); + + final _P4kFileItem _self; + final $Res Function(_P4kFileItem) _then; + +/// Create a copy of P4kFileItem +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? name = null,Object? isDirectory = null,Object? size = null,Object? compressedSize = null,Object? dateModified = null,}) { + return _then(_P4kFileItem( +name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nullable +as String,isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable +as bool,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable +as BigInt,compressedSize: null == compressedSize ? _self.compressedSize : compressedSize // ignore: cast_nullable_to_non_nullable +as BigInt,dateModified: null == dateModified ? _self.dateModified : dateModified // ignore: cast_nullable_to_non_nullable +as PlatformInt64, + )); +} + + +} + +// dart format on diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index e0ceb9d..64a9bb9 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -7,6 +7,7 @@ import 'api/asar_api.dart'; import 'api/http_api.dart'; import 'api/ort_api.dart'; import 'api/rs_process.dart'; +import 'api/unp4k_api.dart'; import 'api/win32_api.dart'; import 'dart:async'; import 'dart:convert'; @@ -69,7 +70,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => -518970253; + int get rustContentHash => 1801517256; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -119,6 +120,23 @@ abstract class RustLibApi extends BaseApi { required bool useXnnpack, }); + Future crateApiUnp4KApiP4KClose(); + + Future crateApiUnp4KApiP4KExtractToDisk({ + required String filePath, + required String outputPath, + }); + + Future crateApiUnp4KApiP4KExtractToMemory({ + required String filePath, + }); + + Future> crateApiUnp4KApiP4KGetAllFiles(); + + Future crateApiUnp4KApiP4KGetFileCount(); + + Future crateApiUnp4KApiP4KOpen({required String p4KPath}); + Future crateApiAsarApiRsiLauncherAsarDataWriteMainJs({ required RsiLauncherAsarData that, required List content, @@ -456,6 +474,154 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ["modelPath", "modelKey", "quantizationSuffix", "useXnnpack"], ); + @override + Future crateApiUnp4KApiP4KClose() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__unp4k_api__p4k_close(port_); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KCloseConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KCloseConstMeta => + const TaskConstMeta(debugName: "p4k_close", argNames: []); + + @override + Future crateApiUnp4KApiP4KExtractToDisk({ + required String filePath, + required String outputPath, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(filePath); + var arg1 = cst_encode_String(outputPath); + return wire.wire__crate__api__unp4k_api__p4k_extract_to_disk( + port_, + arg0, + arg1, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KExtractToDiskConstMeta, + argValues: [filePath, outputPath], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KExtractToDiskConstMeta => + const TaskConstMeta( + debugName: "p4k_extract_to_disk", + argNames: ["filePath", "outputPath"], + ); + + @override + Future crateApiUnp4KApiP4KExtractToMemory({ + required String filePath, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(filePath); + return wire.wire__crate__api__unp4k_api__p4k_extract_to_memory( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_list_prim_u_8_strict, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KExtractToMemoryConstMeta, + argValues: [filePath], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KExtractToMemoryConstMeta => + const TaskConstMeta( + debugName: "p4k_extract_to_memory", + argNames: ["filePath"], + ); + + @override + Future> crateApiUnp4KApiP4KGetAllFiles() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__unp4k_api__p4k_get_all_files(port_); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_list_p_4_k_file_item, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KGetAllFilesConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KGetAllFilesConstMeta => + const TaskConstMeta(debugName: "p4k_get_all_files", argNames: []); + + @override + Future crateApiUnp4KApiP4KGetFileCount() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__unp4k_api__p4k_get_file_count(port_); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_usize, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KGetFileCountConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KGetFileCountConstMeta => + const TaskConstMeta(debugName: "p4k_get_file_count", argNames: []); + + @override + Future crateApiUnp4KApiP4KOpen({required String p4KPath}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(p4KPath); + return wire.wire__crate__api__unp4k_api__p4k_open(port_, arg0); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiUnp4KApiP4KOpenConstMeta, + argValues: [p4KPath], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiUnp4KApiP4KOpenConstMeta => + const TaskConstMeta(debugName: "p4k_open", argNames: ["p4KPath"]); + @override Future crateApiAsarApiRsiLauncherAsarDataWriteMainJs({ required RsiLauncherAsarData that, @@ -814,12 +980,24 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + PlatformInt64 dco_decode_i_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeI64(raw); + } + @protected List dco_decode_list_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs return (raw as List).map(dco_decode_String).toList(); } + @protected + List dco_decode_list_p_4_k_file_item(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_p_4_k_file_item).toList(); + } + @protected List dco_decode_list_prim_u_8_loose(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -886,6 +1064,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_list_prim_u_8_strict(raw); } + @protected + P4kFileItem dco_decode_p_4_k_file_item(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return P4kFileItem( + name: dco_decode_String(arr[0]), + isDirectory: dco_decode_bool(arr[1]), + size: dco_decode_u_64(arr[2]), + compressedSize: dco_decode_u_64(arr[3]), + dateModified: dco_decode_i_64(arr[4]), + ); + } + @protected ProcessInfo dco_decode_process_info(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -988,6 +1181,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return; } + @protected + BigInt dco_decode_usize(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dcoDecodeU64(raw); + } + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1052,6 +1251,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getInt32(); } + @protected + PlatformInt64 sse_decode_i_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getPlatformInt64(); + } + @protected List sse_decode_list_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1064,6 +1269,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List sse_decode_list_p_4_k_file_item( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_p_4_k_file_item(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1175,6 +1394,23 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + P4kFileItem sse_decode_p_4_k_file_item(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_name = sse_decode_String(deserializer); + var var_isDirectory = sse_decode_bool(deserializer); + var var_size = sse_decode_u_64(deserializer); + var var_compressedSize = sse_decode_u_64(deserializer); + var var_dateModified = sse_decode_i_64(deserializer); + return P4kFileItem( + name: var_name, + isDirectory: var_isDirectory, + size: var_size, + compressedSize: var_compressedSize, + dateModified: var_dateModified, + ); + } + @protected ProcessInfo sse_decode_process_info(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1283,6 +1519,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs } + @protected + BigInt sse_decode_usize(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getBigUint64(); + } + @protected bool cst_encode_bool(bool raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -1414,6 +1656,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putInt32(self); } + @protected + void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putPlatformInt64(self); + } + @protected void sse_encode_list_String(List self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1423,6 +1671,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_p_4_k_file_item( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_p_4_k_file_item(item, serializer); + } + } + @protected void sse_encode_list_prim_u_8_loose( List self, @@ -1540,6 +1800,16 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_p_4_k_file_item(P4kFileItem self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.name, serializer); + sse_encode_bool(self.isDirectory, serializer); + sse_encode_u_64(self.size, serializer); + sse_encode_u_64(self.compressedSize, serializer); + sse_encode_i_64(self.dateModified, serializer); + } + @protected void sse_encode_process_info(ProcessInfo self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -1632,4 +1902,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { void sse_encode_unit(void self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs } + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putBigUint64(self); + } } diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index 28c84db..cee2de8 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -7,6 +7,7 @@ import 'api/asar_api.dart'; import 'api/http_api.dart'; import 'api/ort_api.dart'; import 'api/rs_process.dart'; +import 'api/unp4k_api.dart'; import 'api/win32_api.dart'; import 'dart:async'; import 'dart:convert'; @@ -53,9 +54,15 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_i_32(dynamic raw); + @protected + PlatformInt64 dco_decode_i_64(dynamic raw); + @protected List dco_decode_list_String(dynamic raw); + @protected + List dco_decode_list_p_4_k_file_item(dynamic raw); + @protected List dco_decode_list_prim_u_8_loose(dynamic raw); @@ -89,6 +96,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + @protected + P4kFileItem dco_decode_p_4_k_file_item(dynamic raw); + @protected ProcessInfo dco_decode_process_info(dynamic raw); @@ -122,6 +132,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void dco_decode_unit(dynamic raw); + @protected + BigInt dco_decode_usize(dynamic raw); + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); @@ -156,9 +169,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_i_32(SseDeserializer deserializer); + @protected + PlatformInt64 sse_decode_i_64(SseDeserializer deserializer); + @protected List sse_decode_list_String(SseDeserializer deserializer); + @protected + List sse_decode_list_p_4_k_file_item( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_prim_u_8_loose(SseDeserializer deserializer); @@ -196,6 +217,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + @protected + P4kFileItem sse_decode_p_4_k_file_item(SseDeserializer deserializer); + @protected ProcessInfo sse_decode_process_info(SseDeserializer deserializer); @@ -237,6 +261,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_decode_unit(SseDeserializer deserializer); + @protected + BigInt sse_decode_usize(SseDeserializer deserializer); + @protected ffi.Pointer cst_encode_AnyhowException( AnyhowException raw, @@ -297,6 +324,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return wire.cst_new_box_autoadd_u_64(cst_encode_u_64(raw)); } + @protected + int cst_encode_i_64(PlatformInt64 raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw.toInt(); + } + @protected ffi.Pointer cst_encode_list_String(List raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -307,6 +340,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ans; } + @protected + ffi.Pointer cst_encode_list_p_4_k_file_item( + List raw, + ) { + // Codec=Cst (C-struct based), see doc to use other codecs + final ans = wire.cst_new_list_p_4_k_file_item(raw.length); + for (var i = 0; i < raw.length; ++i) { + cst_api_fill_to_wire_p_4_k_file_item(raw[i], ans.ref.ptr[i]); + } + return ans; + } + @protected ffi.Pointer cst_encode_list_prim_u_8_loose( List raw, @@ -390,6 +435,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return raw.toSigned(64).toInt(); } + @protected + int cst_encode_usize(BigInt raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw.toSigned(64).toInt(); + } + @protected void cst_api_fill_to_wire_box_autoadd_rsi_launcher_asar_data( RsiLauncherAsarData apiObj, @@ -398,6 +449,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_rsi_launcher_asar_data(apiObj, wireObj.ref); } + @protected + void cst_api_fill_to_wire_p_4_k_file_item( + P4kFileItem apiObj, + wire_cst_p_4_k_file_item wireObj, + ) { + wireObj.name = cst_encode_String(apiObj.name); + wireObj.is_directory = cst_encode_bool(apiObj.isDirectory); + wireObj.size = cst_encode_u_64(apiObj.size); + wireObj.compressed_size = cst_encode_u_64(apiObj.compressedSize); + wireObj.date_modified = cst_encode_i_64(apiObj.dateModified); + } + @protected void cst_api_fill_to_wire_process_info( ProcessInfo apiObj, @@ -521,9 +584,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_i_32(int self, SseSerializer serializer); + @protected + void sse_encode_i_64(PlatformInt64 self, SseSerializer serializer); + @protected void sse_encode_list_String(List self, SseSerializer serializer); + @protected + void sse_encode_list_p_4_k_file_item( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_prim_u_8_loose(List self, SseSerializer serializer); @@ -572,6 +644,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_p_4_k_file_item(P4kFileItem self, SseSerializer serializer); + @protected void sse_encode_process_info(ProcessInfo self, SseSerializer serializer); @@ -619,6 +694,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_unit(void self, SseSerializer serializer); + + @protected + void sse_encode_usize(BigInt self, SseSerializer serializer); } // Section: wire_class @@ -917,6 +995,125 @@ class RustLibWire implements BaseWire { ) >(); + void wire__crate__api__unp4k_api__p4k_close(int port_) { + return _wire__crate__api__unp4k_api__p4k_close(port_); + } + + late final _wire__crate__api__unp4k_api__p4k_closePtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_close', + ); + late final _wire__crate__api__unp4k_api__p4k_close = + _wire__crate__api__unp4k_api__p4k_closePtr + .asFunction(); + + void wire__crate__api__unp4k_api__p4k_extract_to_disk( + int port_, + ffi.Pointer file_path, + ffi.Pointer output_path, + ) { + return _wire__crate__api__unp4k_api__p4k_extract_to_disk( + port_, + file_path, + output_path, + ); + } + + late final _wire__crate__api__unp4k_api__p4k_extract_to_diskPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_extract_to_disk', + ); + late final _wire__crate__api__unp4k_api__p4k_extract_to_disk = + _wire__crate__api__unp4k_api__p4k_extract_to_diskPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__unp4k_api__p4k_extract_to_memory( + int port_, + ffi.Pointer file_path, + ) { + return _wire__crate__api__unp4k_api__p4k_extract_to_memory( + port_, + file_path, + ); + } + + late final _wire__crate__api__unp4k_api__p4k_extract_to_memoryPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_extract_to_memory', + ); + late final _wire__crate__api__unp4k_api__p4k_extract_to_memory = + _wire__crate__api__unp4k_api__p4k_extract_to_memoryPtr + .asFunction< + void Function(int, ffi.Pointer) + >(); + + void wire__crate__api__unp4k_api__p4k_get_all_files(int port_) { + return _wire__crate__api__unp4k_api__p4k_get_all_files(port_); + } + + late final _wire__crate__api__unp4k_api__p4k_get_all_filesPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_get_all_files', + ); + late final _wire__crate__api__unp4k_api__p4k_get_all_files = + _wire__crate__api__unp4k_api__p4k_get_all_filesPtr + .asFunction(); + + void wire__crate__api__unp4k_api__p4k_get_file_count(int port_) { + return _wire__crate__api__unp4k_api__p4k_get_file_count(port_); + } + + late final _wire__crate__api__unp4k_api__p4k_get_file_countPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_get_file_count', + ); + late final _wire__crate__api__unp4k_api__p4k_get_file_count = + _wire__crate__api__unp4k_api__p4k_get_file_countPtr + .asFunction(); + + void wire__crate__api__unp4k_api__p4k_open( + int port_, + ffi.Pointer p4k_path, + ) { + return _wire__crate__api__unp4k_api__p4k_open(port_, p4k_path); + } + + late final _wire__crate__api__unp4k_api__p4k_openPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ) + > + >('frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_open'); + late final _wire__crate__api__unp4k_api__p4k_open = + _wire__crate__api__unp4k_api__p4k_openPtr + .asFunction< + void Function(int, ffi.Pointer) + >(); + void wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js( int port_, ffi.Pointer that, @@ -1247,6 +1444,21 @@ class RustLibWire implements BaseWire { late final _cst_new_list_String = _cst_new_list_StringPtr .asFunction Function(int)>(); + ffi.Pointer cst_new_list_p_4_k_file_item( + int len, + ) { + return _cst_new_list_p_4_k_file_item(len); + } + + late final _cst_new_list_p_4_k_file_itemPtr = + _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Int32) + > + >('frbgen_starcitizen_doctor_cst_new_list_p_4_k_file_item'); + late final _cst_new_list_p_4_k_file_item = _cst_new_list_p_4_k_file_itemPtr + .asFunction Function(int)>(); + ffi.Pointer cst_new_list_prim_u_8_loose( int len, ) { @@ -1370,6 +1582,29 @@ final class wire_cst_list_prim_u_8_loose extends ffi.Struct { external int len; } +final class wire_cst_p_4_k_file_item extends ffi.Struct { + external ffi.Pointer name; + + @ffi.Bool() + external bool is_directory; + + @ffi.Uint64() + external int size; + + @ffi.Uint64() + external int compressed_size; + + @ffi.Int64() + external int date_modified; +} + +final class wire_cst_list_p_4_k_file_item extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_process_info extends ffi.Struct { @ffi.Uint32() external int pid; diff --git a/lib/data/app_unp4k_p4k_item_data.dart b/lib/data/app_unp4k_p4k_item_data.dart index 465eb86..24eb83f 100644 --- a/lib/data/app_unp4k_p4k_item_data.dart +++ b/lib/data/app_unp4k_p4k_item_data.dart @@ -2,46 +2,25 @@ /// 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({this.name, this.size, this.compressedSize, this.isDirectory, this.dateModified}); 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']; + dateModified = json['dateModified']; } String? name; num? size; num? compressedSize; bool? isDirectory; - bool? isFile; - bool? isEncrypted; - bool? isUnicodeText; - String? dateTime; - num? version; + + /// 文件修改时间(毫秒时间戳) + int? dateModified; List children = []; Map toJson() { @@ -50,11 +29,7 @@ class AppUnp4kP4kItemData { map['size'] = size; map['compressedSize'] = compressedSize; map['isDirectory'] = isDirectory; - map['isFile'] = isFile; - map['isEncrypted'] = isEncrypted; - map['isUnicodeText'] = isUnicodeText; - map['dateTime'] = dateTime; - map['version'] = version; + map['dateModified'] = dateModified; return map; } } diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index e6ee247..314683c 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -261,18 +261,32 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "Launcher internal version information: ${v0}"; - static String m104(v0) => "Opening file: ${v0}"; + static String m104(v0) => "Export Selected (${v0})"; - static String m105(v0, v1) => + static String m105(v0) => "Extraction failed: ${v0}"; + + static String m106(v0) => "Extraction complete: ${v0}"; + + static String m107(v0) => "Extracting: ${v0}"; + + static String m108(v0) => "Extraction completed, ${v0} files total"; + + static String m109(v0) => "Current file: ${v0}"; + + static String m110(v0, v1) => "Extracting (${v0}/${v1})"; + + static String m111(v0) => "Opening file: ${v0}"; + + static String m112(v0, v1) => "Loading complete: ${v0} files, time taken: ${v1} ms"; - static String m106(v0) => "Reading file: ${v0}..."; + static String m113(v0) => "Reading file: ${v0}..."; - static String m107(v0, v1) => "Processing files (${v0}/${v1})..."; + static String m114(v0, v1) => "Processing files (${v0}/${v1})..."; - static String m108(v0) => "Unknown file type\n${v0}"; + static String m115(v0) => "Unknown file type\n${v0}"; - static String m109(v0) => "P4K Viewer -> ${v0}"; + static String m116(v0) => "P4K Viewer -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -2184,6 +2198,33 @@ class MessageLookup extends MessageLookupByLibrary { "tools_rsi_launcher_enhance_working_msg2": MessageLookupByLibrary.simpleMessage( "Installing patch, this will take some time depending on your computer\'s performance...", ), + "tools_unp4k_action_cancel_multi_select": + MessageLookupByLibrary.simpleMessage("Cancel Multi-Select"), + "tools_unp4k_action_deselect_all": MessageLookupByLibrary.simpleMessage( + "Deselect All", + ), + "tools_unp4k_action_export_selected": m104, + "tools_unp4k_action_extract_failed": m105, + "tools_unp4k_action_extract_success": m106, + "tools_unp4k_action_extracting": m107, + "tools_unp4k_action_multi_select": MessageLookupByLibrary.simpleMessage( + "Multi-Select", + ), + "tools_unp4k_action_save_as": MessageLookupByLibrary.simpleMessage( + "Save As...", + ), + "tools_unp4k_action_select_all": MessageLookupByLibrary.simpleMessage( + "Select All", + ), + "tools_unp4k_extract_cancelled": MessageLookupByLibrary.simpleMessage( + "Extraction cancelled", + ), + "tools_unp4k_extract_completed": m108, + "tools_unp4k_extract_current_file": m109, + "tools_unp4k_extract_dialog_title": MessageLookupByLibrary.simpleMessage( + "Extract Files", + ), + "tools_unp4k_extract_progress": m110, "tools_unp4k_missing_runtime": MessageLookupByLibrary.simpleMessage( "Missing Runtime", ), @@ -2195,18 +2236,42 @@ class MessageLookup extends MessageLookupByLibrary { "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage( "Initializing...", ), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "Reading P4K file...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "Processing files...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_search_no_result": MessageLookupByLibrary.simpleMessage( + "No matching files found", + ), + "tools_unp4k_search_placeholder": MessageLookupByLibrary.simpleMessage( + "Search files (supports regex)...", + ), + "tools_unp4k_searching": MessageLookupByLibrary.simpleMessage( + "Searching...", + ), + "tools_unp4k_sort_date_asc": MessageLookupByLibrary.simpleMessage( + "Older First", + ), + "tools_unp4k_sort_date_desc": MessageLookupByLibrary.simpleMessage( + "Newer First", + ), + "tools_unp4k_sort_default": MessageLookupByLibrary.simpleMessage( + "Default Sort", + ), + "tools_unp4k_sort_size_asc": MessageLookupByLibrary.simpleMessage( + "Smaller First", + ), + "tools_unp4k_sort_size_desc": MessageLookupByLibrary.simpleMessage( + "Larger First", + ), + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "Click file to preview", ), diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index 2214ff4..be8be2e 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -242,17 +242,17 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "ランチャー内部バージョン情報:${v0}"; - static String m104(v0) => "ファイルを開く:${v0}"; + static String m111(v0) => "ファイルを開く:${v0}"; - static String m105(v0, v1) => "読み込み完了:${v0}ファイル、所要時間:${v1} ms"; + static String m112(v0, v1) => "読み込み完了:${v0}ファイル、所要時間:${v1} ms"; - static String m106(v0) => "ファイルを読み込み中:${v0}..."; + static String m113(v0) => "ファイルを読み込み中:${v0}..."; - static String m107(v0, v1) => "ファイルを処理中(${v0}/${v1})..."; + static String m114(v0, v1) => "ファイルを処理中(${v0}/${v1})..."; - static String m108(v0) => "不明なファイルタイプ\n${v0}"; + static String m115(v0) => "不明なファイルタイプ\n${v0}"; - static String m109(v0) => "P4Kビューア -> ${v0}"; + static String m116(v0) => "P4Kビューア -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1971,18 +1971,18 @@ class MessageLookup extends MessageLookupByLibrary { "この機能を使用するには.NET8ランタイムをインストールする必要があります。下のボタンをクリックしてダウンロードしてインストールし、インストールが成功したらこのページを再度開いて使用を続行してください。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初期化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "P4Kファイルを読み込み中...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "ファイルを処理中...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "プレビューするファイルをクリック", ), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 8641422..df9f428 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -256,18 +256,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "Внутренняя версия лаунчера: ${v0}"; - static String m104(v0) => "Открытие файла: ${v0}"; + static String m111(v0) => "Открытие файла: ${v0}"; - static String m105(v0, v1) => + static String m112(v0, v1) => "Загрузка завершена: ${v0} файлов, время: ${v1} мс"; - static String m106(v0) => "Чтение файла: ${v0}..."; + static String m113(v0) => "Чтение файла: ${v0}..."; - static String m107(v0, v1) => "Обработка файлов (${v0}/${v1})..."; + static String m114(v0, v1) => "Обработка файлов (${v0}/${v1})..."; - static String m108(v0) => "Неизвестный тип файла\n${v0}"; + static String m115(v0) => "Неизвестный тип файла\n${v0}"; - static String m109(v0) => "Просмотрщик P4K -> ${v0}"; + static String m116(v0) => "Просмотрщик P4K -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -2224,18 +2224,18 @@ class MessageLookup extends MessageLookupByLibrary { "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage( "Инициализация...", ), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "Чтение файла P4K...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "Обработка файлов...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "Нажмите на файл для предварительного просмотра", ), diff --git a/lib/generated/intl/messages_zh_CN.dart b/lib/generated/intl/messages_zh_CN.dart index 6f0a362..e4fb4f8 100644 --- a/lib/generated/intl/messages_zh_CN.dart +++ b/lib/generated/intl/messages_zh_CN.dart @@ -240,17 +240,31 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "启动器内部版本信息:${v0}"; - static String m104(v0) => "打开文件:${v0}"; + static String m104(v0) => "导出选中项 (${v0})"; - static String m105(v0, v1) => "加载完毕:${v0} 个文件,用时:${v1} ms"; + static String m105(v0) => "提取失败:${v0}"; - static String m106(v0) => "读取文件:${v0} ..."; + static String m106(v0) => "提取完成:${v0}"; - static String m107(v0, v1) => "正在处理文件 (${v0}/${v1}) ..."; + static String m107(v0) => "正在提取:${v0}"; - static String m108(v0) => "未知文件类型\n${v0}"; + static String m108(v0) => "提取完成,共 ${v0} 个文件"; - static String m109(v0) => "P4K 查看器 -> ${v0}"; + static String m109(v0) => "当前文件:${v0}"; + + static String m110(v0, v1) => "正在提取 (${v0}/${v1})"; + + static String m111(v0) => "打开文件:${v0}"; + + static String m112(v0, v1) => "加载完毕:${v0} 个文件,用时:${v1} ms"; + + static String m113(v0) => "读取文件:${v0} ..."; + + static String m114(v0, v1) => "正在处理文件 (${v0}/${v1}) ..."; + + static String m115(v0) => "未知文件类型\n${v0}"; + + static String m116(v0) => "P4K 查看器 -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1849,6 +1863,31 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("生成补丁 ..."), "tools_rsi_launcher_enhance_working_msg2": MessageLookupByLibrary.simpleMessage("安装补丁,这需要一点时间,取决于您的计算机性能 ..."), + "tools_unp4k_action_cancel_multi_select": + MessageLookupByLibrary.simpleMessage("取消多选"), + "tools_unp4k_action_deselect_all": MessageLookupByLibrary.simpleMessage( + "取消全选", + ), + "tools_unp4k_action_export_selected": m104, + "tools_unp4k_action_extract_failed": m105, + "tools_unp4k_action_extract_success": m106, + "tools_unp4k_action_extracting": m107, + "tools_unp4k_action_multi_select": MessageLookupByLibrary.simpleMessage( + "多选", + ), + "tools_unp4k_action_save_as": MessageLookupByLibrary.simpleMessage( + "另存为...", + ), + "tools_unp4k_action_select_all": MessageLookupByLibrary.simpleMessage("全选"), + "tools_unp4k_extract_cancelled": MessageLookupByLibrary.simpleMessage( + "提取已取消", + ), + "tools_unp4k_extract_completed": m108, + "tools_unp4k_extract_current_file": m109, + "tools_unp4k_extract_dialog_title": MessageLookupByLibrary.simpleMessage( + "提取文件", + ), + "tools_unp4k_extract_progress": m110, "tools_unp4k_missing_runtime": MessageLookupByLibrary.simpleMessage( "缺少运行库", ), @@ -1858,18 +1897,30 @@ class MessageLookup extends MessageLookupByLibrary { "使用此功能需安装 .NET8 运行库,请点击下方按钮下载安装,安装成功后重新打开此页面即可继续使用。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初始化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "正在读取P4K 文件 ...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "正在处理文件 ...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_search_no_result": MessageLookupByLibrary.simpleMessage( + "未找到匹配文件", + ), + "tools_unp4k_search_placeholder": MessageLookupByLibrary.simpleMessage( + "搜索文件(支持正则)...", + ), + "tools_unp4k_searching": MessageLookupByLibrary.simpleMessage("正在搜索..."), + "tools_unp4k_sort_date_asc": MessageLookupByLibrary.simpleMessage("旧文件优先"), + "tools_unp4k_sort_date_desc": MessageLookupByLibrary.simpleMessage("新文件优先"), + "tools_unp4k_sort_default": MessageLookupByLibrary.simpleMessage("默认排序"), + "tools_unp4k_sort_size_asc": MessageLookupByLibrary.simpleMessage("小文件优先"), + "tools_unp4k_sort_size_desc": MessageLookupByLibrary.simpleMessage("大文件优先"), + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage("单击文件以预览"), "tools_vehicle_sorting_info": MessageLookupByLibrary.simpleMessage( "将左侧载具拖动到右侧列表中,这将会为载具名称增加 001、002 .. 等前缀,方便您在游戏内 UI 快速定位载具。在右侧列表上下拖动可以调整载具的顺序。", diff --git a/lib/generated/intl/messages_zh_TW.dart b/lib/generated/intl/messages_zh_TW.dart index b82d836..59960d1 100644 --- a/lib/generated/intl/messages_zh_TW.dart +++ b/lib/generated/intl/messages_zh_TW.dart @@ -236,17 +236,17 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "啟動器內部版本資訊:${v0}"; - static String m104(v0) => "打開文件:${v0}"; + static String m111(v0) => "打開文件:${v0}"; - static String m105(v0, v1) => "載入完畢:${v0} 個文件,用時:${v1} ms"; + static String m112(v0, v1) => "載入完畢:${v0} 個文件,用時:${v1} ms"; - static String m106(v0) => "讀取文件:${v0} ..."; + static String m113(v0) => "讀取文件:${v0} ..."; - static String m107(v0, v1) => "正在處理文件 (${v0}/${v1}) ..."; + static String m114(v0, v1) => "正在處理文件 (${v0}/${v1}) ..."; - static String m108(v0) => "未知文件類型\n${v0}"; + static String m115(v0) => "未知文件類型\n${v0}"; - static String m109(v0) => "P4K 查看器 -> ${v0}"; + static String m116(v0) => "P4K 查看器 -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1849,18 +1849,18 @@ class MessageLookup extends MessageLookupByLibrary { "使用此功能需安裝 .NET8 運行庫,請點擊下方按鈕下載安裝,安裝成功後重新打開此頁面即可繼續使用。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初始化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "正在讀取P4K 文件 ...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "正在處理文件 ...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage("單擊文件以預覽"), "tools_vehicle_sorting_info": MessageLookupByLibrary.simpleMessage( "將左側載具拖動到右側列表中,這將會為載具名稱增加 001、002 .. 等前綴,方便您在遊戲內 UI 快速定位載具。在右側列表上下拖動可以調整載具的順序。", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 796b4a4..3ea085e 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -7901,6 +7902,226 @@ class S { args: [v0], ); } + + /// `Search files (supports regex)...` + String get tools_unp4k_search_placeholder { + return Intl.message( + 'Search files (supports regex)...', + name: 'tools_unp4k_search_placeholder', + desc: '', + args: [], + ); + } + + /// `Default Sort` + String get tools_unp4k_sort_default { + return Intl.message( + 'Default Sort', + name: 'tools_unp4k_sort_default', + desc: '', + args: [], + ); + } + + /// `Smaller First` + String get tools_unp4k_sort_size_asc { + return Intl.message( + 'Smaller First', + name: 'tools_unp4k_sort_size_asc', + desc: '', + args: [], + ); + } + + /// `Larger First` + String get tools_unp4k_sort_size_desc { + return Intl.message( + 'Larger First', + name: 'tools_unp4k_sort_size_desc', + desc: '', + args: [], + ); + } + + /// `Older First` + String get tools_unp4k_sort_date_asc { + return Intl.message( + 'Older First', + name: 'tools_unp4k_sort_date_asc', + desc: '', + args: [], + ); + } + + /// `Newer First` + String get tools_unp4k_sort_date_desc { + return Intl.message( + 'Newer First', + name: 'tools_unp4k_sort_date_desc', + desc: '', + args: [], + ); + } + + /// `Save As...` + String get tools_unp4k_action_save_as { + return Intl.message( + 'Save As...', + name: 'tools_unp4k_action_save_as', + desc: '', + args: [], + ); + } + + /// `Extracting: {v0}` + String tools_unp4k_action_extracting(Object v0) { + return Intl.message( + 'Extracting: $v0', + name: 'tools_unp4k_action_extracting', + desc: '', + args: [v0], + ); + } + + /// `Extraction complete: {v0}` + String tools_unp4k_action_extract_success(Object v0) { + return Intl.message( + 'Extraction complete: $v0', + name: 'tools_unp4k_action_extract_success', + desc: '', + args: [v0], + ); + } + + /// `Extraction failed: {v0}` + String tools_unp4k_action_extract_failed(Object v0) { + return Intl.message( + 'Extraction failed: $v0', + name: 'tools_unp4k_action_extract_failed', + desc: '', + args: [v0], + ); + } + + /// `No matching files found` + String get tools_unp4k_search_no_result { + return Intl.message( + 'No matching files found', + name: 'tools_unp4k_search_no_result', + desc: '', + args: [], + ); + } + + /// `Searching...` + String get tools_unp4k_searching { + return Intl.message( + 'Searching...', + name: 'tools_unp4k_searching', + desc: '', + args: [], + ); + } + + /// `Extract Files` + String get tools_unp4k_extract_dialog_title { + return Intl.message( + 'Extract Files', + name: 'tools_unp4k_extract_dialog_title', + desc: '', + args: [], + ); + } + + /// `Extracting ({v0}/{v1})` + String tools_unp4k_extract_progress(Object v0, Object v1) { + return Intl.message( + 'Extracting ($v0/$v1)', + name: 'tools_unp4k_extract_progress', + desc: '', + args: [v0, v1], + ); + } + + /// `Current file: {v0}` + String tools_unp4k_extract_current_file(Object v0) { + return Intl.message( + 'Current file: $v0', + name: 'tools_unp4k_extract_current_file', + desc: '', + args: [v0], + ); + } + + /// `Extraction cancelled` + String get tools_unp4k_extract_cancelled { + return Intl.message( + 'Extraction cancelled', + name: 'tools_unp4k_extract_cancelled', + desc: '', + args: [], + ); + } + + /// `Extraction completed, {v0} files total` + String tools_unp4k_extract_completed(Object v0) { + return Intl.message( + 'Extraction completed, $v0 files total', + name: 'tools_unp4k_extract_completed', + desc: '', + args: [v0], + ); + } + + /// `Multi-Select` + String get tools_unp4k_action_multi_select { + return Intl.message( + 'Multi-Select', + name: 'tools_unp4k_action_multi_select', + desc: '', + args: [], + ); + } + + /// `Export Selected ({v0})` + String tools_unp4k_action_export_selected(Object v0) { + return Intl.message( + 'Export Selected ($v0)', + name: 'tools_unp4k_action_export_selected', + desc: '', + args: [v0], + ); + } + + /// `Cancel Multi-Select` + String get tools_unp4k_action_cancel_multi_select { + return Intl.message( + 'Cancel Multi-Select', + name: 'tools_unp4k_action_cancel_multi_select', + desc: '', + args: [], + ); + } + + /// `Select All` + String get tools_unp4k_action_select_all { + return Intl.message( + 'Select All', + name: 'tools_unp4k_action_select_all', + desc: '', + args: [], + ); + } + + /// `Deselect All` + String get tools_unp4k_action_deselect_all { + return Intl.message( + 'Deselect All', + name: 'tools_unp4k_action_deselect_all', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2bf097a..6bde35c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1424,5 +1424,27 @@ "splash_db_reset_done": "[Diagnostic] Database reset complete, preparing to exit application", "splash_db_reset_msg": "Database has been reset, application will exit. Please restart the application.", "splash_reset_db_failed": "[Diagnostic] Failed to reset database: {v0}", - "@splash_reset_db_failed": {} -} + "@splash_reset_db_failed": {}, + "tools_unp4k_search_placeholder": "Search files (supports regex)...", + "tools_unp4k_sort_default": "Default Sort", + "tools_unp4k_sort_size_asc": "Smaller First", + "tools_unp4k_sort_size_desc": "Larger First", + "tools_unp4k_sort_date_asc": "Older First", + "tools_unp4k_sort_date_desc": "Newer First", + "tools_unp4k_action_save_as": "Save As...", + "tools_unp4k_action_extracting": "Extracting: {v0}", + "tools_unp4k_action_extract_success": "Extraction complete: {v0}", + "tools_unp4k_action_extract_failed": "Extraction failed: {v0}", + "tools_unp4k_search_no_result": "No matching files found", + "tools_unp4k_searching": "Searching...", + "tools_unp4k_extract_dialog_title": "Extract Files", + "tools_unp4k_extract_progress": "Extracting ({v0}/{v1})", + "tools_unp4k_extract_current_file": "Current file: {v0}", + "tools_unp4k_extract_cancelled": "Extraction cancelled", + "tools_unp4k_extract_completed": "Extraction completed, {v0} files total", + "tools_unp4k_action_multi_select": "Multi-Select", + "tools_unp4k_action_export_selected": "Export Selected ({v0})", + "tools_unp4k_action_cancel_multi_select": "Cancel Multi-Select", + "tools_unp4k_action_select_all": "Select All", + "tools_unp4k_action_deselect_all": "Deselect All" +} \ No newline at end of file diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index 57f73e1..d824e5f 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -1139,5 +1139,27 @@ "splash_db_reset_done": "[诊断] 数据库重置完成,准备退出应用", "splash_db_reset_msg": "数据库已重置,应用将退出。请重新启动应用。", "splash_reset_db_failed": "[诊断] 重置数据库失败: {v0}", - "@splash_reset_db_failed": {} + "@splash_reset_db_failed": {}, + "tools_unp4k_search_placeholder": "搜索文件(支持正则)...", + "tools_unp4k_sort_default": "默认排序", + "tools_unp4k_sort_size_asc": "小文件优先", + "tools_unp4k_sort_size_desc": "大文件优先", + "tools_unp4k_sort_date_asc": "旧文件优先", + "tools_unp4k_sort_date_desc": "新文件优先", + "tools_unp4k_action_save_as": "另存为...", + "tools_unp4k_action_extracting": "正在提取:{v0}", + "tools_unp4k_action_extract_success": "提取完成:{v0}", + "tools_unp4k_action_extract_failed": "提取失败:{v0}", + "tools_unp4k_search_no_result": "未找到匹配文件", + "tools_unp4k_searching": "正在搜索...", + "tools_unp4k_extract_dialog_title": "提取文件", + "tools_unp4k_extract_progress": "正在提取 ({v0}/{v1})", + "tools_unp4k_extract_current_file": "当前文件:{v0}", + "tools_unp4k_extract_cancelled": "提取已取消", + "tools_unp4k_extract_completed": "提取完成,共 {v0} 个文件", + "tools_unp4k_action_multi_select": "多选", + "tools_unp4k_action_export_selected": "导出选中项 ({v0})", + "tools_unp4k_action_cancel_multi_select": "取消多选", + "tools_unp4k_action_select_all": "全选", + "tools_unp4k_action_deselect_all": "取消全选" } \ No newline at end of file diff --git a/lib/provider/unp4kc.dart b/lib/provider/unp4kc.dart index c5c2236..96feec8 100644 --- a/lib/provider/unp4kc.dart +++ b/lib/provider/unp4kc.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:io'; import 'package:file/memory.dart'; @@ -7,20 +6,34 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:path_provider/path_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; -import 'package:starcitizen_doctor/common/conf/binary_conf.dart'; import 'package:starcitizen_doctor/common/helper/log_helper.dart'; -import 'package:starcitizen_doctor/common/rust/api/rs_process.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'; -import 'package:starcitizen_doctor/common/rust/api/rs_process.dart' - as rs_process; +import 'package:starcitizen_doctor/common/rust/api/unp4k_api.dart' as unp4k_api; part 'unp4kc.freezed.dart'; part 'unp4kc.g.dart'; +/// 排序类型枚举 +enum Unp4kSortType { + /// 默认排序(文件夹优先,按名称) + defaultSort, + + /// 文件大小升序 + sizeAsc, + + /// 文件大小降序 + sizeDesc, + + /// 修改时间升序 + dateAsc, + + /// 修改时间降序 + dateDesc, +} + @freezed abstract class Unp4kcState with _$Unp4kcState { const factory Unp4kcState({ @@ -31,19 +44,29 @@ abstract class Unp4kcState with _$Unp4kcState { String? endMessage, MapEntry? tempOpenFile, @Default("") String errorMessage, + @Default("") String searchQuery, + @Default(false) bool isSearching, + + /// 搜索结果的虚拟文件系统(支持分级展示) + MemoryFileSystem? searchFs, + + /// 搜索匹配的文件路径集合 + Set? searchMatchedFiles, + @Default(Unp4kSortType.defaultSort) Unp4kSortType sortType, + + /// 是否处于多选模式 + @Default(false) bool isMultiSelectMode, + + /// 多选模式下选中的文件路径集合 + @Default({}) Set selectedItems, }) = _Unp4kcState; } @riverpod class Unp4kCModel extends _$Unp4kCModel { - int? _rsPid; - @override Unp4kcState build() { - state = Unp4kcState( - startUp: false, - curPath: '\\', - endMessage: S.current.tools_unp4k_msg_init); + state = Unp4kcState(startUp: false, curPath: '\\', endMessage: S.current.tools_unp4k_msg_init); _init(); return state; } @@ -52,108 +75,333 @@ class Unp4kCModel extends _$Unp4kCModel { String getGamePath() => _toolsState.scInstalledPath; - bool _hasUnp4kRunTimeError = false; - void _init() async { - final execDir = "${appGlobalState.applicationBinaryModuleDir}\\unp4kc"; - await BinaryModuleConf.extractModule( - ["unp4kc"], appGlobalState.applicationBinaryModuleDir!); - final exec = "$execDir\\unp4kc.exe"; + final gamePath = getGamePath(); + final gameP4kPath = "$gamePath\\Data.p4k"; - final stream = rs_process.start( - executable: exec, arguments: [], workingDirectory: execDir); + try { + state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading); - stream.listen((event) async { - switch (event.dataType) { - case RsProcessStreamDataType.output: - _rsPid = event.rsPid; - try { - final eventJson = await compute(json.decode, event.data); - _handleMessage(eventJson, event.rsPid); - } catch (e) { - dPrint("[unp4kc] json error: $e"); - } - break; - case RsProcessStreamDataType.error: - dPrint("[unp4kc] stderr: ${event.data}"); - if (state.errorMessage.isEmpty) { - state = state.copyWith(errorMessage: event.data); - } else { - state = state.copyWith( - errorMessage: "${state.errorMessage}\n${event.data}"); - } - if (!_hasUnp4kRunTimeError) { - if (checkRunTimeError(state.errorMessage)) { - _hasUnp4kRunTimeError = true; - AnalyticsApi.touch("unp4k_no_runtime"); - } - } - break; - case RsProcessStreamDataType.exit: - dPrint("[unp4kc] exit: ${event.data}"); - break; + final loadStartTime = DateTime.now(); + + await unp4k_api.p4KOpen(p4KPath: gameP4kPath); + + state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading2); + + final p4kFiles = await unp4k_api.p4KGetAllFiles(); + + final files = {}; + final fs = MemoryFileSystem(style: FileSystemStyle.posix); + + var nextAwait = 0; + for (var i = 0; i < p4kFiles.length; i++) { + final item = p4kFiles[i]; + final fileData = AppUnp4kP4kItemData( + name: item.name, + isDirectory: item.isDirectory, + size: item.size.toInt(), + compressedSize: item.compressedSize.toInt(), + dateModified: item.dateModified, + ); + + files[item.name] = fileData; + + if (!item.isDirectory) { + await fs.file(item.name.replaceAll("\\", "/")).create(recursive: true); + } + + if (i == nextAwait) { + state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading3(i, p4kFiles.length)); + await Future.delayed(Duration.zero); + nextAwait += 20000; + } } - }); - ref.onDispose(() { - state = state.copyWith(fs: null); - if (_rsPid != null) { - Process.killPid(_rsPid!); - dPrint("[unp4kc] kill ..."); + final endTime = DateTime.now(); + state = state.copyWith( + files: files, + fs: fs, + endMessage: S.current.tools_unp4k_msg_read_completed( + files.length, + endTime.difference(loadStartTime).inMilliseconds, + ), + ); + } catch (e) { + dPrint("[unp4k] error: $e"); + state = state.copyWith(errorMessage: e.toString()); + AnalyticsApi.touch("unp4k_error"); + } + + ref.onDispose(() async { + try { + await unp4k_api.p4KClose(); + } catch (e) { + dPrint("[unp4k] close error: $e"); } }); } - DateTime? _loadStartTime; + List? getFiles() { + final path = state.curPath.replaceAll("\\", "/"); - void _handleMessage(Map eventJson, int rsPid) async { - final action = eventJson["action"]; - final data = eventJson["data"]; - final gamePath = getGamePath(); - final gameP4kPath = "$gamePath\\Data.p4k"; - switch (action.toString().trim()) { - case "info: startup": - rs_process.write(rsPid: rsPid, data: "$gameP4kPath\n"); - break; - case "info: Reading_p4k_file": - _loadStartTime = DateTime.now(); - state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading); - break; - case "info: All Ready": - state = state.copyWith(endMessage: S.current.tools_unp4k_msg_reading2); - break; - case "data: P4K_Files": - final p4kFiles = (data as List); - final files = {}; - final fs = MemoryFileSystem(style: FileSystemStyle.posix); + // 如果有搜索结果,使用搜索的虚拟文件系统 + final fs = state.searchFs ?? state.fs; + if (fs == null) return null; - var nextAwait = 0; - 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); - if (i == nextAwait) { - state = state.copyWith( - endMessage: - S.current.tools_unp4k_msg_reading3(i, p4kFiles.length)); - await Future.delayed(Duration.zero); - nextAwait += 20000; + final dir = fs.directory(path); + if (!dir.existsSync()) return []; + final files = dir.listSync(recursive: false, followLinks: false); + + 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); } - final endTime = DateTime.now(); - state = state.copyWith( - files: files, - fs: fs, - endMessage: S.current.tools_unp4k_msg_read_completed(files.length, - endTime.difference(_loadStartTime!).inMilliseconds)); - _loadStartTime = null; + } else { + result.add(AppUnp4kP4kItemData(name: file.path.replaceAll("/", "\\"), isDirectory: true)); + } + } + + // 应用排序 + _sortFiles(result); + return result; + } + + /// 对文件列表进行排序 + void _sortFiles(List files) { + switch (state.sortType) { + case Unp4kSortType.defaultSort: + // 默认排序:文件夹优先,按名称排序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } else { + return (a.name ?? "").compareTo(b.name ?? ""); + } + }); break; - case "info: Extracted_Open": - final filePath = data.toString(); - dPrint("[unp4kc] Extracted_Open file: $filePath"); + case Unp4kSortType.sizeAsc: + // 文件大小升序(文件夹大小视为0) + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final sizeA = (a.isDirectory ?? false) ? 0 : (a.size ?? 0); + final sizeB = (b.isDirectory ?? false) ? 0 : (b.size ?? 0); + return sizeA.compareTo(sizeB); + }); + break; + case Unp4kSortType.sizeDesc: + // 文件大小降序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final sizeA = (a.isDirectory ?? false) ? 0 : (a.size ?? 0); + final sizeB = (b.isDirectory ?? false) ? 0 : (b.size ?? 0); + return sizeB.compareTo(sizeA); + }); + break; + case Unp4kSortType.dateAsc: + // 修改时间升序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final dateA = a.dateModified ?? 0; + final dateB = b.dateModified ?? 0; + return dateA.compareTo(dateB); + }); + break; + case Unp4kSortType.dateDesc: + // 修改时间降序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final dateA = a.dateModified ?? 0; + final dateB = b.dateModified ?? 0; + return dateB.compareTo(dateA); + }); + break; + } + } + + /// 设置排序类型 + void setSortType(Unp4kSortType sortType) { + state = state.copyWith(sortType: sortType); + } + + /// 执行搜索(异步) + Future search(String query) async { + if (query.isEmpty) { + // 清除搜索,返回根目录 + state = state.copyWith( + searchQuery: "", + searchFs: null, + searchMatchedFiles: null, + isSearching: false, + curPath: "\\", + ); + return; + } + + // 保存当前路径,用于搜索后尝试保持 + final currentPath = state.curPath; + + state = state.copyWith(searchQuery: query, isSearching: true, endMessage: S.current.tools_unp4k_searching); + + // 使用 compute 在后台线程执行搜索 + final allFiles = state.files; + if (allFiles == null) { + state = state.copyWith(isSearching: false); + return; + } + + try { + final searchResult = await compute(_searchFiles, _SearchParams(allFiles, query)); + final matchedFiles = searchResult.matchedFiles; + + // 构建搜索结果的虚拟文件系统 + final searchFs = MemoryFileSystem(style: FileSystemStyle.posix); + for (var filePath in matchedFiles) { + await searchFs.file(filePath.replaceAll("\\", "/")).create(recursive: true); + } + + // 检查当前路径是否有搜索结果 + String targetPath = "\\"; + if (currentPath != "\\") { + final checkPath = currentPath.replaceAll("\\", "/"); + final dir = searchFs.directory(checkPath); + if (dir.existsSync() && dir.listSync().isNotEmpty) { + // 当前目录有结果,保持当前路径 + targetPath = currentPath; + } + } + + state = state.copyWith( + searchFs: searchFs, + searchMatchedFiles: matchedFiles, + isSearching: false, + curPath: targetPath, + endMessage: matchedFiles.isEmpty + ? S.current.tools_unp4k_search_no_result + : S.current.tools_unp4k_msg_read_completed(matchedFiles.length, 0), + ); + } catch (e) { + dPrint("[unp4k] search error: $e"); + state = state.copyWith(isSearching: false, endMessage: e.toString()); + } + } + + /// 清除搜索 + void clearSearch() { + state = state.copyWith( + searchQuery: "", + searchFs: null, + searchMatchedFiles: null, + isSearching: false, + curPath: "\\", + ); + } + + /// 进入多选模式 + void enterMultiSelectMode() { + state = state.copyWith(isMultiSelectMode: true, selectedItems: {}); + } + + /// 退出多选模式 + void exitMultiSelectMode() { + state = state.copyWith(isMultiSelectMode: false, selectedItems: {}); + } + + /// 切换选中状态 + void toggleSelectItem(String itemPath) { + final currentSelected = Set.from(state.selectedItems); + if (currentSelected.contains(itemPath)) { + currentSelected.remove(itemPath); + } else { + currentSelected.add(itemPath); + } + state = state.copyWith(selectedItems: currentSelected); + } + + /// 全选当前目录的文件 + void selectAll(List? files) { + if (files == null) return; + final currentSelected = Set.from(state.selectedItems); + for (var file in files) { + final path = file.name ?? ""; + if (path.isNotEmpty) { + currentSelected.add(path); + } + } + state = state.copyWith(selectedItems: currentSelected); + } + + /// 取消全选当前目录的文件 + void deselectAll(List? files) { + if (files == null) return; + final currentSelected = Set.from(state.selectedItems); + for (var file in files) { + final path = file.name ?? ""; + currentSelected.remove(path); + } + state = state.copyWith(selectedItems: currentSelected); + } + + void changeDir(String name, {bool fullPath = false}) { + // 切换目录时退出多选模式 + if (state.isMultiSelectMode) { + state = state.copyWith(isMultiSelectMode: false, selectedItems: {}); + } + // 切换目录时不清除搜索,只改变当前路径 + if (fullPath) { + state = state.copyWith(curPath: name); + } else { + state = state.copyWith(curPath: "${state.curPath}$name\\"); + } + } + + Future openFile(String filePath) async { + final tempDir = await getTemporaryDirectory(); + final tempPath = "${tempDir.absolute.path}\\SCToolbox_unp4kc\\${SCLoggerHelper.getGameChannelID(getGamePath())}\\"; + state = state.copyWith( + tempOpenFile: const MapEntry("loading", ""), + endMessage: S.current.tools_unp4k_msg_open_file(filePath), + ); + await extractFile(filePath, tempPath, mode: "extract_open"); + } + + Future extractFile(String filePath, String outputPath, {String mode = "extract"}) async { + try { + // remove first \\ + if (filePath.startsWith("\\")) { + filePath = filePath.substring(1); + } + + final fullOutputPath = "$outputPath$filePath"; + dPrint("extractFile .... $filePath -> $fullOutputPath"); + + // 使用 Rust API 提取到磁盘 + await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath); + + if (mode == "extract_open") { const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini"]; const imgExt = [".png"]; String openType = "unknown"; @@ -168,111 +416,274 @@ class Unp4kCModel extends _$Unp4kCModel { } } state = state.copyWith( - tempOpenFile: MapEntry(openType, filePath), - endMessage: S.current.tools_unp4k_msg_open_file(filePath)); - break; - default: - dPrint("[unp4kc] unknown action: $action"); - break; + tempOpenFile: MapEntry(openType, fullOutputPath), + endMessage: S.current.tools_unp4k_msg_open_file(filePath), + ); + } + } catch (e) { + dPrint("[unp4k] extractFile error: $e"); + state = state.copyWith(errorMessage: e.toString()); } } - 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); + /// 提取文件或文件夹到指定目录(带进度回调和取消支持) + /// [item] 要提取的文件或文件夹 + /// [outputDir] 输出目录 + /// [onProgress] 进度回调 (当前文件索引, 总文件数, 当前文件名) + /// [isCancelled] 取消检查函数,返回 true 表示取消 + /// 返回值:(是否成功, 已提取文件数, 错误信息) + Future<(bool, int, String?)> extractToDirectoryWithProgress( + AppUnp4kP4kItemData item, + String outputDir, { + void Function(int current, int total, String currentFile)? onProgress, + bool Function()? isCancelled, + }) async { + try { + final itemPath = item.name ?? ""; + var filePath = itemPath; + if (filePath.startsWith("\\")) { + filePath = filePath.substring(1); } - }); - 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}"; + + if (item.isDirectory ?? false) { + // 提取文件夹:遍历所有以该路径为前缀的文件 + final allFiles = state.files; + if (allFiles != null) { + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + + // 收集所有需要提取的文件 + final filesToExtract = >[]; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry); + } } - result.add(f); + + final total = filesToExtract.length; + var current = 0; + + for (var entry in filesToExtract) { + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, current, S.current.tools_unp4k_extract_cancelled); + } + + var entryPath = entry.key; + if (entryPath.startsWith("\\")) { + entryPath = entryPath.substring(1); + } + + current++; + onProgress?.call(current, total, entryPath); + + final fullOutputPath = "$outputDir\\$entryPath"; + await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: fullOutputPath); + } + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); + return (true, current, null); + } + return (true, 0, null); + } else { + // 提取单个文件 + onProgress?.call(1, 1, filePath); + + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, 0, S.current.tools_unp4k_extract_cancelled); + } + + final fullOutputPath = "$outputDir\\$filePath"; + await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath); + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(1)); + return (true, 1, null); + } + } catch (e) { + dPrint("[unp4k] extractToDirectoryWithProgress error: $e"); + return (false, 0, e.toString()); + } + } + + /// 获取文件夹中需要提取的文件数量 + int getFileCountInDirectory(AppUnp4kP4kItemData item) { + if (!(item.isDirectory ?? false)) { + return 1; + } + + final itemPath = item.name ?? ""; + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + final allFiles = state.files; + + if (allFiles == null) return 0; + + int count = 0; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + count++; + } + } + return count; + } + + /// 获取多选项的总文件数量 + int getFileCountForSelectedItems(Set selectedItems) { + int count = 0; + final allFiles = state.files; + if (allFiles == null) return 0; + + for (var itemPath in selectedItems) { + final item = allFiles[itemPath]; + if (item != null) { + if (item.isDirectory ?? false) { + count += getFileCountInDirectory(item); + } else { + count += 1; } } else { - result.add(AppUnp4kP4kItemData( - name: file.path.replaceAll("/", "\\"), isDirectory: true)); + // 可能是文件夹(虚拟路径) + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + count++; + } + } } } - return result; + return count; } - void changeDir(String name, {bool fullPath = false}) { - if (fullPath) { - state = state.copyWith(curPath: name); - } else { - state = state.copyWith(curPath: "${state.curPath}$name\\"); + /// 批量提取多个选中项到指定目录(带进度回调和取消支持) + Future<(bool, int, String?)> extractSelectedItemsWithProgress( + Set selectedItems, + String outputDir, { + void Function(int current, int total, String currentFile)? onProgress, + bool Function()? isCancelled, + }) async { + try { + final allFiles = state.files; + if (allFiles == null) return (true, 0, null); + + // 收集所有需要提取的文件 + final filesToExtract = []; + + for (var itemPath in selectedItems) { + final item = allFiles[itemPath]; + if (item != null) { + if (item.isDirectory ?? false) { + // 文件夹:收集所有子文件 + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry.key); + } + } + } else { + // 单个文件 + filesToExtract.add(itemPath); + } + } else { + // 可能是虚拟文件夹路径 + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry.key); + } + } + } + } + + final total = filesToExtract.length; + var current = 0; + + for (var filePath in filesToExtract) { + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, current, S.current.tools_unp4k_extract_cancelled); + } + + var extractPath = filePath; + if (extractPath.startsWith("\\")) { + extractPath = extractPath.substring(1); + } + + current++; + onProgress?.call(current, total, extractPath); + + final fullOutputPath = "$outputDir\\$extractPath"; + await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: fullOutputPath); + } + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); + return (true, current, null); + } catch (e) { + dPrint("[unp4k] extractSelectedItemsWithProgress error: $e"); + return (false, 0, e.toString()); } } - Future openFile(String filePath) async { - final tempDir = await getTemporaryDirectory(); - final tempPath = - "${tempDir.absolute.path}\\SCToolbox_unp4kc\\${SCLoggerHelper.getGameChannelID(getGamePath())}\\"; - state = state.copyWith( - tempOpenFile: const MapEntry("loading", ""), - endMessage: S.current.tools_unp4k_msg_open_file(filePath)); - extractFile(filePath, tempPath, mode: "extract_open"); - } - - Future extractFile(String filePath, String outputPath, - {String mode = "extract"}) async { - // remove first \\ - if (filePath.startsWith("\\")) { - filePath = filePath.substring(1); + /// 从 P4K 文件中提取指定文件到内存 + /// [p4kPath] P4K 文件路径 + /// [filePath] 要提取的文件路径(P4K 内部路径) + static Future extractP4kFileToMemory(String p4kPath, String filePath) async { + try { + await unp4k_api.p4KOpen(p4KPath: p4kPath); + final data = await unp4k_api.p4KExtractToMemory(filePath: filePath); + await unp4k_api.p4KClose(); + return Uint8List.fromList(data); + } catch (e) { + throw Exception("extractP4kFileToMemory error: $e"); } - outputPath = "$outputPath$filePath"; - dPrint("extractFile .... $filePath"); - if (_rsPid != null) { - rs_process.write( - rsPid: _rsPid!, data: "$mode<:,:>$filePath<:,:>$outputPath\n"); - } - } - - static bool checkRunTimeError(String errorMessage) { - if (errorMessage - .contains("You must install .NET to run this application") || - errorMessage.contains( - "You must install or update .NET to run this application") || - errorMessage.contains( - "It was not possible to find any compatible framework version")) { - AnalyticsApi.touch("unp4k_no_runtime"); - return true; - } - return false; - } - - static Future unp4kTools( - String applicationBinaryModuleDir, List args) async { - await BinaryModuleConf.extractModule( - ["unp4kc"], applicationBinaryModuleDir); - final execDir = "$applicationBinaryModuleDir\\unp4kc"; - final exec = "$execDir\\unp4kc.exe"; - final r = await Process.run(exec, args); - if (r.exitCode != 0) { - Process.killPid(r.pid); - throw Exception( - "error: ${r.exitCode} , info= ${r.stdout} , err= ${r.stderr}"); - } - final eventJson = await compute(json.decode, r.stdout.toString()); - if (eventJson["action"] == "data: Uint8List") { - final data = eventJson["data"]; - return Uint8List.fromList((data as List).cast()); - } - throw Exception("error: data error"); } } + +/// 搜索参数类 +class _SearchParams { + final Map files; + final String query; + + _SearchParams(this.files, this.query); +} + +/// 搜索结果类 +class _SearchResult { + final Set matchedFiles; + + _SearchResult(this.matchedFiles); +} + +/// 在后台线程执行搜索 +_SearchResult _searchFiles(_SearchParams params) { + final matchedFiles = {}; + + // 尝试编译正则表达式,如果失败则使用普通字符串匹配 + RegExp? regex; + try { + regex = RegExp(params.query, caseSensitive: false); + } catch (e) { + // 正则无效,回退到普通字符串匹配 + regex = null; + } + + for (var entry in params.files.entries) { + final item = entry.value; + final name = item.name ?? ""; + + // 跳过文件夹本身 + if (item.isDirectory ?? false) continue; + + bool matches = false; + if (regex != null) { + matches = regex.hasMatch(name); + } else { + matches = name.toLowerCase().contains(params.query.toLowerCase()); + } + + if (matches) { + // 添加匹配的文件路径 + matchedFiles.add(name.startsWith("\\") ? name : "\\$name"); + } + } + + return _SearchResult(matchedFiles); +} diff --git a/lib/provider/unp4kc.freezed.dart b/lib/provider/unp4kc.freezed.dart index 9ed7d64..cc01f62 100644 --- a/lib/provider/unp4kc.freezed.dart +++ b/lib/provider/unp4kc.freezed.dart @@ -14,7 +14,11 @@ T _$identity(T value) => value; /// @nodoc mixin _$Unp4kcState implements DiagnosticableTreeMixin { - bool get startUp; Map? get files; MemoryFileSystem? get fs; String get curPath; String? get endMessage; MapEntry? get tempOpenFile; String get errorMessage; + bool get startUp; Map? get files; MemoryFileSystem? get fs; String get curPath; String? get endMessage; MapEntry? get tempOpenFile; String get errorMessage; String get searchQuery; bool get isSearching;/// 搜索结果的虚拟文件系统(支持分级展示) + MemoryFileSystem? get searchFs;/// 搜索匹配的文件路径集合 + Set? get searchMatchedFiles; Unp4kSortType get sortType;/// 是否处于多选模式 + bool get isMultiSelectMode;/// 多选模式下选中的文件路径集合 + Set get selectedItems; /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,21 +30,21 @@ $Unp4kcStateCopyWith get copyWith => _$Unp4kcStateCopyWithImpl Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(files),fs,curPath,endMessage,tempOpenFile,errorMessage); +int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(files),fs,curPath,endMessage,tempOpenFile,errorMessage,searchQuery,isSearching,searchFs,const DeepCollectionEquality().hash(searchMatchedFiles),sortType,isMultiSelectMode,const DeepCollectionEquality().hash(selectedItems)); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage)'; + return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage, searchQuery: $searchQuery, isSearching: $isSearching, searchFs: $searchFs, searchMatchedFiles: $searchMatchedFiles, sortType: $sortType, isMultiSelectMode: $isMultiSelectMode, selectedItems: $selectedItems)'; } @@ -51,7 +55,7 @@ abstract mixin class $Unp4kcStateCopyWith<$Res> { factory $Unp4kcStateCopyWith(Unp4kcState value, $Res Function(Unp4kcState) _then) = _$Unp4kcStateCopyWithImpl; @useResult $Res call({ - bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage + bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems }); @@ -68,7 +72,7 @@ class _$Unp4kcStateCopyWithImpl<$Res> /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. -@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,Object? errorMessage = null,}) { +@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,Object? errorMessage = null,Object? searchQuery = null,Object? isSearching = null,Object? searchFs = freezed,Object? searchMatchedFiles = freezed,Object? sortType = null,Object? isMultiSelectMode = null,Object? selectedItems = null,}) { return _then(_self.copyWith( startUp: null == startUp ? _self.startUp : startUp // ignore: cast_nullable_to_non_nullable as bool,files: freezed == files ? _self.files : files // ignore: cast_nullable_to_non_nullable @@ -77,7 +81,14 @@ as MemoryFileSystem?,curPath: null == curPath ? _self.curPath : curPath // ignor as String,endMessage: freezed == endMessage ? _self.endMessage : endMessage // ignore: cast_nullable_to_non_nullable as String?,tempOpenFile: freezed == tempOpenFile ? _self.tempOpenFile : tempOpenFile // ignore: cast_nullable_to_non_nullable as MapEntry?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as String,searchQuery: null == searchQuery ? _self.searchQuery : searchQuery // ignore: cast_nullable_to_non_nullable +as String,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable +as bool,searchFs: freezed == searchFs ? _self.searchFs : searchFs // ignore: cast_nullable_to_non_nullable +as MemoryFileSystem?,searchMatchedFiles: freezed == searchMatchedFiles ? _self.searchMatchedFiles : searchMatchedFiles // ignore: cast_nullable_to_non_nullable +as Set?,sortType: null == sortType ? _self.sortType : sortType // ignore: cast_nullable_to_non_nullable +as Unp4kSortType,isMultiSelectMode: null == isMultiSelectMode ? _self.isMultiSelectMode : isMultiSelectMode // ignore: cast_nullable_to_non_nullable +as bool,selectedItems: null == selectedItems ? _self.selectedItems : selectedItems // ignore: cast_nullable_to_non_nullable +as Set, )); } @@ -162,10 +173,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Unp4kcState() when $default != null: -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: return orElse(); } @@ -183,10 +194,10 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems) $default,) {final _that = this; switch (_that) { case _Unp4kcState(): -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: throw StateError('Unexpected subclass'); } @@ -203,10 +214,10 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems)? $default,) {final _that = this; switch (_that) { case _Unp4kcState() when $default != null: -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: return null; } @@ -218,7 +229,7 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState { - const _Unp4kcState({required this.startUp, final Map? files, this.fs, required this.curPath, this.endMessage, this.tempOpenFile, this.errorMessage = ""}): _files = files; + const _Unp4kcState({required this.startUp, final Map? files, this.fs, required this.curPath, this.endMessage, this.tempOpenFile, this.errorMessage = "", this.searchQuery = "", this.isSearching = false, this.searchFs, final Set? searchMatchedFiles, this.sortType = Unp4kSortType.defaultSort, this.isMultiSelectMode = false, final Set selectedItems = const {}}): _files = files,_searchMatchedFiles = searchMatchedFiles,_selectedItems = selectedItems; @override final bool startUp; @@ -236,6 +247,33 @@ class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState { @override final String? endMessage; @override final MapEntry? tempOpenFile; @override@JsonKey() final String errorMessage; +@override@JsonKey() final String searchQuery; +@override@JsonKey() final bool isSearching; +/// 搜索结果的虚拟文件系统(支持分级展示) +@override final MemoryFileSystem? searchFs; +/// 搜索匹配的文件路径集合 + final Set? _searchMatchedFiles; +/// 搜索匹配的文件路径集合 +@override Set? get searchMatchedFiles { + final value = _searchMatchedFiles; + if (value == null) return null; + if (_searchMatchedFiles is EqualUnmodifiableSetView) return _searchMatchedFiles; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(value); +} + +@override@JsonKey() final Unp4kSortType sortType; +/// 是否处于多选模式 +@override@JsonKey() final bool isMultiSelectMode; +/// 多选模式下选中的文件路径集合 + final Set _selectedItems; +/// 多选模式下选中的文件路径集合 +@override@JsonKey() Set get selectedItems { + if (_selectedItems is EqualUnmodifiableSetView) return _selectedItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_selectedItems); +} + /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. @@ -248,21 +286,21 @@ _$Unp4kcStateCopyWith<_Unp4kcState> get copyWith => __$Unp4kcStateCopyWithImpl<_ void debugFillProperties(DiagnosticPropertiesBuilder 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))..add(DiagnosticsProperty('errorMessage', errorMessage)); + ..add(DiagnosticsProperty('startUp', startUp))..add(DiagnosticsProperty('files', files))..add(DiagnosticsProperty('fs', fs))..add(DiagnosticsProperty('curPath', curPath))..add(DiagnosticsProperty('endMessage', endMessage))..add(DiagnosticsProperty('tempOpenFile', tempOpenFile))..add(DiagnosticsProperty('errorMessage', errorMessage))..add(DiagnosticsProperty('searchQuery', searchQuery))..add(DiagnosticsProperty('isSearching', isSearching))..add(DiagnosticsProperty('searchFs', searchFs))..add(DiagnosticsProperty('searchMatchedFiles', searchMatchedFiles))..add(DiagnosticsProperty('sortType', sortType))..add(DiagnosticsProperty('isMultiSelectMode', isMultiSelectMode))..add(DiagnosticsProperty('selectedItems', selectedItems)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Unp4kcState&&(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)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Unp4kcState&&(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)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.searchQuery, searchQuery) || other.searchQuery == searchQuery)&&(identical(other.isSearching, isSearching) || other.isSearching == isSearching)&&(identical(other.searchFs, searchFs) || other.searchFs == searchFs)&&const DeepCollectionEquality().equals(other._searchMatchedFiles, _searchMatchedFiles)&&(identical(other.sortType, sortType) || other.sortType == sortType)&&(identical(other.isMultiSelectMode, isMultiSelectMode) || other.isMultiSelectMode == isMultiSelectMode)&&const DeepCollectionEquality().equals(other._selectedItems, _selectedItems)); } @override -int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(_files),fs,curPath,endMessage,tempOpenFile,errorMessage); +int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(_files),fs,curPath,endMessage,tempOpenFile,errorMessage,searchQuery,isSearching,searchFs,const DeepCollectionEquality().hash(_searchMatchedFiles),sortType,isMultiSelectMode,const DeepCollectionEquality().hash(_selectedItems)); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage)'; + return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage, searchQuery: $searchQuery, isSearching: $isSearching, searchFs: $searchFs, searchMatchedFiles: $searchMatchedFiles, sortType: $sortType, isMultiSelectMode: $isMultiSelectMode, selectedItems: $selectedItems)'; } @@ -273,7 +311,7 @@ abstract mixin class _$Unp4kcStateCopyWith<$Res> implements $Unp4kcStateCopyWith factory _$Unp4kcStateCopyWith(_Unp4kcState value, $Res Function(_Unp4kcState) _then) = __$Unp4kcStateCopyWithImpl; @override @useResult $Res call({ - bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage + bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems }); @@ -290,7 +328,7 @@ class __$Unp4kcStateCopyWithImpl<$Res> /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,Object? searchQuery = null,Object? isSearching = null,Object? searchFs = freezed,Object? searchMatchedFiles = freezed,Object? sortType = null,Object? isMultiSelectMode = null,Object? selectedItems = null,}) { return _then(_Unp4kcState( startUp: null == startUp ? _self.startUp : startUp // ignore: cast_nullable_to_non_nullable as bool,files: freezed == files ? _self._files : files // ignore: cast_nullable_to_non_nullable @@ -299,7 +337,14 @@ as MemoryFileSystem?,curPath: null == curPath ? _self.curPath : curPath // ignor as String,endMessage: freezed == endMessage ? _self.endMessage : endMessage // ignore: cast_nullable_to_non_nullable as String?,tempOpenFile: freezed == tempOpenFile ? _self.tempOpenFile : tempOpenFile // ignore: cast_nullable_to_non_nullable as MapEntry?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as String,searchQuery: null == searchQuery ? _self.searchQuery : searchQuery // ignore: cast_nullable_to_non_nullable +as String,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable +as bool,searchFs: freezed == searchFs ? _self.searchFs : searchFs // ignore: cast_nullable_to_non_nullable +as MemoryFileSystem?,searchMatchedFiles: freezed == searchMatchedFiles ? _self._searchMatchedFiles : searchMatchedFiles // ignore: cast_nullable_to_non_nullable +as Set?,sortType: null == sortType ? _self.sortType : sortType // ignore: cast_nullable_to_non_nullable +as Unp4kSortType,isMultiSelectMode: null == isMultiSelectMode ? _self.isMultiSelectMode : isMultiSelectMode // ignore: cast_nullable_to_non_nullable +as bool,selectedItems: null == selectedItems ? _self._selectedItems : selectedItems // ignore: cast_nullable_to_non_nullable +as Set, )); } diff --git a/lib/provider/unp4kc.g.dart b/lib/provider/unp4kc.g.dart index 5b04a8a..97991bb 100644 --- a/lib/provider/unp4kc.g.dart +++ b/lib/provider/unp4kc.g.dart @@ -41,7 +41,7 @@ final class Unp4kCModelProvider } } -String _$unp4kCModelHash() => r'410461980f6173fdbb5d92cbaa3f4c2f57c1ad8d'; +String _$unp4kCModelHash() => r'b46274b1409dc904db2d96acf692869edf034b9f'; abstract class _$Unp4kCModel extends $Notifier { Unp4kcState build(); diff --git a/lib/ui/home/localization/advanced_localization_ui_model.dart b/lib/ui/home/localization/advanced_localization_ui_model.dart index a5fb556..8d62174 100644 --- a/lib/ui/home/localization/advanced_localization_ui_model.dart +++ b/lib/ui/home/localization/advanced_localization_ui_model.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; @@ -13,7 +14,6 @@ import 'package:re_highlight/styles/vs2015.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:starcitizen_doctor/api/analytics.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; -import 'package:starcitizen_doctor/common/utils/provider.dart'; import 'package:starcitizen_doctor/data/app_advanced_localization_data.dart'; import 'package:starcitizen_doctor/data/sc_localization_data.dart'; import 'package:starcitizen_doctor/provider/unp4kc.dart'; @@ -218,22 +218,19 @@ class AdvancedLocalizationUIModel extends _$AdvancedLocalizationUIModel { Future readEnglishInI(String gameDir) async { try { - var data = await Unp4kCModel.unp4kTools(appGlobalState.applicationBinaryModuleDir!, [ - "extract_memory", + var data = await Unp4kCModel.extractP4kFileToMemory( "$gameDir\\Data.p4k", "Data\\Localization\\english\\global.ini", - ]); + ); // remove bom if (data.length > 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { data = data.sublist(3); } - final iniData = String.fromCharCodes(data); + final iniData = utf8.decode(data, allowMalformed: true); return iniData; } catch (e) { final errorMessage = e.toString(); - if (Unp4kCModel.checkRunTimeError(errorMessage)) { - AnalyticsApi.touch("advanced_localization_no_runtime"); - } + // Rust 实现不再需要 .NET runtime 检查 state = state.copyWith(errorMessage: errorMessage); // rethrow; } diff --git a/lib/ui/home/localization/advanced_localization_ui_model.g.dart b/lib/ui/home/localization/advanced_localization_ui_model.g.dart index 4b5d99d..bfb578d 100644 --- a/lib/ui/home/localization/advanced_localization_ui_model.g.dart +++ b/lib/ui/home/localization/advanced_localization_ui_model.g.dart @@ -47,7 +47,7 @@ final class AdvancedLocalizationUIModelProvider } String _$advancedLocalizationUIModelHash() => - r'c7cca8935ac7df2281e83297b11b6b82d94f7a59'; + r'4527ea29b07d4e525367d380d2aeb3ece4f99f4f'; abstract class _$AdvancedLocalizationUIModel extends $Notifier { diff --git a/lib/ui/party_room/party_room_ui_model.g.dart b/lib/ui/party_room/party_room_ui_model.g.dart index ef0a433..f3f8dea 100644 --- a/lib/ui/party_room/party_room_ui_model.g.dart +++ b/lib/ui/party_room/party_room_ui_model.g.dart @@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider } } -String _$partyRoomUIModelHash() => r'add4703c9129465718a7850ea09025aa1ff35358'; +String _$partyRoomUIModelHash() => r'b22ad79b6d4a877876b2534f35fb0448b34d4ad5'; abstract class _$PartyRoomUIModel extends $Notifier { PartyRoomUIState build(); diff --git a/lib/ui/party_room/utils/game_log_tracker_provider.g.dart b/lib/ui/party_room/utils/game_log_tracker_provider.g.dart index 67e9814..d03d199 100644 --- a/lib/ui/party_room/utils/game_log_tracker_provider.g.dart +++ b/lib/ui/party_room/utils/game_log_tracker_provider.g.dart @@ -66,7 +66,7 @@ final class PartyRoomGameLogTrackerProviderProvider } String _$partyRoomGameLogTrackerProviderHash() => - r'3e1560b2fffc5461a41bece57b43e27f4112ad0c'; + r'7c9413736b0a3357075ab5309f0e746f0d6e3fc3'; final class PartyRoomGameLogTrackerProviderFamily extends $Family with diff --git a/lib/ui/tools/unp4kc/unp4kc_ui.dart b/lib/ui/tools/unp4kc/unp4kc_ui.dart index 3956fd2..2e91e1e 100644 --- a/lib/ui/tools/unp4kc/unp4kc_ui.dart +++ b/lib/ui/tools/unp4kc/unp4kc_ui.dart @@ -1,5 +1,7 @@ +import 'dart:convert'; import 'dart:io'; +import 'package:file_picker/file_picker.dart'; import 'package:file_sizes/file_sizes.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -11,7 +13,6 @@ import 'package:starcitizen_doctor/data/app_unp4k_p4k_item_data.dart'; import 'package:starcitizen_doctor/provider/unp4kc.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; -import 'package:url_launcher/url_launcher_string.dart'; class UnP4kcUI extends HookConsumerWidget { const UnP4kcUI({super.key}); @@ -28,14 +29,21 @@ class UnP4kcUI extends HookConsumerWidget { return null; }, const []); - return makeDefaultPage(context, - title: S.current.tools_unp4k_title(model.getGamePath()), - useBodyContainer: false, - content: makeBody(context, state, model, files, paths)); + return makeDefaultPage( + context, + title: S.current.tools_unp4k_title(model.getGamePath()), + useBodyContainer: false, + content: makeBody(context, state, model, files, paths), + ); } - Widget makeBody(BuildContext context, Unp4kcState state, Unp4kCModel model, - List? files, List paths) { + Widget makeBody( + BuildContext context, + Unp4kcState state, + Unp4kCModel model, + List? files, + List paths, + ) { if (state.errorMessage.isNotEmpty) { return UnP4kErrorWidget(errorMessage: state.errorMessage); } @@ -47,215 +55,133 @@ class UnP4kcUI extends HookConsumerWidget { if (state.endMessage != null) Padding( padding: const EdgeInsets.all(8.0), - child: Text( - "${state.endMessage}", - style: const TextStyle(fontSize: 12), - ), + child: Text("${state.endMessage}", style: const TextStyle(fontSize: 12)), ), ], ) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, + : Stack( children: [ - Container( - decoration: BoxDecoration( - color: FluentTheme.of(context) - .cardColor - .withValues(alpha: .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( + Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - width: MediaQuery.of(context).size.width * .3, - decoration: BoxDecoration( - color: FluentTheme.of(context) - .cardColor - .withValues(alpha: .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 - .withValues(alpha: .05), - ), - child: IconButton( + decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .06)), + height: 36, + padding: const EdgeInsets.only(left: 12, right: 12), + child: Row( + children: [ + // 搜索模式下显示返回按钮 + if (state.searchFs != null) ...[ + IconButton( + icon: const Icon(FluentIcons.back, size: 14), onPressed: () { - if (item.isDirectory ?? false) { - model.changeDir(fileName); - } else { - model.openFile(item.name ?? ""); - } + model.clearSearch(); }, - 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 - .withValues(alpha: .6)), - ), - const SizedBox(width: 12), - Text( - "${item.dateTime}", - style: TextStyle( - fontSize: 10, - color: Colors.white - .withValues(alpha: .6)), - ), - ], - ), - ], - ], - ), - ), - const SizedBox(width: 3), - Icon( - FluentIcons.chevron_right, - size: 14, - color: Colors.white.withValues(alpha: .6), - ) - ], - ), - ), ), - ); - }, - itemCount: files?.length ?? 0, + const SizedBox(width: 8), + Text( + "[${S.current.tools_unp4k_searching.replaceAll('...', '')}]", + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)), + ), + const SizedBox(width: 8), + ], + Expanded( + 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: Container( - child: state.tempOpenFile == null - ? Center( - child: Text(S.current.tools_unp4k_view_file), - ) - : 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(S.current - .tools_unp4k_msg_unknown_file_type( - state.tempOpenFile - ?.value ?? - "")), - const SizedBox(height: 32), - FilledButton( + child: Row( + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * .3, + child: _FileListPanel(state: state, model: model, files: files), + ), + Expanded( + child: state.tempOpenFile == null + ? Center(child: Text(S.current.tools_unp4k_view_file)) + : 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( + S.current.tools_unp4k_msg_unknown_file_type( + state.tempOpenFile?.value ?? "", + ), + ), + const SizedBox(height: 32), + FilledButton( child: Padding( - padding: - const EdgeInsets.all(4), - child: Text(S.current - .action_open_folder), + padding: const EdgeInsets.all(4), + child: Text(S.current.action_open_folder), ), onPressed: () { - SystemHelper.openDir(state - .tempOpenFile - ?.value ?? - ""); - }) - ], + 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)), + ), ], - )), - if (state.endMessage != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "${state.endMessage}", - style: const TextStyle(fontSize: 12), + ), + // 搜索加载遮罩 + if (state.isSearching) + Container( + color: Colors.black.withValues(alpha: .7), + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const ProgressRing(), + const SizedBox(height: 16), + Text(S.current.tools_unp4k_searching, style: const TextStyle(fontSize: 16)), + ], + ), ), ), ], @@ -263,6 +189,528 @@ class UnP4kcUI extends HookConsumerWidget { } } +/// 文件列表面板组件 +class _FileListPanel extends HookConsumerWidget { + final Unp4kcState state; + final Unp4kCModel model; + final List? files; + + const _FileListPanel({required this.state, required this.model, required this.files}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchController = useTextEditingController(text: state.searchQuery); + + return Container( + decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .01)), + child: Column( + children: [ + // 搜索栏和排序选择器 + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextBox( + controller: searchController, + placeholder: S.current.tools_unp4k_search_placeholder, + prefix: const Padding(padding: EdgeInsets.only(left: 8), child: Icon(FluentIcons.search, size: 14)), + suffix: searchController.text.isNotEmpty || state.searchFs != null + ? IconButton( + icon: const Icon(FluentIcons.clear, size: 12), + onPressed: () { + searchController.clear(); + model.clearSearch(); + }, + ) + : null, + onSubmitted: (value) { + model.search(value); + }, + ), + ), + const SizedBox(width: 8), + ComboBox( + value: state.sortType, + items: [ + ComboBoxItem(value: Unp4kSortType.defaultSort, child: Text(S.current.tools_unp4k_sort_default)), + ComboBoxItem(value: Unp4kSortType.sizeAsc, child: Text(S.current.tools_unp4k_sort_size_asc)), + ComboBoxItem(value: Unp4kSortType.sizeDesc, child: Text(S.current.tools_unp4k_sort_size_desc)), + ComboBoxItem(value: Unp4kSortType.dateAsc, child: Text(S.current.tools_unp4k_sort_date_asc)), + ComboBoxItem(value: Unp4kSortType.dateDesc, child: Text(S.current.tools_unp4k_sort_date_desc)), + ], + onChanged: (value) { + if (value != null) { + model.setSortType(value); + } + }, + ), + ], + ), + ), + // 多选模式工具栏 + if (state.isMultiSelectMode) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: FluentTheme.of(context).accentColor.withValues(alpha: .1), + border: Border( + bottom: BorderSide(color: FluentTheme.of(context).accentColor.withValues(alpha: .3), width: 1), + ), + ), + child: Row( + children: [ + Text( + S.current.tools_unp4k_action_export_selected(state.selectedItems.length), + style: const TextStyle(fontSize: 12), + ), + const Spacer(), + Button(child: Text(S.current.tools_unp4k_action_select_all), onPressed: () => model.selectAll(files)), + const SizedBox(width: 4), + Button( + child: Text(S.current.tools_unp4k_action_deselect_all), + onPressed: () => model.deselectAll(files), + ), + const SizedBox(width: 4), + FilledButton( + onPressed: state.selectedItems.isNotEmpty ? () => _exportSelected(context) : null, + child: Text(S.current.tools_unp4k_action_save_as), + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(FluentIcons.cancel, size: 14), + onPressed: () => model.exitMultiSelectMode(), + ), + ], + ), + ), + // 文件列表 + Expanded( + child: files == null || files!.isEmpty + ? Center( + child: Text( + state.searchFs != null ? S.current.tools_unp4k_search_no_result : '', + style: TextStyle(color: Colors.white.withValues(alpha: .6)), + ), + ) + : SuperListView.builder( + padding: const EdgeInsets.only(top: 6, bottom: 6, left: 3, right: 12), + itemBuilder: (BuildContext context, int index) { + final item = files![index]; + return _FileListItem(item: item, state: state, model: model); + }, + itemCount: files?.length ?? 0, + ), + ), + ], + ), + ); + } + + Future _exportSelected(BuildContext context) async { + final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as); + if (outputDir != null && context.mounted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return _MultiExtractProgressDialog(selectedItems: state.selectedItems, outputDir: outputDir, model: model); + }, + ); + // 提取完成后退出多选模式 + model.exitMultiSelectMode(); + } + } +} + +/// 文件列表项组件 +class _FileListItem extends HookWidget { + final AppUnp4kP4kItemData item; + final Unp4kcState state; + final Unp4kCModel model; + + const _FileListItem({required this.item, required this.state, required this.model}); + + @override + Widget build(BuildContext context) { + final flyoutController = useMemoized(() => FlyoutController()); + // 显示相对于当前路径的文件名 + final fileName = item.name?.replaceAll(state.curPath.trim(), "") ?? "?"; + final itemPath = item.name ?? ""; + final isSelected = state.selectedItems.contains(itemPath); + + return FlyoutTarget( + controller: flyoutController, + child: GestureDetector( + onSecondaryTapUp: (details) => _showContextMenu(context, flyoutController), + child: Container( + margin: const EdgeInsets.only(top: 4, bottom: 4), + decoration: BoxDecoration( + color: isSelected + ? FluentTheme.of(context).accentColor.withValues(alpha: .2) + : FluentTheme.of(context).cardColor.withValues(alpha: .05), + ), + child: IconButton( + onPressed: () { + if (state.isMultiSelectMode) { + // 多选模式下点击切换选中状态 + model.toggleSelectItem(itemPath); + } else if (item.isDirectory ?? false) { + final dirName = item.name?.replaceAll(state.curPath.trim(), "") ?? ""; + model.changeDir(dirName); + } else { + model.openFile(item.name ?? ""); + } + }, + icon: Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: Row( + children: [ + // 多选模式下显示复选框 + if (state.isMultiSelectMode) ...[ + Checkbox( + checked: isSelected, + onChanged: (value) { + model.toggleSelectItem(itemPath); + }, + ), + const SizedBox(width: 8), + ], + 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.withValues(alpha: .6)), + ), + const SizedBox(width: 12), + Text( + item.dateModified != null + ? DateTime.fromMillisecondsSinceEpoch( + item.dateModified!, + ).toString().substring(0, 19) + : "", + style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(width: 3), + Icon(FluentIcons.chevron_right, size: 14, color: Colors.white.withValues(alpha: .6)), + ], + ), + ), + ), + ), + ), + ); + } + + void _showContextMenu(BuildContext context, FlyoutController controller) { + // 保存外部 context,因为 flyout 的 context 在关闭后会失效 + final outerContext = context; + controller.showFlyout( + autoModeConfiguration: FlyoutAutoConfiguration(preferredMode: FlyoutPlacementMode.bottomCenter), + barrierColor: Colors.transparent, + builder: (flyoutContext) { + return MenuFlyout( + items: [ + MenuFlyoutItem( + leading: const Icon(FluentIcons.save_as, size: 16), + text: Text(S.current.tools_unp4k_action_save_as), + onPressed: () async { + Navigator.of(flyoutContext).pop(); + await _saveAs(outerContext); + }, + ), + // 多选模式切换 + if (!state.isMultiSelectMode) + MenuFlyoutItem( + leading: const Icon(FluentIcons.checkbox_composite, size: 16), + text: Text(S.current.tools_unp4k_action_multi_select), + onPressed: () { + Navigator.of(flyoutContext).pop(); + model.enterMultiSelectMode(); + // 自动选中当前项 + model.toggleSelectItem(item.name ?? ""); + }, + ), + ], + ); + }, + ); + } + + Future _saveAs(BuildContext context) async { + final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as); + if (outputDir != null && context.mounted) { + await _showExtractProgressDialog(context, outputDir); + } + } + + Future _showExtractProgressDialog(BuildContext context, String outputDir) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return _ExtractProgressDialog(item: item, outputDir: outputDir, model: model); + }, + ); + } +} + +/// 提取进度对话框 +class _ExtractProgressDialog extends HookWidget { + final AppUnp4kP4kItemData item; + final String outputDir; + final Unp4kCModel model; + + const _ExtractProgressDialog({required this.item, required this.outputDir, required this.model}); + + @override + Widget build(BuildContext context) { + final isCancelled = useState(false); + final currentFile = useState(""); + final currentIndex = useState(0); + final totalFiles = useState(0); + final isCompleted = useState(false); + final errorMessage = useState(null); + final extractedCount = useState(0); + + useEffect(() { + // 获取文件数量 + totalFiles.value = model.getFileCountInDirectory(item); + + // 开始提取 + _startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount); + return null; + }, const []); + + final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0; + + return ContentDialog( + title: Text(S.current.tools_unp4k_extract_dialog_title), + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isCompleted.value && errorMessage.value == null) ...[ + // 进度条 + ProgressBar(value: progress * 100), + const SizedBox(height: 12), + // 进度文本 + Text( + S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value), + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 8), + // 当前文件 + Text( + S.current.tools_unp4k_extract_current_file( + currentFile.value.length > 60 + ? "...${currentFile.value.substring(currentFile.value.length - 60)}" + : currentFile.value, + ), + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ] else if (errorMessage.value != null) ...[ + // 错误信息 + const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)), + const SizedBox(height: 12), + Text(errorMessage.value!, style: const TextStyle(fontSize: 14)), + ] else ...[ + // 完成 + const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)), + const SizedBox(height: 12), + Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)), + ], + ], + ), + actions: [ + if (!isCompleted.value && errorMessage.value == null) + Button( + onPressed: () { + isCancelled.value = true; + }, + child: Text(S.current.home_action_cancel), + ) + else + FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)), + ], + ); + } + + Future _startExtraction( + ValueNotifier isCancelled, + ValueNotifier currentFile, + ValueNotifier currentIndex, + ValueNotifier totalFiles, + ValueNotifier isCompleted, + ValueNotifier errorMessage, + ValueNotifier extractedCount, + ) async { + final result = await model.extractToDirectoryWithProgress( + item, + outputDir, + onProgress: (current, total, file) { + currentIndex.value = current; + totalFiles.value = total; + currentFile.value = file; + }, + isCancelled: () => isCancelled.value, + ); + + final (success, count, error) = result; + extractedCount.value = count; + + if (!success && error != null) { + errorMessage.value = error; + } else { + isCompleted.value = true; + } + } +} + +/// 批量提取进度对话框 +class _MultiExtractProgressDialog extends HookWidget { + final Set selectedItems; + final String outputDir; + final Unp4kCModel model; + + const _MultiExtractProgressDialog({required this.selectedItems, required this.outputDir, required this.model}); + + @override + Widget build(BuildContext context) { + final isCancelled = useState(false); + final currentFile = useState(""); + final currentIndex = useState(0); + final totalFiles = useState(0); + final isCompleted = useState(false); + final errorMessage = useState(null); + final extractedCount = useState(0); + + useEffect(() { + // 获取文件数量 + totalFiles.value = model.getFileCountForSelectedItems(selectedItems); + + // 开始提取 + _startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount); + return null; + }, const []); + + final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0; + + return ContentDialog( + title: Text(S.current.tools_unp4k_extract_dialog_title), + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isCompleted.value && errorMessage.value == null) ...[ + // 进度条 + ProgressBar(value: progress * 100), + const SizedBox(height: 12), + // 进度文本 + Text( + S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value), + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 8), + // 当前文件 + Text( + S.current.tools_unp4k_extract_current_file( + currentFile.value.length > 60 + ? "...${currentFile.value.substring(currentFile.value.length - 60)}" + : currentFile.value, + ), + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ] else if (errorMessage.value != null) ...[ + // 错误信息 + const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)), + const SizedBox(height: 12), + Text(errorMessage.value!, style: const TextStyle(fontSize: 14)), + ] else ...[ + // 完成 + const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)), + const SizedBox(height: 12), + Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)), + ], + ], + ), + actions: [ + if (!isCompleted.value && errorMessage.value == null) + Button( + onPressed: () { + isCancelled.value = true; + }, + child: Text(S.current.home_action_cancel), + ) + else + FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)), + ], + ); + } + + Future _startExtraction( + ValueNotifier isCancelled, + ValueNotifier currentFile, + ValueNotifier currentIndex, + ValueNotifier totalFiles, + ValueNotifier isCompleted, + ValueNotifier errorMessage, + ValueNotifier extractedCount, + ) async { + final result = await model.extractSelectedItemsWithProgress( + selectedItems, + outputDir, + onProgress: (current, total, file) { + currentIndex.value = current; + totalFiles.value = total; + currentFile.value = file; + }, + isCancelled: () => isCancelled.value, + ); + + final (success, count, error) = result; + extractedCount.value = count; + + if (!success && error != null) { + errorMessage.value = error; + } else { + isCompleted.value = true; + } + } +} + class _TextTempWidget extends HookConsumerWidget { final String filePath; @@ -273,20 +721,20 @@ class _TextTempWidget extends HookConsumerWidget { final textData = useState(null); useEffect(() { - File(filePath).readAsString().then((value) { - textData.value = value; - }).catchError((err) { - textData.value = "Error: $err"; + File(filePath).readAsBytes().then((data) { + // 处理可能的 BOM + if (data.length > 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF) { + data = data.sublist(3); + } + final text = utf8.decode(data, allowMalformed: true); + textData.value = text; }); return null; }, const []); if (textData.value == null) return makeLoading(context); - return CodeEditor( - controller: CodeLineEditingController.fromText('${textData.value}'), - readOnly: true, - ); + return CodeEditor(controller: CodeLineEditingController.fromText('${textData.value}'), readOnly: true); } } @@ -295,39 +743,12 @@ class UnP4kErrorWidget extends StatelessWidget { const UnP4kErrorWidget({super.key, required this.errorMessage}); - static const _downloadUrl = - "https://aka.ms/dotnet-core-applaunch?missing_runtime=true&arch=x64&rid=win-x64&os=win10&apphost_version=8.0.0"; - @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(24), child: Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (Unp4kCModel.checkRunTimeError(errorMessage)) ...[ - Text( - S.current.tools_unp4k_missing_runtime, - style: const TextStyle(fontSize: 16), - ), - const SizedBox(height: 6), - Text(S.current.tools_unp4k_missing_runtime_info), - const SizedBox(height: 16), - FilledButton( - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 3), - child: Text( - S.current.tools_unp4k_missing_runtime_action_install), - ), - onPressed: () { - launchUrlString(_downloadUrl); - }), - ] else - Text(errorMessage), - ], - ), + child: Column(mainAxisSize: MainAxisSize.min, children: [Text(errorMessage)]), ), ); } diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7bd436b..730e9df 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -17,6 +17,17 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.8.12" @@ -133,6 +144,15 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "asar" version = "0.3.0" @@ -179,9 +199,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" dependencies = [ "compression-codecs", "compression-core", @@ -363,6 +383,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -385,6 +414,16 @@ dependencies = [ "piper", ] +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "build-target" version = "0.4.0" @@ -415,6 +454,15 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +[[package]] +name = "bzip2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c" +dependencies = [ + "libbz2-rs-sys", +] + [[package]] name = "castaway" version = "0.2.4" @@ -425,12 +473,23 @@ dependencies = [ ] [[package]] -name = "cc" -version = "1.2.46" +name = "cbc" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex", ] @@ -459,10 +518,20 @@ dependencies = [ ] [[package]] -name = "clap" -version = "4.5.52" +name = "cipher" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -470,9 +539,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.52" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -548,9 +617,9 @@ dependencies = [ [[package]] name = "compression-codecs" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" dependencies = [ "compression-core", "flate2", @@ -559,9 +628,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "concurrent-queue" @@ -572,6 +641,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.61.2", +] + [[package]] name = "console_error_panic_hook" version = "0.1.7" @@ -602,6 +684,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + [[package]] name = "cookie" version = "0.18.1" @@ -656,6 +744,21 @@ dependencies = [ "libc", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" @@ -822,6 +925,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "deflate64" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" + [[package]] name = "delegate-attr" version = "0.3.0" @@ -853,6 +962,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "derive_builder" version = "0.20.2" @@ -892,6 +1012,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -936,6 +1057,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -947,9 +1074,9 @@ dependencies = [ [[package]] name = "endi" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enum-as-inner" @@ -1078,6 +1205,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -1298,6 +1426,25 @@ version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + [[package]] name = "h2" version = "0.4.12" @@ -1310,7 +1457,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -1331,9 +1478,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -1400,13 +1547,21 @@ dependencies = [ ] [[package]] -name = "http" -version = "1.3.1" +name = "hmac" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -1496,9 +1651,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56" +checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ "base64 0.22.1", "bytes", @@ -1671,16 +1826,39 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] +[[package]] +name = "indicatif" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9375e112e4b463ec1b1c6c011953545c65a30164fbab5b581df32b3abf0dcb88" +dependencies = [ + "console", + "portable-atomic", + "unicode-width", + "unit-prefix", + "web-time", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "ipconfig" version = "0.3.2" @@ -1749,10 +1927,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "js-sys" -version = "0.3.82" +name = "jobserver" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -1765,10 +1953,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "libc" -version = "0.2.177" +name = "libbz2-rs-sys" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" +checksum = "2c4a545a15244c7d945065b5d392b2d2d7f21526fba56ce51467b06ed445e8f7" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libredox" @@ -1781,6 +1975,15 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libz-rs-sys" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b484ba8d4f775eeca644c452a56650e544bf7e617f1d170fe7298122ead5222" +dependencies = [ + "zlib-rs", +] + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1810,9 +2013,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru-slab" @@ -1821,10 +2024,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" [[package]] -name = "mac-notification-sys" -version = "0.6.8" +name = "lzma-rust2" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ee70bb2bba058d58e252d2944582d634fc884fc9c489a966d428dedcf653e97" +checksum = "c60a23ffb90d527e23192f1246b14746e2f7f071cb84476dd879071696c18a4a" +dependencies = [ + "crc", + "sha2", +] + +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" dependencies = [ "cc", "objc2", @@ -2300,6 +2513,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -2397,6 +2620,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppmd-rust" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d558c559f0450f16f2a27a1f017ef38468c1090c9ce63c8e51366232d53717b4" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2449,6 +2678,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2731,6 +2969,7 @@ dependencies = [ "serde_json", "tokenizers", "tokio", + "unp4k_rs", "url", "walkdir", "win32job", @@ -2787,9 +3026,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -2963,15 +3202,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.1.0", "serde_core", @@ -2982,9 +3221,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", @@ -2992,6 +3231,17 @@ dependencies = [ "syn", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "sha2" version = "0.10.9" @@ -3020,9 +3270,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -3120,9 +3370,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -3193,7 +3443,7 @@ version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" dependencies = [ - "quick-xml", + "quick-xml 0.37.5", "thiserror 2.0.17", "windows 0.61.3", "windows-version", @@ -3328,9 +3578,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokenizers" -version = "0.22.1" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6475a27088c98ea96d00b39a9ddfb63780d1ad4cceb6f48374349a96ab2b7842" +checksum = "b238e22d44a15349529690fb07bd645cf58149a1b1e44d6cb5bd1641ff1a6223" dependencies = [ "ahash", "aho-corasick", @@ -3434,7 +3684,7 @@ version = "0.23.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime", "toml_parser", "winnow", @@ -3466,9 +3716,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags", "bytes", @@ -3496,9 +3746,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -3507,9 +3757,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -3518,9 +3768,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -3538,9 +3788,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "sharded-slab", "thread_local", @@ -3591,6 +3841,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3603,6 +3859,34 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "unp4k_rs" +version = "0.1.0" +source = "git+https://github.com/StarCitizenToolBox/unp4k_rs?tag=V0.0.2#02867472dda1c18e81b0f635b8653fa86bd145cb" +dependencies = [ + "aes", + "anyhow", + "byteorder", + "cbc", + "clap", + "crc32fast", + "flate2", + "glob", + "globset", + "indicatif", + "quick-xml 0.38.4", + "rayon", + "thiserror 2.0.17", + "zip", + "zstd", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -3629,9 +3913,9 @@ dependencies = [ [[package]] name = "ureq-proto" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b4531c118335662134346048ddb0e54cc86bd7e81866757873055f0e38f5d2" +checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f" dependencies = [ "base64 0.22.1", "http", @@ -3671,13 +3955,13 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.18.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" +checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", - "serde", + "serde_core", "wasm-bindgen", ] @@ -3735,9 +4019,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", @@ -3748,9 +4032,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if", "js-sys", @@ -3761,9 +4045,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3771,9 +4055,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", @@ -3784,9 +4068,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -3821,9 +4105,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -4347,9 +4631,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -4472,18 +4756,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" dependencies = [ "proc-macro2", "quote", @@ -4516,6 +4800,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "zerotrie" @@ -4550,6 +4848,79 @@ dependencies = [ "syn", ] +[[package]] +name = "zip" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a05c7c36fde6c09b08576c9f7fb4cda705990f73b58fe011abf7dfb24168b" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "deflate64", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap 2.12.1", + "lzma-rust2", + "memchr", + "pbkdf2", + "ppmd-rust", + "sha1", + "time", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zlib-rs" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36134c44663532e6519d7a6dfdbbe06f6f8192bde8ae9ed076e9b213f0e31df7" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + [[package]] name = "zvariant" version = "5.8.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index a6467f7..06ca71c 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -30,6 +30,7 @@ ort = { version = "2.0.0-rc.10", features = ["xnnpack", "download-binaries", "nd tokenizers = { version = "0.22", default-features = false, features = ["onig"] } ndarray = "0.17" serde_json = "1.0" +unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0.2" } [target.'cfg(windows)'.dependencies] windows = { version = "0.62.2", features = [ diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 1c73f59..d9522d7 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -6,3 +6,4 @@ pub mod rs_process; pub mod win32_api; pub mod asar_api; pub mod ort_api; +pub mod unp4k_api; diff --git a/rust/src/api/unp4k_api.rs b/rust/src/api/unp4k_api.rs new file mode 100644 index 0000000..43f6877 --- /dev/null +++ b/rust/src/api/unp4k_api.rs @@ -0,0 +1,192 @@ +use anyhow::{anyhow, Result}; +use flutter_rust_bridge::frb; +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; +use unp4k::{P4kEntry, P4kFile}; + +/// P4K 文件项信息 +#[frb(dart_metadata=("freezed"))] +pub struct P4kFileItem { + /// 文件名/路径 + pub name: String, + /// 是否为目录 + pub is_directory: bool, + /// 文件大小(字节) + pub size: u64, + /// 压缩后大小(字节) + pub compressed_size: u64, + /// 文件修改时间(毫秒时间戳) + pub date_modified: i64, +} + +/// 将 DOS 日期时间转换为毫秒时间戳 +fn dos_datetime_to_millis(date: u16, time: u16) -> i64 { + let year = ((date >> 9) & 0x7F) as i32 + 1980; + let month = ((date >> 5) & 0x0F) as u32; + let day = (date & 0x1F) as u32; + let hour = ((time >> 11) & 0x1F) as u32; + let minute = ((time >> 5) & 0x3F) as u32; + let second = ((time & 0x1F) * 2) as u32; + + let days_since_epoch = { + let mut days = 0i64; + for y in 1970..year { + days += if (y % 4 == 0 && y % 100 != 0) || (y % 400 == 0) { 366 } else { 365 }; + } + let days_in_months = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; + if month >= 1 && month <= 12 { + days += days_in_months[(month - 1) as usize] as i64; + if month > 2 && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)) { + days += 1; + } + } + days += (day as i64) - 1; + days + }; + + (days_since_epoch * 86400 + (hour as i64) * 3600 + (minute as i64) * 60 + (second as i64)) * 1000 +} + +// 全局 P4K 读取器实例(用于保持状态) +static GLOBAL_P4K_READER: once_cell::sync::Lazy>>> = + once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None))); + +static GLOBAL_P4K_FILES: once_cell::sync::Lazy>>> = + once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(HashMap::new()))); + +/// 打开 P4K 文件(仅打开,不读取文件列表) +pub async fn p4k_open(p4k_path: String) -> Result<()> { + let path = PathBuf::from(&p4k_path); + if !path.exists() { + return Err(anyhow!("P4K file not found: {}", p4k_path)); + } + + // 在后台线程执行阻塞操作 + let reader = tokio::task::spawn_blocking(move || { + let reader = P4kFile::open(&path)?; + Ok::<_, anyhow::Error>(reader) + }) + .await??; + + *GLOBAL_P4K_READER.lock().unwrap() = Some(reader); + // 清空之前的文件列表缓存 + GLOBAL_P4K_FILES.lock().unwrap().clear(); + + Ok(()) +} + +/// 确保文件列表已加载(内部使用) +fn ensure_files_loaded() -> Result { + let mut files = GLOBAL_P4K_FILES.lock().unwrap(); + if !files.is_empty() { + return Ok(files.len()); + } + + let reader = GLOBAL_P4K_READER.lock().unwrap(); + if reader.is_none() { + return Err(anyhow!("P4K reader not initialized")); + } + + let entries = reader.as_ref().unwrap().entries(); + for entry in entries { + let name = if entry.name.starts_with("\\") { + entry.name.clone() + } else { + format!("\\{}", entry.name.replace("/", "\\")) + }; + files.insert(name, entry.clone()); + } + + Ok(files.len()) +} + +/// 获取文件数量(会触发文件列表加载) +pub async fn p4k_get_file_count() -> Result { + tokio::task::spawn_blocking(|| ensure_files_loaded()).await? +} + +/// 获取所有文件列表 +pub async fn p4k_get_all_files() -> Result> { + tokio::task::spawn_blocking(|| { + ensure_files_loaded()?; + let files = GLOBAL_P4K_FILES.lock().unwrap(); + let mut result = Vec::with_capacity(files.len()); + + for (name, entry) in files.iter() { + result.push(P4kFileItem { + name: name.clone(), + is_directory: false, + size: entry.uncompressed_size, + compressed_size: entry.compressed_size, + date_modified: dos_datetime_to_millis(entry.mod_date, entry.mod_time), + }); + } + + Ok(result) + }) + .await? +} + +/// 提取文件到内存 +pub async fn p4k_extract_to_memory(file_path: String) -> Result> { + // 确保文件列表已加载 + tokio::task::spawn_blocking(|| ensure_files_loaded()).await??; + + // 规范化路径 + let normalized_path = if file_path.starts_with("\\") { + file_path.clone() + } else { + format!("\\{}", file_path) + }; + + // 获取文件 entry 的克隆 + let entry = { + let files = GLOBAL_P4K_FILES.lock().unwrap(); + files + .get(&normalized_path) + .ok_or_else(|| anyhow!("File not found: {}", file_path))? + .clone() + }; + + // 在后台线程执行阻塞的提取操作 + let data = tokio::task::spawn_blocking(move || { + let mut reader = GLOBAL_P4K_READER.lock().unwrap(); + if reader.is_none() { + return Err(anyhow!("P4K reader not initialized")); + } + let data = reader.as_mut().unwrap().extract_entry(&entry)?; + Ok::<_, anyhow::Error>(data) + }) + .await??; + + Ok(data) +} + +/// 提取文件到磁盘 +pub async fn p4k_extract_to_disk(file_path: String, output_path: String) -> Result<()> { + let data = p4k_extract_to_memory(file_path).await?; + + // 在后台线程执行阻塞的文件写入操作 + tokio::task::spawn_blocking(move || { + let output = PathBuf::from(&output_path); + + // 创建父目录 + if let Some(parent) = output.parent() { + std::fs::create_dir_all(parent)?; + } + + std::fs::write(output, data)?; + Ok::<_, anyhow::Error>(()) + }) + .await??; + + Ok(()) +} + +/// 关闭 P4K 读取器 +pub async fn p4k_close() -> Result<()> { + *GLOBAL_P4K_READER.lock().unwrap() = None; + GLOBAL_P4K_FILES.lock().unwrap().clear(); + Ok(()) +} diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index 9377193..0dc8575 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -518970253; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1801517256; // Section: executor @@ -291,6 +291,151 @@ fn wire__crate__api__ort_api__load_translation_model_impl( }, ) } +fn wire__crate__api__unp4k_api__p4k_close_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_close", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::unp4k_api::p4k_close().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__unp4k_api__p4k_extract_to_disk_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + file_path: impl CstDecode, + output_path: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_extract_to_disk", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_file_path = file_path.cst_decode(); + let api_output_path = output_path.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::unp4k_api::p4k_extract_to_disk( + api_file_path, + api_output_path, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__unp4k_api__p4k_extract_to_memory_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + file_path: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_extract_to_memory", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_file_path = file_path.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::unp4k_api::p4k_extract_to_memory(api_file_path).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__unp4k_api__p4k_get_all_files_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_get_all_files", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::unp4k_api::p4k_get_all_files().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__unp4k_api__p4k_get_file_count_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_get_file_count", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::unp4k_api::p4k_get_file_count().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__unp4k_api__p4k_open_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + p4k_path: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "p4k_open", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_p4k_path = p4k_path.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::unp4k_api::p4k_open(api_p4k_path).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js_impl( port_: flutter_rust_bridge::for_generated::MessagePort, that: impl CstDecode, @@ -564,6 +709,12 @@ impl CstDecode for i32 { self } } +impl CstDecode for i64 { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> i64 { + self + } +} impl CstDecode for i32 { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::http_package::MyHttpVersion { @@ -630,6 +781,12 @@ impl CstDecode for u8 { self } } +impl CstDecode for usize { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> usize { + self + } +} impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -681,6 +838,13 @@ impl SseDecode for i32 { } } +impl SseDecode for i64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i64::().unwrap() + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -693,6 +857,20 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -823,6 +1001,24 @@ impl SseDecode for Option> { } } +impl SseDecode for crate::api::unp4k_api::P4kFileItem { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_name = ::sse_decode(deserializer); + let mut var_isDirectory = ::sse_decode(deserializer); + let mut var_size = ::sse_decode(deserializer); + let mut var_compressedSize = ::sse_decode(deserializer); + let mut var_dateModified = ::sse_decode(deserializer); + return crate::api::unp4k_api::P4kFileItem { + name: var_name, + is_directory: var_isDirectory, + size: var_size, + compressed_size: var_compressedSize, + date_modified: var_dateModified, + }; + } +} + impl SseDecode for crate::api::win32_api::ProcessInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -943,6 +1139,13 @@ impl SseDecode for () { fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} } +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + fn pde_ffi_dispatcher_primary_impl( func_id: i32, port: flutter_rust_bridge::for_generated::MessagePort, @@ -1024,6 +1227,30 @@ impl flutter_rust_bridge::IntoIntoDart } } // Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::unp4k_api::P4kFileItem { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.name.into_into_dart().into_dart(), + self.is_directory.into_into_dart().into_dart(), + self.size.into_into_dart().into_dart(), + self.compressed_size.into_into_dart().into_dart(), + self.date_modified.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::unp4k_api::P4kFileItem +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::unp4k_api::P4kFileItem +{ + fn into_into_dart(self) -> crate::api::unp4k_api::P4kFileItem { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::api::win32_api::ProcessInfo { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { [ @@ -1185,6 +1412,13 @@ impl SseEncode for i32 { } } +impl SseEncode for i64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i64::(self).unwrap(); + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1195,6 +1429,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1318,6 +1562,17 @@ impl SseEncode for Option> { } } +impl SseEncode for crate::api::unp4k_api::P4kFileItem { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.name, serializer); + ::sse_encode(self.is_directory, serializer); + ::sse_encode(self.size, serializer); + ::sse_encode(self.compressed_size, serializer); + ::sse_encode(self.date_modified, serializer); + } +} + impl SseEncode for crate::api::win32_api::ProcessInfo { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -1416,6 +1671,16 @@ impl SseEncode for () { fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} } +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + #[cfg(not(target_family = "wasm"))] mod io { // This file is automatically generated, so please do not edit it. @@ -1508,6 +1773,16 @@ mod io { vec.into_iter().map(CstDecode::cst_decode).collect() } } + impl CstDecode> for *mut wire_cst_list_p_4_k_file_item { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> Vec { + let vec = unsafe { + let wrap = flutter_rust_bridge::for_generated::box_from_leak_ptr(self); + flutter_rust_bridge::for_generated::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(CstDecode::cst_decode).collect() + } + } impl CstDecode> for *mut wire_cst_list_prim_u_8_loose { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> Vec { @@ -1546,6 +1821,18 @@ mod io { vec.into_iter().map(CstDecode::cst_decode).collect() } } + impl CstDecode for wire_cst_p_4_k_file_item { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::api::unp4k_api::P4kFileItem { + crate::api::unp4k_api::P4kFileItem { + name: self.name.cst_decode(), + is_directory: self.is_directory.cst_decode(), + size: self.size.cst_decode(), + compressed_size: self.compressed_size.cst_decode(), + date_modified: self.date_modified.cst_decode(), + } + } + } impl CstDecode for wire_cst_process_info { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> crate::api::win32_api::ProcessInfo { @@ -1596,6 +1883,22 @@ mod io { } } } + impl NewWithNullPtr for wire_cst_p_4_k_file_item { + fn new_with_null_ptr() -> Self { + Self { + name: core::ptr::null_mut(), + is_directory: Default::default(), + size: Default::default(), + compressed_size: Default::default(), + date_modified: Default::default(), + } + } + } + impl Default for wire_cst_p_4_k_file_item { + fn default() -> Self { + Self::new_with_null_ptr() + } + } impl NewWithNullPtr for wire_cst_process_info { fn new_with_null_ptr() -> Self { Self { @@ -1764,6 +2067,50 @@ mod io { ) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_close(port_: i64) { + wire__crate__api__unp4k_api__p4k_close_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_extract_to_disk( + port_: i64, + file_path: *mut wire_cst_list_prim_u_8_strict, + output_path: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__unp4k_api__p4k_extract_to_disk_impl(port_, file_path, output_path) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_extract_to_memory( + port_: i64, + file_path: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__unp4k_api__p4k_extract_to_memory_impl(port_, file_path) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_get_all_files( + port_: i64, + ) { + wire__crate__api__unp4k_api__p4k_get_all_files_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_get_file_count( + port_: i64, + ) { + wire__crate__api__unp4k_api__p4k_get_file_count_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__unp4k_api__p4k_open( + port_: i64, + p4k_path: *mut wire_cst_list_prim_u_8_strict, + ) { + wire__crate__api__unp4k_api__p4k_open_impl(port_, p4k_path) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__asar_api__rsi_launcher_asar_data_write_main_js( port_: i64, @@ -1884,6 +2231,20 @@ mod io { flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_p_4_k_file_item( + len: i32, + ) -> *mut wire_cst_list_p_4_k_file_item { + let wrap = wire_cst_list_p_4_k_file_item { + ptr: flutter_rust_bridge::for_generated::new_leak_vec_ptr( + ::new_with_null_ptr(), + len, + ), + len, + }; + flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_prim_u_8_loose( len: i32, @@ -1942,6 +2303,12 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_list_p_4_k_file_item { + ptr: *mut wire_cst_p_4_k_file_item, + len: i32, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_list_prim_u_8_loose { ptr: *mut u8, len: i32, @@ -1966,6 +2333,15 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_p_4_k_file_item { + name: *mut wire_cst_list_prim_u_8_strict, + is_directory: bool, + size: u64, + compressed_size: u64, + date_modified: i64, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_process_info { pid: u32, name: *mut wire_cst_list_prim_u_8_strict,