mirror of
https://mirror.ghproxy.com/https://github.com/StarCitizenToolBox/app.git
synced 2024-12-23 00:33:42 +08:00
feat: unp4kc
This commit is contained in:
parent
dd17ddc92a
commit
90bb8e6611
BIN
assets/binary/unp4kc.zip
Normal file
BIN
assets/binary/unp4kc.zip
Normal file
Binary file not shown.
@ -29,6 +29,7 @@ import 'ui/home/downloader/home_downloader_ui.dart';
|
||||
import 'ui/home/game_doctor/game_doctor_ui.dart';
|
||||
import 'ui/index_ui.dart';
|
||||
import 'ui/settings/upgrade_dialog.dart';
|
||||
import 'ui/tools/unp4kc/unp4kc_ui.dart';
|
||||
|
||||
part 'app.g.dart';
|
||||
|
||||
@ -77,6 +78,13 @@ GoRouter router(RouterRef ref) {
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(path: '/tools', builder: (_, __) => const SizedBox(), routes: [
|
||||
GoRoute(
|
||||
path: 'unp4kc',
|
||||
pageBuilder: (context, state) =>
|
||||
myPageBuilder(context, state, const UnP4kcUI()),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ part of 'app.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$routerHash() => r'e7b1e3a9fd74b4f00e3d71017615d7fb82bd649d';
|
||||
String _$routerHash() => r'7ce5ef6a7a4f6f604a457dd050e04ee594c4760a';
|
||||
|
||||
/// See also [router].
|
||||
@ProviderFor(router)
|
||||
@ -20,7 +20,7 @@ final routerProvider = AutoDisposeProvider<GoRouter>.internal(
|
||||
);
|
||||
|
||||
typedef RouterRef = AutoDisposeProviderRef<GoRouter>;
|
||||
String _$appGlobalModelHash() => r'ae7f6704a80297ac3e0f70412c676a1829046831';
|
||||
String _$appGlobalModelHash() => r'a604c415d2d855ede8727dd75ef991ab2afcf234';
|
||||
|
||||
/// See also [AppGlobalModel].
|
||||
@ProviderFor(AppGlobalModel)
|
||||
|
@ -1,12 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:archive/archive.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
|
||||
class BinaryModuleConf {
|
||||
static const _modules = {
|
||||
"aria2c": "0",
|
||||
"unp4kc": "0",
|
||||
};
|
||||
|
||||
static Future extractModule(List<String> modules, String workingDir) async {
|
||||
@ -16,7 +18,8 @@ class BinaryModuleConf {
|
||||
final version = m.value;
|
||||
final dir = "$workingDir\\$name";
|
||||
final versionFile = File("$dir\\version");
|
||||
if (await versionFile.exists() &&
|
||||
if (kReleaseMode &&
|
||||
await versionFile.exists() &&
|
||||
(await versionFile.readAsString()).trim() == version) {
|
||||
dPrint(
|
||||
"BinaryModuleConf.extractModule skip $name version == $version");
|
||||
|
60
lib/data/app_unp4k_p4k_item_data.dart
Normal file
60
lib/data/app_unp4k_p4k_item_data.dart
Normal file
@ -0,0 +1,60 @@
|
||||
/// name : "Data\\Textures\\planets\\surface\\ground\\architecture\\city\\city_suburbs_02_displ.dds.6"
|
||||
/// size : 524288
|
||||
/// compressedSize : 169812
|
||||
/// isDirectory : false
|
||||
/// isFile : true
|
||||
/// isEncrypted : false
|
||||
/// isUnicodeText : false
|
||||
/// dateTime : "2019-12-16T15:11:18"
|
||||
/// version : 45
|
||||
|
||||
class AppUnp4kP4kItemData {
|
||||
AppUnp4kP4kItemData({
|
||||
this.name,
|
||||
this.size,
|
||||
this.compressedSize,
|
||||
this.isDirectory,
|
||||
this.isFile,
|
||||
this.isEncrypted,
|
||||
this.isUnicodeText,
|
||||
this.dateTime,
|
||||
this.version,
|
||||
});
|
||||
|
||||
AppUnp4kP4kItemData.fromJson(dynamic json) {
|
||||
name = json['name'];
|
||||
size = json['size'];
|
||||
compressedSize = json['compressedSize'];
|
||||
isDirectory = json['isDirectory'];
|
||||
isFile = json['isFile'];
|
||||
isEncrypted = json['isEncrypted'];
|
||||
isUnicodeText = json['isUnicodeText'];
|
||||
dateTime = json['dateTime'];
|
||||
version = json['version'];
|
||||
}
|
||||
|
||||
String? name;
|
||||
num? size;
|
||||
num? compressedSize;
|
||||
bool? isDirectory;
|
||||
bool? isFile;
|
||||
bool? isEncrypted;
|
||||
bool? isUnicodeText;
|
||||
String? dateTime;
|
||||
num? version;
|
||||
List<AppUnp4kP4kItemData> children = [];
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final map = <String, dynamic>{};
|
||||
map['name'] = name;
|
||||
map['size'] = size;
|
||||
map['compressedSize'] = compressedSize;
|
||||
map['isDirectory'] = isDirectory;
|
||||
map['isFile'] = isFile;
|
||||
map['isEncrypted'] = isEncrypted;
|
||||
map['isUnicodeText'] = isUnicodeText;
|
||||
map['dateTime'] = dateTime;
|
||||
map['version'] = version;
|
||||
return map;
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ part of 'aria2c.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$aria2cModelHash() => r'6685f6a716016113487de190a61f71196094526e';
|
||||
String _$aria2cModelHash() => r'5431c2d9667f17ff03d0794711af22b015feda0d';
|
||||
|
||||
/// See also [Aria2cModel].
|
||||
@ProviderFor(Aria2cModel)
|
||||
|
191
lib/provider/unp4kc.dart
Normal file
191
lib/provider/unp4kc.dart
Normal file
@ -0,0 +1,191 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:starcitizen_doctor/common/conf/binary_conf.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/provider.dart';
|
||||
import 'package:starcitizen_doctor/data/app_unp4k_p4k_item_data.dart';
|
||||
import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart';
|
||||
|
||||
part 'unp4kc.freezed.dart';
|
||||
|
||||
part 'unp4kc.g.dart';
|
||||
|
||||
@freezed
|
||||
class Unp4kcState with _$Unp4kcState {
|
||||
const factory Unp4kcState({
|
||||
required bool startUp,
|
||||
Map<String, AppUnp4kP4kItemData>? files,
|
||||
MemoryFileSystem? fs,
|
||||
required String curPath,
|
||||
String? endMessage,
|
||||
MapEntry<String, String>? tempOpenFile,
|
||||
}) = _Unp4kcState;
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class Unp4kCModel extends _$Unp4kCModel {
|
||||
Process? _process;
|
||||
|
||||
@override
|
||||
Unp4kcState build() {
|
||||
state =
|
||||
const Unp4kcState(startUp: false, curPath: '\\', endMessage: "初始化中...");
|
||||
_init();
|
||||
return state;
|
||||
}
|
||||
|
||||
ToolsUIState get _toolsState => ref.read(toolsUIModelProvider);
|
||||
|
||||
String getGamePath() => _toolsState.scInstalledPath;
|
||||
|
||||
void _init() async {
|
||||
final execDir = "${appGlobalState.applicationBinaryModuleDir}\\unp4kc";
|
||||
await BinaryModuleConf.extractModule(
|
||||
["unp4kc"], appGlobalState.applicationBinaryModuleDir!);
|
||||
final exec = "$execDir\\unp4kc.exe";
|
||||
final ps = await Process.start(exec, []);
|
||||
StringBuffer stringBuffer = StringBuffer();
|
||||
_process = ps;
|
||||
ps.stdout.listen((event) async {
|
||||
final eventStr = String.fromCharCodes(event);
|
||||
stringBuffer.write(eventStr);
|
||||
if (!eventStr.endsWith("\n")) return;
|
||||
final str = stringBuffer.toString().trim();
|
||||
stringBuffer.clear();
|
||||
try {
|
||||
final eventJson = await compute(json.decode, str);
|
||||
_handleMessage(eventJson, ps);
|
||||
} catch (e) {
|
||||
dPrint("[unp4kc] json error: $e");
|
||||
}
|
||||
});
|
||||
ps.stderr.listen((event) {
|
||||
final eventStr = String.fromCharCodes(event);
|
||||
dPrint("[unp4kc] stderr: $eventStr");
|
||||
});
|
||||
state = state.copyWith(startUp: true);
|
||||
ref.onDispose(() {
|
||||
ps.kill();
|
||||
dPrint("[unp4kc] kill ...");
|
||||
});
|
||||
}
|
||||
|
||||
void _handleMessage(Map<String, dynamic> eventJson, Process ps) async {
|
||||
final action = eventJson["action"];
|
||||
final data = eventJson["data"];
|
||||
final gamePath = getGamePath();
|
||||
final gameP4kPath = "$gamePath\\Data.p4k";
|
||||
switch (action.toString().trim()) {
|
||||
case "info: startup":
|
||||
ps.stdin.writeln(gameP4kPath);
|
||||
state = state.copyWith(endMessage: "正在读取P4K 文件 ...");
|
||||
break;
|
||||
case "data: P4K_Files":
|
||||
final p4kFiles = (data as List<dynamic>);
|
||||
final files = <String, AppUnp4kP4kItemData>{};
|
||||
final fs = MemoryFileSystem(style: FileSystemStyle.posix);
|
||||
state = state.copyWith(endMessage: "正在处理文件 ...");
|
||||
for (var i = 0; i < p4kFiles.length; i++) {
|
||||
final item = AppUnp4kP4kItemData.fromJson(p4kFiles[i]);
|
||||
item.name = "${item.name}";
|
||||
files["\\${item.name}"] = item;
|
||||
await fs
|
||||
.file(item.name?.replaceAll("\\", "/") ?? "")
|
||||
.create(recursive: true);
|
||||
}
|
||||
state = state.copyWith(
|
||||
files: files, fs: fs, endMessage: "加载完毕:${files.length} 个文件");
|
||||
break;
|
||||
case "info: Extracted_Open":
|
||||
final filePath = data.toString();
|
||||
dPrint("[unp4kc] Extracted_Open file: $filePath");
|
||||
const textExt = [".txt", ".xml", ".json", ".lua", ".cfg", ".ini"];
|
||||
const imgExt = [".png"];
|
||||
String openType = "unknown";
|
||||
for (var element in textExt) {
|
||||
if (filePath.endsWith(element)) {
|
||||
openType = "text";
|
||||
}
|
||||
}
|
||||
for (var element in imgExt) {
|
||||
if (filePath.endsWith(element)) {
|
||||
openType = "image";
|
||||
}
|
||||
}
|
||||
|
||||
state = state.copyWith(
|
||||
tempOpenFile: MapEntry(openType, filePath),
|
||||
endMessage: "打开文件:$filePath");
|
||||
break;
|
||||
default:
|
||||
dPrint("[unp4kc] unknown action: $action");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<AppUnp4kP4kItemData>? getFiles() {
|
||||
final path = state.curPath.replaceAll("\\", "/");
|
||||
final fs = state.fs;
|
||||
if (fs == null) return null;
|
||||
final dir = fs.directory(path);
|
||||
if (!dir.existsSync()) return [];
|
||||
final files = dir.listSync(recursive: false, followLinks: false);
|
||||
files.sort((a, b) {
|
||||
if (a is Directory && b is File) {
|
||||
return -1;
|
||||
} else if (a is File && b is Directory) {
|
||||
return 1;
|
||||
} else {
|
||||
return a.path.compareTo(b.path);
|
||||
}
|
||||
});
|
||||
final result = <AppUnp4kP4kItemData>[];
|
||||
for (var file in files) {
|
||||
if (file is File) {
|
||||
final f = state.files?[file.path.replaceAll("/", "\\")];
|
||||
if (f != null) {
|
||||
if (!(f.name?.startsWith("\\") ?? true)) {
|
||||
f.name = "\\${f.name}";
|
||||
}
|
||||
result.add(f);
|
||||
}
|
||||
} else {
|
||||
result.add(AppUnp4kP4kItemData(
|
||||
name: file.path.replaceAll("/", "\\"), isDirectory: true));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void changeDir(String name, {bool fullPath = false}) {
|
||||
if (fullPath) {
|
||||
state = state.copyWith(curPath: name);
|
||||
} else {
|
||||
state = state.copyWith(curPath: "${state.curPath}$name\\");
|
||||
}
|
||||
}
|
||||
|
||||
openFile(String filePath) async {
|
||||
final tempPath = "${appGlobalState.applicationSupportDir}\\temp\\unp4k\\";
|
||||
state = state.copyWith(
|
||||
tempOpenFile: const MapEntry("loading", ""),
|
||||
endMessage: "读取文件:$filePath ...");
|
||||
extractFile(filePath, tempPath, mode: "extract_open");
|
||||
}
|
||||
|
||||
extractFile(String filePath, String outputPath,
|
||||
{String mode = "extract"}) async {
|
||||
// remove first \\
|
||||
if (filePath.startsWith("\\")) {
|
||||
filePath = filePath.substring(1);
|
||||
}
|
||||
outputPath = "$outputPath$filePath";
|
||||
dPrint("extractFile .... $filePath");
|
||||
_process?.stdin.writeln("$mode<:,:>$filePath<:,:>$outputPath");
|
||||
}
|
||||
}
|
269
lib/provider/unp4kc.freezed.dart
Normal file
269
lib/provider/unp4kc.freezed.dart
Normal file
@ -0,0 +1,269 @@
|
||||
// coverage:ignore-file
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'unp4kc.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
final _privateConstructorUsedError = UnsupportedError(
|
||||
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
|
||||
|
||||
/// @nodoc
|
||||
mixin _$Unp4kcState {
|
||||
bool get startUp => throw _privateConstructorUsedError;
|
||||
Map<String, AppUnp4kP4kItemData>? get files =>
|
||||
throw _privateConstructorUsedError;
|
||||
MemoryFileSystem? get fs => throw _privateConstructorUsedError;
|
||||
String get curPath => throw _privateConstructorUsedError;
|
||||
String? get endMessage => throw _privateConstructorUsedError;
|
||||
MapEntry<String, String>? get tempOpenFile =>
|
||||
throw _privateConstructorUsedError;
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
$Unp4kcStateCopyWith<Unp4kcState> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class $Unp4kcStateCopyWith<$Res> {
|
||||
factory $Unp4kcStateCopyWith(
|
||||
Unp4kcState value, $Res Function(Unp4kcState) then) =
|
||||
_$Unp4kcStateCopyWithImpl<$Res, Unp4kcState>;
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool startUp,
|
||||
Map<String, AppUnp4kP4kItemData>? files,
|
||||
MemoryFileSystem? fs,
|
||||
String curPath,
|
||||
String? endMessage,
|
||||
MapEntry<String, String>? tempOpenFile});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class _$Unp4kcStateCopyWithImpl<$Res, $Val extends Unp4kcState>
|
||||
implements $Unp4kcStateCopyWith<$Res> {
|
||||
_$Unp4kcStateCopyWithImpl(this._value, this._then);
|
||||
|
||||
// ignore: unused_field
|
||||
final $Val _value;
|
||||
// ignore: unused_field
|
||||
final $Res Function($Val) _then;
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? startUp = null,
|
||||
Object? files = freezed,
|
||||
Object? fs = freezed,
|
||||
Object? curPath = null,
|
||||
Object? endMessage = freezed,
|
||||
Object? tempOpenFile = freezed,
|
||||
}) {
|
||||
return _then(_value.copyWith(
|
||||
startUp: null == startUp
|
||||
? _value.startUp
|
||||
: startUp // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
files: freezed == files
|
||||
? _value.files
|
||||
: files // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, AppUnp4kP4kItemData>?,
|
||||
fs: freezed == fs
|
||||
? _value.fs
|
||||
: fs // ignore: cast_nullable_to_non_nullable
|
||||
as MemoryFileSystem?,
|
||||
curPath: null == curPath
|
||||
? _value.curPath
|
||||
: curPath // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
endMessage: freezed == endMessage
|
||||
? _value.endMessage
|
||||
: endMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
tempOpenFile: freezed == tempOpenFile
|
||||
? _value.tempOpenFile
|
||||
: tempOpenFile // ignore: cast_nullable_to_non_nullable
|
||||
as MapEntry<String, String>?,
|
||||
) as $Val);
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract class _$$Unp4kcStateImplCopyWith<$Res>
|
||||
implements $Unp4kcStateCopyWith<$Res> {
|
||||
factory _$$Unp4kcStateImplCopyWith(
|
||||
_$Unp4kcStateImpl value, $Res Function(_$Unp4kcStateImpl) then) =
|
||||
__$$Unp4kcStateImplCopyWithImpl<$Res>;
|
||||
@override
|
||||
@useResult
|
||||
$Res call(
|
||||
{bool startUp,
|
||||
Map<String, AppUnp4kP4kItemData>? files,
|
||||
MemoryFileSystem? fs,
|
||||
String curPath,
|
||||
String? endMessage,
|
||||
MapEntry<String, String>? tempOpenFile});
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
class __$$Unp4kcStateImplCopyWithImpl<$Res>
|
||||
extends _$Unp4kcStateCopyWithImpl<$Res, _$Unp4kcStateImpl>
|
||||
implements _$$Unp4kcStateImplCopyWith<$Res> {
|
||||
__$$Unp4kcStateImplCopyWithImpl(
|
||||
_$Unp4kcStateImpl _value, $Res Function(_$Unp4kcStateImpl) _then)
|
||||
: super(_value, _then);
|
||||
|
||||
@pragma('vm:prefer-inline')
|
||||
@override
|
||||
$Res call({
|
||||
Object? startUp = null,
|
||||
Object? files = freezed,
|
||||
Object? fs = freezed,
|
||||
Object? curPath = null,
|
||||
Object? endMessage = freezed,
|
||||
Object? tempOpenFile = freezed,
|
||||
}) {
|
||||
return _then(_$Unp4kcStateImpl(
|
||||
startUp: null == startUp
|
||||
? _value.startUp
|
||||
: startUp // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
files: freezed == files
|
||||
? _value._files
|
||||
: files // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, AppUnp4kP4kItemData>?,
|
||||
fs: freezed == fs
|
||||
? _value.fs
|
||||
: fs // ignore: cast_nullable_to_non_nullable
|
||||
as MemoryFileSystem?,
|
||||
curPath: null == curPath
|
||||
? _value.curPath
|
||||
: curPath // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
endMessage: freezed == endMessage
|
||||
? _value.endMessage
|
||||
: endMessage // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
tempOpenFile: freezed == tempOpenFile
|
||||
? _value.tempOpenFile
|
||||
: tempOpenFile // ignore: cast_nullable_to_non_nullable
|
||||
as MapEntry<String, String>?,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
class _$Unp4kcStateImpl with DiagnosticableTreeMixin implements _Unp4kcState {
|
||||
const _$Unp4kcStateImpl(
|
||||
{required this.startUp,
|
||||
final Map<String, AppUnp4kP4kItemData>? files,
|
||||
this.fs,
|
||||
required this.curPath,
|
||||
this.endMessage,
|
||||
this.tempOpenFile})
|
||||
: _files = files;
|
||||
|
||||
@override
|
||||
final bool startUp;
|
||||
final Map<String, AppUnp4kP4kItemData>? _files;
|
||||
@override
|
||||
Map<String, AppUnp4kP4kItemData>? get files {
|
||||
final value = _files;
|
||||
if (value == null) return null;
|
||||
if (_files is EqualUnmodifiableMapView) return _files;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override
|
||||
final MemoryFileSystem? fs;
|
||||
@override
|
||||
final String curPath;
|
||||
@override
|
||||
final String? endMessage;
|
||||
@override
|
||||
final MapEntry<String, String>? tempOpenFile;
|
||||
|
||||
@override
|
||||
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
|
||||
return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile)';
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties
|
||||
..add(DiagnosticsProperty('type', 'Unp4kcState'))
|
||||
..add(DiagnosticsProperty('startUp', startUp))
|
||||
..add(DiagnosticsProperty('files', files))
|
||||
..add(DiagnosticsProperty('fs', fs))
|
||||
..add(DiagnosticsProperty('curPath', curPath))
|
||||
..add(DiagnosticsProperty('endMessage', endMessage))
|
||||
..add(DiagnosticsProperty('tempOpenFile', tempOpenFile));
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) ||
|
||||
(other.runtimeType == runtimeType &&
|
||||
other is _$Unp4kcStateImpl &&
|
||||
(identical(other.startUp, startUp) || other.startUp == startUp) &&
|
||||
const DeepCollectionEquality().equals(other._files, _files) &&
|
||||
(identical(other.fs, fs) || other.fs == fs) &&
|
||||
(identical(other.curPath, curPath) || other.curPath == curPath) &&
|
||||
(identical(other.endMessage, endMessage) ||
|
||||
other.endMessage == endMessage) &&
|
||||
(identical(other.tempOpenFile, tempOpenFile) ||
|
||||
other.tempOpenFile == tempOpenFile));
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
runtimeType,
|
||||
startUp,
|
||||
const DeepCollectionEquality().hash(_files),
|
||||
fs,
|
||||
curPath,
|
||||
endMessage,
|
||||
tempOpenFile);
|
||||
|
||||
@JsonKey(ignore: true)
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
_$$Unp4kcStateImplCopyWith<_$Unp4kcStateImpl> get copyWith =>
|
||||
__$$Unp4kcStateImplCopyWithImpl<_$Unp4kcStateImpl>(this, _$identity);
|
||||
}
|
||||
|
||||
abstract class _Unp4kcState implements Unp4kcState {
|
||||
const factory _Unp4kcState(
|
||||
{required final bool startUp,
|
||||
final Map<String, AppUnp4kP4kItemData>? files,
|
||||
final MemoryFileSystem? fs,
|
||||
required final String curPath,
|
||||
final String? endMessage,
|
||||
final MapEntry<String, String>? tempOpenFile}) = _$Unp4kcStateImpl;
|
||||
|
||||
@override
|
||||
bool get startUp;
|
||||
@override
|
||||
Map<String, AppUnp4kP4kItemData>? get files;
|
||||
@override
|
||||
MemoryFileSystem? get fs;
|
||||
@override
|
||||
String get curPath;
|
||||
@override
|
||||
String? get endMessage;
|
||||
@override
|
||||
MapEntry<String, String>? get tempOpenFile;
|
||||
@override
|
||||
@JsonKey(ignore: true)
|
||||
_$$Unp4kcStateImplCopyWith<_$Unp4kcStateImpl> get copyWith =>
|
||||
throw _privateConstructorUsedError;
|
||||
}
|
25
lib/provider/unp4kc.g.dart
Normal file
25
lib/provider/unp4kc.g.dart
Normal file
@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'unp4kc.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$unp4kCModelHash() => r'46b6ac12670a6ff6ffb9d5bc8a8d04c07c570a8c';
|
||||
|
||||
/// See also [Unp4kCModel].
|
||||
@ProviderFor(Unp4kCModel)
|
||||
final unp4kCModelProvider =
|
||||
AutoDisposeNotifierProvider<Unp4kCModel, Unp4kcState>.internal(
|
||||
Unp4kCModel.new,
|
||||
name: r'unp4kCModelProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$unp4kCModelHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$Unp4kCModel = AutoDisposeNotifier<Unp4kcState>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
@ -7,7 +7,7 @@ part of 'game_doctor_ui_model.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$homeGameDoctorUIModelHash() =>
|
||||
r'b4132559510e3e59b1e2e330d9327ff8790df461';
|
||||
r'137f6393bbbd76f3af0f7d0dd27d44d8473e42cc';
|
||||
|
||||
/// See also [HomeGameDoctorUIModel].
|
||||
@ProviderFor(HomeGameDoctorUIModel)
|
||||
|
@ -7,7 +7,7 @@ part of 'localization_ui_model.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$localizationUIModelHash() =>
|
||||
r'87152654734d322cd20d62baf050918adc9fd11b';
|
||||
r'da9d0a3ae28825fd9331dd2b6db3d094cf3c0eb9';
|
||||
|
||||
/// See also [LocalizationUIModel].
|
||||
@ProviderFor(LocalizationUIModel)
|
||||
|
@ -6,7 +6,7 @@ part of 'settings_ui_model.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$settingsUIModelHash() => r'acc2a90f5bbfc6ba82b17454e73881ac32b30b6a';
|
||||
String _$settingsUIModelHash() => r'aab08176293b380f09c89e006f373fbfd7a7ba16';
|
||||
|
||||
/// See also [SettingsUIModel].
|
||||
@ProviderFor(SettingsUIModel)
|
||||
|
@ -104,6 +104,13 @@ class ToolsUIModel extends _$ToolsUIModel {
|
||||
const Icon(FluentIcons.admin, size: 24),
|
||||
onTap: () => _adminRSILauncher(context),
|
||||
),
|
||||
ToolsItemData(
|
||||
"unp4kc",
|
||||
"P4K 查看器",
|
||||
"解包星际公民 p4k 文件",
|
||||
const Icon(FontAwesomeIcons.fileZipper, size: 24),
|
||||
onTap: () => _unp4kc(context),
|
||||
),
|
||||
];
|
||||
|
||||
state = state.copyWith(items: items);
|
||||
@ -573,4 +580,8 @@ class ToolsUIModel extends _$ToolsUIModel {
|
||||
context: context,
|
||||
builder: (BuildContext context) => const HostsBoosterDialogUI());
|
||||
}
|
||||
|
||||
_unp4kc(BuildContext context) async {
|
||||
context.push("/tools/unp4kc");
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ part of 'tools_ui_model.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$toolsUIModelHash() => r'e96ded635df8f59fb93d92f8dac337c44482dc1c';
|
||||
String _$toolsUIModelHash() => r'5753016dc2cbcf3fafd2fa561d5b91a3295ca04b';
|
||||
|
||||
/// See also [ToolsUIModel].
|
||||
@ProviderFor(ToolsUIModel)
|
||||
|
272
lib/ui/tools/unp4kc/unp4kc_ui.dart
Normal file
272
lib/ui/tools/unp4kc/unp4kc_ui.dart
Normal file
@ -0,0 +1,272 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_sizes/file_sizes.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:re_editor/re_editor.dart';
|
||||
import 'package:starcitizen_doctor/common/helper/system_helper.dart';
|
||||
import 'package:starcitizen_doctor/provider/unp4kc.dart';
|
||||
import 'package:starcitizen_doctor/widgets/widgets.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
|
||||
class UnP4kcUI extends HookConsumerWidget {
|
||||
const UnP4kcUI({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(unp4kCModelProvider);
|
||||
final model = ref.read(unp4kCModelProvider.notifier);
|
||||
final files = model.getFiles();
|
||||
final paths = state.curPath.trim().split("\\");
|
||||
return makeDefaultPage(context,
|
||||
title: "P4K 查看器 -> ${model.getGamePath()}",
|
||||
useBodyContainer: false,
|
||||
content: state.files == null
|
||||
? Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: makeLoading(context)),
|
||||
if (state.endMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"${state.endMessage}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
FluentTheme.of(context).cardColor.withOpacity(.06)),
|
||||
height: 36,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: SuperListView.builder(
|
||||
itemCount: paths.length - 1,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var path = paths[index];
|
||||
if (path.isEmpty) {
|
||||
path = "\\";
|
||||
}
|
||||
final fullPath =
|
||||
"${paths.sublist(0, index + 1).join("\\")}\\";
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Text(path),
|
||||
onPressed: () {
|
||||
model.changeDir(fullPath, fullPath: true);
|
||||
},
|
||||
),
|
||||
const Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 12,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: MediaQuery.of(context).size.width * .3,
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context)
|
||||
.cardColor
|
||||
.withOpacity(.01),
|
||||
),
|
||||
child: SuperListView.builder(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6, bottom: 6, left: 3, right: 12),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = files![index];
|
||||
final fileName = item.name
|
||||
?.replaceAll(state.curPath.trim(), "") ??
|
||||
"?";
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context)
|
||||
.cardColor
|
||||
.withOpacity(.05),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (item.isDirectory ?? false) {
|
||||
model.changeDir(fileName);
|
||||
} else {
|
||||
model.openFile(item.name ?? "");
|
||||
}
|
||||
},
|
||||
icon: Padding(
|
||||
padding:
|
||||
const EdgeInsets.only(left: 4, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
if (item.isDirectory ?? false)
|
||||
const Icon(
|
||||
FluentIcons.folder_fill,
|
||||
color:
|
||||
Color.fromRGBO(255, 224, 138, 1),
|
||||
)
|
||||
else if (fileName.endsWith(".xml"))
|
||||
const Icon(
|
||||
FluentIcons.file_code,
|
||||
)
|
||||
else
|
||||
const Icon(
|
||||
FluentIcons.open_file,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
fileName,
|
||||
style: const TextStyle(
|
||||
fontSize: 13),
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!(item.isDirectory ??
|
||||
true)) ...[
|
||||
const SizedBox(height: 1),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
FileSize.getSize(item.size),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white
|
||||
.withOpacity(.6)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
"${item.dateTime}",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white
|
||||
.withOpacity(.6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 14,
|
||||
color: Colors.white.withOpacity(.6),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: files?.length ?? 0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
child: state.tempOpenFile == null
|
||||
? const Center(
|
||||
child: Text("单击文件以预览"),
|
||||
)
|
||||
: state.tempOpenFile?.key == "loading"
|
||||
? makeLoading(context)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.tempOpenFile?.key == "text")
|
||||
Expanded(
|
||||
child: _TextTempWidget(
|
||||
state.tempOpenFile?.value ??
|
||||
""))
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"未知文件类型\n${state.tempOpenFile?.value}"),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
child: const Padding(
|
||||
padding:
|
||||
EdgeInsets.all(4),
|
||||
child: Text("打开文件夹"),
|
||||
),
|
||||
onPressed: () {
|
||||
SystemHelper.openDir(
|
||||
state.tempOpenFile
|
||||
?.value ??
|
||||
"");
|
||||
})
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
))
|
||||
],
|
||||
)),
|
||||
if (state.endMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"${state.endMessage}",
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class _TextTempWidget extends HookConsumerWidget {
|
||||
final String filePath;
|
||||
|
||||
const _TextTempWidget(this.filePath);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final textData = useState<String?>(null);
|
||||
|
||||
useEffect(() {
|
||||
File(filePath).readAsString().then((value) {
|
||||
textData.value = value;
|
||||
}).catchError((err) {
|
||||
textData.value = "Error: $err";
|
||||
});
|
||||
return null;
|
||||
}, const []);
|
||||
|
||||
if (textData.value == null) return makeLoading(context);
|
||||
|
||||
return CodeEditor(
|
||||
controller: CodeLineEditingController.fromText('${textData.value}'),
|
||||
);
|
||||
}
|
||||
}
|
@ -84,6 +84,9 @@ dependencies:
|
||||
# path: ../../xkeyC/dart_aria2_rpc
|
||||
intl: ^0.18.0
|
||||
synchronized: ^3.1.0+1
|
||||
super_sliver_list: ^0.4.1
|
||||
file: ^7.0.0
|
||||
re_editor: ^0.1.0
|
||||
dependency_overrides:
|
||||
http: ^1.1.2
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use crate::frb_generated::StreamSink;
|
||||
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
use crate::frb_generated::StreamSink;
|
||||
|
||||
pub async fn start_process(
|
||||
executable: String,
|
||||
@ -37,12 +38,16 @@ pub async fn start_process(
|
||||
}
|
||||
let stdout = child.stdout.take().expect("Failed to open stdout");
|
||||
let stderr = child.stderr.take().expect("Failed to open stderr");
|
||||
// let stdin = child.stdin.take().expect("Failed to open stderr");
|
||||
|
||||
let output_task = tokio::spawn(process_output(stdout, stream_sink_arc.clone()));
|
||||
let error_task = tokio::spawn(process_error(stderr, stream_sink_arc.clone()));
|
||||
// let input_task = tokio::spawn(process_input(stdin));
|
||||
|
||||
tokio::select! {
|
||||
_ = output_task => (),
|
||||
_ = error_task => (),
|
||||
// _ = input_task => (),
|
||||
}
|
||||
|
||||
let exit_status = child.wait().await.expect("Failed to wait for child process");
|
||||
@ -83,4 +88,3 @@ async fn process_error<R>(stderr: R, stream_sink: Arc<StreamSink<String>>)
|
||||
stream_sink.add("error:".to_string() + &*line.trim().to_string()).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user