From 1c4eafccca6371e467a1c4126c523daa86743248 Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Thu, 4 Dec 2025 17:43:12 +0800 Subject: [PATCH] feat: unp4k UI update --- lib/generated/intl/messages_en.dart | 89 +- lib/generated/intl/messages_ja.dart | 24 +- lib/generated/intl/messages_ru.dart | 24 +- lib/generated/intl/messages_zh_CN.dart | 75 +- lib/generated/intl/messages_zh_TW.dart | 24 +- lib/generated/l10n.dart | 227 ++++- lib/l10n/intl_en.arb | 26 +- lib/l10n/intl_zh_CN.arb | 24 +- lib/provider/unp4kc.dart | 496 ++++++++++- lib/provider/unp4kc.freezed.dart | 89 +- lib/provider/unp4kc.g.dart | 2 +- .../advanced_localization_ui_model.g.dart | 2 +- lib/ui/tools/unp4kc/unp4kc_ui.dart | 784 ++++++++++++++---- 13 files changed, 1632 insertions(+), 254 deletions(-) diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index e6ee247..314683c 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -261,18 +261,32 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "Launcher internal version information: ${v0}"; - static String m104(v0) => "Opening file: ${v0}"; + static String m104(v0) => "Export Selected (${v0})"; - static String m105(v0, v1) => + static String m105(v0) => "Extraction failed: ${v0}"; + + static String m106(v0) => "Extraction complete: ${v0}"; + + static String m107(v0) => "Extracting: ${v0}"; + + static String m108(v0) => "Extraction completed, ${v0} files total"; + + static String m109(v0) => "Current file: ${v0}"; + + static String m110(v0, v1) => "Extracting (${v0}/${v1})"; + + static String m111(v0) => "Opening file: ${v0}"; + + static String m112(v0, v1) => "Loading complete: ${v0} files, time taken: ${v1} ms"; - static String m106(v0) => "Reading file: ${v0}..."; + static String m113(v0) => "Reading file: ${v0}..."; - static String m107(v0, v1) => "Processing files (${v0}/${v1})..."; + static String m114(v0, v1) => "Processing files (${v0}/${v1})..."; - static String m108(v0) => "Unknown file type\n${v0}"; + static String m115(v0) => "Unknown file type\n${v0}"; - static String m109(v0) => "P4K Viewer -> ${v0}"; + static String m116(v0) => "P4K Viewer -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -2184,6 +2198,33 @@ class MessageLookup extends MessageLookupByLibrary { "tools_rsi_launcher_enhance_working_msg2": MessageLookupByLibrary.simpleMessage( "Installing patch, this will take some time depending on your computer\'s performance...", ), + "tools_unp4k_action_cancel_multi_select": + MessageLookupByLibrary.simpleMessage("Cancel Multi-Select"), + "tools_unp4k_action_deselect_all": MessageLookupByLibrary.simpleMessage( + "Deselect All", + ), + "tools_unp4k_action_export_selected": m104, + "tools_unp4k_action_extract_failed": m105, + "tools_unp4k_action_extract_success": m106, + "tools_unp4k_action_extracting": m107, + "tools_unp4k_action_multi_select": MessageLookupByLibrary.simpleMessage( + "Multi-Select", + ), + "tools_unp4k_action_save_as": MessageLookupByLibrary.simpleMessage( + "Save As...", + ), + "tools_unp4k_action_select_all": MessageLookupByLibrary.simpleMessage( + "Select All", + ), + "tools_unp4k_extract_cancelled": MessageLookupByLibrary.simpleMessage( + "Extraction cancelled", + ), + "tools_unp4k_extract_completed": m108, + "tools_unp4k_extract_current_file": m109, + "tools_unp4k_extract_dialog_title": MessageLookupByLibrary.simpleMessage( + "Extract Files", + ), + "tools_unp4k_extract_progress": m110, "tools_unp4k_missing_runtime": MessageLookupByLibrary.simpleMessage( "Missing Runtime", ), @@ -2195,18 +2236,42 @@ class MessageLookup extends MessageLookupByLibrary { "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage( "Initializing...", ), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "Reading P4K file...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "Processing files...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_search_no_result": MessageLookupByLibrary.simpleMessage( + "No matching files found", + ), + "tools_unp4k_search_placeholder": MessageLookupByLibrary.simpleMessage( + "Search files (supports regex)...", + ), + "tools_unp4k_searching": MessageLookupByLibrary.simpleMessage( + "Searching...", + ), + "tools_unp4k_sort_date_asc": MessageLookupByLibrary.simpleMessage( + "Older First", + ), + "tools_unp4k_sort_date_desc": MessageLookupByLibrary.simpleMessage( + "Newer First", + ), + "tools_unp4k_sort_default": MessageLookupByLibrary.simpleMessage( + "Default Sort", + ), + "tools_unp4k_sort_size_asc": MessageLookupByLibrary.simpleMessage( + "Smaller First", + ), + "tools_unp4k_sort_size_desc": MessageLookupByLibrary.simpleMessage( + "Larger First", + ), + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "Click file to preview", ), diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index 2214ff4..be8be2e 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -242,17 +242,17 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "ランチャー内部バージョン情報:${v0}"; - static String m104(v0) => "ファイルを開く:${v0}"; + static String m111(v0) => "ファイルを開く:${v0}"; - static String m105(v0, v1) => "読み込み完了:${v0}ファイル、所要時間:${v1} ms"; + static String m112(v0, v1) => "読み込み完了:${v0}ファイル、所要時間:${v1} ms"; - static String m106(v0) => "ファイルを読み込み中:${v0}..."; + static String m113(v0) => "ファイルを読み込み中:${v0}..."; - static String m107(v0, v1) => "ファイルを処理中(${v0}/${v1})..."; + static String m114(v0, v1) => "ファイルを処理中(${v0}/${v1})..."; - static String m108(v0) => "不明なファイルタイプ\n${v0}"; + static String m115(v0) => "不明なファイルタイプ\n${v0}"; - static String m109(v0) => "P4Kビューア -> ${v0}"; + static String m116(v0) => "P4Kビューア -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1971,18 +1971,18 @@ class MessageLookup extends MessageLookupByLibrary { "この機能を使用するには.NET8ランタイムをインストールする必要があります。下のボタンをクリックしてダウンロードしてインストールし、インストールが成功したらこのページを再度開いて使用を続行してください。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初期化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "P4Kファイルを読み込み中...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "ファイルを処理中...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "プレビューするファイルをクリック", ), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index 8641422..df9f428 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -256,18 +256,18 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "Внутренняя версия лаунчера: ${v0}"; - static String m104(v0) => "Открытие файла: ${v0}"; + static String m111(v0) => "Открытие файла: ${v0}"; - static String m105(v0, v1) => + static String m112(v0, v1) => "Загрузка завершена: ${v0} файлов, время: ${v1} мс"; - static String m106(v0) => "Чтение файла: ${v0}..."; + static String m113(v0) => "Чтение файла: ${v0}..."; - static String m107(v0, v1) => "Обработка файлов (${v0}/${v1})..."; + static String m114(v0, v1) => "Обработка файлов (${v0}/${v1})..."; - static String m108(v0) => "Неизвестный тип файла\n${v0}"; + static String m115(v0) => "Неизвестный тип файла\n${v0}"; - static String m109(v0) => "Просмотрщик P4K -> ${v0}"; + static String m116(v0) => "Просмотрщик P4K -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -2224,18 +2224,18 @@ class MessageLookup extends MessageLookupByLibrary { "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage( "Инициализация...", ), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "Чтение файла P4K...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "Обработка файлов...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage( "Нажмите на файл для предварительного просмотра", ), diff --git a/lib/generated/intl/messages_zh_CN.dart b/lib/generated/intl/messages_zh_CN.dart index 6f0a362..e4fb4f8 100644 --- a/lib/generated/intl/messages_zh_CN.dart +++ b/lib/generated/intl/messages_zh_CN.dart @@ -240,17 +240,31 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "启动器内部版本信息:${v0}"; - static String m104(v0) => "打开文件:${v0}"; + static String m104(v0) => "导出选中项 (${v0})"; - static String m105(v0, v1) => "加载完毕:${v0} 个文件,用时:${v1} ms"; + static String m105(v0) => "提取失败:${v0}"; - static String m106(v0) => "读取文件:${v0} ..."; + static String m106(v0) => "提取完成:${v0}"; - static String m107(v0, v1) => "正在处理文件 (${v0}/${v1}) ..."; + static String m107(v0) => "正在提取:${v0}"; - static String m108(v0) => "未知文件类型\n${v0}"; + static String m108(v0) => "提取完成,共 ${v0} 个文件"; - static String m109(v0) => "P4K 查看器 -> ${v0}"; + static String m109(v0) => "当前文件:${v0}"; + + static String m110(v0, v1) => "正在提取 (${v0}/${v1})"; + + static String m111(v0) => "打开文件:${v0}"; + + static String m112(v0, v1) => "加载完毕:${v0} 个文件,用时:${v1} ms"; + + static String m113(v0) => "读取文件:${v0} ..."; + + static String m114(v0, v1) => "正在处理文件 (${v0}/${v1}) ..."; + + static String m115(v0) => "未知文件类型\n${v0}"; + + static String m116(v0) => "P4K 查看器 -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1849,6 +1863,31 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("生成补丁 ..."), "tools_rsi_launcher_enhance_working_msg2": MessageLookupByLibrary.simpleMessage("安装补丁,这需要一点时间,取决于您的计算机性能 ..."), + "tools_unp4k_action_cancel_multi_select": + MessageLookupByLibrary.simpleMessage("取消多选"), + "tools_unp4k_action_deselect_all": MessageLookupByLibrary.simpleMessage( + "取消全选", + ), + "tools_unp4k_action_export_selected": m104, + "tools_unp4k_action_extract_failed": m105, + "tools_unp4k_action_extract_success": m106, + "tools_unp4k_action_extracting": m107, + "tools_unp4k_action_multi_select": MessageLookupByLibrary.simpleMessage( + "多选", + ), + "tools_unp4k_action_save_as": MessageLookupByLibrary.simpleMessage( + "另存为...", + ), + "tools_unp4k_action_select_all": MessageLookupByLibrary.simpleMessage("全选"), + "tools_unp4k_extract_cancelled": MessageLookupByLibrary.simpleMessage( + "提取已取消", + ), + "tools_unp4k_extract_completed": m108, + "tools_unp4k_extract_current_file": m109, + "tools_unp4k_extract_dialog_title": MessageLookupByLibrary.simpleMessage( + "提取文件", + ), + "tools_unp4k_extract_progress": m110, "tools_unp4k_missing_runtime": MessageLookupByLibrary.simpleMessage( "缺少运行库", ), @@ -1858,18 +1897,30 @@ class MessageLookup extends MessageLookupByLibrary { "使用此功能需安装 .NET8 运行库,请点击下方按钮下载安装,安装成功后重新打开此页面即可继续使用。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初始化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "正在读取P4K 文件 ...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "正在处理文件 ...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_search_no_result": MessageLookupByLibrary.simpleMessage( + "未找到匹配文件", + ), + "tools_unp4k_search_placeholder": MessageLookupByLibrary.simpleMessage( + "搜索文件(支持正则)...", + ), + "tools_unp4k_searching": MessageLookupByLibrary.simpleMessage("正在搜索..."), + "tools_unp4k_sort_date_asc": MessageLookupByLibrary.simpleMessage("旧文件优先"), + "tools_unp4k_sort_date_desc": MessageLookupByLibrary.simpleMessage("新文件优先"), + "tools_unp4k_sort_default": MessageLookupByLibrary.simpleMessage("默认排序"), + "tools_unp4k_sort_size_asc": MessageLookupByLibrary.simpleMessage("小文件优先"), + "tools_unp4k_sort_size_desc": MessageLookupByLibrary.simpleMessage("大文件优先"), + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage("单击文件以预览"), "tools_vehicle_sorting_info": MessageLookupByLibrary.simpleMessage( "将左侧载具拖动到右侧列表中,这将会为载具名称增加 001、002 .. 等前缀,方便您在游戏内 UI 快速定位载具。在右侧列表上下拖动可以调整载具的顺序。", diff --git a/lib/generated/intl/messages_zh_TW.dart b/lib/generated/intl/messages_zh_TW.dart index b82d836..59960d1 100644 --- a/lib/generated/intl/messages_zh_TW.dart +++ b/lib/generated/intl/messages_zh_TW.dart @@ -236,17 +236,17 @@ class MessageLookup extends MessageLookupByLibrary { static String m103(v0) => "啟動器內部版本資訊:${v0}"; - static String m104(v0) => "打開文件:${v0}"; + static String m111(v0) => "打開文件:${v0}"; - static String m105(v0, v1) => "載入完畢:${v0} 個文件,用時:${v1} ms"; + static String m112(v0, v1) => "載入完畢:${v0} 個文件,用時:${v1} ms"; - static String m106(v0) => "讀取文件:${v0} ..."; + static String m113(v0) => "讀取文件:${v0} ..."; - static String m107(v0, v1) => "正在處理文件 (${v0}/${v1}) ..."; + static String m114(v0, v1) => "正在處理文件 (${v0}/${v1}) ..."; - static String m108(v0) => "未知文件類型\n${v0}"; + static String m115(v0) => "未知文件類型\n${v0}"; - static String m109(v0) => "P4K 查看器 -> ${v0}"; + static String m116(v0) => "P4K 查看器 -> ${v0}"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -1849,18 +1849,18 @@ class MessageLookup extends MessageLookupByLibrary { "使用此功能需安裝 .NET8 運行庫,請點擊下方按鈕下載安裝,安裝成功後重新打開此頁面即可繼續使用。", ), "tools_unp4k_msg_init": MessageLookupByLibrary.simpleMessage("初始化中..."), - "tools_unp4k_msg_open_file": m104, - "tools_unp4k_msg_read_completed": m105, - "tools_unp4k_msg_read_file": m106, + "tools_unp4k_msg_open_file": m111, + "tools_unp4k_msg_read_completed": m112, + "tools_unp4k_msg_read_file": m113, "tools_unp4k_msg_reading": MessageLookupByLibrary.simpleMessage( "正在讀取P4K 文件 ...", ), "tools_unp4k_msg_reading2": MessageLookupByLibrary.simpleMessage( "正在處理文件 ...", ), - "tools_unp4k_msg_reading3": m107, - "tools_unp4k_msg_unknown_file_type": m108, - "tools_unp4k_title": m109, + "tools_unp4k_msg_reading3": m114, + "tools_unp4k_msg_unknown_file_type": m115, + "tools_unp4k_title": m116, "tools_unp4k_view_file": MessageLookupByLibrary.simpleMessage("單擊文件以預覽"), "tools_vehicle_sorting_info": MessageLookupByLibrary.simpleMessage( "將左側載具拖動到右側列表中,這將會為載具名稱增加 001、002 .. 等前綴,方便您在遊戲內 UI 快速定位載具。在右側列表上下拖動可以調整載具的順序。", diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 796b4a4..3ea085e 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,9 +28,10 @@ class S { static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); static Future load(Locale locale) { - final name = (locale.countryCode?.isEmpty ?? false) - ? locale.languageCode - : locale.toString(); + final name = + (locale.countryCode?.isEmpty ?? false) + ? locale.languageCode + : locale.toString(); final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; @@ -7901,6 +7902,226 @@ class S { args: [v0], ); } + + /// `Search files (supports regex)...` + String get tools_unp4k_search_placeholder { + return Intl.message( + 'Search files (supports regex)...', + name: 'tools_unp4k_search_placeholder', + desc: '', + args: [], + ); + } + + /// `Default Sort` + String get tools_unp4k_sort_default { + return Intl.message( + 'Default Sort', + name: 'tools_unp4k_sort_default', + desc: '', + args: [], + ); + } + + /// `Smaller First` + String get tools_unp4k_sort_size_asc { + return Intl.message( + 'Smaller First', + name: 'tools_unp4k_sort_size_asc', + desc: '', + args: [], + ); + } + + /// `Larger First` + String get tools_unp4k_sort_size_desc { + return Intl.message( + 'Larger First', + name: 'tools_unp4k_sort_size_desc', + desc: '', + args: [], + ); + } + + /// `Older First` + String get tools_unp4k_sort_date_asc { + return Intl.message( + 'Older First', + name: 'tools_unp4k_sort_date_asc', + desc: '', + args: [], + ); + } + + /// `Newer First` + String get tools_unp4k_sort_date_desc { + return Intl.message( + 'Newer First', + name: 'tools_unp4k_sort_date_desc', + desc: '', + args: [], + ); + } + + /// `Save As...` + String get tools_unp4k_action_save_as { + return Intl.message( + 'Save As...', + name: 'tools_unp4k_action_save_as', + desc: '', + args: [], + ); + } + + /// `Extracting: {v0}` + String tools_unp4k_action_extracting(Object v0) { + return Intl.message( + 'Extracting: $v0', + name: 'tools_unp4k_action_extracting', + desc: '', + args: [v0], + ); + } + + /// `Extraction complete: {v0}` + String tools_unp4k_action_extract_success(Object v0) { + return Intl.message( + 'Extraction complete: $v0', + name: 'tools_unp4k_action_extract_success', + desc: '', + args: [v0], + ); + } + + /// `Extraction failed: {v0}` + String tools_unp4k_action_extract_failed(Object v0) { + return Intl.message( + 'Extraction failed: $v0', + name: 'tools_unp4k_action_extract_failed', + desc: '', + args: [v0], + ); + } + + /// `No matching files found` + String get tools_unp4k_search_no_result { + return Intl.message( + 'No matching files found', + name: 'tools_unp4k_search_no_result', + desc: '', + args: [], + ); + } + + /// `Searching...` + String get tools_unp4k_searching { + return Intl.message( + 'Searching...', + name: 'tools_unp4k_searching', + desc: '', + args: [], + ); + } + + /// `Extract Files` + String get tools_unp4k_extract_dialog_title { + return Intl.message( + 'Extract Files', + name: 'tools_unp4k_extract_dialog_title', + desc: '', + args: [], + ); + } + + /// `Extracting ({v0}/{v1})` + String tools_unp4k_extract_progress(Object v0, Object v1) { + return Intl.message( + 'Extracting ($v0/$v1)', + name: 'tools_unp4k_extract_progress', + desc: '', + args: [v0, v1], + ); + } + + /// `Current file: {v0}` + String tools_unp4k_extract_current_file(Object v0) { + return Intl.message( + 'Current file: $v0', + name: 'tools_unp4k_extract_current_file', + desc: '', + args: [v0], + ); + } + + /// `Extraction cancelled` + String get tools_unp4k_extract_cancelled { + return Intl.message( + 'Extraction cancelled', + name: 'tools_unp4k_extract_cancelled', + desc: '', + args: [], + ); + } + + /// `Extraction completed, {v0} files total` + String tools_unp4k_extract_completed(Object v0) { + return Intl.message( + 'Extraction completed, $v0 files total', + name: 'tools_unp4k_extract_completed', + desc: '', + args: [v0], + ); + } + + /// `Multi-Select` + String get tools_unp4k_action_multi_select { + return Intl.message( + 'Multi-Select', + name: 'tools_unp4k_action_multi_select', + desc: '', + args: [], + ); + } + + /// `Export Selected ({v0})` + String tools_unp4k_action_export_selected(Object v0) { + return Intl.message( + 'Export Selected ($v0)', + name: 'tools_unp4k_action_export_selected', + desc: '', + args: [v0], + ); + } + + /// `Cancel Multi-Select` + String get tools_unp4k_action_cancel_multi_select { + return Intl.message( + 'Cancel Multi-Select', + name: 'tools_unp4k_action_cancel_multi_select', + desc: '', + args: [], + ); + } + + /// `Select All` + String get tools_unp4k_action_select_all { + return Intl.message( + 'Select All', + name: 'tools_unp4k_action_select_all', + desc: '', + args: [], + ); + } + + /// `Deselect All` + String get tools_unp4k_action_deselect_all { + return Intl.message( + 'Deselect All', + name: 'tools_unp4k_action_deselect_all', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 2bf097a..6bde35c 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1424,5 +1424,27 @@ "splash_db_reset_done": "[Diagnostic] Database reset complete, preparing to exit application", "splash_db_reset_msg": "Database has been reset, application will exit. Please restart the application.", "splash_reset_db_failed": "[Diagnostic] Failed to reset database: {v0}", - "@splash_reset_db_failed": {} -} + "@splash_reset_db_failed": {}, + "tools_unp4k_search_placeholder": "Search files (supports regex)...", + "tools_unp4k_sort_default": "Default Sort", + "tools_unp4k_sort_size_asc": "Smaller First", + "tools_unp4k_sort_size_desc": "Larger First", + "tools_unp4k_sort_date_asc": "Older First", + "tools_unp4k_sort_date_desc": "Newer First", + "tools_unp4k_action_save_as": "Save As...", + "tools_unp4k_action_extracting": "Extracting: {v0}", + "tools_unp4k_action_extract_success": "Extraction complete: {v0}", + "tools_unp4k_action_extract_failed": "Extraction failed: {v0}", + "tools_unp4k_search_no_result": "No matching files found", + "tools_unp4k_searching": "Searching...", + "tools_unp4k_extract_dialog_title": "Extract Files", + "tools_unp4k_extract_progress": "Extracting ({v0}/{v1})", + "tools_unp4k_extract_current_file": "Current file: {v0}", + "tools_unp4k_extract_cancelled": "Extraction cancelled", + "tools_unp4k_extract_completed": "Extraction completed, {v0} files total", + "tools_unp4k_action_multi_select": "Multi-Select", + "tools_unp4k_action_export_selected": "Export Selected ({v0})", + "tools_unp4k_action_cancel_multi_select": "Cancel Multi-Select", + "tools_unp4k_action_select_all": "Select All", + "tools_unp4k_action_deselect_all": "Deselect All" +} \ No newline at end of file diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index 57f73e1..d824e5f 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -1139,5 +1139,27 @@ "splash_db_reset_done": "[诊断] 数据库重置完成,准备退出应用", "splash_db_reset_msg": "数据库已重置,应用将退出。请重新启动应用。", "splash_reset_db_failed": "[诊断] 重置数据库失败: {v0}", - "@splash_reset_db_failed": {} + "@splash_reset_db_failed": {}, + "tools_unp4k_search_placeholder": "搜索文件(支持正则)...", + "tools_unp4k_sort_default": "默认排序", + "tools_unp4k_sort_size_asc": "小文件优先", + "tools_unp4k_sort_size_desc": "大文件优先", + "tools_unp4k_sort_date_asc": "旧文件优先", + "tools_unp4k_sort_date_desc": "新文件优先", + "tools_unp4k_action_save_as": "另存为...", + "tools_unp4k_action_extracting": "正在提取:{v0}", + "tools_unp4k_action_extract_success": "提取完成:{v0}", + "tools_unp4k_action_extract_failed": "提取失败:{v0}", + "tools_unp4k_search_no_result": "未找到匹配文件", + "tools_unp4k_searching": "正在搜索...", + "tools_unp4k_extract_dialog_title": "提取文件", + "tools_unp4k_extract_progress": "正在提取 ({v0}/{v1})", + "tools_unp4k_extract_current_file": "当前文件:{v0}", + "tools_unp4k_extract_cancelled": "提取已取消", + "tools_unp4k_extract_completed": "提取完成,共 {v0} 个文件", + "tools_unp4k_action_multi_select": "多选", + "tools_unp4k_action_export_selected": "导出选中项 ({v0})", + "tools_unp4k_action_cancel_multi_select": "取消多选", + "tools_unp4k_action_select_all": "全选", + "tools_unp4k_action_deselect_all": "取消全选" } \ No newline at end of file diff --git a/lib/provider/unp4kc.dart b/lib/provider/unp4kc.dart index bd6ea99..96feec8 100644 --- a/lib/provider/unp4kc.dart +++ b/lib/provider/unp4kc.dart @@ -16,6 +16,24 @@ part 'unp4kc.freezed.dart'; part 'unp4kc.g.dart'; +/// 排序类型枚举 +enum Unp4kSortType { + /// 默认排序(文件夹优先,按名称) + defaultSort, + + /// 文件大小升序 + sizeAsc, + + /// 文件大小降序 + sizeDesc, + + /// 修改时间升序 + dateAsc, + + /// 修改时间降序 + dateDesc, +} + @freezed abstract class Unp4kcState with _$Unp4kcState { const factory Unp4kcState({ @@ -26,6 +44,21 @@ abstract class Unp4kcState with _$Unp4kcState { String? endMessage, MapEntry? tempOpenFile, @Default("") String errorMessage, + @Default("") String searchQuery, + @Default(false) bool isSearching, + + /// 搜索结果的虚拟文件系统(支持分级展示) + MemoryFileSystem? searchFs, + + /// 搜索匹配的文件路径集合 + Set? searchMatchedFiles, + @Default(Unp4kSortType.defaultSort) Unp4kSortType sortType, + + /// 是否处于多选模式 + @Default(false) bool isMultiSelectMode, + + /// 多选模式下选中的文件路径集合 + @Default({}) Set selectedItems, }) = _Unp4kcState; } @@ -110,20 +143,15 @@ class Unp4kCModel extends _$Unp4kCModel { List? getFiles() { final path = state.curPath.replaceAll("\\", "/"); - final fs = state.fs; + + // 如果有搜索结果,使用搜索的虚拟文件系统 + final fs = state.searchFs ?? 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 = []; for (var file in files) { if (file is File) { @@ -138,10 +166,211 @@ class Unp4kCModel extends _$Unp4kCModel { result.add(AppUnp4kP4kItemData(name: file.path.replaceAll("/", "\\"), isDirectory: true)); } } + + // 应用排序 + _sortFiles(result); return result; } + /// 对文件列表进行排序 + void _sortFiles(List files) { + switch (state.sortType) { + case Unp4kSortType.defaultSort: + // 默认排序:文件夹优先,按名称排序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } else { + return (a.name ?? "").compareTo(b.name ?? ""); + } + }); + break; + case Unp4kSortType.sizeAsc: + // 文件大小升序(文件夹大小视为0) + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final sizeA = (a.isDirectory ?? false) ? 0 : (a.size ?? 0); + final sizeB = (b.isDirectory ?? false) ? 0 : (b.size ?? 0); + return sizeA.compareTo(sizeB); + }); + break; + case Unp4kSortType.sizeDesc: + // 文件大小降序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final sizeA = (a.isDirectory ?? false) ? 0 : (a.size ?? 0); + final sizeB = (b.isDirectory ?? false) ? 0 : (b.size ?? 0); + return sizeB.compareTo(sizeA); + }); + break; + case Unp4kSortType.dateAsc: + // 修改时间升序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final dateA = a.dateModified ?? 0; + final dateB = b.dateModified ?? 0; + return dateA.compareTo(dateB); + }); + break; + case Unp4kSortType.dateDesc: + // 修改时间降序 + files.sort((a, b) { + if ((a.isDirectory ?? false) && !(b.isDirectory ?? false)) { + return -1; + } else if (!(a.isDirectory ?? false) && (b.isDirectory ?? false)) { + return 1; + } + final dateA = a.dateModified ?? 0; + final dateB = b.dateModified ?? 0; + return dateB.compareTo(dateA); + }); + break; + } + } + + /// 设置排序类型 + void setSortType(Unp4kSortType sortType) { + state = state.copyWith(sortType: sortType); + } + + /// 执行搜索(异步) + Future search(String query) async { + if (query.isEmpty) { + // 清除搜索,返回根目录 + state = state.copyWith( + searchQuery: "", + searchFs: null, + searchMatchedFiles: null, + isSearching: false, + curPath: "\\", + ); + return; + } + + // 保存当前路径,用于搜索后尝试保持 + final currentPath = state.curPath; + + state = state.copyWith(searchQuery: query, isSearching: true, endMessage: S.current.tools_unp4k_searching); + + // 使用 compute 在后台线程执行搜索 + final allFiles = state.files; + if (allFiles == null) { + state = state.copyWith(isSearching: false); + return; + } + + try { + final searchResult = await compute(_searchFiles, _SearchParams(allFiles, query)); + final matchedFiles = searchResult.matchedFiles; + + // 构建搜索结果的虚拟文件系统 + final searchFs = MemoryFileSystem(style: FileSystemStyle.posix); + for (var filePath in matchedFiles) { + await searchFs.file(filePath.replaceAll("\\", "/")).create(recursive: true); + } + + // 检查当前路径是否有搜索结果 + String targetPath = "\\"; + if (currentPath != "\\") { + final checkPath = currentPath.replaceAll("\\", "/"); + final dir = searchFs.directory(checkPath); + if (dir.existsSync() && dir.listSync().isNotEmpty) { + // 当前目录有结果,保持当前路径 + targetPath = currentPath; + } + } + + state = state.copyWith( + searchFs: searchFs, + searchMatchedFiles: matchedFiles, + isSearching: false, + curPath: targetPath, + endMessage: matchedFiles.isEmpty + ? S.current.tools_unp4k_search_no_result + : S.current.tools_unp4k_msg_read_completed(matchedFiles.length, 0), + ); + } catch (e) { + dPrint("[unp4k] search error: $e"); + state = state.copyWith(isSearching: false, endMessage: e.toString()); + } + } + + /// 清除搜索 + void clearSearch() { + state = state.copyWith( + searchQuery: "", + searchFs: null, + searchMatchedFiles: null, + isSearching: false, + curPath: "\\", + ); + } + + /// 进入多选模式 + void enterMultiSelectMode() { + state = state.copyWith(isMultiSelectMode: true, selectedItems: {}); + } + + /// 退出多选模式 + void exitMultiSelectMode() { + state = state.copyWith(isMultiSelectMode: false, selectedItems: {}); + } + + /// 切换选中状态 + void toggleSelectItem(String itemPath) { + final currentSelected = Set.from(state.selectedItems); + if (currentSelected.contains(itemPath)) { + currentSelected.remove(itemPath); + } else { + currentSelected.add(itemPath); + } + state = state.copyWith(selectedItems: currentSelected); + } + + /// 全选当前目录的文件 + void selectAll(List? files) { + if (files == null) return; + final currentSelected = Set.from(state.selectedItems); + for (var file in files) { + final path = file.name ?? ""; + if (path.isNotEmpty) { + currentSelected.add(path); + } + } + state = state.copyWith(selectedItems: currentSelected); + } + + /// 取消全选当前目录的文件 + void deselectAll(List? files) { + if (files == null) return; + final currentSelected = Set.from(state.selectedItems); + for (var file in files) { + final path = file.name ?? ""; + currentSelected.remove(path); + } + state = state.copyWith(selectedItems: currentSelected); + } + void changeDir(String name, {bool fullPath = false}) { + // 切换目录时退出多选模式 + if (state.isMultiSelectMode) { + state = state.copyWith(isMultiSelectMode: false, selectedItems: {}); + } + // 切换目录时不清除搜索,只改变当前路径 if (fullPath) { state = state.copyWith(curPath: name); } else { @@ -197,6 +426,202 @@ class Unp4kCModel extends _$Unp4kCModel { } } + /// 提取文件或文件夹到指定目录(带进度回调和取消支持) + /// [item] 要提取的文件或文件夹 + /// [outputDir] 输出目录 + /// [onProgress] 进度回调 (当前文件索引, 总文件数, 当前文件名) + /// [isCancelled] 取消检查函数,返回 true 表示取消 + /// 返回值:(是否成功, 已提取文件数, 错误信息) + Future<(bool, int, String?)> extractToDirectoryWithProgress( + AppUnp4kP4kItemData item, + String outputDir, { + void Function(int current, int total, String currentFile)? onProgress, + bool Function()? isCancelled, + }) async { + try { + final itemPath = item.name ?? ""; + var filePath = itemPath; + if (filePath.startsWith("\\")) { + filePath = filePath.substring(1); + } + + if (item.isDirectory ?? false) { + // 提取文件夹:遍历所有以该路径为前缀的文件 + final allFiles = state.files; + if (allFiles != null) { + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + + // 收集所有需要提取的文件 + final filesToExtract = >[]; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry); + } + } + + final total = filesToExtract.length; + var current = 0; + + for (var entry in filesToExtract) { + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, current, S.current.tools_unp4k_extract_cancelled); + } + + var entryPath = entry.key; + if (entryPath.startsWith("\\")) { + entryPath = entryPath.substring(1); + } + + current++; + onProgress?.call(current, total, entryPath); + + final fullOutputPath = "$outputDir\\$entryPath"; + await unp4k_api.p4KExtractToDisk(filePath: entryPath, outputPath: fullOutputPath); + } + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); + return (true, current, null); + } + return (true, 0, null); + } else { + // 提取单个文件 + onProgress?.call(1, 1, filePath); + + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, 0, S.current.tools_unp4k_extract_cancelled); + } + + final fullOutputPath = "$outputDir\\$filePath"; + await unp4k_api.p4KExtractToDisk(filePath: filePath, outputPath: fullOutputPath); + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(1)); + return (true, 1, null); + } + } catch (e) { + dPrint("[unp4k] extractToDirectoryWithProgress error: $e"); + return (false, 0, e.toString()); + } + } + + /// 获取文件夹中需要提取的文件数量 + int getFileCountInDirectory(AppUnp4kP4kItemData item) { + if (!(item.isDirectory ?? false)) { + return 1; + } + + final itemPath = item.name ?? ""; + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + final allFiles = state.files; + + if (allFiles == null) return 0; + + int count = 0; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + count++; + } + } + return count; + } + + /// 获取多选项的总文件数量 + int getFileCountForSelectedItems(Set selectedItems) { + int count = 0; + final allFiles = state.files; + if (allFiles == null) return 0; + + for (var itemPath in selectedItems) { + final item = allFiles[itemPath]; + if (item != null) { + if (item.isDirectory ?? false) { + count += getFileCountInDirectory(item); + } else { + count += 1; + } + } else { + // 可能是文件夹(虚拟路径) + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + count++; + } + } + } + } + return count; + } + + /// 批量提取多个选中项到指定目录(带进度回调和取消支持) + Future<(bool, int, String?)> extractSelectedItemsWithProgress( + Set selectedItems, + String outputDir, { + void Function(int current, int total, String currentFile)? onProgress, + bool Function()? isCancelled, + }) async { + try { + final allFiles = state.files; + if (allFiles == null) return (true, 0, null); + + // 收集所有需要提取的文件 + final filesToExtract = []; + + for (var itemPath in selectedItems) { + final item = allFiles[itemPath]; + if (item != null) { + if (item.isDirectory ?? false) { + // 文件夹:收集所有子文件 + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry.key); + } + } + } else { + // 单个文件 + filesToExtract.add(itemPath); + } + } else { + // 可能是虚拟文件夹路径 + final prefix = itemPath.endsWith("\\") ? itemPath : "$itemPath\\"; + for (var entry in allFiles.entries) { + if (entry.key.startsWith(prefix) && !(entry.value.isDirectory ?? false)) { + filesToExtract.add(entry.key); + } + } + } + } + + final total = filesToExtract.length; + var current = 0; + + for (var filePath in filesToExtract) { + // 检查是否取消 + if (isCancelled?.call() == true) { + return (false, current, S.current.tools_unp4k_extract_cancelled); + } + + var extractPath = filePath; + if (extractPath.startsWith("\\")) { + extractPath = extractPath.substring(1); + } + + current++; + onProgress?.call(current, total, extractPath); + + final fullOutputPath = "$outputDir\\$extractPath"; + await unp4k_api.p4KExtractToDisk(filePath: extractPath, outputPath: fullOutputPath); + } + + state = state.copyWith(endMessage: S.current.tools_unp4k_extract_completed(current)); + return (true, current, null); + } catch (e) { + dPrint("[unp4k] extractSelectedItemsWithProgress error: $e"); + return (false, 0, e.toString()); + } + } + /// 从 P4K 文件中提取指定文件到内存 /// [p4kPath] P4K 文件路径 /// [filePath] 要提取的文件路径(P4K 内部路径) @@ -211,3 +636,54 @@ class Unp4kCModel extends _$Unp4kCModel { } } } + +/// 搜索参数类 +class _SearchParams { + final Map files; + final String query; + + _SearchParams(this.files, this.query); +} + +/// 搜索结果类 +class _SearchResult { + final Set matchedFiles; + + _SearchResult(this.matchedFiles); +} + +/// 在后台线程执行搜索 +_SearchResult _searchFiles(_SearchParams params) { + final matchedFiles = {}; + + // 尝试编译正则表达式,如果失败则使用普通字符串匹配 + RegExp? regex; + try { + regex = RegExp(params.query, caseSensitive: false); + } catch (e) { + // 正则无效,回退到普通字符串匹配 + regex = null; + } + + for (var entry in params.files.entries) { + final item = entry.value; + final name = item.name ?? ""; + + // 跳过文件夹本身 + if (item.isDirectory ?? false) continue; + + bool matches = false; + if (regex != null) { + matches = regex.hasMatch(name); + } else { + matches = name.toLowerCase().contains(params.query.toLowerCase()); + } + + if (matches) { + // 添加匹配的文件路径 + matchedFiles.add(name.startsWith("\\") ? name : "\\$name"); + } + } + + return _SearchResult(matchedFiles); +} diff --git a/lib/provider/unp4kc.freezed.dart b/lib/provider/unp4kc.freezed.dart index 9ed7d64..cc01f62 100644 --- a/lib/provider/unp4kc.freezed.dart +++ b/lib/provider/unp4kc.freezed.dart @@ -14,7 +14,11 @@ T _$identity(T value) => value; /// @nodoc mixin _$Unp4kcState implements DiagnosticableTreeMixin { - bool get startUp; Map? get files; MemoryFileSystem? get fs; String get curPath; String? get endMessage; MapEntry? get tempOpenFile; String get errorMessage; + bool get startUp; Map? get files; MemoryFileSystem? get fs; String get curPath; String? get endMessage; MapEntry? get tempOpenFile; String get errorMessage; String get searchQuery; bool get isSearching;/// 搜索结果的虚拟文件系统(支持分级展示) + MemoryFileSystem? get searchFs;/// 搜索匹配的文件路径集合 + Set? get searchMatchedFiles; Unp4kSortType get sortType;/// 是否处于多选模式 + bool get isMultiSelectMode;/// 多选模式下选中的文件路径集合 + Set get selectedItems; /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -26,21 +30,21 @@ $Unp4kcStateCopyWith get copyWith => _$Unp4kcStateCopyWithImpl Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(files),fs,curPath,endMessage,tempOpenFile,errorMessage); +int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(files),fs,curPath,endMessage,tempOpenFile,errorMessage,searchQuery,isSearching,searchFs,const DeepCollectionEquality().hash(searchMatchedFiles),sortType,isMultiSelectMode,const DeepCollectionEquality().hash(selectedItems)); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage)'; + return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage, searchQuery: $searchQuery, isSearching: $isSearching, searchFs: $searchFs, searchMatchedFiles: $searchMatchedFiles, sortType: $sortType, isMultiSelectMode: $isMultiSelectMode, selectedItems: $selectedItems)'; } @@ -51,7 +55,7 @@ abstract mixin class $Unp4kcStateCopyWith<$Res> { factory $Unp4kcStateCopyWith(Unp4kcState value, $Res Function(Unp4kcState) _then) = _$Unp4kcStateCopyWithImpl; @useResult $Res call({ - bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage + bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems }); @@ -68,7 +72,7 @@ class _$Unp4kcStateCopyWithImpl<$Res> /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,Object? searchQuery = null,Object? isSearching = null,Object? searchFs = freezed,Object? searchMatchedFiles = freezed,Object? sortType = null,Object? isMultiSelectMode = null,Object? selectedItems = null,}) { return _then(_self.copyWith( startUp: null == startUp ? _self.startUp : startUp // ignore: cast_nullable_to_non_nullable as bool,files: freezed == files ? _self.files : files // ignore: cast_nullable_to_non_nullable @@ -77,7 +81,14 @@ as MemoryFileSystem?,curPath: null == curPath ? _self.curPath : curPath // ignor as String,endMessage: freezed == endMessage ? _self.endMessage : endMessage // ignore: cast_nullable_to_non_nullable as String?,tempOpenFile: freezed == tempOpenFile ? _self.tempOpenFile : tempOpenFile // ignore: cast_nullable_to_non_nullable as MapEntry?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as String,searchQuery: null == searchQuery ? _self.searchQuery : searchQuery // ignore: cast_nullable_to_non_nullable +as String,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable +as bool,searchFs: freezed == searchFs ? _self.searchFs : searchFs // ignore: cast_nullable_to_non_nullable +as MemoryFileSystem?,searchMatchedFiles: freezed == searchMatchedFiles ? _self.searchMatchedFiles : searchMatchedFiles // ignore: cast_nullable_to_non_nullable +as Set?,sortType: null == sortType ? _self.sortType : sortType // ignore: cast_nullable_to_non_nullable +as Unp4kSortType,isMultiSelectMode: null == isMultiSelectMode ? _self.isMultiSelectMode : isMultiSelectMode // ignore: cast_nullable_to_non_nullable +as bool,selectedItems: null == selectedItems ? _self.selectedItems : selectedItems // ignore: cast_nullable_to_non_nullable +as Set, )); } @@ -162,10 +173,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _Unp4kcState() when $default != null: -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: return orElse(); } @@ -183,10 +194,10 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems) $default,) {final _that = this; switch (_that) { case _Unp4kcState(): -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: throw StateError('Unexpected subclass'); } @@ -203,10 +214,10 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems)? $default,) {final _that = this; switch (_that) { case _Unp4kcState() when $default != null: -return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage);case _: +return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessage,_that.tempOpenFile,_that.errorMessage,_that.searchQuery,_that.isSearching,_that.searchFs,_that.searchMatchedFiles,_that.sortType,_that.isMultiSelectMode,_that.selectedItems);case _: return null; } @@ -218,7 +229,7 @@ return $default(_that.startUp,_that.files,_that.fs,_that.curPath,_that.endMessag class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState { - const _Unp4kcState({required this.startUp, final Map? files, this.fs, required this.curPath, this.endMessage, this.tempOpenFile, this.errorMessage = ""}): _files = files; + const _Unp4kcState({required this.startUp, final Map? files, this.fs, required this.curPath, this.endMessage, this.tempOpenFile, this.errorMessage = "", this.searchQuery = "", this.isSearching = false, this.searchFs, final Set? searchMatchedFiles, this.sortType = Unp4kSortType.defaultSort, this.isMultiSelectMode = false, final Set selectedItems = const {}}): _files = files,_searchMatchedFiles = searchMatchedFiles,_selectedItems = selectedItems; @override final bool startUp; @@ -236,6 +247,33 @@ class _Unp4kcState with DiagnosticableTreeMixin implements Unp4kcState { @override final String? endMessage; @override final MapEntry? tempOpenFile; @override@JsonKey() final String errorMessage; +@override@JsonKey() final String searchQuery; +@override@JsonKey() final bool isSearching; +/// 搜索结果的虚拟文件系统(支持分级展示) +@override final MemoryFileSystem? searchFs; +/// 搜索匹配的文件路径集合 + final Set? _searchMatchedFiles; +/// 搜索匹配的文件路径集合 +@override Set? get searchMatchedFiles { + final value = _searchMatchedFiles; + if (value == null) return null; + if (_searchMatchedFiles is EqualUnmodifiableSetView) return _searchMatchedFiles; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(value); +} + +@override@JsonKey() final Unp4kSortType sortType; +/// 是否处于多选模式 +@override@JsonKey() final bool isMultiSelectMode; +/// 多选模式下选中的文件路径集合 + final Set _selectedItems; +/// 多选模式下选中的文件路径集合 +@override@JsonKey() Set get selectedItems { + if (_selectedItems is EqualUnmodifiableSetView) return _selectedItems; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_selectedItems); +} + /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. @@ -248,21 +286,21 @@ _$Unp4kcStateCopyWith<_Unp4kcState> get copyWith => __$Unp4kcStateCopyWithImpl<_ void debugFillProperties(DiagnosticPropertiesBuilder properties) { properties ..add(DiagnosticsProperty('type', 'Unp4kcState')) - ..add(DiagnosticsProperty('startUp', startUp))..add(DiagnosticsProperty('files', files))..add(DiagnosticsProperty('fs', fs))..add(DiagnosticsProperty('curPath', curPath))..add(DiagnosticsProperty('endMessage', endMessage))..add(DiagnosticsProperty('tempOpenFile', tempOpenFile))..add(DiagnosticsProperty('errorMessage', errorMessage)); + ..add(DiagnosticsProperty('startUp', startUp))..add(DiagnosticsProperty('files', files))..add(DiagnosticsProperty('fs', fs))..add(DiagnosticsProperty('curPath', curPath))..add(DiagnosticsProperty('endMessage', endMessage))..add(DiagnosticsProperty('tempOpenFile', tempOpenFile))..add(DiagnosticsProperty('errorMessage', errorMessage))..add(DiagnosticsProperty('searchQuery', searchQuery))..add(DiagnosticsProperty('isSearching', isSearching))..add(DiagnosticsProperty('searchFs', searchFs))..add(DiagnosticsProperty('searchMatchedFiles', searchMatchedFiles))..add(DiagnosticsProperty('sortType', sortType))..add(DiagnosticsProperty('isMultiSelectMode', isMultiSelectMode))..add(DiagnosticsProperty('selectedItems', selectedItems)); } @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Unp4kcState&&(identical(other.startUp, startUp) || other.startUp == startUp)&&const DeepCollectionEquality().equals(other._files, _files)&&(identical(other.fs, fs) || other.fs == fs)&&(identical(other.curPath, curPath) || other.curPath == curPath)&&(identical(other.endMessage, endMessage) || other.endMessage == endMessage)&&(identical(other.tempOpenFile, tempOpenFile) || other.tempOpenFile == tempOpenFile)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _Unp4kcState&&(identical(other.startUp, startUp) || other.startUp == startUp)&&const DeepCollectionEquality().equals(other._files, _files)&&(identical(other.fs, fs) || other.fs == fs)&&(identical(other.curPath, curPath) || other.curPath == curPath)&&(identical(other.endMessage, endMessage) || other.endMessage == endMessage)&&(identical(other.tempOpenFile, tempOpenFile) || other.tempOpenFile == tempOpenFile)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.searchQuery, searchQuery) || other.searchQuery == searchQuery)&&(identical(other.isSearching, isSearching) || other.isSearching == isSearching)&&(identical(other.searchFs, searchFs) || other.searchFs == searchFs)&&const DeepCollectionEquality().equals(other._searchMatchedFiles, _searchMatchedFiles)&&(identical(other.sortType, sortType) || other.sortType == sortType)&&(identical(other.isMultiSelectMode, isMultiSelectMode) || other.isMultiSelectMode == isMultiSelectMode)&&const DeepCollectionEquality().equals(other._selectedItems, _selectedItems)); } @override -int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(_files),fs,curPath,endMessage,tempOpenFile,errorMessage); +int get hashCode => Object.hash(runtimeType,startUp,const DeepCollectionEquality().hash(_files),fs,curPath,endMessage,tempOpenFile,errorMessage,searchQuery,isSearching,searchFs,const DeepCollectionEquality().hash(_searchMatchedFiles),sortType,isMultiSelectMode,const DeepCollectionEquality().hash(_selectedItems)); @override String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage)'; + return 'Unp4kcState(startUp: $startUp, files: $files, fs: $fs, curPath: $curPath, endMessage: $endMessage, tempOpenFile: $tempOpenFile, errorMessage: $errorMessage, searchQuery: $searchQuery, isSearching: $isSearching, searchFs: $searchFs, searchMatchedFiles: $searchMatchedFiles, sortType: $sortType, isMultiSelectMode: $isMultiSelectMode, selectedItems: $selectedItems)'; } @@ -273,7 +311,7 @@ abstract mixin class _$Unp4kcStateCopyWith<$Res> implements $Unp4kcStateCopyWith factory _$Unp4kcStateCopyWith(_Unp4kcState value, $Res Function(_Unp4kcState) _then) = __$Unp4kcStateCopyWithImpl; @override @useResult $Res call({ - bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage + bool startUp, Map? files, MemoryFileSystem? fs, String curPath, String? endMessage, MapEntry? tempOpenFile, String errorMessage, String searchQuery, bool isSearching, MemoryFileSystem? searchFs, Set? searchMatchedFiles, Unp4kSortType sortType, bool isMultiSelectMode, Set selectedItems }); @@ -290,7 +328,7 @@ class __$Unp4kcStateCopyWithImpl<$Res> /// Create a copy of Unp4kcState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? startUp = null,Object? files = freezed,Object? fs = freezed,Object? curPath = null,Object? endMessage = freezed,Object? tempOpenFile = freezed,Object? errorMessage = null,Object? searchQuery = null,Object? isSearching = null,Object? searchFs = freezed,Object? searchMatchedFiles = freezed,Object? sortType = null,Object? isMultiSelectMode = null,Object? selectedItems = null,}) { return _then(_Unp4kcState( startUp: null == startUp ? _self.startUp : startUp // ignore: cast_nullable_to_non_nullable as bool,files: freezed == files ? _self._files : files // ignore: cast_nullable_to_non_nullable @@ -299,7 +337,14 @@ as MemoryFileSystem?,curPath: null == curPath ? _self.curPath : curPath // ignor as String,endMessage: freezed == endMessage ? _self.endMessage : endMessage // ignore: cast_nullable_to_non_nullable as String?,tempOpenFile: freezed == tempOpenFile ? _self.tempOpenFile : tempOpenFile // ignore: cast_nullable_to_non_nullable as MapEntry?,errorMessage: null == errorMessage ? _self.errorMessage : errorMessage // ignore: cast_nullable_to_non_nullable -as String, +as String,searchQuery: null == searchQuery ? _self.searchQuery : searchQuery // ignore: cast_nullable_to_non_nullable +as String,isSearching: null == isSearching ? _self.isSearching : isSearching // ignore: cast_nullable_to_non_nullable +as bool,searchFs: freezed == searchFs ? _self.searchFs : searchFs // ignore: cast_nullable_to_non_nullable +as MemoryFileSystem?,searchMatchedFiles: freezed == searchMatchedFiles ? _self._searchMatchedFiles : searchMatchedFiles // ignore: cast_nullable_to_non_nullable +as Set?,sortType: null == sortType ? _self.sortType : sortType // ignore: cast_nullable_to_non_nullable +as Unp4kSortType,isMultiSelectMode: null == isMultiSelectMode ? _self.isMultiSelectMode : isMultiSelectMode // ignore: cast_nullable_to_non_nullable +as bool,selectedItems: null == selectedItems ? _self._selectedItems : selectedItems // ignore: cast_nullable_to_non_nullable +as Set, )); } diff --git a/lib/provider/unp4kc.g.dart b/lib/provider/unp4kc.g.dart index 2dd3284..97991bb 100644 --- a/lib/provider/unp4kc.g.dart +++ b/lib/provider/unp4kc.g.dart @@ -41,7 +41,7 @@ final class Unp4kCModelProvider } } -String _$unp4kCModelHash() => r'a296a499158e78848a698c3fda92c4c88ff039be'; +String _$unp4kCModelHash() => r'b46274b1409dc904db2d96acf692869edf034b9f'; abstract class _$Unp4kCModel extends $Notifier { Unp4kcState build(); diff --git a/lib/ui/home/localization/advanced_localization_ui_model.g.dart b/lib/ui/home/localization/advanced_localization_ui_model.g.dart index 7873116..bfb578d 100644 --- a/lib/ui/home/localization/advanced_localization_ui_model.g.dart +++ b/lib/ui/home/localization/advanced_localization_ui_model.g.dart @@ -47,7 +47,7 @@ final class AdvancedLocalizationUIModelProvider } String _$advancedLocalizationUIModelHash() => - r'5ff4d8156fbae4dcf69cb3fbcabfb9abda69ffbb'; + r'4527ea29b07d4e525367d380d2aeb3ece4f99f4f'; abstract class _$AdvancedLocalizationUIModel extends $Notifier { diff --git a/lib/ui/tools/unp4kc/unp4kc_ui.dart b/lib/ui/tools/unp4kc/unp4kc_ui.dart index 2d3f430..2e91e1e 100644 --- a/lib/ui/tools/unp4kc/unp4kc_ui.dart +++ b/lib/ui/tools/unp4kc/unp4kc_ui.dart @@ -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? files; + + const _FileListPanel({required this.state, required this.model, required this.files}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final searchController = useTextEditingController(text: state.searchQuery); + + return Container( + decoration: BoxDecoration(color: FluentTheme.of(context).cardColor.withValues(alpha: .01)), + child: Column( + children: [ + // 搜索栏和排序选择器 + Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Expanded( + child: TextBox( + controller: searchController, + placeholder: S.current.tools_unp4k_search_placeholder, + prefix: const Padding(padding: EdgeInsets.only(left: 8), child: Icon(FluentIcons.search, size: 14)), + suffix: searchController.text.isNotEmpty || state.searchFs != null + ? IconButton( + icon: const Icon(FluentIcons.clear, size: 12), + onPressed: () { + searchController.clear(); + model.clearSearch(); + }, + ) + : null, + onSubmitted: (value) { + model.search(value); + }, + ), + ), + const SizedBox(width: 8), + ComboBox( + value: state.sortType, + items: [ + ComboBoxItem(value: Unp4kSortType.defaultSort, child: Text(S.current.tools_unp4k_sort_default)), + ComboBoxItem(value: Unp4kSortType.sizeAsc, child: Text(S.current.tools_unp4k_sort_size_asc)), + ComboBoxItem(value: Unp4kSortType.sizeDesc, child: Text(S.current.tools_unp4k_sort_size_desc)), + ComboBoxItem(value: Unp4kSortType.dateAsc, child: Text(S.current.tools_unp4k_sort_date_asc)), + ComboBoxItem(value: Unp4kSortType.dateDesc, child: Text(S.current.tools_unp4k_sort_date_desc)), + ], + onChanged: (value) { + if (value != null) { + model.setSortType(value); + } + }, + ), + ], + ), + ), + // 多选模式工具栏 + if (state.isMultiSelectMode) + Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + decoration: BoxDecoration( + color: FluentTheme.of(context).accentColor.withValues(alpha: .1), + border: Border( + bottom: BorderSide(color: FluentTheme.of(context).accentColor.withValues(alpha: .3), width: 1), + ), + ), + child: Row( + children: [ + Text( + S.current.tools_unp4k_action_export_selected(state.selectedItems.length), + style: const TextStyle(fontSize: 12), + ), + const Spacer(), + Button(child: Text(S.current.tools_unp4k_action_select_all), onPressed: () => model.selectAll(files)), + const SizedBox(width: 4), + Button( + child: Text(S.current.tools_unp4k_action_deselect_all), + onPressed: () => model.deselectAll(files), + ), + const SizedBox(width: 4), + FilledButton( + onPressed: state.selectedItems.isNotEmpty ? () => _exportSelected(context) : null, + child: Text(S.current.tools_unp4k_action_save_as), + ), + const SizedBox(width: 4), + IconButton( + icon: const Icon(FluentIcons.cancel, size: 14), + onPressed: () => model.exitMultiSelectMode(), + ), + ], + ), + ), + // 文件列表 + Expanded( + child: files == null || files!.isEmpty + ? Center( + child: Text( + state.searchFs != null ? S.current.tools_unp4k_search_no_result : '', + style: TextStyle(color: Colors.white.withValues(alpha: .6)), + ), + ) + : SuperListView.builder( + padding: const EdgeInsets.only(top: 6, bottom: 6, left: 3, right: 12), + itemBuilder: (BuildContext context, int index) { + final item = files![index]; + return _FileListItem(item: item, state: state, model: model); + }, + itemCount: files?.length ?? 0, + ), + ), + ], + ), + ); + } + + Future _exportSelected(BuildContext context) async { + final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as); + if (outputDir != null && context.mounted) { + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return _MultiExtractProgressDialog(selectedItems: state.selectedItems, outputDir: outputDir, model: model); + }, + ); + // 提取完成后退出多选模式 + model.exitMultiSelectMode(); + } + } +} + +/// 文件列表项组件 +class _FileListItem extends HookWidget { + final AppUnp4kP4kItemData item; + final Unp4kcState state; + final Unp4kCModel model; + + const _FileListItem({required this.item, required this.state, required this.model}); + + @override + Widget build(BuildContext context) { + final flyoutController = useMemoized(() => FlyoutController()); + // 显示相对于当前路径的文件名 + final fileName = item.name?.replaceAll(state.curPath.trim(), "") ?? "?"; + final itemPath = item.name ?? ""; + final isSelected = state.selectedItems.contains(itemPath); + + return FlyoutTarget( + controller: flyoutController, + child: GestureDetector( + onSecondaryTapUp: (details) => _showContextMenu(context, flyoutController), + child: Container( + margin: const EdgeInsets.only(top: 4, bottom: 4), + decoration: BoxDecoration( + color: isSelected + ? FluentTheme.of(context).accentColor.withValues(alpha: .2) + : FluentTheme.of(context).cardColor.withValues(alpha: .05), + ), + child: IconButton( + onPressed: () { + if (state.isMultiSelectMode) { + // 多选模式下点击切换选中状态 + model.toggleSelectItem(itemPath); + } else if (item.isDirectory ?? false) { + final dirName = item.name?.replaceAll(state.curPath.trim(), "") ?? ""; + model.changeDir(dirName); + } else { + model.openFile(item.name ?? ""); + } + }, + icon: Padding( + padding: const EdgeInsets.only(left: 4, right: 4), + child: Row( + children: [ + // 多选模式下显示复选框 + if (state.isMultiSelectMode) ...[ + Checkbox( + checked: isSelected, + onChanged: (value) { + model.toggleSelectItem(itemPath); + }, + ), + const SizedBox(width: 8), + ], + if (item.isDirectory ?? false) + const Icon(FluentIcons.folder_fill, color: Color.fromRGBO(255, 224, 138, 1)) + else if (fileName.endsWith(".xml")) + const Icon(FluentIcons.file_code) + else + const Icon(FluentIcons.open_file), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(fileName, style: const TextStyle(fontSize: 13), textAlign: TextAlign.start), + ), + ], + ), + if (!(item.isDirectory ?? true)) ...[ + const SizedBox(height: 1), + Row( + children: [ + Text( + FileSize.getSize(item.size), + style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)), + ), + const SizedBox(width: 12), + Text( + item.dateModified != null + ? DateTime.fromMillisecondsSinceEpoch( + item.dateModified!, + ).toString().substring(0, 19) + : "", + style: TextStyle(fontSize: 10, color: Colors.white.withValues(alpha: .6)), + ), + ], + ), + ], + ], + ), + ), + const SizedBox(width: 3), + Icon(FluentIcons.chevron_right, size: 14, color: Colors.white.withValues(alpha: .6)), + ], + ), + ), + ), + ), + ), + ); + } + + void _showContextMenu(BuildContext context, FlyoutController controller) { + // 保存外部 context,因为 flyout 的 context 在关闭后会失效 + final outerContext = context; + controller.showFlyout( + autoModeConfiguration: FlyoutAutoConfiguration(preferredMode: FlyoutPlacementMode.bottomCenter), + barrierColor: Colors.transparent, + builder: (flyoutContext) { + return MenuFlyout( + items: [ + MenuFlyoutItem( + leading: const Icon(FluentIcons.save_as, size: 16), + text: Text(S.current.tools_unp4k_action_save_as), + onPressed: () async { + Navigator.of(flyoutContext).pop(); + await _saveAs(outerContext); + }, + ), + // 多选模式切换 + if (!state.isMultiSelectMode) + MenuFlyoutItem( + leading: const Icon(FluentIcons.checkbox_composite, size: 16), + text: Text(S.current.tools_unp4k_action_multi_select), + onPressed: () { + Navigator.of(flyoutContext).pop(); + model.enterMultiSelectMode(); + // 自动选中当前项 + model.toggleSelectItem(item.name ?? ""); + }, + ), + ], + ); + }, + ); + } + + Future _saveAs(BuildContext context) async { + final outputDir = await FilePicker.platform.getDirectoryPath(dialogTitle: S.current.tools_unp4k_action_save_as); + if (outputDir != null && context.mounted) { + await _showExtractProgressDialog(context, outputDir); + } + } + + Future _showExtractProgressDialog(BuildContext context, String outputDir) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + return _ExtractProgressDialog(item: item, outputDir: outputDir, model: model); + }, + ); + } +} + +/// 提取进度对话框 +class _ExtractProgressDialog extends HookWidget { + final AppUnp4kP4kItemData item; + final String outputDir; + final Unp4kCModel model; + + const _ExtractProgressDialog({required this.item, required this.outputDir, required this.model}); + + @override + Widget build(BuildContext context) { + final isCancelled = useState(false); + final currentFile = useState(""); + final currentIndex = useState(0); + final totalFiles = useState(0); + final isCompleted = useState(false); + final errorMessage = useState(null); + final extractedCount = useState(0); + + useEffect(() { + // 获取文件数量 + totalFiles.value = model.getFileCountInDirectory(item); + + // 开始提取 + _startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount); + return null; + }, const []); + + final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0; + + return ContentDialog( + title: Text(S.current.tools_unp4k_extract_dialog_title), + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isCompleted.value && errorMessage.value == null) ...[ + // 进度条 + ProgressBar(value: progress * 100), + const SizedBox(height: 12), + // 进度文本 + Text( + S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value), + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 8), + // 当前文件 + Text( + S.current.tools_unp4k_extract_current_file( + currentFile.value.length > 60 + ? "...${currentFile.value.substring(currentFile.value.length - 60)}" + : currentFile.value, + ), + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ] else if (errorMessage.value != null) ...[ + // 错误信息 + const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)), + const SizedBox(height: 12), + Text(errorMessage.value!, style: const TextStyle(fontSize: 14)), + ] else ...[ + // 完成 + const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)), + const SizedBox(height: 12), + Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)), + ], + ], + ), + actions: [ + if (!isCompleted.value && errorMessage.value == null) + Button( + onPressed: () { + isCancelled.value = true; + }, + child: Text(S.current.home_action_cancel), + ) + else + FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)), + ], + ); + } + + Future _startExtraction( + ValueNotifier isCancelled, + ValueNotifier currentFile, + ValueNotifier currentIndex, + ValueNotifier totalFiles, + ValueNotifier isCompleted, + ValueNotifier errorMessage, + ValueNotifier extractedCount, + ) async { + final result = await model.extractToDirectoryWithProgress( + item, + outputDir, + onProgress: (current, total, file) { + currentIndex.value = current; + totalFiles.value = total; + currentFile.value = file; + }, + isCancelled: () => isCancelled.value, + ); + + final (success, count, error) = result; + extractedCount.value = count; + + if (!success && error != null) { + errorMessage.value = error; + } else { + isCompleted.value = true; + } + } +} + +/// 批量提取进度对话框 +class _MultiExtractProgressDialog extends HookWidget { + final Set selectedItems; + final String outputDir; + final Unp4kCModel model; + + const _MultiExtractProgressDialog({required this.selectedItems, required this.outputDir, required this.model}); + + @override + Widget build(BuildContext context) { + final isCancelled = useState(false); + final currentFile = useState(""); + final currentIndex = useState(0); + final totalFiles = useState(0); + final isCompleted = useState(false); + final errorMessage = useState(null); + final extractedCount = useState(0); + + useEffect(() { + // 获取文件数量 + totalFiles.value = model.getFileCountForSelectedItems(selectedItems); + + // 开始提取 + _startExtraction(isCancelled, currentFile, currentIndex, totalFiles, isCompleted, errorMessage, extractedCount); + return null; + }, const []); + + final progress = totalFiles.value > 0 ? currentIndex.value / totalFiles.value : 0.0; + + return ContentDialog( + title: Text(S.current.tools_unp4k_extract_dialog_title), + constraints: const BoxConstraints(maxWidth: 500, maxHeight: 300), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isCompleted.value && errorMessage.value == null) ...[ + // 进度条 + ProgressBar(value: progress * 100), + const SizedBox(height: 12), + // 进度文本 + Text( + S.current.tools_unp4k_extract_progress(currentIndex.value, totalFiles.value), + style: const TextStyle(fontSize: 14), + ), + const SizedBox(height: 8), + // 当前文件 + Text( + S.current.tools_unp4k_extract_current_file( + currentFile.value.length > 60 + ? "...${currentFile.value.substring(currentFile.value.length - 60)}" + : currentFile.value, + ), + style: TextStyle(fontSize: 12, color: Colors.white.withValues(alpha: .7)), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ] else if (errorMessage.value != null) ...[ + // 错误信息 + const Icon(FluentIcons.error_badge, size: 48, color: Color(0xFFE81123)), + const SizedBox(height: 12), + Text(errorMessage.value!, style: const TextStyle(fontSize: 14)), + ] else ...[ + // 完成 + const Icon(FluentIcons.completed_solid, size: 48, color: Color(0xFF107C10)), + const SizedBox(height: 12), + Text(S.current.tools_unp4k_extract_completed(extractedCount.value), style: const TextStyle(fontSize: 14)), + ], + ], + ), + actions: [ + if (!isCompleted.value && errorMessage.value == null) + Button( + onPressed: () { + isCancelled.value = true; + }, + child: Text(S.current.home_action_cancel), + ) + else + FilledButton(onPressed: () => Navigator.of(context).pop(), child: Text(S.current.action_close)), + ], + ); + } + + Future _startExtraction( + ValueNotifier isCancelled, + ValueNotifier currentFile, + ValueNotifier currentIndex, + ValueNotifier totalFiles, + ValueNotifier isCompleted, + ValueNotifier errorMessage, + ValueNotifier extractedCount, + ) async { + final result = await model.extractSelectedItemsWithProgress( + selectedItems, + outputDir, + onProgress: (current, total, file) { + currentIndex.value = current; + totalFiles.value = total; + currentFile.value = file; + }, + isCancelled: () => isCancelled.value, + ); + + final (success, count, error) = result; + extractedCount.value = count; + + if (!success && error != null) { + errorMessage.value = error; + } else { + isCompleted.value = true; + } + } +} + class _TextTempWidget extends HookConsumerWidget { final String filePath;