feat: unp4kc

This commit is contained in:
xkeyC 2024-04-14 19:52:42 +08:00
parent dd17ddc92a
commit 90bb8e6611
18 changed files with 859 additions and 13 deletions

BIN
assets/binary/unp4kc.zip Normal file

Binary file not shown.

View File

@ -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()),
),
]),
],
);
}

View File

@ -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)

View File

@ -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");

View 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;
}
}

View File

@ -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
View 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");
}
}

View 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;
}

View 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

View File

@ -7,7 +7,7 @@ part of 'game_doctor_ui_model.dart';
// **************************************************************************
String _$homeGameDoctorUIModelHash() =>
r'b4132559510e3e59b1e2e330d9327ff8790df461';
r'137f6393bbbd76f3af0f7d0dd27d44d8473e42cc';
/// See also [HomeGameDoctorUIModel].
@ProviderFor(HomeGameDoctorUIModel)

View File

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

View File

@ -6,7 +6,7 @@ part of 'settings_ui_model.dart';
// RiverpodGenerator
// **************************************************************************
String _$settingsUIModelHash() => r'acc2a90f5bbfc6ba82b17454e73881ac32b30b6a';
String _$settingsUIModelHash() => r'aab08176293b380f09c89e006f373fbfd7a7ba16';
/// See also [SettingsUIModel].
@ProviderFor(SettingsUIModel)

View File

@ -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");
}
}

View File

@ -6,7 +6,7 @@ part of 'tools_ui_model.dart';
// RiverpodGenerator
// **************************************************************************
String _$toolsUIModelHash() => r'e96ded635df8f59fb93d92f8dac337c44482dc1c';
String _$toolsUIModelHash() => r'5753016dc2cbcf3fafd2fa561d5b91a3295ca04b';
/// See also [ToolsUIModel].
@ProviderFor(ToolsUIModel)

View 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}'),
);
}
}

View File

@ -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

View File

@ -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();
}
}