feat: unp4k data forge support

This commit is contained in:
xkeyC
2025-12-11 00:19:13 +08:00
parent 23e909e330
commit 0126ae811e
29 changed files with 6235 additions and 1154 deletions

View File

@@ -0,0 +1,323 @@
import 'dart:io';
import 'dart:typed_data';
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/common/rust/api/unp4k_api.dart' as unp4k_api;
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/data/dcb_data.dart';
part 'dcb_viewer.freezed.dart';
part 'dcb_viewer.g.dart';
/// DCB 查看器视图模式
enum DcbViewMode {
/// 普通列表浏览模式
browse,
/// 全文搜索结果模式
searchResults,
}
/// DCB 查看器输入源类型
enum DcbSourceType {
/// 未初始化
none,
/// 从文件路径加载
filePath,
/// 从 P4K 内存数据加载
p4kMemory,
}
@freezed
abstract class DcbViewerState with _$DcbViewerState {
const factory DcbViewerState({
/// 是否正在加载
@Default(true) bool isLoading,
/// 加载/错误消息
@Default('') String message,
/// 错误消息
String? errorMessage,
/// DCB 文件路径(用于显示标题)
@Default('') String dcbFilePath,
/// 数据源类型
@Default(DcbSourceType.none) DcbSourceType sourceType,
/// 所有记录列表
@Default([]) List<DcbRecordData> allRecords,
/// 当前过滤后的记录列表(用于列表搜索)
@Default([]) List<DcbRecordData> filteredRecords,
/// 当前选中的记录路径
String? selectedRecordPath,
/// 当前显示的 XML 内容
@Default('') String currentXml,
/// 列表搜索查询
@Default('') String listSearchQuery,
/// 全文搜索查询
@Default('') String fullTextSearchQuery,
/// 当前视图模式
@Default(DcbViewMode.browse) DcbViewMode viewMode,
/// 全文搜索结果
@Default([]) List<DcbSearchResultData> searchResults,
/// 是否正在搜索
@Default(false) bool isSearching,
/// 是否正在加载 XML
@Default(false) bool isLoadingXml,
/// 是否正在导出
@Default(false) bool isExporting,
/// 是否需要选择文件
@Default(false) bool needSelectFile,
}) = _DcbViewerState;
}
@riverpod
class DcbViewerModel extends _$DcbViewerModel {
@override
DcbViewerState build() {
ref.onDispose(() async {
try {
await unp4k_api.dcbClose();
} catch (e) {
dPrint('[DCB Viewer] close error: $e');
}
});
return const DcbViewerState(isLoading: false, needSelectFile: true);
}
/// 从磁盘文件路径加载 DCB
Future<void> initFromFilePath(String filePath) async {
state = state.copyWith(
isLoading: true,
message: S.current.dcb_viewer_loading,
dcbFilePath: filePath,
sourceType: DcbSourceType.filePath,
needSelectFile: false,
errorMessage: null,
);
try {
final file = File(filePath);
if (!await file.exists()) {
state = state.copyWith(isLoading: false, errorMessage: 'File not found: $filePath');
return;
}
final data = await file.readAsBytes();
await _loadDcbData(data, filePath);
} catch (e) {
dPrint('[DCB Viewer] init from file error: $e');
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
/// 从 P4K 文件中提取并加载 DCB (Data/Game2.dcb)
Future<void> initFromP4kFile(String p4kPath) async {
state = state.copyWith(
isLoading: true,
message: S.current.dcb_viewer_loading,
dcbFilePath: 'Data/Game2.dcb',
sourceType: DcbSourceType.p4kMemory,
needSelectFile: false,
errorMessage: null,
);
try {
// 打开 P4K 文件
state = state.copyWith(message: S.current.tools_unp4k_msg_reading);
await unp4k_api.p4KOpen(p4KPath: p4kPath);
// 提取 DCB 文件到内存
state = state.copyWith(message: S.current.dcb_viewer_loading);
final data = await unp4k_api.p4KExtractToMemory(filePath: '\\Data\\Game2.dcb');
// 关闭 P4K已完成提取
await unp4k_api.p4KClose();
// 将数据写入临时文件并加载
final tempDir = await getTemporaryDirectory();
final tempPath = '${tempDir.path}/SCToolbox_dcb/Game2.dcb';
final tempFile = File(tempPath);
await tempFile.parent.create(recursive: true);
await tempFile.writeAsBytes(data);
state = state.copyWith(dcbFilePath: tempPath);
await _loadDcbData(Uint8List.fromList(data), tempPath);
} catch (e) {
dPrint('[DCB Viewer] init from P4K error: $e');
// 确保关闭 P4K
try {
await unp4k_api.p4KClose();
} catch (_) {}
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
/// 从内存数据初始化 DCB 查看器
Future<void> initFromData(Uint8List data, String filePath) async {
state = state.copyWith(
isLoading: true,
message: S.current.dcb_viewer_loading,
dcbFilePath: filePath,
sourceType: DcbSourceType.p4kMemory,
needSelectFile: false,
errorMessage: null,
);
await _loadDcbData(data, filePath);
}
/// 内部方法:加载 DCB 数据
Future<void> _loadDcbData(Uint8List data, String filePath) async {
try {
// 检查是否为 DCB 格式
final isDataforge = await unp4k_api.dcbIsDataforge(data: data);
if (!isDataforge) {
state = state.copyWith(isLoading: false, errorMessage: S.current.dcb_viewer_error_not_dcb);
return;
}
// 解析 DCB 文件
state = state.copyWith(message: S.current.dcb_viewer_parsing);
await unp4k_api.dcbOpen(data: data);
// 获取记录列表
state = state.copyWith(message: S.current.dcb_viewer_loading_records);
final apiRecords = await unp4k_api.dcbGetRecordList();
// 转换为本地数据类型
final records = apiRecords.map((r) => DcbRecordData(path: r.path, index: r.index.toInt())).toList();
state = state.copyWith(
isLoading: false,
message: S.current.dcb_viewer_loaded_records(records.length),
allRecords: records,
filteredRecords: records,
);
} catch (e) {
dPrint('[DCB Viewer] load data error: $e');
state = state.copyWith(isLoading: false, errorMessage: e.toString());
}
}
/// 选择一条记录并加载其 XML
Future<void> selectRecord(DcbRecordData record) async {
if (state.selectedRecordPath == record.path) return;
state = state.copyWith(selectedRecordPath: record.path, isLoadingXml: true, currentXml: '');
try {
final xml = await unp4k_api.dcbRecordToXml(path: record.path);
state = state.copyWith(isLoadingXml: false, currentXml: xml);
} catch (e) {
dPrint('[DCB Viewer] load xml error: $e');
state = state.copyWith(isLoadingXml: false, currentXml: '<!-- Error loading XML: $e -->');
}
}
/// 列表搜索(过滤路径)
void searchList(String query) {
state = state.copyWith(listSearchQuery: query);
if (query.isEmpty) {
state = state.copyWith(filteredRecords: state.allRecords);
return;
}
final queryLower = query.toLowerCase();
final filtered = state.allRecords.where((record) {
return record.path.toLowerCase().contains(queryLower);
}).toList();
state = state.copyWith(filteredRecords: filtered);
}
/// 全文搜索
Future<void> searchFullText(String query) async {
if (query.isEmpty) {
// 退出搜索模式
state = state.copyWith(viewMode: DcbViewMode.browse, fullTextSearchQuery: '', searchResults: []);
return;
}
state = state.copyWith(isSearching: true, fullTextSearchQuery: query, viewMode: DcbViewMode.searchResults);
try {
final apiResults = await unp4k_api.dcbSearchAll(query: query, maxResults: BigInt.from(500));
// 转换为本地数据类型
final results = apiResults.map((r) {
return DcbSearchResultData(
path: r.path,
index: r.index.toInt(),
matches: r.matches
.map((m) => DcbSearchMatchData(lineNumber: m.lineNumber.toInt(), lineContent: m.lineContent))
.toList(),
);
}).toList();
state = state.copyWith(
isSearching: false,
searchResults: results,
message: S.current.dcb_viewer_search_results(results.length),
);
} catch (e) {
dPrint('[DCB Viewer] search error: $e');
state = state.copyWith(isSearching: false, message: 'Search error: $e');
}
}
/// 从搜索结果选择记录
Future<void> selectFromSearchResult(DcbSearchResultData result) async {
final record = DcbRecordData(path: result.path, index: result.index);
await selectRecord(record);
}
/// 退出搜索模式
void exitSearchMode() {
state = state.copyWith(
viewMode: DcbViewMode.browse,
fullTextSearchQuery: '',
searchResults: [],
message: S.current.dcb_viewer_loaded_records(state.allRecords.length),
);
}
/// 导出 DCB合并或分离模式
Future<String?> exportToDisk(String outputPath, bool merge) async {
state = state.copyWith(isExporting: true);
try {
await unp4k_api.dcbExportToDisk(outputPath: outputPath, merge: merge, dcbPath: state.dcbFilePath);
state = state.copyWith(isExporting: false);
return null; // 成功
} catch (e) {
dPrint('[DCB Viewer] export error: $e');
state = state.copyWith(isExporting: false);
return e.toString();
}
}
/// 重置状态,回到选择文件界面
void reset() {
state = const DcbViewerState(isLoading: false, needSelectFile: true);
}
}

View File

@@ -0,0 +1,374 @@
// 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 'dcb_viewer.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$DcbViewerState {
/// 是否正在加载
bool get isLoading;/// 加载/错误消息
String get message;/// 错误消息
String? get errorMessage;/// DCB 文件路径(用于显示标题)
String get dcbFilePath;/// 数据源类型
DcbSourceType get sourceType;/// 所有记录列表
List<DcbRecordData> get allRecords;/// 当前过滤后的记录列表(用于列表搜索)
List<DcbRecordData> get filteredRecords;/// 当前选中的记录路径
String? get selectedRecordPath;/// 当前显示的 XML 内容
String get currentXml;/// 列表搜索查询
String get listSearchQuery;/// 全文搜索查询
String get fullTextSearchQuery;/// 当前视图模式
DcbViewMode get viewMode;/// 全文搜索结果
List<DcbSearchResultData> get searchResults;/// 是否正在搜索
bool get isSearching;/// 是否正在加载 XML
bool get isLoadingXml;/// 是否正在导出
bool get isExporting;/// 是否需要选择文件
bool get needSelectFile;
/// Create a copy of DcbViewerState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$DcbViewerStateCopyWith<DcbViewerState> get copyWith => _$DcbViewerStateCopyWithImpl<DcbViewerState>(this as DcbViewerState, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is DcbViewerState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.message, message) || other.message == message)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.dcbFilePath, dcbFilePath) || other.dcbFilePath == dcbFilePath)&&(identical(other.sourceType, sourceType) || other.sourceType == sourceType)&&const DeepCollectionEquality().equals(other.allRecords, allRecords)&&const DeepCollectionEquality().equals(other.filteredRecords, filteredRecords)&&(identical(other.selectedRecordPath, selectedRecordPath) || other.selectedRecordPath == selectedRecordPath)&&(identical(other.currentXml, currentXml) || other.currentXml == currentXml)&&(identical(other.listSearchQuery, listSearchQuery) || other.listSearchQuery == listSearchQuery)&&(identical(other.fullTextSearchQuery, fullTextSearchQuery) || other.fullTextSearchQuery == fullTextSearchQuery)&&(identical(other.viewMode, viewMode) || other.viewMode == viewMode)&&const DeepCollectionEquality().equals(other.searchResults, searchResults)&&(identical(other.isSearching, isSearching) || other.isSearching == isSearching)&&(identical(other.isLoadingXml, isLoadingXml) || other.isLoadingXml == isLoadingXml)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.needSelectFile, needSelectFile) || other.needSelectFile == needSelectFile));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,message,errorMessage,dcbFilePath,sourceType,const DeepCollectionEquality().hash(allRecords),const DeepCollectionEquality().hash(filteredRecords),selectedRecordPath,currentXml,listSearchQuery,fullTextSearchQuery,viewMode,const DeepCollectionEquality().hash(searchResults),isSearching,isLoadingXml,isExporting,needSelectFile);
@override
String toString() {
return 'DcbViewerState(isLoading: $isLoading, message: $message, errorMessage: $errorMessage, dcbFilePath: $dcbFilePath, sourceType: $sourceType, allRecords: $allRecords, filteredRecords: $filteredRecords, selectedRecordPath: $selectedRecordPath, currentXml: $currentXml, listSearchQuery: $listSearchQuery, fullTextSearchQuery: $fullTextSearchQuery, viewMode: $viewMode, searchResults: $searchResults, isSearching: $isSearching, isLoadingXml: $isLoadingXml, isExporting: $isExporting, needSelectFile: $needSelectFile)';
}
}
/// @nodoc
abstract mixin class $DcbViewerStateCopyWith<$Res> {
factory $DcbViewerStateCopyWith(DcbViewerState value, $Res Function(DcbViewerState) _then) = _$DcbViewerStateCopyWithImpl;
@useResult
$Res call({
bool isLoading, String message, String? errorMessage, String dcbFilePath, DcbSourceType sourceType, List<DcbRecordData> allRecords, List<DcbRecordData> filteredRecords, String? selectedRecordPath, String currentXml, String listSearchQuery, String fullTextSearchQuery, DcbViewMode viewMode, List<DcbSearchResultData> searchResults, bool isSearching, bool isLoadingXml, bool isExporting, bool needSelectFile
});
}
/// @nodoc
class _$DcbViewerStateCopyWithImpl<$Res>
implements $DcbViewerStateCopyWith<$Res> {
_$DcbViewerStateCopyWithImpl(this._self, this._then);
final DcbViewerState _self;
final $Res Function(DcbViewerState) _then;
/// Create a copy of DcbViewerState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? message = null,Object? errorMessage = freezed,Object? dcbFilePath = null,Object? sourceType = null,Object? allRecords = null,Object? filteredRecords = null,Object? selectedRecordPath = freezed,Object? currentXml = null,Object? listSearchQuery = null,Object? fullTextSearchQuery = null,Object? viewMode = null,Object? searchResults = null,Object? isSearching = null,Object? isLoadingXml = null,Object? isExporting = null,Object? needSelectFile = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,dcbFilePath: null == dcbFilePath ? _self.dcbFilePath : dcbFilePath // ignore: cast_nullable_to_non_nullable
as String,sourceType: null == sourceType ? _self.sourceType : sourceType // ignore: cast_nullable_to_non_nullable
as DcbSourceType,allRecords: null == allRecords ? _self.allRecords : allRecords // ignore: cast_nullable_to_non_nullable
as List<DcbRecordData>,filteredRecords: null == filteredRecords ? _self.filteredRecords : filteredRecords // ignore: cast_nullable_to_non_nullable
as List<DcbRecordData>,selectedRecordPath: freezed == selectedRecordPath ? _self.selectedRecordPath : selectedRecordPath // ignore: cast_nullable_to_non_nullable
as String?,currentXml: null == currentXml ? _self.currentXml : currentXml // ignore: cast_nullable_to_non_nullable
as String,listSearchQuery: null == listSearchQuery ? _self.listSearchQuery : listSearchQuery // ignore: cast_nullable_to_non_nullable
as String,fullTextSearchQuery: null == fullTextSearchQuery ? _self.fullTextSearchQuery : fullTextSearchQuery // ignore: cast_nullable_to_non_nullable
as String,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
as DcbViewMode,searchResults: null == searchResults ? _self.searchResults : searchResults // ignore: cast_nullable_to_non_nullable
as List<DcbSearchResultData>,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable
as bool,isLoadingXml: null == isLoadingXml ? _self.isLoadingXml : isLoadingXml // ignore: cast_nullable_to_non_nullable
as bool,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable
as bool,needSelectFile: null == needSelectFile ? _self.needSelectFile : needSelectFile // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
/// Adds pattern-matching-related methods to [DcbViewerState].
extension DcbViewerStatePatterns on DcbViewerState {
/// 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 extends Object?>(TResult Function( _DcbViewerState value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _DcbViewerState() 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 extends Object?>(TResult Function( _DcbViewerState value) $default,){
final _that = this;
switch (_that) {
case _DcbViewerState():
return $default(_that);case _:
throw StateError('Unexpected subclass');
}
}
/// 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 extends Object?>(TResult? Function( _DcbViewerState value)? $default,){
final _that = this;
switch (_that) {
case _DcbViewerState() 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 extends Object?>(TResult Function( bool isLoading, String message, String? errorMessage, String dcbFilePath, DcbSourceType sourceType, List<DcbRecordData> allRecords, List<DcbRecordData> filteredRecords, String? selectedRecordPath, String currentXml, String listSearchQuery, String fullTextSearchQuery, DcbViewMode viewMode, List<DcbSearchResultData> searchResults, bool isSearching, bool isLoadingXml, bool isExporting, bool needSelectFile)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _DcbViewerState() when $default != null:
return $default(_that.isLoading,_that.message,_that.errorMessage,_that.dcbFilePath,_that.sourceType,_that.allRecords,_that.filteredRecords,_that.selectedRecordPath,_that.currentXml,_that.listSearchQuery,_that.fullTextSearchQuery,_that.viewMode,_that.searchResults,_that.isSearching,_that.isLoadingXml,_that.isExporting,_that.needSelectFile);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 extends Object?>(TResult Function( bool isLoading, String message, String? errorMessage, String dcbFilePath, DcbSourceType sourceType, List<DcbRecordData> allRecords, List<DcbRecordData> filteredRecords, String? selectedRecordPath, String currentXml, String listSearchQuery, String fullTextSearchQuery, DcbViewMode viewMode, List<DcbSearchResultData> searchResults, bool isSearching, bool isLoadingXml, bool isExporting, bool needSelectFile) $default,) {final _that = this;
switch (_that) {
case _DcbViewerState():
return $default(_that.isLoading,_that.message,_that.errorMessage,_that.dcbFilePath,_that.sourceType,_that.allRecords,_that.filteredRecords,_that.selectedRecordPath,_that.currentXml,_that.listSearchQuery,_that.fullTextSearchQuery,_that.viewMode,_that.searchResults,_that.isSearching,_that.isLoadingXml,_that.isExporting,_that.needSelectFile);case _:
throw StateError('Unexpected subclass');
}
}
/// 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 extends Object?>(TResult? Function( bool isLoading, String message, String? errorMessage, String dcbFilePath, DcbSourceType sourceType, List<DcbRecordData> allRecords, List<DcbRecordData> filteredRecords, String? selectedRecordPath, String currentXml, String listSearchQuery, String fullTextSearchQuery, DcbViewMode viewMode, List<DcbSearchResultData> searchResults, bool isSearching, bool isLoadingXml, bool isExporting, bool needSelectFile)? $default,) {final _that = this;
switch (_that) {
case _DcbViewerState() when $default != null:
return $default(_that.isLoading,_that.message,_that.errorMessage,_that.dcbFilePath,_that.sourceType,_that.allRecords,_that.filteredRecords,_that.selectedRecordPath,_that.currentXml,_that.listSearchQuery,_that.fullTextSearchQuery,_that.viewMode,_that.searchResults,_that.isSearching,_that.isLoadingXml,_that.isExporting,_that.needSelectFile);case _:
return null;
}
}
}
/// @nodoc
class _DcbViewerState implements DcbViewerState {
const _DcbViewerState({this.isLoading = true, this.message = '', this.errorMessage, this.dcbFilePath = '', this.sourceType = DcbSourceType.none, final List<DcbRecordData> allRecords = const [], final List<DcbRecordData> filteredRecords = const [], this.selectedRecordPath, this.currentXml = '', this.listSearchQuery = '', this.fullTextSearchQuery = '', this.viewMode = DcbViewMode.browse, final List<DcbSearchResultData> searchResults = const [], this.isSearching = false, this.isLoadingXml = false, this.isExporting = false, this.needSelectFile = false}): _allRecords = allRecords,_filteredRecords = filteredRecords,_searchResults = searchResults;
/// 是否正在加载
@override@JsonKey() final bool isLoading;
/// 加载/错误消息
@override@JsonKey() final String message;
/// 错误消息
@override final String? errorMessage;
/// DCB 文件路径(用于显示标题)
@override@JsonKey() final String dcbFilePath;
/// 数据源类型
@override@JsonKey() final DcbSourceType sourceType;
/// 所有记录列表
final List<DcbRecordData> _allRecords;
/// 所有记录列表
@override@JsonKey() List<DcbRecordData> get allRecords {
if (_allRecords is EqualUnmodifiableListView) return _allRecords;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_allRecords);
}
/// 当前过滤后的记录列表(用于列表搜索)
final List<DcbRecordData> _filteredRecords;
/// 当前过滤后的记录列表(用于列表搜索)
@override@JsonKey() List<DcbRecordData> get filteredRecords {
if (_filteredRecords is EqualUnmodifiableListView) return _filteredRecords;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_filteredRecords);
}
/// 当前选中的记录路径
@override final String? selectedRecordPath;
/// 当前显示的 XML 内容
@override@JsonKey() final String currentXml;
/// 列表搜索查询
@override@JsonKey() final String listSearchQuery;
/// 全文搜索查询
@override@JsonKey() final String fullTextSearchQuery;
/// 当前视图模式
@override@JsonKey() final DcbViewMode viewMode;
/// 全文搜索结果
final List<DcbSearchResultData> _searchResults;
/// 全文搜索结果
@override@JsonKey() List<DcbSearchResultData> get searchResults {
if (_searchResults is EqualUnmodifiableListView) return _searchResults;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_searchResults);
}
/// 是否正在搜索
@override@JsonKey() final bool isSearching;
/// 是否正在加载 XML
@override@JsonKey() final bool isLoadingXml;
/// 是否正在导出
@override@JsonKey() final bool isExporting;
/// 是否需要选择文件
@override@JsonKey() final bool needSelectFile;
/// Create a copy of DcbViewerState
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$DcbViewerStateCopyWith<_DcbViewerState> get copyWith => __$DcbViewerStateCopyWithImpl<_DcbViewerState>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _DcbViewerState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.message, message) || other.message == message)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.dcbFilePath, dcbFilePath) || other.dcbFilePath == dcbFilePath)&&(identical(other.sourceType, sourceType) || other.sourceType == sourceType)&&const DeepCollectionEquality().equals(other._allRecords, _allRecords)&&const DeepCollectionEquality().equals(other._filteredRecords, _filteredRecords)&&(identical(other.selectedRecordPath, selectedRecordPath) || other.selectedRecordPath == selectedRecordPath)&&(identical(other.currentXml, currentXml) || other.currentXml == currentXml)&&(identical(other.listSearchQuery, listSearchQuery) || other.listSearchQuery == listSearchQuery)&&(identical(other.fullTextSearchQuery, fullTextSearchQuery) || other.fullTextSearchQuery == fullTextSearchQuery)&&(identical(other.viewMode, viewMode) || other.viewMode == viewMode)&&const DeepCollectionEquality().equals(other._searchResults, _searchResults)&&(identical(other.isSearching, isSearching) || other.isSearching == isSearching)&&(identical(other.isLoadingXml, isLoadingXml) || other.isLoadingXml == isLoadingXml)&&(identical(other.isExporting, isExporting) || other.isExporting == isExporting)&&(identical(other.needSelectFile, needSelectFile) || other.needSelectFile == needSelectFile));
}
@override
int get hashCode => Object.hash(runtimeType,isLoading,message,errorMessage,dcbFilePath,sourceType,const DeepCollectionEquality().hash(_allRecords),const DeepCollectionEquality().hash(_filteredRecords),selectedRecordPath,currentXml,listSearchQuery,fullTextSearchQuery,viewMode,const DeepCollectionEquality().hash(_searchResults),isSearching,isLoadingXml,isExporting,needSelectFile);
@override
String toString() {
return 'DcbViewerState(isLoading: $isLoading, message: $message, errorMessage: $errorMessage, dcbFilePath: $dcbFilePath, sourceType: $sourceType, allRecords: $allRecords, filteredRecords: $filteredRecords, selectedRecordPath: $selectedRecordPath, currentXml: $currentXml, listSearchQuery: $listSearchQuery, fullTextSearchQuery: $fullTextSearchQuery, viewMode: $viewMode, searchResults: $searchResults, isSearching: $isSearching, isLoadingXml: $isLoadingXml, isExporting: $isExporting, needSelectFile: $needSelectFile)';
}
}
/// @nodoc
abstract mixin class _$DcbViewerStateCopyWith<$Res> implements $DcbViewerStateCopyWith<$Res> {
factory _$DcbViewerStateCopyWith(_DcbViewerState value, $Res Function(_DcbViewerState) _then) = __$DcbViewerStateCopyWithImpl;
@override @useResult
$Res call({
bool isLoading, String message, String? errorMessage, String dcbFilePath, DcbSourceType sourceType, List<DcbRecordData> allRecords, List<DcbRecordData> filteredRecords, String? selectedRecordPath, String currentXml, String listSearchQuery, String fullTextSearchQuery, DcbViewMode viewMode, List<DcbSearchResultData> searchResults, bool isSearching, bool isLoadingXml, bool isExporting, bool needSelectFile
});
}
/// @nodoc
class __$DcbViewerStateCopyWithImpl<$Res>
implements _$DcbViewerStateCopyWith<$Res> {
__$DcbViewerStateCopyWithImpl(this._self, this._then);
final _DcbViewerState _self;
final $Res Function(_DcbViewerState) _then;
/// Create a copy of DcbViewerState
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? message = null,Object? errorMessage = freezed,Object? dcbFilePath = null,Object? sourceType = null,Object? allRecords = null,Object? filteredRecords = null,Object? selectedRecordPath = freezed,Object? currentXml = null,Object? listSearchQuery = null,Object? fullTextSearchQuery = null,Object? viewMode = null,Object? searchResults = null,Object? isSearching = null,Object? isLoadingXml = null,Object? isExporting = null,Object? needSelectFile = null,}) {
return _then(_DcbViewerState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,message: null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,errorMessage: freezed == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable
as String?,dcbFilePath: null == dcbFilePath ? _self.dcbFilePath : dcbFilePath // ignore: cast_nullable_to_non_nullable
as String,sourceType: null == sourceType ? _self.sourceType : sourceType // ignore: cast_nullable_to_non_nullable
as DcbSourceType,allRecords: null == allRecords ? _self._allRecords : allRecords // ignore: cast_nullable_to_non_nullable
as List<DcbRecordData>,filteredRecords: null == filteredRecords ? _self._filteredRecords : filteredRecords // ignore: cast_nullable_to_non_nullable
as List<DcbRecordData>,selectedRecordPath: freezed == selectedRecordPath ? _self.selectedRecordPath : selectedRecordPath // ignore: cast_nullable_to_non_nullable
as String?,currentXml: null == currentXml ? _self.currentXml : currentXml // ignore: cast_nullable_to_non_nullable
as String,listSearchQuery: null == listSearchQuery ? _self.listSearchQuery : listSearchQuery // ignore: cast_nullable_to_non_nullable
as String,fullTextSearchQuery: null == fullTextSearchQuery ? _self.fullTextSearchQuery : fullTextSearchQuery // ignore: cast_nullable_to_non_nullable
as String,viewMode: null == viewMode ? _self.viewMode : viewMode // ignore: cast_nullable_to_non_nullable
as DcbViewMode,searchResults: null == searchResults ? _self._searchResults : searchResults // ignore: cast_nullable_to_non_nullable
as List<DcbSearchResultData>,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable
as bool,isLoadingXml: null == isLoadingXml ? _self.isLoadingXml : isLoadingXml // ignore: cast_nullable_to_non_nullable
as bool,isExporting: null == isExporting ? _self.isExporting : isExporting // ignore: cast_nullable_to_non_nullable
as bool,needSelectFile: null == needSelectFile ? _self.needSelectFile : needSelectFile // ignore: cast_nullable_to_non_nullable
as bool,
));
}
}
// dart format on

