diff --git a/lib/common/rust/api/downloader_api.dart b/lib/common/rust/api/downloader_api.dart index 6443be9..064e137 100644 --- a/lib/common/rust/api/downloader_api.dart +++ b/lib/common/rust/api/downloader_api.dart @@ -86,6 +86,7 @@ Future downloaderResume({required BigInt taskId}) => RustLib.instance.api.crateApiDownloaderApiDownloaderResume(taskId: taskId); /// Remove a download task +/// Handles both active tasks (task_id < 10000) and cached completed tasks (task_id >= 10000) Future downloaderRemove({ required BigInt taskId, required bool deleteFiles, @@ -100,7 +101,7 @@ Future downloaderGetTaskInfo({required BigInt taskId}) => taskId: taskId, ); -/// Get all tasks +/// Get all tasks (includes both active and completed tasks from cache) Future> downloaderGetAllTasks() => RustLib.instance.api.crateApiDownloaderApiDownloaderGetAllTasks(); @@ -109,10 +110,17 @@ Future downloaderGetGlobalStats() => RustLib.instance.api.crateApiDownloaderApiDownloaderGetGlobalStats(); /// Check if a task with given name exists -Future downloaderIsNameInTask({required String name}) => RustLib - .instance - .api - .crateApiDownloaderApiDownloaderIsNameInTask(name: name); +/// +/// Parameters: +/// - name: Task name to search for +/// - downloading_only: If true, only search in active/waiting tasks. If false, include completed tasks (default: true) +Future downloaderIsNameInTask({ + required String name, + bool? downloadingOnly, +}) => RustLib.instance.api.crateApiDownloaderApiDownloaderIsNameInTask( + name: name, + downloadingOnly: downloadingOnly, +); /// Pause all tasks Future downloaderPauseAll() => diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index d6a8ec0..c7288eb 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -148,6 +148,7 @@ abstract class RustLibApi extends BaseApi { Future crateApiDownloaderApiDownloaderIsNameInTask({ required String name, + bool? downloadingOnly, }); Future crateApiDownloaderApiDownloaderPause({required BigInt taskId}); @@ -912,15 +913,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { @override Future crateApiDownloaderApiDownloaderIsNameInTask({ required String name, + bool? downloadingOnly, }) { return handler.executeNormal( NormalTask( callFfi: (port_) { var arg0 = cst_encode_String(name); + var arg1 = cst_encode_opt_box_autoadd_bool(downloadingOnly); return wire .wire__crate__api__downloader_api__downloader_is_name_in_task( port_, arg0, + arg1, ); }, codec: DcoCodec( @@ -928,7 +932,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { decodeErrorData: null, ), constMeta: kCrateApiDownloaderApiDownloaderIsNameInTaskConstMeta, - argValues: [name], + argValues: [name, downloadingOnly], apiImpl: this, ), ); @@ -937,7 +941,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiDownloaderApiDownloaderIsNameInTaskConstMeta => const TaskConstMeta( debugName: "downloader_is_name_in_task", - argNames: ["name"], + argNames: ["name", "downloadingOnly"], ); @override diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index 9780a83..4ef46d3 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -1494,10 +1494,12 @@ class RustLibWire implements BaseWire { void wire__crate__api__downloader_api__downloader_is_name_in_task( int port_, ffi.Pointer name, + ffi.Pointer downloading_only, ) { return _wire__crate__api__downloader_api__downloader_is_name_in_task( port_, name, + downloading_only, ); } @@ -1507,6 +1509,7 @@ class RustLibWire implements BaseWire { ffi.Void Function( ffi.Int64, ffi.Pointer, + ffi.Pointer, ) > >( @@ -1515,7 +1518,11 @@ class RustLibWire implements BaseWire { late final _wire__crate__api__downloader_api__downloader_is_name_in_task = _wire__crate__api__downloader_api__downloader_is_name_in_taskPtr .asFunction< - void Function(int, ffi.Pointer) + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ) >(); void wire__crate__api__downloader_api__downloader_pause( diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index 6636da3..5ed3c25 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -596,6 +596,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_cancel_download": MessageLookupByLibrary.simpleMessage( "Cancel Download", ), + "downloader_action_clear_completed": MessageLookupByLibrary.simpleMessage( + "Clear Completed", + ), "downloader_action_confirm_cancel_all_tasks": MessageLookupByLibrary.simpleMessage( "Confirm cancellation of all tasks?", @@ -616,6 +619,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_pause_download": MessageLookupByLibrary.simpleMessage( "Pause Download", ), + "downloader_action_remove_record": MessageLookupByLibrary.simpleMessage( + "Remove Record", + ), "downloader_action_restart_later": MessageLookupByLibrary.simpleMessage( "Apply Later", ), diff --git a/lib/generated/intl/messages_ja.dart b/lib/generated/intl/messages_ja.dart index d012caf..4dab681 100644 --- a/lib/generated/intl/messages_ja.dart +++ b/lib/generated/intl/messages_ja.dart @@ -539,6 +539,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_cancel_download": MessageLookupByLibrary.simpleMessage( "ダウンロードをキャンセル", ), + "downloader_action_clear_completed": MessageLookupByLibrary.simpleMessage( + "完了をクリア", + ), "downloader_action_confirm_cancel_all_tasks": MessageLookupByLibrary.simpleMessage("すべてのタスクをキャンセルしますか?"), "downloader_action_confirm_cancel_download": @@ -553,6 +556,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_pause_download": MessageLookupByLibrary.simpleMessage( "ダウンロードを一時停止", ), + "downloader_action_remove_record": MessageLookupByLibrary.simpleMessage( + "レコードを削除", + ), "downloader_action_restart_later": MessageLookupByLibrary.simpleMessage( "後で適用", ), diff --git a/lib/generated/intl/messages_ru.dart b/lib/generated/intl/messages_ru.dart index e993401..446b067 100644 --- a/lib/generated/intl/messages_ru.dart +++ b/lib/generated/intl/messages_ru.dart @@ -579,6 +579,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_cancel_download": MessageLookupByLibrary.simpleMessage( "Отменить загрузку", ), + "downloader_action_clear_completed": MessageLookupByLibrary.simpleMessage( + "Очистить завершённые", + ), "downloader_action_confirm_cancel_all_tasks": MessageLookupByLibrary.simpleMessage("Подтвердите отмену всех задач?"), "downloader_action_confirm_cancel_download": @@ -593,6 +596,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_pause_download": MessageLookupByLibrary.simpleMessage( "Приостановить загрузку", ), + "downloader_action_remove_record": MessageLookupByLibrary.simpleMessage( + "Удалить запись", + ), "downloader_action_restart_later": MessageLookupByLibrary.simpleMessage( "Применить позже", ), diff --git a/lib/generated/intl/messages_zh_CN.dart b/lib/generated/intl/messages_zh_CN.dart index b76ae50..b7901a5 100644 --- a/lib/generated/intl/messages_zh_CN.dart +++ b/lib/generated/intl/messages_zh_CN.dart @@ -528,6 +528,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_cancel_download": MessageLookupByLibrary.simpleMessage( "取消下载", ), + "downloader_action_clear_completed": MessageLookupByLibrary.simpleMessage( + "清除已完成", + ), "downloader_action_confirm_cancel_all_tasks": MessageLookupByLibrary.simpleMessage("确认取消全部任务?"), "downloader_action_confirm_cancel_download": @@ -540,6 +543,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_pause_download": MessageLookupByLibrary.simpleMessage( "暂停下载", ), + "downloader_action_remove_record": MessageLookupByLibrary.simpleMessage( + "移除记录", + ), "downloader_action_restart_later": MessageLookupByLibrary.simpleMessage( "下次启动时生效", ), diff --git a/lib/generated/intl/messages_zh_TW.dart b/lib/generated/intl/messages_zh_TW.dart index fb21e4f..7b35b41 100644 --- a/lib/generated/intl/messages_zh_TW.dart +++ b/lib/generated/intl/messages_zh_TW.dart @@ -512,6 +512,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_cancel_download": MessageLookupByLibrary.simpleMessage( "取消下載", ), + "downloader_action_clear_completed": MessageLookupByLibrary.simpleMessage( + "清除已完成", + ), "downloader_action_confirm_cancel_all_tasks": MessageLookupByLibrary.simpleMessage("確認取消全部任務?"), "downloader_action_confirm_cancel_download": @@ -524,6 +527,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloader_action_pause_download": MessageLookupByLibrary.simpleMessage( "暫停下載", ), + "downloader_action_remove_record": MessageLookupByLibrary.simpleMessage( + "移除記錄", + ), "downloader_action_restart_later": MessageLookupByLibrary.simpleMessage( "稍後套用", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index b653564..50f5136 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -28,10 +28,9 @@ 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; @@ -425,6 +424,26 @@ class S { ); } + /// `Clear Completed` + String get downloader_action_clear_completed { + return Intl.message( + 'Clear Completed', + name: 'downloader_action_clear_completed', + desc: '', + args: [], + ); + } + + /// `Remove Record` + String get downloader_action_remove_record { + return Intl.message( + 'Remove Record', + name: 'downloader_action_remove_record', + desc: '', + args: [], + ); + } + /// `No download tasks` String get downloader_info_no_download_tasks { return Intl.message( diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index db6a94c..009a0d9 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -77,6 +77,10 @@ "@downloader_action_resume_all": {}, "downloader_action_cancel_all": "Cancel All", "@downloader_action_cancel_all": {}, + "downloader_action_clear_completed": "Clear Completed", + "@downloader_action_clear_completed": {}, + "downloader_action_remove_record": "Remove Record", + "@downloader_action_remove_record": {}, "downloader_info_no_download_tasks": "No download tasks", "@downloader_info_no_download_tasks": {}, "downloader_info_total_size": "Total Size: {v1}", diff --git a/lib/l10n/intl_ja.arb b/lib/l10n/intl_ja.arb index 5c24ad2..205f0a6 100644 --- a/lib/l10n/intl_ja.arb +++ b/lib/l10n/intl_ja.arb @@ -77,6 +77,10 @@ "@downloader_action_resume_all": {}, "downloader_action_cancel_all": "すべてキャンセル", "@downloader_action_cancel_all": {}, + "downloader_action_clear_completed": "完了をクリア", + "@downloader_action_clear_completed": {}, + "downloader_action_remove_record": "レコードを削除", + "@downloader_action_remove_record": {}, "downloader_info_no_download_tasks": "ダウンロードタスクはありません", "@downloader_info_no_download_tasks": {}, "downloader_info_total_size": "合計サイズ:{v1}", diff --git a/lib/l10n/intl_ru.arb b/lib/l10n/intl_ru.arb index 06fe990..8de16f9 100644 --- a/lib/l10n/intl_ru.arb +++ b/lib/l10n/intl_ru.arb @@ -77,6 +77,10 @@ "@downloader_action_resume_all": {}, "downloader_action_cancel_all": "Отменить все", "@downloader_action_cancel_all": {}, + "downloader_action_clear_completed": "Очистить завершённые", + "@downloader_action_clear_completed": {}, + "downloader_action_remove_record": "Удалить запись", + "@downloader_action_remove_record": {}, "downloader_info_no_download_tasks": "Нет задач загрузки", "@downloader_info_no_download_tasks": {}, "downloader_info_total_size": "Общий размер: {v1}", diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index da96552..a6c60d8 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -76,6 +76,10 @@ "@downloader_action_resume_all": {}, "downloader_action_cancel_all": "全部取消", "@downloader_action_cancel_all": {}, + "downloader_action_clear_completed": "清除已完成", + "@downloader_action_clear_completed": {}, + "downloader_action_remove_record": "移除记录", + "@downloader_action_remove_record": {}, "downloader_info_no_download_tasks": "无下载任务", "@downloader_info_no_download_tasks": {}, "downloader_info_total_size": "总大小:{v1}", diff --git a/lib/l10n/intl_zh_TW.arb b/lib/l10n/intl_zh_TW.arb index 795d57f..35c6175 100644 --- a/lib/l10n/intl_zh_TW.arb +++ b/lib/l10n/intl_zh_TW.arb @@ -77,6 +77,10 @@ "@downloader_action_resume_all": {}, "downloader_action_cancel_all": "全部取消", "@downloader_action_cancel_all": {}, + "downloader_action_clear_completed": "清除已完成", + "@downloader_action_clear_completed": {}, + "downloader_action_remove_record": "移除記錄", + "@downloader_action_remove_record": {}, "downloader_info_no_download_tasks": "無下載任務", "@downloader_info_no_download_tasks": {}, "downloader_info_total_size": "總大小:{v1}", diff --git a/lib/provider/download_manager.dart b/lib/provider/download_manager.dart index 6d2f2c4..944ffd5 100644 --- a/lib/provider/download_manager.dart +++ b/lib/provider/download_manager.dart @@ -178,11 +178,11 @@ class DownloadManager extends _$DownloadManager { return await downloader_api.downloaderGetAllTasks(); } - Future isNameInTask(String name) async { + Future isNameInTask(String name, {bool downloadingOnly = true}) async { if (!state.isInitialized) { return false; } - return await downloader_api.downloaderIsNameInTask(name: name); + return await downloader_api.downloaderIsNameInTask(name: name, downloadingOnly: downloadingOnly); } Future pauseAll() async { diff --git a/lib/provider/download_manager.g.dart b/lib/provider/download_manager.g.dart index 46bde05..3a385d1 100644 --- a/lib/provider/download_manager.g.dart +++ b/lib/provider/download_manager.g.dart @@ -41,7 +41,7 @@ final class DownloadManagerProvider } } -String _$downloadManagerHash() => r'55c92224a5eb6bb0f84f0a97fd0585b94f61f711'; +String _$downloadManagerHash() => r'feed17eda191d6b618b30e01afb75b7245fe0a83'; abstract class _$DownloadManager extends $Notifier { DownloadManagerState build(); diff --git a/lib/ui/home/downloader/home_downloader_ui.dart b/lib/ui/home/downloader/home_downloader_ui.dart index eb74f33..7e3b0b9 100644 --- a/lib/ui/home/downloader/home_downloader_ui.dart +++ b/lib/ui/home/downloader/home_downloader_ui.dart @@ -33,6 +33,8 @@ class HomeDownloaderUI extends HookConsumerWidget { const MapEntry("resume_all", FluentIcons.download): S.current.downloader_action_resume_all, if (state.activeTasks.isNotEmpty || state.waitingTasks.isNotEmpty) const MapEntry("cancel_all", FluentIcons.cancel): S.current.downloader_action_cancel_all, + if (state.completedTasks.isNotEmpty || state.errorTasks.isNotEmpty) + const MapEntry("clear_completed", FluentIcons.clear): S.current.downloader_action_clear_completed, }.entries) Padding( padding: const EdgeInsets.only(left: 6, right: 6), @@ -151,7 +153,7 @@ class HomeDownloaderUI extends HookConsumerWidget { ], ), const SizedBox(width: 32), - if (type != "stopped") + if (type != "completed" && type != "error") DropDownButton( closeAfterClick: true, title: Padding( @@ -183,6 +185,26 @@ class HomeDownloaderUI extends HookConsumerWidget { onPressed: () => model.openFolder(task), ), ], + ) + else + DropDownButton( + closeAfterClick: true, + title: Padding( + padding: const EdgeInsets.all(3), + child: Text(S.current.downloader_action_options), + ), + items: [ + MenuFlyoutItem( + leading: const Icon(FluentIcons.chrome_close, size: 14), + text: Text(S.current.downloader_action_remove_record), + onPressed: () => model.removeTask(task.id.toInt()), + ), + MenuFlyoutItem( + leading: const Icon(FluentIcons.folder_open, size: 14), + text: Text(S.current.action_open_folder), + onPressed: () => model.openFolder(task), + ), + ], ), const SizedBox(width: 12), ], diff --git a/lib/ui/home/downloader/home_downloader_ui_model.dart b/lib/ui/home/downloader/home_downloader_ui_model.dart index 4067246..6a6537d 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.dart @@ -22,7 +22,8 @@ abstract class HomeDownloaderUIState with _$HomeDownloaderUIState { factory HomeDownloaderUIState({ @Default([]) List activeTasks, @Default([]) List waitingTasks, - @Default([]) List stoppedTasks, + @Default([]) List completedTasks, + @Default([]) List errorTasks, DownloadGlobalStat? globalStat, }) = _HomeDownloaderUIState; } @@ -48,7 +49,8 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { final listHeaderStatusMap = { "active": S.current.downloader_title_downloading, "waiting": S.current.downloader_info_waiting, - "stopped": S.current.downloader_title_ended, + "completed": S.current.downloader_title_ended, + "error": S.current.downloader_info_download_failed, }; @override @@ -92,6 +94,17 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } } return; + case "clear_completed": + if (!downloadManagerState.isRunning) return; + try { + final allTasks = [...state.completedTasks, ...state.errorTasks]; + for (var task in allTasks) { + await downloadManager.removeTask(task.id.toInt(), deleteFiles: false); + } + } catch (e) { + dPrint("DownloadsUIModel clear_completed Error: $e"); + } + return; case "settings": _showDownloadSpeedSettings(context); return; @@ -99,19 +112,33 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } int getTasksLen() { - return state.activeTasks.length + state.waitingTasks.length + state.stoppedTasks.length; + return state.activeTasks.length + state.waitingTasks.length + state.completedTasks.length + state.errorTasks.length; } (DownloadTaskInfo, String, bool) getTaskAndType(int index) { - final tempList = [...state.activeTasks, ...state.waitingTasks, ...state.stoppedTasks]; + final tempList = [ + ...state.activeTasks, + ...state.waitingTasks, + ...state.completedTasks, + ...state.errorTasks, + ]; if (index >= 0 && index < state.activeTasks.length) { return (tempList[index], "active", index == 0); } if (index >= state.activeTasks.length && index < state.activeTasks.length + state.waitingTasks.length) { return (tempList[index], "waiting", index == state.activeTasks.length); } - if (index >= state.activeTasks.length + state.waitingTasks.length && index < tempList.length) { - return (tempList[index], "stopped", index == state.activeTasks.length + state.waitingTasks.length); + if (index >= state.activeTasks.length + state.waitingTasks.length && + index < state.activeTasks.length + state.waitingTasks.length + state.completedTasks.length) { + return (tempList[index], "completed", index == state.activeTasks.length + state.waitingTasks.length); + } + if (index >= state.activeTasks.length + state.waitingTasks.length + state.completedTasks.length && + index < tempList.length) { + return ( + tempList[index], + "error", + index == state.activeTasks.length + state.waitingTasks.length + state.completedTasks.length, + ); } throw Exception("Index out of range or element is null"); } @@ -172,6 +199,11 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } } + Future removeTask(int taskId) async { + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.removeTask(taskId, deleteFiles: false); + } + void openFolder(DownloadTaskInfo task) { final outputFolder = task.outputFolder; if (outputFolder.isNotEmpty) { @@ -190,7 +222,8 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { final activeTasks = []; final waitingTasks = []; - final stoppedTasks = []; + final completedTasks = []; + final errorTasks = []; for (var task in allTasks) { switch (task.status) { @@ -202,8 +235,10 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { waitingTasks.add(task); break; case DownloadTaskStatus.finished: + completedTasks.add(task); + break; case DownloadTaskStatus.error: - stoppedTasks.add(task); + errorTasks.add(task); break; } } @@ -211,11 +246,18 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { state = state.copyWith( activeTasks: activeTasks, waitingTasks: waitingTasks, - stoppedTasks: stoppedTasks, + completedTasks: completedTasks, + errorTasks: errorTasks, globalStat: downloadManagerState.globalStat, ); } else { - state = state.copyWith(activeTasks: [], waitingTasks: [], stoppedTasks: [], globalStat: null); + state = state.copyWith( + activeTasks: [], + waitingTasks: [], + completedTasks: [], + errorTasks: [], + globalStat: null, + ); } await Future.delayed(const Duration(seconds: 1)); } diff --git a/lib/ui/home/downloader/home_downloader_ui_model.freezed.dart b/lib/ui/home/downloader/home_downloader_ui_model.freezed.dart index 57a3db1..dd1a2fd 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.freezed.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.freezed.dart @@ -14,7 +14,7 @@ T _$identity(T value) => value; /// @nodoc mixin _$HomeDownloaderUIState { - List get activeTasks; List get waitingTasks; List get stoppedTasks; DownloadGlobalStat? get globalStat; + List get activeTasks; List get waitingTasks; List get completedTasks; List get errorTasks; DownloadGlobalStat? get globalStat; /// Create a copy of HomeDownloaderUIState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -25,16 +25,16 @@ $HomeDownloaderUIStateCopyWith get copyWith => _$HomeDown @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeDownloaderUIState&&const DeepCollectionEquality().equals(other.activeTasks, activeTasks)&&const DeepCollectionEquality().equals(other.waitingTasks, waitingTasks)&&const DeepCollectionEquality().equals(other.stoppedTasks, stoppedTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is HomeDownloaderUIState&&const DeepCollectionEquality().equals(other.activeTasks, activeTasks)&&const DeepCollectionEquality().equals(other.waitingTasks, waitingTasks)&&const DeepCollectionEquality().equals(other.completedTasks, completedTasks)&&const DeepCollectionEquality().equals(other.errorTasks, errorTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(activeTasks),const DeepCollectionEquality().hash(waitingTasks),const DeepCollectionEquality().hash(stoppedTasks),globalStat); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(activeTasks),const DeepCollectionEquality().hash(waitingTasks),const DeepCollectionEquality().hash(completedTasks),const DeepCollectionEquality().hash(errorTasks),globalStat); @override String toString() { - return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; + return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, completedTasks: $completedTasks, errorTasks: $errorTasks, globalStat: $globalStat)'; } @@ -45,7 +45,7 @@ abstract mixin class $HomeDownloaderUIStateCopyWith<$Res> { factory $HomeDownloaderUIStateCopyWith(HomeDownloaderUIState value, $Res Function(HomeDownloaderUIState) _then) = _$HomeDownloaderUIStateCopyWithImpl; @useResult $Res call({ - List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat + List activeTasks, List waitingTasks, List completedTasks, List errorTasks, DownloadGlobalStat? globalStat }); @@ -62,11 +62,12 @@ class _$HomeDownloaderUIStateCopyWithImpl<$Res> /// Create a copy of HomeDownloaderUIState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? completedTasks = null,Object? errorTasks = null,Object? globalStat = freezed,}) { return _then(_self.copyWith( activeTasks: null == activeTasks ? _self.activeTasks : activeTasks // ignore: cast_nullable_to_non_nullable as List,waitingTasks: null == waitingTasks ? _self.waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable -as List,stoppedTasks: null == stoppedTasks ? _self.stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable +as List,completedTasks: null == completedTasks ? _self.completedTasks : completedTasks // ignore: cast_nullable_to_non_nullable +as List,errorTasks: null == errorTasks ? _self.errorTasks : errorTasks // ignore: cast_nullable_to_non_nullable as List,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable as DownloadGlobalStat?, )); @@ -153,10 +154,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List activeTasks, List waitingTasks, List completedTasks, List errorTasks, DownloadGlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _HomeDownloaderUIState() when $default != null: -return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.completedTasks,_that.errorTasks,_that.globalStat);case _: return orElse(); } @@ -174,10 +175,10 @@ return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.gl /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List activeTasks, List waitingTasks, List completedTasks, List errorTasks, DownloadGlobalStat? globalStat) $default,) {final _that = this; switch (_that) { case _HomeDownloaderUIState(): -return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.completedTasks,_that.errorTasks,_that.globalStat);case _: throw StateError('Unexpected subclass'); } @@ -194,10 +195,10 @@ return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.gl /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List activeTasks, List waitingTasks, List completedTasks, List errorTasks, DownloadGlobalStat? globalStat)? $default,) {final _that = this; switch (_that) { case _HomeDownloaderUIState() when $default != null: -return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.completedTasks,_that.errorTasks,_that.globalStat);case _: return null; } @@ -209,7 +210,7 @@ return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.gl class _HomeDownloaderUIState implements HomeDownloaderUIState { - _HomeDownloaderUIState({final List activeTasks = const [], final List waitingTasks = const [], final List stoppedTasks = const [], this.globalStat}): _activeTasks = activeTasks,_waitingTasks = waitingTasks,_stoppedTasks = stoppedTasks; + _HomeDownloaderUIState({final List activeTasks = const [], final List waitingTasks = const [], final List completedTasks = const [], final List errorTasks = const [], this.globalStat}): _activeTasks = activeTasks,_waitingTasks = waitingTasks,_completedTasks = completedTasks,_errorTasks = errorTasks; final List _activeTasks; @@ -226,11 +227,18 @@ class _HomeDownloaderUIState implements HomeDownloaderUIState { return EqualUnmodifiableListView(_waitingTasks); } - final List _stoppedTasks; -@override@JsonKey() List get stoppedTasks { - if (_stoppedTasks is EqualUnmodifiableListView) return _stoppedTasks; + final List _completedTasks; +@override@JsonKey() List get completedTasks { + if (_completedTasks is EqualUnmodifiableListView) return _completedTasks; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_stoppedTasks); + return EqualUnmodifiableListView(_completedTasks); +} + + final List _errorTasks; +@override@JsonKey() List get errorTasks { + if (_errorTasks is EqualUnmodifiableListView) return _errorTasks; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_errorTasks); } @override final DownloadGlobalStat? globalStat; @@ -245,16 +253,16 @@ _$HomeDownloaderUIStateCopyWith<_HomeDownloaderUIState> get copyWith => __$HomeD @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeDownloaderUIState&&const DeepCollectionEquality().equals(other._activeTasks, _activeTasks)&&const DeepCollectionEquality().equals(other._waitingTasks, _waitingTasks)&&const DeepCollectionEquality().equals(other._stoppedTasks, _stoppedTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _HomeDownloaderUIState&&const DeepCollectionEquality().equals(other._activeTasks, _activeTasks)&&const DeepCollectionEquality().equals(other._waitingTasks, _waitingTasks)&&const DeepCollectionEquality().equals(other._completedTasks, _completedTasks)&&const DeepCollectionEquality().equals(other._errorTasks, _errorTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_activeTasks),const DeepCollectionEquality().hash(_waitingTasks),const DeepCollectionEquality().hash(_stoppedTasks),globalStat); +int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_activeTasks),const DeepCollectionEquality().hash(_waitingTasks),const DeepCollectionEquality().hash(_completedTasks),const DeepCollectionEquality().hash(_errorTasks),globalStat); @override String toString() { - return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; + return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, completedTasks: $completedTasks, errorTasks: $errorTasks, globalStat: $globalStat)'; } @@ -265,7 +273,7 @@ abstract mixin class _$HomeDownloaderUIStateCopyWith<$Res> implements $HomeDownl factory _$HomeDownloaderUIStateCopyWith(_HomeDownloaderUIState value, $Res Function(_HomeDownloaderUIState) _then) = __$HomeDownloaderUIStateCopyWithImpl; @override @useResult $Res call({ - List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat + List activeTasks, List waitingTasks, List completedTasks, List errorTasks, DownloadGlobalStat? globalStat }); @@ -282,11 +290,12 @@ class __$HomeDownloaderUIStateCopyWithImpl<$Res> /// Create a copy of HomeDownloaderUIState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? completedTasks = null,Object? errorTasks = null,Object? globalStat = freezed,}) { return _then(_HomeDownloaderUIState( activeTasks: null == activeTasks ? _self._activeTasks : activeTasks // ignore: cast_nullable_to_non_nullable as List,waitingTasks: null == waitingTasks ? _self._waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable -as List,stoppedTasks: null == stoppedTasks ? _self._stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable +as List,completedTasks: null == completedTasks ? _self._completedTasks : completedTasks // ignore: cast_nullable_to_non_nullable +as List,errorTasks: null == errorTasks ? _self._errorTasks : errorTasks // ignore: cast_nullable_to_non_nullable as List,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable as DownloadGlobalStat?, )); diff --git a/lib/ui/home/downloader/home_downloader_ui_model.g.dart b/lib/ui/home/downloader/home_downloader_ui_model.g.dart index a2205b7..995ad33 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.g.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.g.dart @@ -42,7 +42,7 @@ final class HomeDownloaderUIModelProvider } String _$homeDownloaderUIModelHash() => - r'bf7d095d761fff078de707562cf311c20db664d9'; + r'b230746a782b511dd58b0b46def7845c01412762'; abstract class _$HomeDownloaderUIModel extends $Notifier { diff --git a/lib/ui/home/input_method/input_method_dialog_ui_model.dart b/lib/ui/home/input_method/input_method_dialog_ui_model.dart index eef6555..5e70732 100644 --- a/lib/ui/home/input_method/input_method_dialog_ui_model.dart +++ b/lib/ui/home/input_method/input_method_dialog_ui_model.dart @@ -257,7 +257,7 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { } // get torrent Data final data = await RSHttp.get(torrentUrl!); - final taskId = await downloadManager.addTorrent(data.data!, outputFolder: _localTranslateModelDir); + final taskId = await downloadManager.addTorrent(data.data!, outputFolder: "$_localTranslateModelDir/$_localTranslateModelName"); return taskId.toString(); } catch (e) { dPrint("[InputMethodDialogUIModel] doDownloadTranslateModel error: $e"); diff --git a/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart b/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart index a607b04..a9ee6db 100644 --- a/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart +++ b/lib/ui/home/input_method/input_method_dialog_ui_model.g.dart @@ -43,7 +43,7 @@ final class InputMethodDialogUIModelProvider } String _$inputMethodDialogUIModelHash() => - r'5c2989faf94d43bb814e5b80e10d68416c8241ec'; + r'77bf2b02a7b7ea66e9a06be068b791b3a4295a44'; abstract class _$InputMethodDialogUIModel extends $Notifier { diff --git a/rust/src/api/downloader_api.rs b/rust/src/api/downloader_api.rs index 454771e..3ff619f 100644 --- a/rust/src/api/downloader_api.rs +++ b/rust/src/api/downloader_api.rs @@ -27,6 +27,7 @@ static TORRENT_HANDLES: once_cell::sync::Lazy= 10000 and are assigned sequentially (10000 + cache_index) static COMPLETED_TASKS_CACHE: once_cell::sync::Lazy>> = once_cell::sync::Lazy::new(|| RwLock::new(Vec::new())); @@ -369,7 +370,20 @@ pub async fn downloader_resume(task_id: usize) -> Result<()> { } /// Remove a download task +/// Handles both active tasks (task_id < 10000) and cached completed tasks (task_id >= 10000) pub async fn downloader_remove(task_id: usize, delete_files: bool) -> Result<()> { + // Check if this is a cached completed task (ID >= 10000) + if task_id >= 10000 { + // Remove from completed tasks cache by index (10000 + index) + let cache_index = task_id - 10000; + let mut cache = COMPLETED_TASKS_CACHE.write(); + if cache_index < cache.len() { + cache.remove(cache_index); + } + return Ok(()); + } + + // Otherwise, it's an active task let session = get_session()?; session @@ -454,12 +468,15 @@ fn get_task_status(stats: &TorrentStats) -> DownloadTaskStatus { } } -/// Get all tasks +/// Get all tasks (includes both active and completed tasks from cache) pub async fn downloader_get_all_tasks() -> Result> { let session_guard = SESSION.read(); let session = match session_guard.as_ref() { Some(s) => s.clone(), - None => return Ok(vec![]), + None => { + // If session is not initialized, return only cached completed tasks + return Ok(COMPLETED_TASKS_CACHE.read().clone()); + } }; drop(session_guard); @@ -516,7 +533,18 @@ pub async fn downloader_get_all_tasks() -> Result> { } }); - Ok(tasks.into_inner()) + // Merge cached completed tasks with IDs based on cache index (10000 + index) + let mut result = tasks.into_inner(); + let completed_tasks_cache = COMPLETED_TASKS_CACHE.read(); + + for (cache_index, task) in completed_tasks_cache.iter().enumerate() { + let mut task_with_id = task.clone(); + // Assign ID based on cache index: 10000, 10001, 10002, etc. + task_with_id.id = 10000 + cache_index; + result.push(task_with_id); + } + + Ok(result) } /// Get global statistics @@ -540,9 +568,20 @@ pub async fn downloader_get_global_stats() -> Result { } /// Check if a task with given name exists -pub async fn downloader_is_name_in_task(name: String) -> bool { +/// +/// Parameters: +/// - name: Task name to search for +/// - downloading_only: If true, only search in active/waiting tasks. If false, include completed tasks (default: true) +pub async fn downloader_is_name_in_task(name: String, downloading_only: Option) -> bool { + let downloading_only = downloading_only.unwrap_or(true); + if let Ok(tasks) = downloader_get_all_tasks().await { for task in tasks { + // If downloading_only is true, skip finished and error tasks + if downloading_only && (task.status == DownloadTaskStatus::Finished || task.status == DownloadTaskStatus::Error) { + continue; + } + if task.name.contains(&name) { return true; } @@ -640,16 +679,17 @@ pub async fn downloader_remove_completed_tasks() -> Result { for task in tasks { if task.status == DownloadTaskStatus::Finished { - // Check if handle exists (drop lock before await) - let has_handle = TORRENT_HANDLES.read().contains_key(&task.id); - if has_handle { - // Use TorrentIdOrHash::Id for deletion (TorrentId is just usize) - if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() { - // Save task info to cache before removing - COMPLETED_TASKS_CACHE.write().push(task.clone()); - - TORRENT_HANDLES.write().remove(&task.id); - removed_count += 1; + // Only process active tasks (id < 10000) + if task.id < 10000 { + let has_handle = TORRENT_HANDLES.read().contains_key(&task.id); + if has_handle { + // Use TorrentIdOrHash::Id for deletion + if session.delete(TorrentIdOrHash::Id(task.id), false).await.is_ok() { + // Cache the task - it will get ID based on cache length + COMPLETED_TASKS_CACHE.write().push(task.clone()); + TORRENT_HANDLES.write().remove(&task.id); + removed_count += 1; + } } } } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index a8cbb18..83b0fd2 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -514,6 +514,7 @@ fn wire__crate__api__downloader_api__downloader_is_initialized_impl( fn wire__crate__api__downloader_api__downloader_is_name_in_task_impl( port_: flutter_rust_bridge::for_generated::MessagePort, name: impl CstDecode, + downloading_only: impl CstDecode>, ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { @@ -523,11 +524,16 @@ fn wire__crate__api__downloader_api__downloader_is_name_in_task_impl( }, move || { let api_name = name.cst_decode(); + let api_downloading_only = downloading_only.cst_decode(); move |context| async move { transform_result_dco::<_, _, ()>( (move || async move { let output_ok = Result::<_, ()>::Ok( - crate::api::downloader_api::downloader_is_name_in_task(api_name).await, + crate::api::downloader_api::downloader_is_name_in_task( + api_name, + api_downloading_only, + ) + .await, )?; Ok(output_ok) })() @@ -4200,8 +4206,13 @@ mod io { pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_is_name_in_task( port_: i64, name: *mut wire_cst_list_prim_u_8_strict, + downloading_only: *mut bool, ) { - wire__crate__api__downloader_api__downloader_is_name_in_task_impl(port_, name) + wire__crate__api__downloader_api__downloader_is_name_in_task_impl( + port_, + name, + downloading_only, + ) } #[unsafe(no_mangle)]