mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-02-06 15:10:20 +00:00
feat: unp4k UI update
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:file_sizes/file_sizes.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -58,183 +59,658 @@ class UnP4kcUI extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
)
|
||||
: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
: Stack(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .06)),
|
||||
height: 36,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: SuperListView.builder(
|
||||
itemCount: paths.length - 1,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var path = paths[index];
|
||||
if (path.isEmpty) {
|
||||
path = "\\";
|
||||
}
|
||||
final fullPath = "${paths.sublist(0, index + 1).join("\\")}\\";
|
||||
return Row(
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .06)),
|
||||
height: 36,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Text(path),
|
||||
onPressed: () {
|
||||
model.changeDir(fullPath, fullPath: true);
|
||||
},
|
||||
// 搜索模式下显示返回按钮
|
||||
if (state.searchFs != null) ...[
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.back, size: 14),
|
||||
onPressed: () {
|
||||
model.clearSearch();
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"[${S.current.tools_unp4k_searching.replaceAll('...', '')}]",
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Expanded(
|
||||
child: SuperListView.builder(
|
||||
itemCount: paths.length - 1,
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
var path = paths[index];
|
||||
if (path.isEmpty) {
|
||||
path = "\\";
|
||||
}
|
||||
final fullPath = "${paths.sublist(0, index + 1).join("\\")}\\";
|
||||
return Row(
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Text(path),
|
||||
onPressed: () {
|
||||
model.changeDir(fullPath, fullPath: true);
|
||||
},
|
||||
),
|
||||
const Icon(FluentIcons.chevron_right, size: 12),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
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.withValues(alpha: .01)),
|
||||
child: SuperListView.builder(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 6, left: 3, right: 12),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = files![index];
|
||||
final fileName = item.name?.replaceAll(state.curPath.trim(), "") ?? "?";
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .05)),
|
||||
child: IconButton(
|
||||
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(
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * .3,
|
||||
child: _FileListPanel(state: state, model: model, files: files),
|
||||
),
|
||||
Expanded(
|
||||
child: state.tempOpenFile == null
|
||||
? Center(child: Text(S.current.tools_unp4k_view_file))
|
||||
: state.tempOpenFile?.key == "loading"
|
||||
? makeLoading(context)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.tempOpenFile?.key == "text")
|
||||
Expanded(child: _TextTempWidget(state.tempOpenFile?.value ?? ""))
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
FileSize.getSize(item.size),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withValues(alpha: .6),
|
||||
S.current.tools_unp4k_msg_unknown_file_type(
|
||||
state.tempOpenFile?.value ?? "",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
item.dateModified != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
item.dateModified!,
|
||||
).toString().substring(0, 19)
|
||||
: "",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withValues(alpha: .6),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(S.current.action_open_folder),
|
||||
),
|
||||
onPressed: () {
|
||||
SystemHelper.openDir(state.tempOpenFile?.value ?? "");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Icon(
|
||||
FluentIcons.chevron_right,
|
||||
size: 14,
|
||||
color: Colors.white.withValues(alpha: .6),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemCount: files?.length ?? 0,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
child: state.tempOpenFile == null
|
||||
? Center(child: Text(S.current.tools_unp4k_view_file))
|
||||
: state.tempOpenFile?.key == "loading"
|
||||
? makeLoading(context)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Column(
|
||||
children: [
|
||||
if (state.tempOpenFile?.key == "text")
|
||||
Expanded(child: _TextTempWidget(state.tempOpenFile?.value ?? ""))
|
||||
else
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
S.current.tools_unp4k_msg_unknown_file_type(
|
||||
state.tempOpenFile?.value ?? "",
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
FilledButton(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: Text(S.current.action_open_folder),
|
||||
),
|
||||
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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (state.endMessage != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text("${state.endMessage}", style: const TextStyle(fontSize: 12)),
|
||||
// 搜索加载遮罩
|
||||
if (state.isSearching)
|
||||
Container(
|
||||
color: Colors.black.withValues(alpha: .7),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 16),
|
||||
Text(S.current.tools_unp4k_searching, style: const TextStyle(fontSize: 16)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件列表面板组件
|
||||
class _FileListPanel extends HookConsumerWidget {
|
||||
final Unp4kcState state;
|
||||
final Unp4kCModel model;
|
||||
final List<AppUnp4kP4kItemData>? files;
|
||||
|
||||
const _FileListPanel({required this.state, required this.model, required this.files});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final searchController = useTextEditingController(text: state.searchQuery);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .01)),
|
||||
child: Column(
|
||||
children: [
|
||||
// 搜索栏和排序选择器
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextBox(
|
||||
controller: searchController,
|
||||
placeholder: S.current.tools_unp4k_search_placeholder,
|
||||
prefix: const Padding(padding: EdgeInsets.only(left: 8), child: Icon(FluentIcons.search, size: 14)),
|
||||
suffix: searchController.text.isNotEmpty || state.searchFs != null
|
||||
? IconButton(
|
||||
icon: const Icon(FluentIcons.clear, size: 12),
|
||||
onPressed: () {
|
||||
searchController.clear();
|
||||
model.clearSearch();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
onSubmitted: (value) {
|
||||
model.search(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ComboBox<Unp4kSortType>(
|
||||
value: state.sortType,
|
||||
items: [
|
||||
ComboBoxItem(value: Unp4kSortType.defaultSort, child: Text(S.current.tools_unp4k_sort_default)),
|
||||
ComboBoxItem(value: Unp4kSortType.sizeAsc, child: Text(S.current.tools_unp4k_sort_size_asc)),
|
||||
ComboBoxItem(value: Unp4kSortType.sizeDesc, child: Text(S.current.tools_unp4k_sort_size_desc)),
|
||||
ComboBoxItem(value: Unp4kSortType.dateAsc, child: Text(S.current.tools_unp4k_sort_date_asc)),
|
||||
ComboBoxItem(value: Unp4kSortType.dateDesc, child: Text(S.current.tools_unp4k_sort_date_desc)),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
model.setSortType(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 多选模式工具栏
|
||||
if (state.isMultiSelectMode)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
|
||||
decoration: BoxDecoration(
|
||||
color: FluentTheme.of(context).accentColor.withValues(alpha: .1),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: FluentTheme.of(context).accentColor.withValues(alpha: .3), width: 1),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
S.current.tools_unp4k_action_export_selected(state.selectedItems.length),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
const Spacer(),
|
||||
Button(child: Text(S.current.tools_unp4k_action_select_all), onPressed: () => model.selectAll(files)),
|
||||
const SizedBox(width: 4),
|
||||
Button(
|
||||
child: Text(S.current.tools_unp4k_action_deselect_all),
|
||||
onPressed: () => model.deselectAll(files),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
FilledButton(
|
||||
onPressed: state.selectedItems.isNotEmpty ? () => _exportSelected(context) : null,
|
||||
child: Text(S.current.tools_unp4k_action_save_as),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
IconButton(
|
||||
icon: const Icon(FluentIcons.cancel, size: 14),
|
||||
onPressed: () => model.exitMultiSelectMode(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 文件列表
|
||||
Expanded(
|
||||
child: files == null || files!.isEmpty
|
||||
? Center(
|
||||
child: Text(
|
||||
state.searchFs != null ? S.current.tools_unp4k_search_no_result : '',
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
)
|
||||
: SuperListView.builder(
|
||||
padding: const EdgeInsets.only(top: 6, bottom: 6, left: 3, right: 12),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final item = files![index];
|
||||
return _FileListItem(item: item, state: state, model: model);
|
||||
},
|
||||
itemCount: files?.length ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportSelected(BuildContext context) async {
|
||||
final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as);
|
||||
if (outputDir != null && context.mounted) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return _MultiExtractProgressDialog(selectedItems: state.selectedItems, outputDir: outputDir, model: model);
|
||||
},
|
||||
);
|
||||
// 提取完成后退出多选模式
|
||||
model.exitMultiSelectMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件列表项组件
|
||||
class _FileListItem extends HookWidget {
|
||||
final AppUnp4kP4kItemData item;
|
||||
final Unp4kcState state;
|
||||
final Unp4kCModel model;
|
||||
|
||||
const _FileListItem({required this.item, required this.state, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final flyoutController = useMemoized(() => FlyoutController());
|
||||
// 显示相对于当前路径的文件名
|
||||
final fileName = item.name?.replaceAll(state.curPath.trim(), "") ?? "?";
|
||||
final itemPath = item.name ?? "";
|
||||
final isSelected = state.selectedItems.contains(itemPath);
|
||||
|
||||
return FlyoutTarget(
|
||||
controller: flyoutController,
|
||||
child: GestureDetector(
|
||||
onSecondaryTapUp: (details) => _showContextMenu(context, flyoutController),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(top: 4, bottom: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? FluentTheme.of(context).accentColor.withValues(alpha: .2)
|
||||
: FluentTheme.of(context).cardColor.withValues(alpha: .05),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (state.isMultiSelectMode) {
|
||||
// 多选模式下点击切换选中状态
|
||||
model.toggleSelectItem(itemPath);
|
||||
} else if (item.isDirectory ?? false) {
|
||||
final dirName = item.name?.replaceAll(state.curPath.trim(), "") ?? "";
|
||||
model.changeDir(dirName);
|
||||
} else {
|
||||
model.openFile(item.name ?? "");
|
||||
}
|
||||
},
|
||||
icon: Padding(
|
||||
padding: const EdgeInsets.only(left: 4, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
// 多选模式下显示复选框
|
||||
if (state.isMultiSelectMode) ...[
|
||||
Checkbox(
|
||||
checked: isSelected,
|
||||
onChanged: (value) {
|
||||
model.toggleSelectItem(itemPath);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (item.isDirectory ?? false)
|
||||
const Icon(FluentIcons.folder_fill, color: Color.fromRGBO(255, 224, 138, 1))
|
||||
else if (fileName.endsWith(".xml"))
|
||||
const Icon(FluentIcons.file_code)
|
||||
else
|
||||
const Icon(FluentIcons.open_file),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(fileName, style: const TextStyle(fontSize: 13), textAlign: TextAlign.start),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (!(item.isDirectory ?? true)) ...[
|
||||
const SizedBox(height: 1),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
FileSize.getSize(item.size),
|
||||
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
item.dateModified != null
|
||||
? DateTime.fromMillisecondsSinceEpoch(
|
||||
item.dateModified!,
|
||||
).toString().substring(0, 19)
|
||||
: "",
|
||||
style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
Icon(FluentIcons.chevron_right, size: 14, color: Colors.white.withValues(alpha: .6)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContextMenu(BuildContext context, FlyoutController controller) {
|
||||
// 保存外部 context,因为 flyout 的 context 在关闭后会失效
|
||||
final outerContext = context;
|
||||
controller.showFlyout(
|
||||
autoModeConfiguration: FlyoutAutoConfiguration(preferredMode: FlyoutPlacementMode.bottomCenter),
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (flyoutContext) {
|
||||
return MenuFlyout(
|
||||
items: [
|
||||
MenuFlyoutItem(
|
||||
leading: const Icon(FluentIcons.save_as, size: 16),
|
||||
text: Text(S.current.tools_unp4k_action_save_as),
|
||||
onPressed: () async {
|
||||
Navigator.of(flyoutContext).pop();
|
||||
await _saveAs(outerContext);
|
||||
},
|
||||
),
|
||||
// 多选模式切换
|
||||
if (!state.isMultiSelectMode)
|
||||
MenuFlyoutItem(
|
||||
leading: const Icon(FluentIcons.checkbox_composite, size: 16),
|
||||
text: Text(S.current.tools_unp4k_action_multi_select),
|
||||
onPressed: () {
|
||||
Navigator.of(flyoutContext).pop();
|
||||
model.enterMultiSelectMode();
|
||||
// 自动选中当前项
|
||||
model.toggleSelectItem(item.name ?? "");
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _saveAs(BuildContext context) async {
|
||||
final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as);
|
||||
if (outputDir != null && context.mounted) {
|
||||
await _showExtractProgressDialog(context, outputDir);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showExtractProgressDialog(BuildContext context, String outputDir) async {
|
||||
await showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (dialogContext) {
|
||||
return _ExtractProgressDialog(item: item, outputDir: outputDir, model: model);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 提取进度对话框
|
||||
class _ExtractProgressDialog extends HookWidget {
|
||||
final AppUnp4kP4kItemData item;
|
||||
final String outputDir;
|
||||
final Unp4kCModel model;
|
||||
|
||||
const _ExtractProgressDialog({required this.item, required this.outputDir, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCancelled = useState(false);
|
||||
final currentFile = useState("");
|
||||
final currentIndex = useState(0);
|
||||
final totalFiles = useState(0);
|
||||
final isCompleted = useState(false);
|
||||
final errorMessage = useState<String?>(null);
|
||||
final extractedCount = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
// 获取文件数量
|
||||
totalFiles.value = model.getFileCountInDirectory(item);
|
||||
|
||||
// 开始提取
|
||||
_startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount);
|
||||
return null;
|
||||
}, const []);
|
||||
|
||||
final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0;
|
||||
|
||||
return ContentDialog(
|
||||
title: Text(S.current.tools_unp4k_extract_dialog_title),
|
||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isCompleted.value && errorMessage.value == null) ...[
|
||||
// 进度条
|
||||
ProgressBar(value: progress * 100),
|
||||
const SizedBox(height: 12),
|
||||
// 进度文本
|
||||
Text(
|
||||
S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 当前文件
|
||||
Text(
|
||||
S.current.tools_unp4k_extract_current_file(
|
||||
currentFile.value.length > 60
|
||||
? "...${currentFile.value.substring(currentFile.value.length - 60)}"
|
||||
: currentFile.value,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
] else if (errorMessage.value != null) ...[
|
||||
// 错误信息
|
||||
const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)),
|
||||
const SizedBox(height: 12),
|
||||
Text(errorMessage.value!, style: const TextStyle(fontSize: 14)),
|
||||
] else ...[
|
||||
// 完成
|
||||
const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)),
|
||||
const SizedBox(height: 12),
|
||||
Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (!isCompleted.value && errorMessage.value == null)
|
||||
Button(
|
||||
onPressed: () {
|
||||
isCancelled.value = true;
|
||||
},
|
||||
child: Text(S.current.home_action_cancel),
|
||||
)
|
||||
else
|
||||
FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startExtraction(
|
||||
ValueNotifier<bool> isCancelled,
|
||||
ValueNotifier<String> currentFile,
|
||||
ValueNotifier<int> currentIndex,
|
||||
ValueNotifier<int> totalFiles,
|
||||
ValueNotifier<bool> isCompleted,
|
||||
ValueNotifier<String?> errorMessage,
|
||||
ValueNotifier<int> extractedCount,
|
||||
) async {
|
||||
final result = await model.extractToDirectoryWithProgress(
|
||||
item,
|
||||
outputDir,
|
||||
onProgress: (current, total, file) {
|
||||
currentIndex.value = current;
|
||||
totalFiles.value = total;
|
||||
currentFile.value = file;
|
||||
},
|
||||
isCancelled: () => isCancelled.value,
|
||||
);
|
||||
|
||||
final (success, count, error) = result;
|
||||
extractedCount.value = count;
|
||||
|
||||
if (!success && error != null) {
|
||||
errorMessage.value = error;
|
||||
} else {
|
||||
isCompleted.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 批量提取进度对话框
|
||||
class _MultiExtractProgressDialog extends HookWidget {
|
||||
final Set<String> selectedItems;
|
||||
final String outputDir;
|
||||
final Unp4kCModel model;
|
||||
|
||||
const _MultiExtractProgressDialog({required this.selectedItems, required this.outputDir, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isCancelled = useState(false);
|
||||
final currentFile = useState("");
|
||||
final currentIndex = useState(0);
|
||||
final totalFiles = useState(0);
|
||||
final isCompleted = useState(false);
|
||||
final errorMessage = useState<String?>(null);
|
||||
final extractedCount = useState(0);
|
||||
|
||||
useEffect(() {
|
||||
// 获取文件数量
|
||||
totalFiles.value = model.getFileCountForSelectedItems(selectedItems);
|
||||
|
||||
// 开始提取
|
||||
_startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount);
|
||||
return null;
|
||||
}, const []);
|
||||
|
||||
final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0;
|
||||
|
||||
return ContentDialog(
|
||||
title: Text(S.current.tools_unp4k_extract_dialog_title),
|
||||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (!isCompleted.value && errorMessage.value == null) ...[
|
||||
// 进度条
|
||||
ProgressBar(value: progress * 100),
|
||||
const SizedBox(height: 12),
|
||||
// 进度文本
|
||||
Text(
|
||||
S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// 当前文件
|
||||
Text(
|
||||
S.current.tools_unp4k_extract_current_file(
|
||||
currentFile.value.length > 60
|
||||
? "...${currentFile.value.substring(currentFile.value.length - 60)}"
|
||||
: currentFile.value,
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
] else if (errorMessage.value != null) ...[
|
||||
// 错误信息
|
||||
const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)),
|
||||
const SizedBox(height: 12),
|
||||
Text(errorMessage.value!, style: const TextStyle(fontSize: 14)),
|
||||
] else ...[
|
||||
// 完成
|
||||
const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)),
|
||||
const SizedBox(height: 12),
|
||||
Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
if (!isCompleted.value && errorMessage.value == null)
|
||||
Button(
|
||||
onPressed: () {
|
||||
isCancelled.value = true;
|
||||
},
|
||||
child: Text(S.current.home_action_cancel),
|
||||
)
|
||||
else
|
||||
FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _startExtraction(
|
||||
ValueNotifier<bool> isCancelled,
|
||||
ValueNotifier<String> currentFile,
|
||||
ValueNotifier<int> currentIndex,
|
||||
ValueNotifier<int> totalFiles,
|
||||
ValueNotifier<bool> isCompleted,
|
||||
ValueNotifier<String?> errorMessage,
|
||||
ValueNotifier<int> extractedCount,
|
||||
) async {
|
||||
final result = await model.extractSelectedItemsWithProgress(
|
||||
selectedItems,
|
||||
outputDir,
|
||||
onProgress: (current, total, file) {
|
||||
currentIndex.value = current;
|
||||
totalFiles.value = total;
|
||||
currentFile.value = file;
|
||||
},
|
||||
isCancelled: () => isCancelled.value,
|
||||
);
|
||||
|
||||
final (success, count, error) = result;
|
||||
extractedCount.value = count;
|
||||
|
||||
if (!success && error != null) {
|
||||
errorMessage.value = error;
|
||||
} else {
|
||||
isCompleted.value = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _TextTempWidget extends HookConsumerWidget {
|
||||
final String filePath;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user