View File

@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'dcb_viewer.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(DcbViewerModel)
const dcbViewerModelProvider = DcbViewerModelProvider._();
final class DcbViewerModelProvider
extends $NotifierProvider<DcbViewerModel, DcbViewerState> {
const DcbViewerModelProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'dcbViewerModelProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$dcbViewerModelHash();
@$internal
@override
DcbViewerModel create() => DcbViewerModel();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(DcbViewerState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<DcbViewerState>(value),
);
}
}
String _$dcbViewerModelHash() => r'94c3542282f64917efadbe14a0ee4967220bec77';
abstract class _$DcbViewerModel extends $Notifier<DcbViewerState> {
DcbViewerState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<DcbViewerState, DcbViewerState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<DcbViewerState, DcbViewerState>,
DcbViewerState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@@ -1,8 +1,11 @@
// ignore_for_file: avoid_build_context_in_providers
import 'dart:io';
import 'package:file/memory.dart';
import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:go_router/go_router.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:starcitizen_doctor/api/analytics.dart';
@@ -378,17 +381,18 @@ class Unp4kCModel extends _$Unp4kCModel {
}
}
Future<void> openFile(String filePath) async {
Future<void> openFile(String filePath, {BuildContext? context}) 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");
// ignore: use_build_context_synchronously
await extractFile(filePath, tempPath, mode: "extract_open", context: context);
}
Future<void> extractFile(String filePath, String outputPath, {String mode = "extract"}) async {
Future<void> extractFile(String filePath, String outputPath, {String mode = "extract", BuildContext? context}) async {
try {
// remove first \\
if (filePath.startsWith("\\")) {
@@ -402,6 +406,16 @@ class Unp4kCModel extends _$Unp4kCModel {
await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: outputPath);
if (mode == "extract_open") {
if (context != null && filePath.toLowerCase().endsWith(".dcb")) {
// 关闭 loading 状态
state = state.copyWith(tempOpenFile: null, endMessage: S.current.tools_unp4k_msg_open_file(filePath));
// 跳转至 DCBViewer
if (context.mounted) {
context.push("/tools/dcb_viewer", extra: {"path": fullOutputPath});
}
return;
}
const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini", ".mtl"];
const imgExt = [".png"];
String openType = "unknown";

View File

@@ -12,7 +12,7 @@ part of 'unp4kc.dart';
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$Unp4kcState implements DiagnosticableTreeMixin {
mixin _$Unp4kcState {
bool get startUp; Map<String, AppUnp4kP4kItemData>? get files; MemoryFileSystem? get fs; String get curPath; String? get endMessage; MapEntry<String, String>? get tempOpenFile; String get errorMessage; String get searchQuery; bool get isSearching;/// 搜索结果的虚拟文件系统(支持分级展示)
MemoryFileSystem? get searchFs;/// 搜索匹配的文件路径集合
@@ -26,12 +26,6 @@ mixin _$Unp4kcState implements DiagnosticableTreeMixin {
$Unp4kcStateCopyWith<Unp4kcState> get copyWith => _$Unp4kcStateCopyWithImpl<Unp4kcState>(this as Unp4kcState, _$identity);
@override
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('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) {
@@ -43,7 +37,7 @@ bool operator ==(Object other) {
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 }) {
String toString() {
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)';
}
@@ -228,7 +222,7 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag
/// @nodoc
class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState {
class _Unp4kcState implements Unp4kcState {
const _Unp4kcState({required this.startUp, final Map<String, AppUnp4kP4kItemData>? files, this.fs, required this.curPath, this.endMessage, this.tempOpenFile, this.errorMessage = "", this.searchQuery = "", this.isSearching = false, this.searchFs, final Set<String>? searchMatchedFiles, this.sortType = Unp4kSortType.defaultSort, this.isMultiSelectMode = false, final Set<String> selectedItems = const {}}): _files = files,_searchMatchedFiles = searchMatchedFiles,_selectedItems = selectedItems;
@@ -282,12 +276,6 @@ class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState {
_$Unp4kcStateCopyWith<_Unp4kcState> get copyWith => __$Unp4kcStateCopyWithImpl<_Unp4kcState>(this, _$identity);
@override
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('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) {
@@ -299,7 +287,7 @@ bool operator ==(Object other) {
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 }) {
String toString() {
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)';
}

View File

@@ -41,7 +41,7 @@ final class Unp4kCModelProvider
}
}
String _$unp4kCModelHash() => r'72ee23ad9864cdfb73a588ea1a0509b083e7dee8';
String _$unp4kCModelHash() => r'68c24d50113e9e734ae8d277f65999bbef05dc05';
abstract class _$Unp4kCModel extends $Notifier<Unp4kcState> {
Unp4kcState build();