diff --git a/lib/app.g.dart b/lib/app.g.dart index 048e97c..858fd00 100644 --- a/lib/app.g.dart +++ b/lib/app.g.dart @@ -82,7 +82,7 @@ final class AppGlobalModelProvider } } -String _$appGlobalModelHash() => r'9729c3ffb891e5899abbb3dc7d2d25ef13a442e7'; +String _$appGlobalModelHash() => r'0e46d72594d94e2beb4d2ccb8616eb37facba288'; abstract class _$AppGlobalModel extends $Notifier { AppGlobalState build(); diff --git a/lib/common/conf/binary_conf.dart b/lib/common/conf/binary_conf.dart index 39246ac..75bb8e3 100644 --- a/lib/common/conf/binary_conf.dart +++ b/lib/common/conf/binary_conf.dart @@ -6,7 +6,8 @@ import 'package:flutter/services.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; class BinaryModuleConf { - static const _modules = {"aria2c": "0"}; + // aria2c has been replaced by rqbit (Rust-based torrent library) + static const _modules = {}; static Future extractModule(List modules, String workingDir) async { for (var m in _modules.entries) { diff --git a/lib/common/rust/api/downloader_api.dart b/lib/common/rust/api/downloader_api.dart new file mode 100644 index 0000000..b7c0d65 --- /dev/null +++ b/lib/common/rust/api/downloader_api.dart @@ -0,0 +1,198 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.11.1. + +// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import + +import '../frb_generated.dart'; +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; + +// These functions are ignored because they are not marked as `pub`: `get_task_status` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt` + +/// Initialize the download manager session +void downloaderInit({required String downloadDir}) => RustLib.instance.api + .crateApiDownloaderApiDownloaderInit(downloadDir: downloadDir); + +/// Check if the downloader is initialized +bool downloaderIsInitialized() => + RustLib.instance.api.crateApiDownloaderApiDownloaderIsInitialized(); + +/// Add a torrent from bytes (e.g., .torrent file content) +Future downloaderAddTorrent({ + required List torrentBytes, + String? outputFolder, + List? trackers, +}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddTorrent( + torrentBytes: torrentBytes, + outputFolder: outputFolder, + trackers: trackers, +); + +/// Add a torrent from a magnet link +Future downloaderAddMagnet({ + required String magnetLink, + String? outputFolder, + List? trackers, +}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddMagnet( + magnetLink: magnetLink, + outputFolder: outputFolder, + trackers: trackers, +); + +/// Add a torrent from URL (HTTP download not supported, only torrent file URLs) +Future downloaderAddUrl({ + required String url, + String? outputFolder, + List? trackers, +}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddUrl( + url: url, + outputFolder: outputFolder, + trackers: trackers, +); + +/// Pause a download task +Future downloaderPause({required BigInt taskId}) => + RustLib.instance.api.crateApiDownloaderApiDownloaderPause(taskId: taskId); + +/// Resume a download task +Future downloaderResume({required BigInt taskId}) => + RustLib.instance.api.crateApiDownloaderApiDownloaderResume(taskId: taskId); + +/// Remove a download task +Future downloaderRemove({ + required BigInt taskId, + required bool deleteFiles, +}) => RustLib.instance.api.crateApiDownloaderApiDownloaderRemove( + taskId: taskId, + deleteFiles: deleteFiles, +); + +/// Get information about a specific task +Future downloaderGetTaskInfo({required BigInt taskId}) => + RustLib.instance.api.crateApiDownloaderApiDownloaderGetTaskInfo( + taskId: taskId, + ); + +/// Get all tasks +Future> downloaderGetAllTasks() => + RustLib.instance.api.crateApiDownloaderApiDownloaderGetAllTasks(); + +/// Get global statistics +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); + +/// Pause all tasks +Future downloaderPauseAll() => + RustLib.instance.api.crateApiDownloaderApiDownloaderPauseAll(); + +/// Resume all tasks +Future downloaderResumeAll() => + RustLib.instance.api.crateApiDownloaderApiDownloaderResumeAll(); + +/// Stop the downloader session +Future downloaderStop() => + RustLib.instance.api.crateApiDownloaderApiDownloaderStop(); + +/// Global statistics +class DownloadGlobalStat { + final BigInt downloadSpeed; + final BigInt uploadSpeed; + final BigInt numActive; + final BigInt numWaiting; + + const DownloadGlobalStat({ + required this.downloadSpeed, + required this.uploadSpeed, + required this.numActive, + required this.numWaiting, + }); + + static Future default_() => + RustLib.instance.api.crateApiDownloaderApiDownloadGlobalStatDefault(); + + @override + int get hashCode => + downloadSpeed.hashCode ^ + uploadSpeed.hashCode ^ + numActive.hashCode ^ + numWaiting.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DownloadGlobalStat && + runtimeType == other.runtimeType && + downloadSpeed == other.downloadSpeed && + uploadSpeed == other.uploadSpeed && + numActive == other.numActive && + numWaiting == other.numWaiting; +} + +/// Download task information +class DownloadTaskInfo { + final BigInt id; + final String name; + final DownloadTaskStatus status; + final BigInt totalBytes; + final BigInt downloadedBytes; + final BigInt uploadedBytes; + final BigInt downloadSpeed; + final BigInt uploadSpeed; + final double progress; + final BigInt numPeers; + final String outputFolder; + + const DownloadTaskInfo({ + required this.id, + required this.name, + required this.status, + required this.totalBytes, + required this.downloadedBytes, + required this.uploadedBytes, + required this.downloadSpeed, + required this.uploadSpeed, + required this.progress, + required this.numPeers, + required this.outputFolder, + }); + + @override + int get hashCode => + id.hashCode ^ + name.hashCode ^ + status.hashCode ^ + totalBytes.hashCode ^ + downloadedBytes.hashCode ^ + uploadedBytes.hashCode ^ + downloadSpeed.hashCode ^ + uploadSpeed.hashCode ^ + progress.hashCode ^ + numPeers.hashCode ^ + outputFolder.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DownloadTaskInfo && + runtimeType == other.runtimeType && + id == other.id && + name == other.name && + status == other.status && + totalBytes == other.totalBytes && + downloadedBytes == other.downloadedBytes && + uploadedBytes == other.uploadedBytes && + downloadSpeed == other.downloadSpeed && + uploadSpeed == other.uploadSpeed && + progress == other.progress && + numPeers == other.numPeers && + outputFolder == other.outputFolder; +} + +/// Download task status +enum DownloadTaskStatus { initializing, live, paused, error, finished } diff --git a/lib/common/rust/api/win32_api.dart b/lib/common/rust/api/win32_api.dart index cda3105..d0c7f96 100644 --- a/lib/common/rust/api/win32_api.dart +++ b/lib/common/rust/api/win32_api.dart @@ -6,7 +6,6 @@ import '../frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `get_process_path` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `fmt`, `fmt` Future sendNotify({ @@ -21,27 +20,21 @@ Future sendNotify({ appId: appId, ); -/// Get system memory size in GB Future getSystemMemorySizeGb() => RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb(); -/// Get number of logical processors Future getNumberOfLogicalProcessors() => RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors(); -/// Get all system information at once Future getSystemInfo() => RustLib.instance.api.crateApiWin32ApiGetSystemInfo(); -/// Get GPU info from registry (more accurate VRAM) Future getGpuInfoFromRegistry() => RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry(); -/// Resolve shortcut (.lnk) file to get target path Future resolveShortcut({required String lnkPath}) => RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath); -/// Open file explorer and select file/folder Future openDirWithExplorer({ required String path, required bool isFile, @@ -65,19 +58,16 @@ Future> getProcessListByName({required String processName}) => processName: processName, ); -/// Kill processes by name Future killProcessByName({required String processName}) => RustLib .instance .api .crateApiWin32ApiKillProcessByName(processName: processName); -/// Get disk physical sector size for performance Future getDiskPhysicalSectorSize({required String driveLetter}) => RustLib .instance .api .crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter); -/// Create a desktop shortcut Future createDesktopShortcut({ required String targetPath, required String shortcutName, @@ -86,14 +76,12 @@ Future createDesktopShortcut({ shortcutName: shortcutName, ); -/// Run a program with admin privileges (UAC) Future runAsAdmin({required String program, required String args}) => RustLib.instance.api.crateApiWin32ApiRunAsAdmin( program: program, args: args, ); -/// Start a program (without waiting) Future startProcess({ required String program, required List args, @@ -102,15 +90,12 @@ Future startProcess({ args: args, ); -/// Check if NVME patch is applied Future checkNvmePatchStatus() => RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus(); -/// Add NVME patch to registry Future addNvmePatch() => RustLib.instance.api.crateApiWin32ApiAddNvmePatch(); -/// Remove NVME patch from registry Future removeNvmePatch() => RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch(); diff --git a/lib/common/rust/frb_generated.dart b/lib/common/rust/frb_generated.dart index 2a1d56c..8dcddb1 100644 --- a/lib/common/rust/frb_generated.dart +++ b/lib/common/rust/frb_generated.dart @@ -4,6 +4,7 @@ // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field import 'api/asar_api.dart'; +import 'api/downloader_api.dart'; import 'api/http_api.dart'; import 'api/ort_api.dart'; import 'api/rs_process.dart'; @@ -71,7 +72,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.11.1'; @override - int get rustContentHash => 1161621087; + int get rustContentHash => -1465039096; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -97,6 +98,57 @@ abstract class RustLibApi extends BaseApi { Future> crateApiHttpApiDnsLookupTxt({required String host}); + Future crateApiDownloaderApiDownloadGlobalStatDefault(); + + Future crateApiDownloaderApiDownloaderAddMagnet({ + required String magnetLink, + String? outputFolder, + List? trackers, + }); + + Future crateApiDownloaderApiDownloaderAddTorrent({ + required List torrentBytes, + String? outputFolder, + List? trackers, + }); + + Future crateApiDownloaderApiDownloaderAddUrl({ + required String url, + String? outputFolder, + List? trackers, + }); + + Future> crateApiDownloaderApiDownloaderGetAllTasks(); + + Future crateApiDownloaderApiDownloaderGetGlobalStats(); + + Future crateApiDownloaderApiDownloaderGetTaskInfo({ + required BigInt taskId, + }); + + void crateApiDownloaderApiDownloaderInit({required String downloadDir}); + + bool crateApiDownloaderApiDownloaderIsInitialized(); + + Future crateApiDownloaderApiDownloaderIsNameInTask({ + required String name, + }); + + Future crateApiDownloaderApiDownloaderPause({required BigInt taskId}); + + Future crateApiDownloaderApiDownloaderPauseAll(); + + Future crateApiDownloaderApiDownloaderRemove({ + required BigInt taskId, + required bool deleteFiles, + }); + + Future crateApiDownloaderApiDownloaderResume({required BigInt taskId}); + + Future crateApiDownloaderApiDownloaderResumeAll(); + + Future crateApiDownloaderApiDownloaderStop(); + Future crateApiHttpApiFetch({ required MyMethod method, required String url, @@ -430,6 +482,451 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { TaskConstMeta get kCrateApiHttpApiDnsLookupTxtConstMeta => const TaskConstMeta(debugName: "dns_lookup_txt", argNames: ["host"]); + @override + Future crateApiDownloaderApiDownloadGlobalStatDefault() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire + .wire__crate__api__downloader_api__download_global_stat_default( + port_, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_download_global_stat, + decodeErrorData: null, + ), + constMeta: kCrateApiDownloaderApiDownloadGlobalStatDefaultConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloadGlobalStatDefaultConstMeta => + const TaskConstMeta( + debugName: "download_global_stat_default", + argNames: [], + ); + + @override + Future crateApiDownloaderApiDownloaderAddMagnet({ + required String magnetLink, + String? outputFolder, + List? trackers, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(magnetLink); + var arg1 = cst_encode_opt_String(outputFolder); + var arg2 = cst_encode_opt_list_String(trackers); + return wire.wire__crate__api__downloader_api__downloader_add_magnet( + port_, + arg0, + arg1, + arg2, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_usize, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderAddMagnetConstMeta, + argValues: [magnetLink, outputFolder, trackers], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderAddMagnetConstMeta => + const TaskConstMeta( + debugName: "downloader_add_magnet", + argNames: ["magnetLink", "outputFolder", "trackers"], + ); + + @override + Future crateApiDownloaderApiDownloaderAddTorrent({ + required List torrentBytes, + String? outputFolder, + List? trackers, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_list_prim_u_8_loose(torrentBytes); + var arg1 = cst_encode_opt_String(outputFolder); + var arg2 = cst_encode_opt_list_String(trackers); + return wire.wire__crate__api__downloader_api__downloader_add_torrent( + port_, + arg0, + arg1, + arg2, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_usize, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderAddTorrentConstMeta, + argValues: [torrentBytes, outputFolder, trackers], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderAddTorrentConstMeta => + const TaskConstMeta( + debugName: "downloader_add_torrent", + argNames: ["torrentBytes", "outputFolder", "trackers"], + ); + + @override + Future crateApiDownloaderApiDownloaderAddUrl({ + required String url, + String? outputFolder, + List? trackers, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(url); + var arg1 = cst_encode_opt_String(outputFolder); + var arg2 = cst_encode_opt_list_String(trackers); + return wire.wire__crate__api__downloader_api__downloader_add_url( + port_, + arg0, + arg1, + arg2, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_usize, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderAddUrlConstMeta, + argValues: [url, outputFolder, trackers], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderAddUrlConstMeta => + const TaskConstMeta( + debugName: "downloader_add_url", + argNames: ["url", "outputFolder", "trackers"], + ); + + @override + Future> crateApiDownloaderApiDownloaderGetAllTasks() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire + .wire__crate__api__downloader_api__downloader_get_all_tasks( + port_, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_list_download_task_info, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderGetAllTasksConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderGetAllTasksConstMeta => + const TaskConstMeta(debugName: "downloader_get_all_tasks", argNames: []); + + @override + Future crateApiDownloaderApiDownloaderGetGlobalStats() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire + .wire__crate__api__downloader_api__downloader_get_global_stats( + port_, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_download_global_stat, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderGetGlobalStatsConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderGetGlobalStatsConstMeta => + const TaskConstMeta( + debugName: "downloader_get_global_stats", + argNames: [], + ); + + @override + Future crateApiDownloaderApiDownloaderGetTaskInfo({ + required BigInt taskId, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_usize(taskId); + return wire + .wire__crate__api__downloader_api__downloader_get_task_info( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_download_task_info, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderGetTaskInfoConstMeta, + argValues: [taskId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderGetTaskInfoConstMeta => + const TaskConstMeta( + debugName: "downloader_get_task_info", + argNames: ["taskId"], + ); + + @override + void crateApiDownloaderApiDownloaderInit({required String downloadDir}) { + return handler.executeSync( + SyncTask( + callFfi: () { + var arg0 = cst_encode_String(downloadDir); + return wire.wire__crate__api__downloader_api__downloader_init(arg0); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderInitConstMeta, + argValues: [downloadDir], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderInitConstMeta => + const TaskConstMeta( + debugName: "downloader_init", + argNames: ["downloadDir"], + ); + + @override + bool crateApiDownloaderApiDownloaderIsInitialized() { + return handler.executeSync( + SyncTask( + callFfi: () { + return wire + .wire__crate__api__downloader_api__downloader_is_initialized(); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateApiDownloaderApiDownloaderIsInitializedConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderIsInitializedConstMeta => + const TaskConstMeta(debugName: "downloader_is_initialized", argNames: []); + + @override + Future crateApiDownloaderApiDownloaderIsNameInTask({ + required String name, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_String(name); + return wire + .wire__crate__api__downloader_api__downloader_is_name_in_task( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_bool, + decodeErrorData: null, + ), + constMeta: kCrateApiDownloaderApiDownloaderIsNameInTaskConstMeta, + argValues: [name], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderIsNameInTaskConstMeta => + const TaskConstMeta( + debugName: "downloader_is_name_in_task", + argNames: ["name"], + ); + + @override + Future crateApiDownloaderApiDownloaderPause({required BigInt taskId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_usize(taskId); + return wire.wire__crate__api__downloader_api__downloader_pause( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderPauseConstMeta, + argValues: [taskId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderPauseConstMeta => + const TaskConstMeta(debugName: "downloader_pause", argNames: ["taskId"]); + + @override + Future crateApiDownloaderApiDownloaderPauseAll() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__downloader_api__downloader_pause_all( + port_, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderPauseAllConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderPauseAllConstMeta => + const TaskConstMeta(debugName: "downloader_pause_all", argNames: []); + + @override + Future crateApiDownloaderApiDownloaderRemove({ + required BigInt taskId, + required bool deleteFiles, + }) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_usize(taskId); + var arg1 = cst_encode_bool(deleteFiles); + return wire.wire__crate__api__downloader_api__downloader_remove( + port_, + arg0, + arg1, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderRemoveConstMeta, + argValues: [taskId, deleteFiles], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderRemoveConstMeta => + const TaskConstMeta( + debugName: "downloader_remove", + argNames: ["taskId", "deleteFiles"], + ); + + @override + Future crateApiDownloaderApiDownloaderResume({required BigInt taskId}) { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + var arg0 = cst_encode_usize(taskId); + return wire.wire__crate__api__downloader_api__downloader_resume( + port_, + arg0, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderResumeConstMeta, + argValues: [taskId], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderResumeConstMeta => + const TaskConstMeta(debugName: "downloader_resume", argNames: ["taskId"]); + + @override + Future crateApiDownloaderApiDownloaderResumeAll() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__downloader_api__downloader_resume_all( + port_, + ); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderResumeAllConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderResumeAllConstMeta => + const TaskConstMeta(debugName: "downloader_resume_all", argNames: []); + + @override + Future crateApiDownloaderApiDownloaderStop() { + return handler.executeNormal( + NormalTask( + callFfi: (port_) { + return wire.wire__crate__api__downloader_api__downloader_stop(port_); + }, + codec: DcoCodec( + decodeSuccessData: dco_decode_unit, + decodeErrorData: dco_decode_AnyhowException, + ), + constMeta: kCrateApiDownloaderApiDownloaderStopConstMeta, + argValues: [], + apiImpl: this, + ), + ); + } + + TaskConstMeta get kCrateApiDownloaderApiDownloaderStopConstMeta => + const TaskConstMeta(debugName: "downloader_stop", argNames: []); + @override Future crateApiHttpApiFetch({ required MyMethod method, @@ -1903,6 +2400,53 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dco_decode_web_view_configuration(raw); } + @protected + DownloadGlobalStat dco_decode_download_global_stat(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return DownloadGlobalStat( + downloadSpeed: dco_decode_u_64(arr[0]), + uploadSpeed: dco_decode_u_64(arr[1]), + numActive: dco_decode_usize(arr[2]), + numWaiting: dco_decode_usize(arr[3]), + ); + } + + @protected + DownloadTaskInfo dco_decode_download_task_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 11) + throw Exception('unexpected arr length: expect 11 but see ${arr.length}'); + return DownloadTaskInfo( + id: dco_decode_usize(arr[0]), + name: dco_decode_String(arr[1]), + status: dco_decode_download_task_status(arr[2]), + totalBytes: dco_decode_u_64(arr[3]), + downloadedBytes: dco_decode_u_64(arr[4]), + uploadedBytes: dco_decode_u_64(arr[5]), + downloadSpeed: dco_decode_u_64(arr[6]), + uploadSpeed: dco_decode_u_64(arr[7]), + progress: dco_decode_f_64(arr[8]), + numPeers: dco_decode_usize(arr[9]), + outputFolder: dco_decode_String(arr[10]), + ); + } + + @protected + DownloadTaskStatus dco_decode_download_task_status(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return DownloadTaskStatus.values[raw as int]; + } + + @protected + double dco_decode_f_64(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw as double; + } + @protected int dco_decode_i_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1921,6 +2465,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (raw as List).map(dco_decode_String).toList(); } + @protected + List dco_decode_list_download_task_info(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_download_task_info).toList(); + } + @protected List dco_decode_list_p_4_k_file_item(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -1993,6 +2543,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_u_64(raw); } + @protected + List? dco_decode_opt_list_String(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null ? null : dco_decode_list_String(raw); + } + @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -2255,6 +2811,67 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_web_view_configuration(deserializer)); } + @protected + DownloadGlobalStat sse_decode_download_global_stat( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_downloadSpeed = sse_decode_u_64(deserializer); + var var_uploadSpeed = sse_decode_u_64(deserializer); + var var_numActive = sse_decode_usize(deserializer); + var var_numWaiting = sse_decode_usize(deserializer); + return DownloadGlobalStat( + downloadSpeed: var_downloadSpeed, + uploadSpeed: var_uploadSpeed, + numActive: var_numActive, + numWaiting: var_numWaiting, + ); + } + + @protected + DownloadTaskInfo sse_decode_download_task_info(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_id = sse_decode_usize(deserializer); + var var_name = sse_decode_String(deserializer); + var var_status = sse_decode_download_task_status(deserializer); + var var_totalBytes = sse_decode_u_64(deserializer); + var var_downloadedBytes = sse_decode_u_64(deserializer); + var var_uploadedBytes = sse_decode_u_64(deserializer); + var var_downloadSpeed = sse_decode_u_64(deserializer); + var var_uploadSpeed = sse_decode_u_64(deserializer); + var var_progress = sse_decode_f_64(deserializer); + var var_numPeers = sse_decode_usize(deserializer); + var var_outputFolder = sse_decode_String(deserializer); + return DownloadTaskInfo( + id: var_id, + name: var_name, + status: var_status, + totalBytes: var_totalBytes, + downloadedBytes: var_downloadedBytes, + uploadedBytes: var_uploadedBytes, + downloadSpeed: var_downloadSpeed, + uploadSpeed: var_uploadSpeed, + progress: var_progress, + numPeers: var_numPeers, + outputFolder: var_outputFolder, + ); + } + + @protected + DownloadTaskStatus sse_decode_download_task_status( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return DownloadTaskStatus.values[inner]; + } + + @protected + double sse_decode_f_64(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return deserializer.buffer.getFloat64(); + } + @protected int sse_decode_i_32(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2279,6 +2896,20 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List sse_decode_list_download_task_info( + SseDeserializer deserializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_download_task_info(deserializer)); + } + return ans_; + } + @protected List sse_decode_list_p_4_k_file_item( SseDeserializer deserializer, @@ -2407,6 +3038,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + List? sse_decode_opt_list_String(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_String(deserializer)); + } else { + return null; + } + } + @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2640,6 +3282,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw; } + @protected + int cst_encode_download_task_status(DownloadTaskStatus raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return cst_encode_i_32(raw.index); + } + + @protected + double cst_encode_f_64(double raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw; + } + @protected int cst_encode_i_32(int raw) { // Codec=Cst (C-struct based), see doc to use other codecs @@ -2768,6 +3422,52 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_web_view_configuration(self, serializer); } + @protected + void sse_encode_download_global_stat( + DownloadGlobalStat self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_64(self.downloadSpeed, serializer); + sse_encode_u_64(self.uploadSpeed, serializer); + sse_encode_usize(self.numActive, serializer); + sse_encode_usize(self.numWaiting, serializer); + } + + @protected + void sse_encode_download_task_info( + DownloadTaskInfo self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_usize(self.id, serializer); + sse_encode_String(self.name, serializer); + sse_encode_download_task_status(self.status, serializer); + sse_encode_u_64(self.totalBytes, serializer); + sse_encode_u_64(self.downloadedBytes, serializer); + sse_encode_u_64(self.uploadedBytes, serializer); + sse_encode_u_64(self.downloadSpeed, serializer); + sse_encode_u_64(self.uploadSpeed, serializer); + sse_encode_f_64(self.progress, serializer); + sse_encode_usize(self.numPeers, serializer); + sse_encode_String(self.outputFolder, serializer); + } + + @protected + void sse_encode_download_task_status( + DownloadTaskStatus self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_f_64(double self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + serializer.buffer.putFloat64(self); + } + @protected void sse_encode_i_32(int self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -2789,6 +3489,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_download_task_info( + List self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_download_task_info(item, serializer); + } + } + @protected void sse_encode_list_p_4_k_file_item( List self, @@ -2917,6 +3629,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_list_String( + List? self, + SseSerializer serializer, + ) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_list_String(self, serializer); + } + } + @protected void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, diff --git a/lib/common/rust/frb_generated.io.dart b/lib/common/rust/frb_generated.io.dart index b87ec5e..0fac834 100644 --- a/lib/common/rust/frb_generated.io.dart +++ b/lib/common/rust/frb_generated.io.dart @@ -4,6 +4,7 @@ // ignore_for_file: unused_import, unused_element, unnecessary_import, duplicate_ignore, invalid_use_of_internal_member, annotate_overrides, non_constant_identifier_names, curly_braces_in_flow_control_structures, prefer_const_literals_to_create_immutables, unused_field import 'api/asar_api.dart'; +import 'api/downloader_api.dart'; import 'api/http_api.dart'; import 'api/ort_api.dart'; import 'api/rs_process.dart'; @@ -57,6 +58,18 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dynamic raw, ); + @protected + DownloadGlobalStat dco_decode_download_global_stat(dynamic raw); + + @protected + DownloadTaskInfo dco_decode_download_task_info(dynamic raw); + + @protected + DownloadTaskStatus dco_decode_download_task_status(dynamic raw); + + @protected + double dco_decode_f_64(dynamic raw); + @protected int dco_decode_i_32(dynamic raw); @@ -66,6 +79,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List dco_decode_list_String(dynamic raw); + @protected + List dco_decode_list_download_task_info(dynamic raw); + @protected List dco_decode_list_p_4_k_file_item(dynamic raw); @@ -102,6 +118,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw); + @protected + List? dco_decode_opt_list_String(dynamic raw); + @protected Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); @@ -192,6 +211,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseDeserializer deserializer, ); + @protected + DownloadGlobalStat sse_decode_download_global_stat( + SseDeserializer deserializer, + ); + + @protected + DownloadTaskInfo sse_decode_download_task_info(SseDeserializer deserializer); + + @protected + DownloadTaskStatus sse_decode_download_task_status( + SseDeserializer deserializer, + ); + + @protected + double sse_decode_f_64(SseDeserializer deserializer); + @protected int sse_decode_i_32(SseDeserializer deserializer); @@ -201,6 +236,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected List sse_decode_list_String(SseDeserializer deserializer); + @protected + List sse_decode_list_download_task_info( + SseDeserializer deserializer, + ); + @protected List sse_decode_list_p_4_k_file_item( SseDeserializer deserializer, @@ -245,6 +285,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer); + @protected + List? sse_decode_opt_list_String(SseDeserializer deserializer); + @protected Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); @@ -396,6 +439,17 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return ans; } + @protected + ffi.Pointer + cst_encode_list_download_task_info(List raw) { + // Codec=Cst (C-struct based), see doc to use other codecs + final ans = wire.cst_new_list_download_task_info(raw.length); + for (var i = 0; i < raw.length; ++i) { + cst_api_fill_to_wire_download_task_info(raw[i], ans.ref.ptr[i]); + } + return ans; + } + @protected ffi.Pointer cst_encode_list_p_4_k_file_item( List raw, @@ -490,6 +544,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { return raw == null ? ffi.nullptr : cst_encode_box_autoadd_u_64(raw); } + @protected + ffi.Pointer cst_encode_opt_list_String( + List? raw, + ) { + // Codec=Cst (C-struct based), see doc to use other codecs + return raw == null ? ffi.nullptr : cst_encode_list_String(raw); + } + @protected ffi.Pointer cst_encode_opt_list_prim_u_8_strict(Uint8List? raw) { @@ -525,6 +587,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { cst_api_fill_to_wire_web_view_configuration(apiObj, wireObj.ref); } + @protected + void cst_api_fill_to_wire_download_global_stat( + DownloadGlobalStat apiObj, + wire_cst_download_global_stat wireObj, + ) { + wireObj.download_speed = cst_encode_u_64(apiObj.downloadSpeed); + wireObj.upload_speed = cst_encode_u_64(apiObj.uploadSpeed); + wireObj.num_active = cst_encode_usize(apiObj.numActive); + wireObj.num_waiting = cst_encode_usize(apiObj.numWaiting); + } + + @protected + void cst_api_fill_to_wire_download_task_info( + DownloadTaskInfo apiObj, + wire_cst_download_task_info wireObj, + ) { + wireObj.id = cst_encode_usize(apiObj.id); + wireObj.name = cst_encode_String(apiObj.name); + wireObj.status = cst_encode_download_task_status(apiObj.status); + wireObj.total_bytes = cst_encode_u_64(apiObj.totalBytes); + wireObj.downloaded_bytes = cst_encode_u_64(apiObj.downloadedBytes); + wireObj.uploaded_bytes = cst_encode_u_64(apiObj.uploadedBytes); + wireObj.download_speed = cst_encode_u_64(apiObj.downloadSpeed); + wireObj.upload_speed = cst_encode_u_64(apiObj.uploadSpeed); + wireObj.progress = cst_encode_f_64(apiObj.progress); + wireObj.num_peers = cst_encode_usize(apiObj.numPeers); + wireObj.output_folder = cst_encode_String(apiObj.outputFolder); + } + @protected void cst_api_fill_to_wire_p_4_k_file_item( P4kFileItem apiObj, @@ -675,6 +766,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected bool cst_encode_bool(bool raw); + @protected + int cst_encode_download_task_status(DownloadTaskStatus raw); + + @protected + double cst_encode_f_64(double raw); + @protected int cst_encode_i_32(int raw); @@ -741,6 +838,27 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { SseSerializer serializer, ); + @protected + void sse_encode_download_global_stat( + DownloadGlobalStat self, + SseSerializer serializer, + ); + + @protected + void sse_encode_download_task_info( + DownloadTaskInfo self, + SseSerializer serializer, + ); + + @protected + void sse_encode_download_task_status( + DownloadTaskStatus self, + SseSerializer serializer, + ); + + @protected + void sse_encode_f_64(double self, SseSerializer serializer); + @protected void sse_encode_i_32(int self, SseSerializer serializer); @@ -750,6 +868,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_list_String(List self, SseSerializer serializer); + @protected + void sse_encode_list_download_task_info( + List self, + SseSerializer serializer, + ); + @protected void sse_encode_list_p_4_k_file_item( List self, @@ -804,6 +928,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer); + @protected + void sse_encode_opt_list_String(List? self, SseSerializer serializer); + @protected void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, @@ -1034,6 +1161,334 @@ class RustLibWire implements BaseWire { void Function(int, ffi.Pointer) >(); + void wire__crate__api__downloader_api__download_global_stat_default( + int port_, + ) { + return _wire__crate__api__downloader_api__download_global_stat_default( + port_, + ); + } + + late final _wire__crate__api__downloader_api__download_global_stat_defaultPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__download_global_stat_default', + ); + late final _wire__crate__api__downloader_api__download_global_stat_default = + _wire__crate__api__downloader_api__download_global_stat_defaultPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_add_magnet( + int port_, + ffi.Pointer magnet_link, + ffi.Pointer output_folder, + ffi.Pointer trackers, + ) { + return _wire__crate__api__downloader_api__downloader_add_magnet( + port_, + magnet_link, + output_folder, + trackers, + ); + } + + late final _wire__crate__api__downloader_api__downloader_add_magnetPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_magnet', + ); + late final _wire__crate__api__downloader_api__downloader_add_magnet = + _wire__crate__api__downloader_api__downloader_add_magnetPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__downloader_api__downloader_add_torrent( + int port_, + ffi.Pointer torrent_bytes, + ffi.Pointer output_folder, + ffi.Pointer trackers, + ) { + return _wire__crate__api__downloader_api__downloader_add_torrent( + port_, + torrent_bytes, + output_folder, + trackers, + ); + } + + late final _wire__crate__api__downloader_api__downloader_add_torrentPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_torrent', + ); + late final _wire__crate__api__downloader_api__downloader_add_torrent = + _wire__crate__api__downloader_api__downloader_add_torrentPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__downloader_api__downloader_add_url( + int port_, + ffi.Pointer url, + ffi.Pointer output_folder, + ffi.Pointer trackers, + ) { + return _wire__crate__api__downloader_api__downloader_add_url( + port_, + url, + output_folder, + trackers, + ); + } + + late final _wire__crate__api__downloader_api__downloader_add_urlPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_url', + ); + late final _wire__crate__api__downloader_api__downloader_add_url = + _wire__crate__api__downloader_api__downloader_add_urlPtr + .asFunction< + void Function( + int, + ffi.Pointer, + ffi.Pointer, + ffi.Pointer, + ) + >(); + + void wire__crate__api__downloader_api__downloader_get_all_tasks(int port_) { + return _wire__crate__api__downloader_api__downloader_get_all_tasks(port_); + } + + late final _wire__crate__api__downloader_api__downloader_get_all_tasksPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_all_tasks', + ); + late final _wire__crate__api__downloader_api__downloader_get_all_tasks = + _wire__crate__api__downloader_api__downloader_get_all_tasksPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_get_global_stats( + int port_, + ) { + return _wire__crate__api__downloader_api__downloader_get_global_stats( + port_, + ); + } + + late final _wire__crate__api__downloader_api__downloader_get_global_statsPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_global_stats', + ); + late final _wire__crate__api__downloader_api__downloader_get_global_stats = + _wire__crate__api__downloader_api__downloader_get_global_statsPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_get_task_info( + int port_, + int task_id, + ) { + return _wire__crate__api__downloader_api__downloader_get_task_info( + port_, + task_id, + ); + } + + late final _wire__crate__api__downloader_api__downloader_get_task_infoPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_task_info', + ); + late final _wire__crate__api__downloader_api__downloader_get_task_info = + _wire__crate__api__downloader_api__downloader_get_task_infoPtr + .asFunction(); + + WireSyncRust2DartDco wire__crate__api__downloader_api__downloader_init( + ffi.Pointer download_dir, + ) { + return _wire__crate__api__downloader_api__downloader_init(download_dir); + } + + late final _wire__crate__api__downloader_api__downloader_initPtr = + _lookup< + ffi.NativeFunction< + WireSyncRust2DartDco Function( + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_init', + ); + late final _wire__crate__api__downloader_api__downloader_init = + _wire__crate__api__downloader_api__downloader_initPtr + .asFunction< + WireSyncRust2DartDco Function( + ffi.Pointer, + ) + >(); + + WireSyncRust2DartDco + wire__crate__api__downloader_api__downloader_is_initialized() { + return _wire__crate__api__downloader_api__downloader_is_initialized(); + } + + late final _wire__crate__api__downloader_api__downloader_is_initializedPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_is_initialized', + ); + late final _wire__crate__api__downloader_api__downloader_is_initialized = + _wire__crate__api__downloader_api__downloader_is_initializedPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_is_name_in_task( + int port_, + ffi.Pointer name, + ) { + return _wire__crate__api__downloader_api__downloader_is_name_in_task( + port_, + name, + ); + } + + late final _wire__crate__api__downloader_api__downloader_is_name_in_taskPtr = + _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Int64, + ffi.Pointer, + ) + > + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_is_name_in_task', + ); + 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 wire__crate__api__downloader_api__downloader_pause( + int port_, + int task_id, + ) { + return _wire__crate__api__downloader_api__downloader_pause(port_, task_id); + } + + late final _wire__crate__api__downloader_api__downloader_pausePtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_pause', + ); + late final _wire__crate__api__downloader_api__downloader_pause = + _wire__crate__api__downloader_api__downloader_pausePtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_pause_all(int port_) { + return _wire__crate__api__downloader_api__downloader_pause_all(port_); + } + + late final _wire__crate__api__downloader_api__downloader_pause_allPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_pause_all', + ); + late final _wire__crate__api__downloader_api__downloader_pause_all = + _wire__crate__api__downloader_api__downloader_pause_allPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_remove( + int port_, + int task_id, + bool delete_files, + ) { + return _wire__crate__api__downloader_api__downloader_remove( + port_, + task_id, + delete_files, + ); + } + + late final _wire__crate__api__downloader_api__downloader_removePtr = + _lookup< + ffi.NativeFunction + >( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_remove', + ); + late final _wire__crate__api__downloader_api__downloader_remove = + _wire__crate__api__downloader_api__downloader_removePtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_resume( + int port_, + int task_id, + ) { + return _wire__crate__api__downloader_api__downloader_resume(port_, task_id); + } + + late final _wire__crate__api__downloader_api__downloader_resumePtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_resume', + ); + late final _wire__crate__api__downloader_api__downloader_resume = + _wire__crate__api__downloader_api__downloader_resumePtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_resume_all(int port_) { + return _wire__crate__api__downloader_api__downloader_resume_all(port_); + } + + late final _wire__crate__api__downloader_api__downloader_resume_allPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_resume_all', + ); + late final _wire__crate__api__downloader_api__downloader_resume_all = + _wire__crate__api__downloader_api__downloader_resume_allPtr + .asFunction(); + + void wire__crate__api__downloader_api__downloader_stop(int port_) { + return _wire__crate__api__downloader_api__downloader_stop(port_); + } + + late final _wire__crate__api__downloader_api__downloader_stopPtr = + _lookup>( + 'frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_stop', + ); + late final _wire__crate__api__downloader_api__downloader_stop = + _wire__crate__api__downloader_api__downloader_stopPtr + .asFunction(); + void wire__crate__api__http_api__fetch( int port_, int method, @@ -1506,9 +1961,9 @@ class RustLibWire implements BaseWire { void wire__crate__api__win32_api__resolve_shortcut( int port_, - ffi.Pointer lnk_path, + ffi.Pointer _lnk_path, ) { - return _wire__crate__api__win32_api__resolve_shortcut(port_, lnk_path); + return _wire__crate__api__win32_api__resolve_shortcut(port_, _lnk_path); } late final _wire__crate__api__win32_api__resolve_shortcutPtr = @@ -2341,6 +2796,24 @@ class RustLibWire implements BaseWire { late final _cst_new_list_String = _cst_new_list_StringPtr .asFunction Function(int)>(); + ffi.Pointer cst_new_list_download_task_info( + int len, + ) { + return _cst_new_list_download_task_info(len); + } + + late final _cst_new_list_download_task_infoPtr = + _lookup< + ffi.NativeFunction< + ffi.Pointer Function(ffi.Int32) + > + >('frbgen_starcitizen_doctor_cst_new_list_download_task_info'); + late final _cst_new_list_download_task_info = + _cst_new_list_download_task_infoPtr + .asFunction< + ffi.Pointer Function(int) + >(); + ffi.Pointer cst_new_list_p_4_k_file_item( int len, ) { @@ -2459,6 +2932,20 @@ final class wire_cst_list_prim_u_8_strict extends ffi.Struct { external int len; } +final class wire_cst_list_String extends ffi.Struct { + external ffi.Pointer> ptr; + + @ffi.Int32() + external int len; +} + +final class wire_cst_list_prim_u_8_loose extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_record_string_string extends ffi.Struct { external ffi.Pointer field0; @@ -2472,13 +2959,6 @@ final class wire_cst_list_record_string_string extends ffi.Struct { external int len; } -final class wire_cst_list_String extends ffi.Struct { - external ffi.Pointer> ptr; - - @ffi.Int32() - external int len; -} - final class wire_cst_rsi_launcher_asar_data extends ffi.Struct { external ffi.Pointer asar_path; @@ -2487,13 +2967,6 @@ final class wire_cst_rsi_launcher_asar_data extends ffi.Struct { external ffi.Pointer main_js_content; } -final class wire_cst_list_prim_u_8_loose extends ffi.Struct { - external ffi.Pointer ptr; - - @ffi.Int32() - external int len; -} - final class wire_cst_web_view_configuration extends ffi.Struct { external ffi.Pointer title; @@ -2514,6 +2987,46 @@ final class wire_cst_web_view_configuration extends ffi.Struct { external ffi.Pointer user_agent; } +final class wire_cst_download_task_info extends ffi.Struct { + @ffi.UintPtr() + external int id; + + external ffi.Pointer name; + + @ffi.Int32() + external int status; + + @ffi.Uint64() + external int total_bytes; + + @ffi.Uint64() + external int downloaded_bytes; + + @ffi.Uint64() + external int uploaded_bytes; + + @ffi.Uint64() + external int download_speed; + + @ffi.Uint64() + external int upload_speed; + + @ffi.Double() + external double progress; + + @ffi.UintPtr() + external int num_peers; + + external ffi.Pointer output_folder; +} + +final class wire_cst_list_download_task_info extends ffi.Struct { + external ffi.Pointer ptr; + + @ffi.Int32() + external int len; +} + final class wire_cst_p_4_k_file_item extends ffi.Struct { external ffi.Pointer name; @@ -2599,6 +3112,20 @@ final class wire_cst_list_web_view_event extends ffi.Struct { external int len; } +final class wire_cst_download_global_stat extends ffi.Struct { + @ffi.Uint64() + external int download_speed; + + @ffi.Uint64() + external int upload_speed; + + @ffi.UintPtr() + external int num_active; + + @ffi.UintPtr() + external int num_waiting; +} + final class wire_cst_rs_process_stream_data extends ffi.Struct { @ffi.Int32() external int data_type; diff --git a/lib/provider/aria2c.dart b/lib/provider/aria2c.dart deleted file mode 100644 index 47b5ecc..0000000 --- a/lib/provider/aria2c.dart +++ /dev/null @@ -1,222 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; -import 'package:starcitizen_doctor/common/conf/binary_conf.dart'; -import 'dart:io'; -import 'dart:math'; -import 'package:aria2/aria2.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hive_ce/hive.dart'; -import 'package:starcitizen_doctor/api/api.dart'; -import 'package:starcitizen_doctor/common/helper/system_helper.dart'; -import 'package:starcitizen_doctor/common/rust/api/rs_process.dart' as rs_process; - -import 'package:starcitizen_doctor/common/utils/log.dart'; -import 'package:starcitizen_doctor/common/utils/provider.dart'; -import 'package:starcitizen_doctor/ui/home/downloader/home_downloader_ui_model.dart'; - -part 'aria2c.g.dart'; - -part 'aria2c.freezed.dart'; - -@freezed -abstract class Aria2cModelState with _$Aria2cModelState { - const factory Aria2cModelState({required String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat}) = - _Aria2cModelState; -} - -extension Aria2cModelExt on Aria2cModelState { - bool get isRunning => aria2c != null; - - bool get hasDownloadTask => aria2globalStat != null && aria2TotalTaskNum > 0; - - int get aria2TotalTaskNum => - aria2globalStat == null ? 0 : ((aria2globalStat!.numActive ?? 0) + (aria2globalStat!.numWaiting ?? 0)); -} - -@riverpod -class Aria2cModel extends _$Aria2cModel { - bool _disposed = false; - - @override - Aria2cModelState build() { - if (appGlobalState.applicationBinaryModuleDir == null) { - throw Exception("applicationBinaryModuleDir is null"); - } - ref.onDispose(() { - _disposed = true; - }); - ref.keepAlive(); - final aria2cDir = "${appGlobalState.applicationBinaryModuleDir}\\aria2c"; - // LazyLoad init - () async { - try { - final sessionFile = File("$aria2cDir\\aria2.session"); - // 有下载任务则第一时间初始化 - if (await sessionFile.exists () - && (await sessionFile.readAsString()).trim().isNotEmpty) { - dPrint("launch Aria2c daemon"); - await launchDaemon(appGlobalState.applicationBinaryModuleDir!); - } else { - dPrint("LazyLoad Aria2c daemon"); - } - } catch (e) { - dPrint("Aria2cManager.checkLazyLoad Error:$e"); - } - }(); - - return Aria2cModelState(aria2cDir: aria2cDir); - } - - Future launchDaemon(String applicationBinaryModuleDir) async { - if (state.aria2c != null) return; - await BinaryModuleConf.extractModule(["aria2c"], applicationBinaryModuleDir); - - /// skip for debug hot reload - if (kDebugMode) { - if ((await SystemHelper.getPID("aria2c")).isNotEmpty) { - dPrint("[Aria2cManager] debug skip for hot reload"); - return; - } - } - - final sessionFile = File("${state.aria2cDir}\\aria2.session"); - if (!await sessionFile.exists()) { - await sessionFile.create(recursive: true); - } - - final exePath = "${state.aria2cDir}\\aria2c.exe"; - final port = await getFreePort(); - final pwd = generateRandomPassword(16); - dPrint("pwd === $pwd"); - final trackerList = await Api.getTorrentTrackerList(); - dPrint("trackerList === $trackerList"); - dPrint("Aria2cManager .----- aria2c start $port------"); - - final stream = rs_process.start( - executable: exePath, - arguments: [ - "-V", - "-c", - "-x 16", - "--dir=${state.aria2cDir}\\downloads", - "--disable-ipv6", - "--enable-rpc", - "--pause", - "--rpc-listen-port=$port", - "--rpc-secret=$pwd", - "--input-file=${sessionFile.absolute.path.trim()}", - "--save-session=${sessionFile.absolute.path.trim()}", - "--save-session-interval=60", - "--file-allocation=trunc", - "--seed-time=0", - ], - workingDirectory: state.aria2cDir, - ); - - String launchError = ""; - - stream.listen((event) { - dPrint("Aria2cManager.rs_process event === [${event.rsPid}] ${event.dataType} >> ${event.data}"); - switch (event.dataType) { - case rs_process.RsProcessStreamDataType.output: - if (event.data.contains("IPv4 RPC: listening on TCP port")) { - _onLaunch(port, pwd, trackerList); - } - break; - case rs_process.RsProcessStreamDataType.error: - launchError = event.data; - state = state.copyWith(aria2c: null); - break; - case rs_process.RsProcessStreamDataType.exit: - launchError = event.data; - state = state.copyWith(aria2c: null); - break; - } - }); - - while (true) { - if (state.aria2c != null) return; - if (launchError.isNotEmpty) throw launchError; - await Future.delayed(const Duration(milliseconds: 100)); - } - } - - Future getFreePort() async { - final serverSocket = await ServerSocket.bind("127.0.0.1", 0); - final port = serverSocket.port; - await serverSocket.close(); - return port; - } - - String generateRandomPassword(int length) { - const String charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - Random random = Random(); - StringBuffer buffer = StringBuffer(); - for (int i = 0; i < length; i++) { - int randomIndex = random.nextInt(charset.length); - buffer.write(charset[randomIndex]); - } - return buffer.toString(); - } - - int textToByte(String text) { - if (text.length == 1) { - return 0; - } - if (int.tryParse(text) != null) { - return int.parse(text); - } - if (text.endsWith("k")) { - return int.parse(text.substring(0, text.length - 1)) * 1024; - } - if (text.endsWith("m")) { - return int.parse(text.substring(0, text.length - 1)) * 1024 * 1024; - } - return 0; - } - - Future _onLaunch(int port, String pwd, String trackerList) async { - final aria2c = Aria2c("ws://127.0.0.1:$port/jsonrpc", "websocket", pwd); - state = state.copyWith(aria2c: aria2c); - aria2c.getVersion().then((value) { - dPrint("Aria2cManager.connected! version == ${value.version}"); - _listenState(aria2c); - }); - final box = await Hive.openBox("app_conf"); - aria2c.changeGlobalOption( - Aria2Option() - ..maxOverallUploadLimit = textToByte(box.get("downloader_up_limit", defaultValue: "0")) - ..maxOverallDownloadLimit = textToByte(box.get("downloader_down_limit", defaultValue: "0")) - ..btTracker = trackerList, - ); - } - - Future _listenState(Aria2c aria2c) async { - dPrint("Aria2cModel._listenState start"); - while (true) { - if (_disposed || state.aria2c == null) { - dPrint("Aria2cModel._listenState end"); - return; - } - try { - final aria2globalStat = await aria2c.getGlobalStat(); - state = state.copyWith(aria2globalStat: aria2globalStat); - } catch (e) { - dPrint("aria2globalStat update error:$e"); - } - await Future.delayed(const Duration(seconds: 1)); - } - } - - Future isNameInTask(String name) async { - final aria2c = state.aria2c; - if (aria2c == null) return false; - for (var value in [...await aria2c.tellActive(), ...await aria2c.tellWaiting(0, 100000)]) { - final t = HomeDownloaderUIModel.getTaskTypeAndName(value); - if (t.key == "torrent" && t.value.contains(name)) { - return true; - } - } - return false; - } -} diff --git a/lib/provider/aria2c.freezed.dart b/lib/provider/aria2c.freezed.dart deleted file mode 100644 index 4a4c616..0000000 --- a/lib/provider/aria2c.freezed.dart +++ /dev/null @@ -1,289 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND -// coverage:ignore-file -// ignore_for_file: type=lint -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark - -part of 'aria2c.dart'; - -// ************************************************************************** -// FreezedGenerator -// ************************************************************************** - -// dart format off -T _$identity(T value) => value; -/// @nodoc -mixin _$Aria2cModelState implements DiagnosticableTreeMixin { - - String get aria2cDir; Aria2c? get aria2c; Aria2GlobalStat? get aria2globalStat; -/// Create a copy of Aria2cModelState -/// with the given fields replaced by the non-null parameter values. -@JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -$Aria2cModelStateCopyWith get copyWith => _$Aria2cModelStateCopyWithImpl(this as Aria2cModelState, _$identity); - - -@override -void debugFillProperties(DiagnosticPropertiesBuilder properties) { - properties - ..add(DiagnosticsProperty('type', 'Aria2cModelState')) - ..add(DiagnosticsProperty('aria2cDir', aria2cDir))..add(DiagnosticsProperty('aria2c', aria2c))..add(DiagnosticsProperty('aria2globalStat', aria2globalStat)); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is Aria2cModelState&&(identical(other.aria2cDir, aria2cDir) || other.aria2cDir == aria2cDir)&&(identical(other.aria2c, aria2c) || other.aria2c == aria2c)&&(identical(other.aria2globalStat, aria2globalStat) || other.aria2globalStat == aria2globalStat)); -} - - -@override -int get hashCode => Object.hash(runtimeType,aria2cDir,aria2c,aria2globalStat); - -@override -String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Aria2cModelState(aria2cDir: $aria2cDir, aria2c: $aria2c, aria2globalStat: $aria2globalStat)'; -} - - -} - -/// @nodoc -abstract mixin class $Aria2cModelStateCopyWith<$Res> { - factory $Aria2cModelStateCopyWith(Aria2cModelState value, $Res Function(Aria2cModelState) _then) = _$Aria2cModelStateCopyWithImpl; -@useResult -$Res call({ - String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat -}); - - - - -} -/// @nodoc -class _$Aria2cModelStateCopyWithImpl<$Res> - implements $Aria2cModelStateCopyWith<$Res> { - _$Aria2cModelStateCopyWithImpl(this._self, this._then); - - final Aria2cModelState _self; - final $Res Function(Aria2cModelState) _then; - -/// Create a copy of Aria2cModelState -/// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? aria2cDir = null,Object? aria2c = freezed,Object? aria2globalStat = freezed,}) { - return _then(_self.copyWith( -aria2cDir: null == aria2cDir ? _self.aria2cDir : aria2cDir // ignore: cast_nullable_to_non_nullable -as String,aria2c: freezed == aria2c ? _self.aria2c : aria2c // ignore: cast_nullable_to_non_nullable -as Aria2c?,aria2globalStat: freezed == aria2globalStat ? _self.aria2globalStat : aria2globalStat // ignore: cast_nullable_to_non_nullable -as Aria2GlobalStat?, - )); -} - -} - - -/// Adds pattern-matching-related methods to [Aria2cModelState]. -extension Aria2cModelStatePatterns on Aria2cModelState { -/// A variant of `map` that fallback to returning `orElse`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeMap(TResult Function( _Aria2cModelState value)? $default,{required TResult orElse(),}){ -final _that = this; -switch (_that) { -case _Aria2cModelState() when $default != null: -return $default(_that);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// Callbacks receives the raw object, upcasted. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case final Subclass2 value: -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult map(TResult Function( _Aria2cModelState value) $default,){ -final _that = this; -switch (_that) { -case _Aria2cModelState(): -return $default(_that);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `map` that fallback to returning `null`. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case final Subclass value: -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? mapOrNull(TResult? Function( _Aria2cModelState value)? $default,){ -final _that = this; -switch (_that) { -case _Aria2cModelState() when $default != null: -return $default(_that);case _: - return null; - -} -} -/// A variant of `when` that fallback to an `orElse` callback. -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return orElse(); -/// } -/// ``` - -@optionalTypeArgs TResult maybeWhen(TResult Function( String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat)? $default,{required TResult orElse(),}) {final _that = this; -switch (_that) { -case _Aria2cModelState() when $default != null: -return $default(_that.aria2cDir,_that.aria2c,_that.aria2globalStat);case _: - return orElse(); - -} -} -/// A `switch`-like method, using callbacks. -/// -/// As opposed to `map`, this offers destructuring. -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case Subclass2(:final field2): -/// return ...; -/// } -/// ``` - -@optionalTypeArgs TResult when(TResult Function( String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat) $default,) {final _that = this; -switch (_that) { -case _Aria2cModelState(): -return $default(_that.aria2cDir,_that.aria2c,_that.aria2globalStat);case _: - throw StateError('Unexpected subclass'); - -} -} -/// A variant of `when` that fallback to returning `null` -/// -/// It is equivalent to doing: -/// ```dart -/// switch (sealedClass) { -/// case Subclass(:final field): -/// return ...; -/// case _: -/// return null; -/// } -/// ``` - -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat)? $default,) {final _that = this; -switch (_that) { -case _Aria2cModelState() when $default != null: -return $default(_that.aria2cDir,_that.aria2c,_that.aria2globalStat);case _: - return null; - -} -} - -} - -/// @nodoc - - -class _Aria2cModelState with DiagnosticableTreeMixin implements Aria2cModelState { - const _Aria2cModelState({required this.aria2cDir, this.aria2c, this.aria2globalStat}); - - -@override final String aria2cDir; -@override final Aria2c? aria2c; -@override final Aria2GlobalStat? aria2globalStat; - -/// Create a copy of Aria2cModelState -/// with the given fields replaced by the non-null parameter values. -@override @JsonKey(includeFromJson: false, includeToJson: false) -@pragma('vm:prefer-inline') -_$Aria2cModelStateCopyWith<_Aria2cModelState> get copyWith => __$Aria2cModelStateCopyWithImpl<_Aria2cModelState>(this, _$identity); - - -@override -void debugFillProperties(DiagnosticPropertiesBuilder properties) { - properties - ..add(DiagnosticsProperty('type', 'Aria2cModelState')) - ..add(DiagnosticsProperty('aria2cDir', aria2cDir))..add(DiagnosticsProperty('aria2c', aria2c))..add(DiagnosticsProperty('aria2globalStat', aria2globalStat)); -} - -@override -bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _Aria2cModelState&&(identical(other.aria2cDir, aria2cDir) || other.aria2cDir == aria2cDir)&&(identical(other.aria2c, aria2c) || other.aria2c == aria2c)&&(identical(other.aria2globalStat, aria2globalStat) || other.aria2globalStat == aria2globalStat)); -} - - -@override -int get hashCode => Object.hash(runtimeType,aria2cDir,aria2c,aria2globalStat); - -@override -String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) { - return 'Aria2cModelState(aria2cDir: $aria2cDir, aria2c: $aria2c, aria2globalStat: $aria2globalStat)'; -} - - -} - -/// @nodoc -abstract mixin class _$Aria2cModelStateCopyWith<$Res> implements $Aria2cModelStateCopyWith<$Res> { - factory _$Aria2cModelStateCopyWith(_Aria2cModelState value, $Res Function(_Aria2cModelState) _then) = __$Aria2cModelStateCopyWithImpl; -@override @useResult -$Res call({ - String aria2cDir, Aria2c? aria2c, Aria2GlobalStat? aria2globalStat -}); - - - - -} -/// @nodoc -class __$Aria2cModelStateCopyWithImpl<$Res> - implements _$Aria2cModelStateCopyWith<$Res> { - __$Aria2cModelStateCopyWithImpl(this._self, this._then); - - final _Aria2cModelState _self; - final $Res Function(_Aria2cModelState) _then; - -/// Create a copy of Aria2cModelState -/// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? aria2cDir = null,Object? aria2c = freezed,Object? aria2globalStat = freezed,}) { - return _then(_Aria2cModelState( -aria2cDir: null == aria2cDir ? _self.aria2cDir : aria2cDir // ignore: cast_nullable_to_non_nullable -as String,aria2c: freezed == aria2c ? _self.aria2c : aria2c // ignore: cast_nullable_to_non_nullable -as Aria2c?,aria2globalStat: freezed == aria2globalStat ? _self.aria2globalStat : aria2globalStat // ignore: cast_nullable_to_non_nullable -as Aria2GlobalStat?, - )); -} - - -} - -// dart format on diff --git a/lib/provider/aria2c.g.dart b/lib/provider/aria2c.g.dart deleted file mode 100644 index 8e2b783..0000000 --- a/lib/provider/aria2c.g.dart +++ /dev/null @@ -1,63 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'aria2c.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -// GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: type=lint, type=warning - -@ProviderFor(Aria2cModel) -const aria2cModelProvider = Aria2cModelProvider._(); - -final class Aria2cModelProvider - extends $NotifierProvider { - const Aria2cModelProvider._() - : super( - from: null, - argument: null, - retry: null, - name: r'aria2cModelProvider', - isAutoDispose: true, - dependencies: null, - $allTransitiveDependencies: null, - ); - - @override - String debugGetCreateSourceHash() => _$aria2cModelHash(); - - @$internal - @override - Aria2cModel create() => Aria2cModel(); - - /// {@macro riverpod.override_with_value} - Override overrideWithValue(Aria2cModelState value) { - return $ProviderOverride( - origin: this, - providerOverride: $SyncValueProvider(value), - ); - } -} - -String _$aria2cModelHash() => r'17956c60a79c68ae13b8b8e700ebbafb70e93194'; - -abstract class _$Aria2cModel extends $Notifier { - Aria2cModelState build(); - @$mustCallSuper - @override - void runBuild() { - final created = build(); - final ref = this.ref as $Ref; - final element = - ref.element - as $ClassProviderElement< - AnyNotifier, - Aria2cModelState, - Object?, - Object? - >; - element.handleValue(ref, created); - } -} diff --git a/lib/provider/download_manager.dart b/lib/provider/download_manager.dart new file mode 100644 index 0000000..4874257 --- /dev/null +++ b/lib/provider/download_manager.dart @@ -0,0 +1,198 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'dart:io'; + +import 'package:starcitizen_doctor/common/rust/api/downloader_api.dart' as downloader_api; +import 'package:starcitizen_doctor/common/utils/log.dart'; +import 'package:starcitizen_doctor/common/utils/provider.dart'; + +part 'download_manager.g.dart'; + +part 'download_manager.freezed.dart'; + +@freezed +abstract class DownloadManagerState with _$DownloadManagerState { + const factory DownloadManagerState({ + required String downloadDir, + @Default(false) bool isInitialized, + downloader_api.DownloadGlobalStat? globalStat, + }) = _DownloadManagerState; +} + +extension DownloadManagerStateExt on DownloadManagerState { + bool get isRunning => isInitialized; + + bool get hasDownloadTask => globalStat != null && (globalStat!.numActive + globalStat!.numWaiting) > BigInt.zero; + + int get totalTaskNum => globalStat == null ? 0 : (globalStat!.numActive + globalStat!.numWaiting).toInt(); +} + +@riverpod +class DownloadManager extends _$DownloadManager { + bool _disposed = false; + + @override + DownloadManagerState build() { + if (appGlobalState.applicationBinaryModuleDir == null) { + throw Exception("applicationBinaryModuleDir is null"); + } + ref.onDispose(() { + _disposed = true; + }); + ref.keepAlive(); + + final downloadDir = "${appGlobalState.applicationBinaryModuleDir}${Platform.pathSeparator}downloads"; + + // Lazy load init + () async { + try { + // Check if there are existing tasks + final dir = Directory(downloadDir); + if (await dir.exists()) { + dPrint("Launch download manager"); + await initDownloader(); + } else { + dPrint("LazyLoad download manager"); + } + } catch (e) { + dPrint("DownloadManager.checkLazyLoad Error:$e"); + } + }(); + + return DownloadManagerState(downloadDir: downloadDir); + } + + Future initDownloader() async { + if (state.isInitialized) return; + + try { + // Create download directory if it doesn't exist + final dir = Directory(state.downloadDir); + if (!await dir.exists()) { + await dir.create(recursive: true); + } + + // Initialize the Rust downloader + downloader_api.downloaderInit(downloadDir: state.downloadDir); + + state = state.copyWith(isInitialized: true); + + // Start listening to state updates + _listenState(); + + dPrint("DownloadManager initialized"); + } catch (e) { + dPrint("DownloadManager.initDownloader Error: $e"); + rethrow; + } + } + + Future _listenState() async { + dPrint("DownloadManager._listenState start"); + while (true) { + if (_disposed || !state.isInitialized) { + dPrint("DownloadManager._listenState end"); + return; + } + try { + final globalStat = await downloader_api.downloaderGetGlobalStats(); + state = state.copyWith(globalStat: globalStat); + } catch (e) { + dPrint("globalStat update error:$e"); + } + await Future.delayed(const Duration(seconds: 1)); + } + } + + /// Add a torrent from base64 encoded bytes + Future addTorrent(List torrentBytes, {String? outputFolder, List? trackers}) async { + await initDownloader(); + final taskId = await downloader_api.downloaderAddTorrent( + torrentBytes: torrentBytes, + outputFolder: outputFolder, + trackers: trackers, + ); + return taskId.toInt(); + } + + /// Add a torrent from magnet link + Future addMagnet(String magnetLink, {String? outputFolder, List? trackers}) async { + await initDownloader(); + final taskId = await downloader_api.downloaderAddMagnet( + magnetLink: magnetLink, + outputFolder: outputFolder, + trackers: trackers, + ); + return taskId.toInt(); + } + + /// Add a torrent from URL (only .torrent file URLs are supported) + /// HTTP downloads are NOT supported - will throw an exception + Future addUrl(String url, {String? outputFolder, List? trackers}) async { + await initDownloader(); + final taskId = await downloader_api.downloaderAddUrl(url: url, outputFolder: outputFolder, trackers: trackers); + return taskId.toInt(); + } + + Future pauseTask(int taskId) async { + await downloader_api.downloaderPause(taskId: BigInt.from(taskId)); + } + + Future resumeTask(int taskId) async { + await downloader_api.downloaderResume(taskId: BigInt.from(taskId)); + } + + Future removeTask(int taskId, {bool deleteFiles = false}) async { + await downloader_api.downloaderRemove(taskId: BigInt.from(taskId), deleteFiles: deleteFiles); + } + + Future getTaskInfo(int taskId) async { + return await downloader_api.downloaderGetTaskInfo(taskId: BigInt.from(taskId)); + } + + Future> getAllTasks() async { + if (!state.isInitialized) { + return []; + } + return await downloader_api.downloaderGetAllTasks(); + } + + Future isNameInTask(String name) async { + if (!state.isInitialized) { + return false; + } + return await downloader_api.downloaderIsNameInTask(name: name); + } + + Future pauseAll() async { + await downloader_api.downloaderPauseAll(); + } + + Future resumeAll() async { + await downloader_api.downloaderResumeAll(); + } + + Future stop() async { + await downloader_api.downloaderStop(); + state = state.copyWith(isInitialized: false, globalStat: null); + } + + /// Convert speed limit text to bytes per second + /// Supports formats like: "1", "100k", "10m", "0" + int textToByte(String text) { + if (text.isEmpty || text == "0") { + return 0; + } + final trimmed = text.trim().toLowerCase(); + if (int.tryParse(trimmed) != null) { + return int.parse(trimmed); + } + if (trimmed.endsWith("k")) { + return int.parse(trimmed.substring(0, trimmed.length - 1)) * 1024; + } + if (trimmed.endsWith("m")) { + return int.parse(trimmed.substring(0, trimmed.length - 1)) * 1024 * 1024; + } + return 0; + } +} diff --git a/lib/provider/download_manager.freezed.dart b/lib/provider/download_manager.freezed.dart new file mode 100644 index 0000000..34db254 --- /dev/null +++ b/lib/provider/download_manager.freezed.dart @@ -0,0 +1,277 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND +// coverage:ignore-file +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'download_manager.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +/// @nodoc +mixin _$DownloadManagerState { + + String get downloadDir; bool get isInitialized; downloader_api.DownloadGlobalStat? get globalStat; +/// Create a copy of DownloadManagerState +/// with the given fields replaced by the non-null parameter values. +@JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +$DownloadManagerStateCopyWith get copyWith => _$DownloadManagerStateCopyWithImpl(this as DownloadManagerState, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is DownloadManagerState&&(identical(other.downloadDir, downloadDir) || other.downloadDir == downloadDir)&&(identical(other.isInitialized, isInitialized) || other.isInitialized == isInitialized)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); +} + + +@override +int get hashCode => Object.hash(runtimeType,downloadDir,isInitialized,globalStat); + +@override +String toString() { + return 'DownloadManagerState(downloadDir: $downloadDir, isInitialized: $isInitialized, globalStat: $globalStat)'; +} + + +} + +/// @nodoc +abstract mixin class $DownloadManagerStateCopyWith<$Res> { + factory $DownloadManagerStateCopyWith(DownloadManagerState value, $Res Function(DownloadManagerState) _then) = _$DownloadManagerStateCopyWithImpl; +@useResult +$Res call({ + String downloadDir, bool isInitialized, downloader_api.DownloadGlobalStat? globalStat +}); + + + + +} +/// @nodoc +class _$DownloadManagerStateCopyWithImpl<$Res> + implements $DownloadManagerStateCopyWith<$Res> { + _$DownloadManagerStateCopyWithImpl(this._self, this._then); + + final DownloadManagerState _self; + final $Res Function(DownloadManagerState) _then; + +/// Create a copy of DownloadManagerState +/// with the given fields replaced by the non-null parameter values. +@pragma('vm:prefer-inline') @override $Res call({Object? downloadDir = null,Object? isInitialized = null,Object? globalStat = freezed,}) { + return _then(_self.copyWith( +downloadDir: null == downloadDir ? _self.downloadDir : downloadDir // ignore: cast_nullable_to_non_nullable +as String,isInitialized: null == isInitialized ? _self.isInitialized : isInitialized // ignore: cast_nullable_to_non_nullable +as bool,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable +as downloader_api.DownloadGlobalStat?, + )); +} + +} + + +/// Adds pattern-matching-related methods to [DownloadManagerState]. +extension DownloadManagerStatePatterns on DownloadManagerState { +/// A variant of `map` that fallback to returning `orElse`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeMap(TResult Function( _DownloadManagerState value)? $default,{required TResult orElse(),}){ +final _that = this; +switch (_that) { +case _DownloadManagerState() when $default != null: +return $default(_that);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// Callbacks receives the raw object, upcasted. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case final Subclass2 value: +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult map(TResult Function( _DownloadManagerState value) $default,){ +final _that = this; +switch (_that) { +case _DownloadManagerState(): +return $default(_that);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `map` that fallback to returning `null`. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case final Subclass value: +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? mapOrNull(TResult? Function( _DownloadManagerState value)? $default,){ +final _that = this; +switch (_that) { +case _DownloadManagerState() when $default != null: +return $default(_that);case _: + return null; + +} +} +/// A variant of `when` that fallback to an `orElse` callback. +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return orElse(); +/// } +/// ``` + +@optionalTypeArgs TResult maybeWhen(TResult Function( String downloadDir, bool isInitialized, downloader_api.DownloadGlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this; +switch (_that) { +case _DownloadManagerState() when $default != null: +return $default(_that.downloadDir,_that.isInitialized,_that.globalStat);case _: + return orElse(); + +} +} +/// A `switch`-like method, using callbacks. +/// +/// As opposed to `map`, this offers destructuring. +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case Subclass2(:final field2): +/// return ...; +/// } +/// ``` + +@optionalTypeArgs TResult when(TResult Function( String downloadDir, bool isInitialized, downloader_api.DownloadGlobalStat? globalStat) $default,) {final _that = this; +switch (_that) { +case _DownloadManagerState(): +return $default(_that.downloadDir,_that.isInitialized,_that.globalStat);case _: + throw StateError('Unexpected subclass'); + +} +} +/// A variant of `when` that fallback to returning `null` +/// +/// It is equivalent to doing: +/// ```dart +/// switch (sealedClass) { +/// case Subclass(:final field): +/// return ...; +/// case _: +/// return null; +/// } +/// ``` + +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String downloadDir, bool isInitialized, downloader_api.DownloadGlobalStat? globalStat)? $default,) {final _that = this; +switch (_that) { +case _DownloadManagerState() when $default != null: +return $default(_that.downloadDir,_that.isInitialized,_that.globalStat);case _: + return null; + +} +} + +} + +/// @nodoc + + +class _DownloadManagerState implements DownloadManagerState { + const _DownloadManagerState({required this.downloadDir, this.isInitialized = false, this.globalStat}); + + +@override final String downloadDir; +@override@JsonKey() final bool isInitialized; +@override final downloader_api.DownloadGlobalStat? globalStat; + +/// Create a copy of DownloadManagerState +/// with the given fields replaced by the non-null parameter values. +@override @JsonKey(includeFromJson: false, includeToJson: false) +@pragma('vm:prefer-inline') +_$DownloadManagerStateCopyWith<_DownloadManagerState> get copyWith => __$DownloadManagerStateCopyWithImpl<_DownloadManagerState>(this, _$identity); + + + +@override +bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType&&other is _DownloadManagerState&&(identical(other.downloadDir, downloadDir) || other.downloadDir == downloadDir)&&(identical(other.isInitialized, isInitialized) || other.isInitialized == isInitialized)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); +} + + +@override +int get hashCode => Object.hash(runtimeType,downloadDir,isInitialized,globalStat); + +@override +String toString() { + return 'DownloadManagerState(downloadDir: $downloadDir, isInitialized: $isInitialized, globalStat: $globalStat)'; +} + + +} + +/// @nodoc +abstract mixin class _$DownloadManagerStateCopyWith<$Res> implements $DownloadManagerStateCopyWith<$Res> { + factory _$DownloadManagerStateCopyWith(_DownloadManagerState value, $Res Function(_DownloadManagerState) _then) = __$DownloadManagerStateCopyWithImpl; +@override @useResult +$Res call({ + String downloadDir, bool isInitialized, downloader_api.DownloadGlobalStat? globalStat +}); + + + + +} +/// @nodoc +class __$DownloadManagerStateCopyWithImpl<$Res> + implements _$DownloadManagerStateCopyWith<$Res> { + __$DownloadManagerStateCopyWithImpl(this._self, this._then); + + final _DownloadManagerState _self; + final $Res Function(_DownloadManagerState) _then; + +/// Create a copy of DownloadManagerState +/// with the given fields replaced by the non-null parameter values. +@override @pragma('vm:prefer-inline') $Res call({Object? downloadDir = null,Object? isInitialized = null,Object? globalStat = freezed,}) { + return _then(_DownloadManagerState( +downloadDir: null == downloadDir ? _self.downloadDir : downloadDir // ignore: cast_nullable_to_non_nullable +as String,isInitialized: null == isInitialized ? _self.isInitialized : isInitialized // ignore: cast_nullable_to_non_nullable +as bool,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable +as downloader_api.DownloadGlobalStat?, + )); +} + + +} + +// dart format on diff --git a/lib/provider/download_manager.g.dart b/lib/provider/download_manager.g.dart new file mode 100644 index 0000000..c1905c8 --- /dev/null +++ b/lib/provider/download_manager.g.dart @@ -0,0 +1,63 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'download_manager.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(DownloadManager) +const downloadManagerProvider = DownloadManagerProvider._(); + +final class DownloadManagerProvider + extends $NotifierProvider { + const DownloadManagerProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'downloadManagerProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$downloadManagerHash(); + + @$internal + @override + DownloadManager create() => DownloadManager(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(DownloadManagerState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$downloadManagerHash() => r'adc9a147522afbfcfc8a2e16310649220a75d6a3'; + +abstract class _$DownloadManager extends $Notifier { + DownloadManagerState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + DownloadManagerState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/ui/home/downloader/home_downloader_ui.dart b/lib/ui/home/downloader/home_downloader_ui.dart index 2f28826..95516f5 100644 --- a/lib/ui/home/downloader/home_downloader_ui.dart +++ b/lib/ui/home/downloader/home_downloader_ui.dart @@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:file_sizes/file_sizes.dart'; +import 'package:starcitizen_doctor/common/rust/api/downloader_api.dart'; import 'home_downloader_ui_model.dart'; @@ -13,59 +14,51 @@ class HomeDownloaderUI extends HookConsumerWidget { final state = ref.watch(homeDownloaderUIModelProvider); final model = ref.read(homeDownloaderUIModelProvider.notifier); - return makeDefaultPage(context, - content: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 12), - Row( - children: [ - const Spacer(), - const SizedBox(width: 24), - const SizedBox(width: 12), - for (final item in , String>{ - const MapEntry("settings", FluentIcons.settings): - S.current.downloader_speed_limit_settings, - if (state.tasks.isNotEmpty) - const MapEntry("pause_all", FluentIcons.pause): - S.current.downloader_action_pause_all, - if (state.waitingTasks.isNotEmpty) - const MapEntry("resume_all", FluentIcons.download): - S.current.downloader_action_resume_all, - if (state.tasks.isNotEmpty || state.waitingTasks.isNotEmpty) - const MapEntry("cancel_all", FluentIcons.cancel): - S.current.downloader_action_cancel_all, - }.entries) - Padding( - padding: const EdgeInsets.only(left: 6, right: 6), - child: Button( - child: Padding( - padding: const EdgeInsets.all(4), - child: Row( - children: [ - Icon(item.key.value), - const SizedBox(width: 6), - Text(item.value), - ], - ), - ), - onPressed: () => - model.onTapButton(context, item.key.key)), + return makeDefaultPage( + context, + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + Row( + children: [ + const Spacer(), + const SizedBox(width: 24), + const SizedBox(width: 12), + for (final item in , String>{ + const MapEntry("settings", FluentIcons.settings): S.current.downloader_speed_limit_settings, + if (state.activeTasks.isNotEmpty) + const MapEntry("pause_all", FluentIcons.pause): S.current.downloader_action_pause_all, + if (state.waitingTasks.isNotEmpty) + 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, + }.entries) + Padding( + padding: const EdgeInsets.only(left: 6, right: 6), + child: Button( + child: Padding( + padding: const EdgeInsets.all(4), + child: Row(children: [Icon(item.key.value), const SizedBox(width: 6), Text(item.value)]), + ), + onPressed: () => model.onTapButton(context, item.key.key), ), - const SizedBox(width: 12), - ], - ), - if (model.getTasksLen() == 0) - Expanded( - child: Center( - child: Text(S.current.downloader_info_no_download_tasks), - )) - else - Expanded( - child: ListView.builder( + ), + const SizedBox(width: 12), + ], + ), + if (model.getTasksLen() == 0) + Expanded(child: Center(child: Text(S.current.downloader_info_no_download_tasks))) + else + Expanded( + child: ListView.builder( itemBuilder: (BuildContext context, int index) { final (task, type, isFirstType) = model.getTaskAndType(index); final nt = HomeDownloaderUIModel.getTaskTypeAndName(task); + final statusStr = model.getStatusString(task.status); + final isActive = task.status == DownloadTaskStatus.live; + final isPaused = task.status == DownloadTaskStatus.paused; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -73,39 +66,30 @@ class HomeDownloaderUI extends HookConsumerWidget { Column( children: [ Container( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: index == 0 ? 0 : 12, - bottom: 12), + padding: EdgeInsets.only(left: 24, right: 24, top: index == 0 ? 0 : 12, bottom: 12), margin: const EdgeInsets.only(top: 6, bottom: 6), child: Row( children: [ Expanded( - child: Row( - children: [ - Text( - "${model.listHeaderStatusMap[type]}", - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold), - ), - ], - )), + child: Row( + children: [ + Text( + "${model.listHeaderStatusMap[type]}", + style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + ), + ], + ), + ), ], ), ), ], ), Container( - padding: const EdgeInsets.only( - left: 12, right: 12, top: 12, bottom: 12), - margin: const EdgeInsets.only( - left: 12, right: 12, top: 6, bottom: 6), + padding: const EdgeInsets.only(left: 12, right: 12, top: 12, bottom: 12), + margin: const EdgeInsets.only(left: 12, right: 12, top: 6, bottom: 6), decoration: BoxDecoration( - color: FluentTheme.of(context) - .cardColor - .withValues(alpha: .06), + color: FluentTheme.of(context).cardColor.withValues(alpha: .06), borderRadius: BorderRadius.circular(7), ), child: Row( @@ -113,47 +97,26 @@ class HomeDownloaderUI extends HookConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - nt.value, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold), - ), + Text(nt.value, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), const SizedBox(height: 6), Row( children: [ Text( - S.current.downloader_info_total_size( - FileSize.getSize( - task.totalLength ?? 0)), + S.current.downloader_info_total_size(FileSize.getSize(task.totalBytes.toInt())), style: const TextStyle(fontSize: 14), ), const SizedBox(width: 12), - if (nt.key == "torrent" && - task.verifiedLength != null && - task.verifiedLength != 0) + if (isActive) Text( - S.current.downloader_info_verifying( - FileSize.getSize( - task.verifiedLength)), - style: const TextStyle(fontSize: 14), + S.current.downloader_info_downloading((task.progress * 100).toStringAsFixed(2)), ) - else if (task.status == "active") - Text(S.current - .downloader_info_downloading( - ((task.completedLength ?? 0) * - 100 / - (task.totalLength ?? 1)) - .toStringAsFixed(4))) else - Text(S.current.downloader_info_status( - model.statusMap[task.status] ?? - "Unknown")), + Text(S.current.downloader_info_status(model.statusMap[statusStr] ?? "Unknown")), const SizedBox(width: 24), - if (task.status == "active" && - task.verifiedLength == null) + if (isActive) Text( - "ETA: ${model.formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}"), + "ETA: ${model.formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}", + ), ], ), ], @@ -162,20 +125,18 @@ class HomeDownloaderUI extends HookConsumerWidget { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.current.downloader_info_uploaded( - FileSize.getSize(task.uploadLength))), - Text(S.current.downloader_info_downloaded( - FileSize.getSize(task.completedLength))), + Text(S.current.downloader_info_uploaded(FileSize.getSize(task.uploadedBytes.toInt()))), + Text( + S.current.downloader_info_downloaded(FileSize.getSize(task.downloadedBytes.toInt())), + ), ], ), const SizedBox(width: 18), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "↑:${FileSize.getSize(task.uploadSpeed)}/s"), - Text( - "↓:${FileSize.getSize(task.downloadSpeed)}/s"), + Text("↑:${FileSize.getSize(task.uploadSpeed.toInt())}/s"), + Text("↓:${FileSize.getSize(task.downloadSpeed.toInt())}/s"), ], ), const SizedBox(width: 32), @@ -184,42 +145,32 @@ class HomeDownloaderUI extends HookConsumerWidget { closeAfterClick: false, title: Padding( padding: const EdgeInsets.all(3), - child: - Text(S.current.downloader_action_options), + child: Text(S.current.downloader_action_options), ), items: [ - if (task.status == "paused") + if (isPaused) MenuFlyoutItem( - leading: - const Icon(FluentIcons.download), - text: Text(S.current - .downloader_action_continue_download), - onPressed: () => - model.resumeTask(task.gid)) - else if (task.status == "active") + leading: const Icon(FluentIcons.download), + text: Text(S.current.downloader_action_continue_download), + onPressed: () => model.resumeTask(task.id.toInt()), + ) + else if (isActive) MenuFlyoutItem( - leading: const Icon(FluentIcons.pause), - text: Text(S.current - .downloader_action_pause_download), - onPressed: () => - model.pauseTask(task.gid)), + leading: const Icon(FluentIcons.pause), + text: Text(S.current.downloader_action_pause_download), + onPressed: () => model.pauseTask(task.id.toInt()), + ), const MenuFlyoutSeparator(), MenuFlyoutItem( - leading: const Icon( - FluentIcons.chrome_close, - size: 14, - ), - text: Text(S.current - .downloader_action_cancel_download), - onPressed: () => - model.cancelTask(context, task.gid)), + leading: const Icon(FluentIcons.chrome_close, size: 14), + text: Text(S.current.downloader_action_cancel_download), + onPressed: () => model.cancelTask(context, task.id.toInt()), + ), MenuFlyoutItem( - leading: const Icon( - FluentIcons.folder_open, - size: 14, - ), - text: Text(S.current.action_open_folder), - onPressed: () => model.openFolder(task)), + leading: const Icon(FluentIcons.folder_open, size: 14), + text: Text(S.current.action_open_folder), + onPressed: () => model.openFolder(task), + ), ], ), const SizedBox(width: 12), @@ -230,31 +181,36 @@ class HomeDownloaderUI extends HookConsumerWidget { ); }, itemCount: model.getTasksLen(), - )), - Container( - color: FluentTheme.of(context).cardColor.withValues(alpha: .06), - child: Padding( - padding: const EdgeInsets.only(left: 12, bottom: 3, top: 3), - child: Row( - children: [ - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - color: state.isAvailable ? Colors.green : Colors.white, - borderRadius: BorderRadius.circular(1000), - ), - ), - const SizedBox(width: 12), - Text(S.current.downloader_info_download_upload_speed( - FileSize.getSize(state.globalStat?.downloadSpeed ?? 0), - FileSize.getSize(state.globalStat?.uploadSpeed ?? 0))) - ], - ), ), ), - ], - ), - useBodyContainer: true); + Container( + color: FluentTheme.of(context).cardColor.withValues(alpha: .06), + child: Padding( + padding: const EdgeInsets.only(left: 12, bottom: 3, top: 3), + child: Row( + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: state.isAvailable ? Colors.green : Colors.white, + borderRadius: BorderRadius.circular(1000), + ), + ), + const SizedBox(width: 12), + Text( + S.current.downloader_info_download_upload_speed( + FileSize.getSize((state.globalStat?.downloadSpeed ?? BigInt.zero).toInt()), + FileSize.getSize((state.globalStat?.uploadSpeed ?? BigInt.zero).toInt()), + ), + ), + ], + ), + ), + ), + ], + ), + useBodyContainer: true, + ); } } diff --git a/lib/ui/home/downloader/home_downloader_ui_model.dart b/lib/ui/home/downloader/home_downloader_ui_model.dart index 0d3dbcc..f44863c 100644 --- a/lib/ui/home/downloader/home_downloader_ui_model.dart +++ b/lib/ui/home/downloader/home_downloader_ui_model.dart @@ -1,7 +1,5 @@ // ignore_for_file: avoid_build_context_in_providers, avoid_public_notifier_properties -import 'dart:io'; -import 'package:aria2/aria2.dart'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; @@ -9,9 +7,9 @@ import 'package:hive_ce/hive.dart'; import 'package:intl/intl.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/rust/api/downloader_api.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; -import 'package:starcitizen_doctor/common/utils/provider.dart'; -import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/provider/download_manager.dart'; import '../../../widgets/widgets.dart'; @@ -22,10 +20,10 @@ part 'home_downloader_ui_model.freezed.dart'; @freezed abstract class HomeDownloaderUIState with _$HomeDownloaderUIState { factory HomeDownloaderUIState({ - @Default([]) List tasks, - @Default([]) List waitingTasks, - @Default([]) List stoppedTasks, - Aria2GlobalStat? globalStat, + @Default([]) List activeTasks, + @Default([]) List waitingTasks, + @Default([]) List stoppedTasks, + DownloadGlobalStat? globalStat, }) = _HomeDownloaderUIState; } @@ -40,12 +38,11 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { bool _disposed = false; final statusMap = { - "active": S.current.downloader_info_downloading_status, - "waiting": S.current.downloader_info_waiting, + "live": S.current.downloader_info_downloading_status, + "initializing": S.current.downloader_info_waiting, "paused": S.current.downloader_info_paused, "error": S.current.downloader_info_download_failed, - "complete": S.current.downloader_info_download_completed, - "removed": S.current.downloader_info_deleted, + "finished": S.current.downloader_info_download_completed, }; final listHeaderStatusMap = { @@ -65,17 +62,17 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } Future onTapButton(BuildContext context, String key) async { - final aria2cState = ref.read(aria2cModelProvider); + final downloadManagerState = ref.read(downloadManagerProvider); + final downloadManager = ref.read(downloadManagerProvider.notifier); + switch (key) { case "pause_all": - if (!aria2cState.isRunning) return; - await aria2cState.aria2c?.pauseAll(); - await aria2cState.aria2c?.saveSession(); + if (!downloadManagerState.isRunning) return; + await downloadManager.pauseAll(); return; case "resume_all": - if (!aria2cState.isRunning) return; - await aria2cState.aria2c?.unpauseAll(); - await aria2cState.aria2c?.saveSession(); + if (!downloadManagerState.isRunning) return; + await downloadManager.resumeAll(); return; case "cancel_all": final userOK = await showConfirmDialogs( @@ -84,12 +81,12 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { Text(S.current.downloader_info_manual_file_deletion_note), ); if (userOK == true) { - if (!aria2cState.isRunning) return; + if (!downloadManagerState.isRunning) return; try { - for (var value in [...state.tasks, ...state.waitingTasks]) { - await aria2cState.aria2c?.remove(value.gid!); + final allTasks = [...state.activeTasks, ...state.waitingTasks]; + for (var task in allTasks) { + await downloadManager.removeTask(task.id.toInt(), deleteFiles: false); } - await aria2cState.aria2c?.saveSession(); } catch (e) { dPrint("DownloadsUIModel cancel_all Error: $e"); } @@ -102,91 +99,77 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { } int getTasksLen() { - return state.tasks.length + state.waitingTasks.length + state.stoppedTasks.length; + return state.activeTasks.length + state.waitingTasks.length + state.stoppedTasks.length; } - (Aria2Task, String, bool) getTaskAndType(int index) { - final tempList = [...state.tasks, ...state.waitingTasks, ...state.stoppedTasks]; - if (index >= 0 && index < state.tasks.length) { + (DownloadTaskInfo, String, bool) getTaskAndType(int index) { + final tempList = [...state.activeTasks, ...state.waitingTasks, ...state.stoppedTasks]; + if (index >= 0 && index < state.activeTasks.length) { return (tempList[index], "active", index == 0); } - if (index >= state.tasks.length && index < state.tasks.length + state.waitingTasks.length) { - return (tempList[index], "waiting", index == state.tasks.length); + if (index >= state.activeTasks.length && index < state.activeTasks.length + state.waitingTasks.length) { + return (tempList[index], "waiting", index == state.activeTasks.length); } - if (index >= state.tasks.length + state.waitingTasks.length && index < tempList.length) { - return (tempList[index], "stopped", index == state.tasks.length + state.waitingTasks.length); + if (index >= state.activeTasks.length + state.waitingTasks.length && index < tempList.length) { + return (tempList[index], "stopped", index == state.activeTasks.length + state.waitingTasks.length); } throw Exception("Index out of range or element is null"); } - static MapEntry getTaskTypeAndName(Aria2Task task) { - if (task.bittorrent == null) { - String uri = task.files?[0]['uris'][0]['uri'] as String; - return MapEntry("url", uri.split('/').last); - } else if (task.bittorrent != null) { - if (task.bittorrent!.containsKey('info')) { - var btName = task.bittorrent?["info"]["name"]; - return MapEntry("torrent", btName ?? 'torrent'); - } else { - return MapEntry("magnet", '[METADATA]${task.infoHash}'); - } - } else { - return const MapEntry("metaLink", '==========metaLink============'); + static MapEntry getTaskTypeAndName(DownloadTaskInfo task) { + // All tasks in rqbit are torrent-based + return MapEntry("torrent", task.name); + } + + int getETA(DownloadTaskInfo task) { + if (task.downloadSpeed == BigInt.zero) return 0; + final remainingBytes = task.totalBytes - task.downloadedBytes; + return (remainingBytes ~/ task.downloadSpeed).toInt(); + } + + String getStatusString(DownloadTaskStatus status) { + switch (status) { + case DownloadTaskStatus.live: + return "live"; + case DownloadTaskStatus.initializing: + return "initializing"; + case DownloadTaskStatus.paused: + return "paused"; + case DownloadTaskStatus.error: + return "error"; + case DownloadTaskStatus.finished: + return "finished"; } } - int getETA(Aria2Task task) { - if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0; - final remainingBytes = (task.totalLength ?? 0) - (task.completedLength ?? 0); - return remainingBytes ~/ (task.downloadSpeed!); + Future resumeTask(int taskId) async { + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.resumeTask(taskId); } - Future resumeTask(String? gid) async { - final aria2c = ref.read(aria2cModelProvider).aria2c; - if (gid != null) { - await aria2c?.unpause(gid); - } + Future pauseTask(int taskId) async { + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.pauseTask(taskId); } - Future pauseTask(String? gid) async { - final aria2c = ref.read(aria2cModelProvider).aria2c; - if (gid != null) { - await aria2c?.pause(gid); - } - } - - Future cancelTask(BuildContext context, String? gid) async { + Future cancelTask(BuildContext context, int taskId) async { await Future.delayed(const Duration(milliseconds: 300)); - if (gid != null) { - if (!context.mounted) return; - final ok = await showConfirmDialogs( - context, - S.current.downloader_action_confirm_cancel_download, - Text(S.current.downloader_info_manual_file_deletion_note), - ); - if (ok == true) { - final aria2c = ref.read(aria2cModelProvider).aria2c; - await aria2c?.remove(gid); - await aria2c?.saveSession(); - } + if (!context.mounted) return; + final ok = await showConfirmDialogs( + context, + S.current.downloader_action_confirm_cancel_download, + Text(S.current.downloader_info_manual_file_deletion_note), + ); + if (ok == true) { + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.removeTask(taskId, deleteFiles: false); } } - List getFilesFormTask(Aria2Task task) { - List l = []; - if (task.files != null) { - for (var element in task.files!) { - final f = Aria2File.fromJson(element); - l.add(f); - } - } - return l; - } - - void openFolder(Aria2Task task) { - final f = getFilesFormTask(task).firstOrNull; - if (f != null) { - SystemHelper.openDir(File(f.path!).absolute.path.replaceAll("/", "\\")); + void openFolder(DownloadTaskInfo task) { + final outputFolder = task.outputFolder; + if (outputFolder.isNotEmpty) { + SystemHelper.openDir(outputFolder.replaceAll("/", "\\")); } } @@ -194,21 +177,39 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { try { while (true) { if (_disposed) return; - final aria2cState = ref.read(aria2cModelProvider); - if (aria2cState.isRunning) { - final aria2c = aria2cState.aria2c!; - final tasks = await aria2c.tellActive(); - final waitingTasks = await aria2c.tellWaiting(0, 1000000); - final stoppedTasks = await aria2c.tellStopped(0, 1000000); - final globalStat = await aria2c.getGlobalStat(); + final downloadManagerState = ref.read(downloadManagerProvider); + if (downloadManagerState.isRunning) { + final downloadManager = ref.read(downloadManagerProvider.notifier); + final allTasks = await downloadManager.getAllTasks(); + + final activeTasks = []; + final waitingTasks = []; + final stoppedTasks = []; + + for (var task in allTasks) { + switch (task.status) { + case DownloadTaskStatus.live: + activeTasks.add(task); + break; + case DownloadTaskStatus.initializing: + case DownloadTaskStatus.paused: + waitingTasks.add(task); + break; + case DownloadTaskStatus.finished: + case DownloadTaskStatus.error: + stoppedTasks.add(task); + break; + } + } + state = state.copyWith( - tasks: tasks, + activeTasks: activeTasks, waitingTasks: waitingTasks, stoppedTasks: stoppedTasks, - globalStat: globalStat, + globalStat: downloadManagerState.globalStat, ); } else { - state = state.copyWith(tasks: [], waitingTasks: [], stoppedTasks: [], globalStat: null); + state = state.copyWith(activeTasks: [], waitingTasks: [], stoppedTasks: [], globalStat: null); } await Future.delayed(const Duration(seconds: 1)); } @@ -266,22 +267,14 @@ class HomeDownloaderUIModel extends _$HomeDownloaderUIModel { ), ); if (ok == true) { - final aria2cState = ref.read(aria2cModelProvider); - final aria2cModel = ref.read(aria2cModelProvider.notifier); - await aria2cModel.launchDaemon(appGlobalState.applicationBinaryModuleDir!); - final aria2c = aria2cState.aria2c!; - final upByte = aria2cModel.textToByte(upCtrl.text.trim()); - final downByte = aria2cModel.textToByte(downCtrl.text.trim()); - final r = await aria2c - .changeGlobalOption( - Aria2Option() - ..maxOverallUploadLimit = upByte - ..maxOverallDownloadLimit = downByte, - ) - .unwrap(); - if (r != null) { - await box.put('downloader_up_limit', upCtrl.text.trim()); - await box.put('downloader_down_limit', downCtrl.text.trim()); + // Note: rqbit doesn't support dynamic speed limit changes yet + // Just save the settings for now + await box.put('downloader_up_limit', upCtrl.text.trim()); + await box.put('downloader_down_limit', downCtrl.text.trim()); + + // Show info that speed limits will apply on next restart + if (context.mounted) { + showToast(context, "Speed limit settings saved. Will apply on next download."); } } } 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 d0d38bf..57a3db1 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 tasks; List get waitingTasks; List get stoppedTasks; Aria2GlobalStat? get globalStat; + List get activeTasks; List get waitingTasks; List get stoppedTasks; 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.tasks, tasks)&&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.stoppedTasks, stoppedTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(tasks),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(stoppedTasks),globalStat); @override String toString() { - return 'HomeDownloaderUIState(tasks: $tasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; + return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; } @@ -45,7 +45,7 @@ abstract mixin class $HomeDownloaderUIStateCopyWith<$Res> { factory $HomeDownloaderUIStateCopyWith(HomeDownloaderUIState value, $Res Function(HomeDownloaderUIState) _then) = _$HomeDownloaderUIStateCopyWithImpl; @useResult $Res call({ - List tasks, List waitingTasks, List stoppedTasks, Aria2GlobalStat? globalStat + List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat }); @@ -62,13 +62,13 @@ 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? tasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { return _then(_self.copyWith( -tasks: null == tasks ? _self.tasks : tasks // 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,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable -as Aria2GlobalStat?, +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,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable +as DownloadGlobalStat?, )); } @@ -153,10 +153,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( List tasks, List waitingTasks, List stoppedTasks, Aria2GlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _HomeDownloaderUIState() when $default != null: -return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: return orElse(); } @@ -174,10 +174,10 @@ return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalSt /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( List tasks, List waitingTasks, List stoppedTasks, Aria2GlobalStat? globalStat) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat) $default,) {final _that = this; switch (_that) { case _HomeDownloaderUIState(): -return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: throw StateError('Unexpected subclass'); } @@ -194,10 +194,10 @@ return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalSt /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( List tasks, List waitingTasks, List stoppedTasks, Aria2GlobalStat? globalStat)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat)? $default,) {final _that = this; switch (_that) { case _HomeDownloaderUIState() when $default != null: -return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: +return $default(_that.activeTasks,_that.waitingTasks,_that.stoppedTasks,_that.globalStat);case _: return null; } @@ -209,31 +209,31 @@ return $default(_that.tasks,_that.waitingTasks,_that.stoppedTasks,_that.globalSt class _HomeDownloaderUIState implements HomeDownloaderUIState { - _HomeDownloaderUIState({final List tasks = const [], final List waitingTasks = const [], final List stoppedTasks = const [], this.globalStat}): _tasks = tasks,_waitingTasks = waitingTasks,_stoppedTasks = stoppedTasks; + _HomeDownloaderUIState({final List activeTasks = const [], final List waitingTasks = const [], final List stoppedTasks = const [], this.globalStat}): _activeTasks = activeTasks,_waitingTasks = waitingTasks,_stoppedTasks = stoppedTasks; - final List _tasks; -@override@JsonKey() List get tasks { - if (_tasks is EqualUnmodifiableListView) return _tasks; + final List _activeTasks; +@override@JsonKey() List get activeTasks { + if (_activeTasks is EqualUnmodifiableListView) return _activeTasks; // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(_tasks); + return EqualUnmodifiableListView(_activeTasks); } - final List _waitingTasks; -@override@JsonKey() List get waitingTasks { + final List _waitingTasks; +@override@JsonKey() List get waitingTasks { if (_waitingTasks is EqualUnmodifiableListView) return _waitingTasks; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_waitingTasks); } - final List _stoppedTasks; -@override@JsonKey() List get stoppedTasks { + final List _stoppedTasks; +@override@JsonKey() List get stoppedTasks { if (_stoppedTasks is EqualUnmodifiableListView) return _stoppedTasks; // ignore: implicit_dynamic_type return EqualUnmodifiableListView(_stoppedTasks); } -@override final Aria2GlobalStat? globalStat; +@override final DownloadGlobalStat? globalStat; /// Create a copy of HomeDownloaderUIState /// with the given fields replaced by the non-null parameter values. @@ -245,16 +245,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._tasks, _tasks)&&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._stoppedTasks, _stoppedTasks)&&(identical(other.globalStat, globalStat) || other.globalStat == globalStat)); } @override -int get hashCode => Object.hash(runtimeType,const DeepCollectionEquality().hash(_tasks),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(_stoppedTasks),globalStat); @override String toString() { - return 'HomeDownloaderUIState(tasks: $tasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; + return 'HomeDownloaderUIState(activeTasks: $activeTasks, waitingTasks: $waitingTasks, stoppedTasks: $stoppedTasks, globalStat: $globalStat)'; } @@ -265,7 +265,7 @@ abstract mixin class _$HomeDownloaderUIStateCopyWith<$Res> implements $HomeDownl factory _$HomeDownloaderUIStateCopyWith(_HomeDownloaderUIState value, $Res Function(_HomeDownloaderUIState) _then) = __$HomeDownloaderUIStateCopyWithImpl; @override @useResult $Res call({ - List tasks, List waitingTasks, List stoppedTasks, Aria2GlobalStat? globalStat + List activeTasks, List waitingTasks, List stoppedTasks, DownloadGlobalStat? globalStat }); @@ -282,13 +282,13 @@ 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? tasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? activeTasks = null,Object? waitingTasks = null,Object? stoppedTasks = null,Object? globalStat = freezed,}) { return _then(_HomeDownloaderUIState( -tasks: null == tasks ? _self._tasks : tasks // 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,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable -as Aria2GlobalStat?, +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,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 67e4a2a..f386c68 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'cb5d0973d56bbf40673afc2a734b49f5d034ab98'; + r'27e2e4b7a5103eee9d489a347410131edef46be4'; 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 2336c54..eef6555 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 @@ -1,6 +1,5 @@ // ignore_for_file: avoid_build_context_in_providers, use_build_context_synchronously import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:flutter/services.dart'; @@ -14,7 +13,7 @@ import 'package:starcitizen_doctor/common/utils/async.dart'; import 'package:starcitizen_doctor/common/utils/base_utils.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; -import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/provider/download_manager.dart'; import 'package:starcitizen_doctor/ui/home/localization/localization_ui_model.dart'; import 'package:starcitizen_doctor/common/rust/api/ort_api.dart' as ort; @@ -240,11 +239,10 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { Future doDownloadTranslateModel() async { state = state.copyWith(isAutoTranslateWorking: true); try { - final aria2cManager = ref.read(aria2cModelProvider.notifier); - await aria2cManager.launchDaemon(appGlobalState.applicationBinaryModuleDir!); - final aria2c = ref.read(aria2cModelProvider).aria2c!; + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.initDownloader(); - if (await aria2cManager.isNameInTask(_localTranslateModelName)) { + if (await downloadManager.isNameInTask(_localTranslateModelName)) { throw Exception("Model is already downloading"); } @@ -259,9 +257,8 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { } // get torrent Data final data = await RSHttp.get(torrentUrl!); - final b64Str = base64Encode(data.data!); - final gid = await aria2c.addTorrent(b64Str, extraParams: {"dir": _localTranslateModelDir}); - return gid; + final taskId = await downloadManager.addTorrent(data.data!, outputFolder: _localTranslateModelDir); + return taskId.toString(); } catch (e) { dPrint("[InputMethodDialogUIModel] doDownloadTranslateModel error: $e"); rethrow; @@ -330,8 +327,8 @@ class InputMethodDialogUIModel extends _$InputMethodDialogUIModel { } Future isTranslateModelDownloading() async { - final aria2cManager = ref.read(aria2cModelProvider.notifier); - return await aria2cManager.isNameInTask(_localTranslateModelName); + final downloadManager = ref.read(downloadManagerProvider.notifier); + return await downloadManager.isNameInTask(_localTranslateModelName); } } 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 857c266..a607b04 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'bd96c85ef2073d80de6eba71748b41adb8861e1c'; + r'5c2989faf94d43bb814e5b80e10d68416c8241ec'; abstract class _$InputMethodDialogUIModel extends $Notifier { diff --git a/lib/ui/index_ui.dart b/lib/ui/index_ui.dart index 9114519..e7d7f5b 100644 --- a/lib/ui/index_ui.dart +++ b/lib/ui/index_ui.dart @@ -4,7 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:starcitizen_doctor/app.dart'; import 'package:starcitizen_doctor/common/conf/conf.dart'; -import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/provider/download_manager.dart'; import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/ui/party_room/party_room_ui.dart'; import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart'; @@ -61,7 +61,7 @@ class IndexUI extends HookConsumerWidget { padding: const EdgeInsets.all(6), child: Icon(FluentIcons.installation, size: 22, color: Colors.white.withValues(alpha: .6)), ), - _makeAria2TaskNumWidget(), + _makeDownloadTaskNumWidget(), ], ), onPressed: () => _goDownloader(context), @@ -124,11 +124,11 @@ class IndexUI extends HookConsumerWidget { curIndexState.value = pageIndex; } - Widget _makeAria2TaskNumWidget() { + Widget _makeDownloadTaskNumWidget() { return Consumer( builder: (BuildContext context, WidgetRef ref, Widget? child) { - final aria2cState = ref.watch(aria2cModelProvider); - if (!aria2cState.hasDownloadTask) { + final downloadState = ref.watch(downloadManagerProvider); + if (!downloadState.hasDownloadTask) { return const SizedBox(); } return Positioned( @@ -137,7 +137,7 @@ class IndexUI extends HookConsumerWidget { child: Container( decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(12)), padding: const EdgeInsets.only(left: 6, right: 6, bottom: 1.5, top: 1.5), - child: Text("${aria2cState.aria2TotalTaskNum}", style: const TextStyle(fontSize: 8, color: Colors.white)), + child: Text("${downloadState.totalTaskNum}", style: const TextStyle(fontSize: 8, color: Colors.white)), ), ); }, diff --git a/lib/ui/splash_ui.dart b/lib/ui/splash_ui.dart index 56e41ae..15b22dd 100644 --- a/lib/ui/splash_ui.dart +++ b/lib/ui/splash_ui.dart @@ -13,7 +13,7 @@ import 'package:starcitizen_doctor/app.dart'; import 'package:starcitizen_doctor/common/conf/conf.dart'; import 'package:starcitizen_doctor/common/conf/url_conf.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; -import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/provider/download_manager.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; class SplashUI extends HookConsumerWidget { @@ -42,36 +42,36 @@ class SplashUI extends HookConsumerWidget { child: diagnosticMode.value ? _buildDiagnosticView(diagnosticLogs, step, context) : Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - onTap: () { - final now = DateTime.now(); - final lastClick = lastClickTime.value; + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + onTap: () { + final now = DateTime.now(); + final lastClick = lastClickTime.value; - // 重置计数器如果距离上次点击超过2秒 - if (lastClick != null && now.difference(lastClick).inSeconds > 2) { - clickCount.value = 0; - } + // 重置计数器如果距离上次点击超过2秒 + if (lastClick != null && now.difference(lastClick).inSeconds > 2) { + clickCount.value = 0; + } - lastClickTime.value = now; - clickCount.value++; + lastClickTime.value = now; + clickCount.value++; - if (clickCount.value >= 10) { - diagnosticMode.value = true; - clickCount.value = 0; - } - }, - child: Image.asset("assets/app_logo.png", width: 192, height: 192), - ), - const SizedBox(height: 32), - const ProgressRing(), - const SizedBox(height: 32), - if (step == 0) Text(S.current.app_splash_checking_availability), - if (step == 1) Text(S.current.app_splash_checking_for_updates), - if (step == 2) Text(S.current.app_splash_almost_done) - ], - ), + if (clickCount.value >= 10) { + diagnosticMode.value = true; + clickCount.value = 0; + } + }, + child: Image.asset("assets/app_logo.png", width: 192, height: 192), + ), + const SizedBox(height: 32), + const ProgressRing(), + const SizedBox(height: 32), + if (step == 0) Text(S.current.app_splash_checking_availability), + if (step == 1) Text(S.current.app_splash_checking_for_updates), + if (step == 2) Text(S.current.app_splash_almost_done), + ], + ), ), automaticallyImplyLeading: false, titleRow: Align( @@ -124,26 +124,28 @@ class SplashUI extends HookConsumerWidget { child: logs.isEmpty ? Center(child: Text(S.current.splash_waiting_log)) : ListView.builder( - itemCount: logs.length, - itemBuilder: (context, index) { - final log = logs[index]; - Color textColor = Colors.white; - if (log.contains('✓')) { - textColor = Colors.green; - } else if (log.contains('✗') || log.contains(S.current.splash_timeout) || log.contains(S.current.splash_error)) { - textColor = Colors.red; - } else if (log.contains('⚠')) { - textColor = Colors.orange; - } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Text( - log, - style: TextStyle(fontFamily: 'Consolas', fontSize: 12, color: textColor), + itemCount: logs.length, + itemBuilder: (context, index) { + final log = logs[index]; + Color textColor = Colors.white; + if (log.contains('✓')) { + textColor = Colors.green; + } else if (log.contains('✗') || + log.contains(S.current.splash_timeout) || + log.contains(S.current.splash_error)) { + textColor = Colors.red; + } else if (log.contains('⚠')) { + textColor = Colors.orange; + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2), + child: Text( + log, + style: TextStyle(fontFamily: 'Consolas', fontSize: 12, color: textColor), + ), + ); + }, ), - ); - }, - ), ); }, ), @@ -154,12 +156,12 @@ class SplashUI extends HookConsumerWidget { } void _initApp( - BuildContext context, - AppGlobalModel appModel, - ValueNotifier stepState, - WidgetRef ref, - ValueNotifier> diagnosticLogs, - ) async { + BuildContext context, + AppGlobalModel appModel, + ValueNotifier stepState, + WidgetRef ref, + ValueNotifier> diagnosticLogs, + ) async { void addLog(String message) { final logMessage = '[${DateTime.now().toString().substring(11, 23)}] $message'; diagnosticLogs.value = [...diagnosticLogs.value, logMessage]; @@ -263,12 +265,12 @@ class SplashUI extends HookConsumerWidget { await appModel .checkUpdate(context) .timeout( - const Duration(seconds: 10), - onTimeout: () { - addLog(S.current.splash_check_update_timeout); - return false; - }, - ); + const Duration(seconds: 10), + onTimeout: () { + addLog(S.current.splash_check_update_timeout); + return false; + }, + ); addLog(S.current.splash_check_update_done); } catch (e) { addLog('⚠ appModel.checkUpdate() 错误: $e - 继续执行'); @@ -277,14 +279,14 @@ class SplashUI extends HookConsumerWidget { addLog(S.current.splash_step1_done); stepState.value = 2; - // Step 2: Initialize aria2c + // Step 2: Initialize download manager addLog(S.current.splash_init_aria2c); - dPrint("_initApp aria2cModelProvider"); + dPrint("_initApp downloadManagerProvider"); try { - ref.read(aria2cModelProvider); + ref.read(downloadManagerProvider); addLog(S.current.splash_aria2c_done); } catch (e) { - addLog('⚠ aria2cModelProvider 初始化错误: $e'); + addLog('⚠ downloadManagerProvider 初始化错误: $e'); } if (!context.mounted) { @@ -385,4 +387,4 @@ class SplashUI extends HookConsumerWidget { dPrint(S.current.splash_reset_db_failed(e.toString())); } } -} \ No newline at end of file +} diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index a3edebd..c1fa3c6 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -18,7 +18,7 @@ import 'package:starcitizen_doctor/common/io/rs_http.dart'; import 'package:starcitizen_doctor/common/utils/log.dart'; import 'package:starcitizen_doctor/common/utils/multi_window_manager.dart'; import 'package:starcitizen_doctor/common/utils/provider.dart'; -import 'package:starcitizen_doctor/provider/aria2c.dart'; +import 'package:starcitizen_doctor/provider/download_manager.dart'; import 'package:starcitizen_doctor/widgets/widgets.dart'; import 'package:url_launcher/url_launcher_string.dart'; import 'package:xml/xml.dart'; @@ -27,7 +27,6 @@ import 'dialogs/hosts_booster_dialog_ui.dart'; import 'dialogs/rsi_launcher_enhance_dialog_ui.dart'; part 'tools_ui_model.g.dart'; - part 'tools_ui_model.freezed.dart'; class ToolsItemData { @@ -577,12 +576,11 @@ class ToolsUIModel extends _$ToolsUIModel { if (!ok) return; try { state = state.copyWith(working: true); - final aria2cManager = ref.read(aria2cModelProvider.notifier); - await aria2cManager.launchDaemon(appGlobalState.applicationBinaryModuleDir!); - final aria2c = ref.read(aria2cModelProvider).aria2c!; + final downloadManager = ref.read(downloadManagerProvider.notifier); + await downloadManager.initDownloader(); // check download task list - if (await aria2cManager.isNameInTask("Data.p4k")) { + if (await downloadManager.isNameInTask("Data.p4k")) { if (!context.mounted) return; showToast(context, S.current.tools_action_info_p4k_download_in_progress); state = state.copyWith(working: false); @@ -619,12 +617,10 @@ class ToolsUIModel extends _$ToolsUIModel { state = state.copyWith(working: false); return; } - final b64Str = base64Encode(btData.data!); - final gid = await aria2c.addTorrent(b64Str, extraParams: {"dir": savePath}); + final taskId = await downloadManager.addTorrent(btData.data!, outputFolder: savePath); state = state.copyWith(working: false); - dPrint("Aria2cManager.aria2c.addUri resp === $gid"); - await aria2c.saveSession(); + dPrint("DownloadManager.addTorrent resp === $taskId"); AnalyticsApi.touch("p4k_download"); if (!context.mounted) return; context.push("/index/downloader"); diff --git a/lib/ui/tools/tools_ui_model.g.dart b/lib/ui/tools/tools_ui_model.g.dart index 462698f..3de238c 100644 --- a/lib/ui/tools/tools_ui_model.g.dart +++ b/lib/ui/tools/tools_ui_model.g.dart @@ -41,7 +41,7 @@ final class ToolsUIModelProvider } } -String _$toolsUIModelHash() => r'ee1de3d555443f72b4fbb395a5728b2de1e8aaf4'; +String _$toolsUIModelHash() => r'17890d6d16d8e9d98c42d06d9e6c6165965bcee0'; abstract class _$ToolsUIModel extends $Notifier { ToolsUIState build(); diff --git a/pubspec.lock b/pubspec.lock index d148b65..03e70f9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,15 +57,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" - aria2: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: b274c4c25e7ab49940b43d20e2fd335cfeef1c37 - url: "https://github.com/xkeyC/dart_aria2_rpc.git" - source: git - version: "0.1.3" async: dependency: transitive description: @@ -774,14 +765,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.0" - json_rpc_2: - dependency: transitive - description: - name: json_rpc_2 - sha256: "246b321532f0e8e2ba474b4d757eaa558ae4fdd0688fdbc1e1ca9705f9b8ca0e" - url: "https://pub.dev" - source: hosted - version: "3.0.3" json_serializable: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 021b3b6..443bfbc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -55,9 +55,7 @@ dependencies: fixnum: ^1.1.1 rust_builder: path: rust_builder - aria2: - git: https://github.com/xkeyC/dart_aria2_rpc.git - # path: ../../xkeyC/dart_aria2_rpc + # aria2 has been replaced by rqbit (Rust-based torrent library) intl: ^0.20.2 synchronized: ^3.4.0 super_sliver_list: ^0.4.1 diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 25e45b2..7b3c3ab 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -62,6 +62,12 @@ dependencies = [ "backtrace", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -153,6 +159,18 @@ dependencies = [ "derive_arbitrary", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "asar" version = "0.3.0" @@ -173,6 +191,15 @@ dependencies = [ "wax", ] +[[package]] +name = "assert_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04e2651f366b7ee3f97729fded1441539b49d5f39eeb05b842689e11e84501b2" +dependencies = [ + "const_panic", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -300,6 +327,28 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + [[package]] name = "async-task" version = "4.7.1" @@ -340,6 +389,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + [[package]] name = "atomic" version = "0.5.3" @@ -358,6 +416,113 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b5ce75405893cd713f9ab8e297d8e438f624dde7d706108285f7e17a25a180f" +dependencies = [ + "aws-lc-sys", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "179c3777a8b5e70e90ea426114ffc565b2c1a9f82f6c4a0c5a34aa6ef5e781b6" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "axum" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b098575ebe77cb6d14fc7f32749631a6e44edbef6b796f89b020e99ba20d425" +dependencies = [ + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9963ff19f40c6102c76756ef0a46004c0d58957d87259fc9208ff8441c12ab96" +dependencies = [ + "axum", + "axum-core", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "serde_core", + "serde_html_form", + "serde_path_to_error", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -391,6 +556,26 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -403,13 +588,25 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -418,7 +615,7 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" dependencies = [ - "generic-array", + "generic-array 0.14.7", ] [[package]] @@ -450,6 +647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" dependencies = [ "memchr", + "regex-automata", "serde", ] @@ -581,6 +779,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + [[package]] name = "chrono" version = "0.4.42" @@ -588,8 +797,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link 0.2.1", ] @@ -643,6 +854,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + [[package]] name = "color-eyre" version = "0.6.5" @@ -770,6 +990,15 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "const_panic" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262cdaac42494e3ae34c43969f9cdeb7da178bdb4b66fa6a1ea2edb4c8ae652" +dependencies = [ + "typewit", +] + [[package]] name = "constant_time_eq" version = "0.3.1" @@ -940,7 +1169,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ - "generic-array", + "generic-array 0.14.7", "typenum", ] @@ -1072,6 +1301,21 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "serde", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -1181,6 +1425,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "6.0.0" @@ -1261,6 +1514,18 @@ dependencies = [ "litrs", ] +[[package]] +name = "dontfrag" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117949f5a8b25ba471b4dfea927a07a2f8730c585532a2eb4294e18f5155397" +dependencies = [ + "cfg-if", + "libc", + "tokio", + "windows-sys 0.52.0", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1522,6 +1787,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1573,6 +1844,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1667,6 +1950,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + [[package]] name = "futures-util" version = "0.3.31" @@ -1793,6 +2082,15 @@ dependencies = [ "x11", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1945,6 +2243,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1956,6 +2266,29 @@ dependencies = [ "system-deps", ] +[[package]] +name = "governor" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e23d5986fd4364c2fb7498523540618b4b8d92eec6c36a02e565f66748e2f79" +dependencies = [ + "cfg-if", + "dashmap 6.1.0", + "futures-sink", + "futures-timer", + "futures-util", + "getrandom 0.3.4", + "hashbrown 0.16.1", + "nonzero_ext", + "parking_lot", + "portable-atomic", + "quanta", + "rand 0.9.2", + "smallvec 1.15.1", + "spinning_top", + "web-time", +] + [[package]] name = "gtk" version = "0.18.2" @@ -2044,6 +2377,11 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] [[package]] name = "heck" @@ -2175,6 +2513,12 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "hyper" version = "1.8.1" @@ -2189,6 +2533,7 @@ dependencies = [ "http", "http-body", "httparse", + "httpdate", "itoa", "pin-project-lite", "pin-utils", @@ -2450,7 +2795,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" dependencies = [ "block-padding", - "generic-array", + "generic-array 0.14.7", +] + +[[package]] +name = "intervaltree" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "270bc34e57047cab801a8c871c124d9dc7132f6473c6401f645524f4e6edd111" +dependencies = [ + "smallvec 1.15.1", ] [[package]] @@ -2603,6 +2957,17 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "leaky-bucket" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a396bb213c2d09ed6c5495fd082c991b6ab39c9daf4fff59e6727f85c73e4c5" +dependencies = [ + "parking_lot", + "pin-project-lite", + "tokio", +] + [[package]] name = "libbz2-rs-sys" version = "0.2.2" @@ -2626,6 +2991,302 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "librqbit" +version = "9.0.0-beta.1" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "arc-swap", + "async-compression", + "async-stream", + "async-trait", + "axum-extra", + "backon", + "base64 0.22.1", + "bincode", + "bitvec", + "byteorder", + "bytes", + "dashmap 6.1.0", + "futures", + "governor", + "hex", + "http", + "intervaltree", + "itertools 0.14.0", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "librqbit-dht", + "librqbit-dualstack-sockets", + "librqbit-lsd", + "librqbit-peer-protocol", + "librqbit-sha1-wrapper", + "librqbit-tracker-comms", + "librqbit-upnp", + "librqbit-utp", + "memmap2", + "mime_guess", + "nix", + "parking_lot", + "rand 0.9.2", + "regex", + "reqwest", + "rlimit", + "serde", + "serde_derive", + "serde_json", + "serde_urlencoded", + "serde_with", + "size_format", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tokio-socks", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "urlencoding", + "uuid", + "walkdir", + "windows 0.62.2", +] + +[[package]] +name = "librqbit-bencode" +version = "3.1.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "arrayvec", + "atoi", + "bytes", + "librqbit-buffers", + "librqbit-clone-to-owned", + "serde", + "serde_derive", + "thiserror 2.0.17", +] + +[[package]] +name = "librqbit-buffers" +version = "4.2.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "bytes", + "librqbit-clone-to-owned", + "serde", + "serde_derive", +] + +[[package]] +name = "librqbit-clone-to-owned" +version = "3.0.1" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "bytes", +] + +[[package]] +name = "librqbit-core" +version = "5.0.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "bytes", + "chardetng", + "data-encoding", + "directories", + "encoding_rs", + "hex", + "itertools 0.14.0", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-sha1-wrapper", + "memchr", + "parking_lot", + "rand 0.9.2", + "serde", + "serde_derive", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", + "url", +] + +[[package]] +name = "librqbit-dht" +version = "5.3.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "backon", + "bytes", + "chrono", + "dashmap 6.1.0", + "futures", + "indexmap 2.12.1", + "leaky-bucket", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "librqbit-dualstack-sockets", + "parking_lot", + "rand 0.9.2", + "serde", + "serde_derive", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", +] + +[[package]] +name = "librqbit-dualstack-sockets" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52135d1583bc772b74d5dc72a75ca8ed810463159c028d0b28609f597fffa816" +dependencies = [ + "axum", + "backon", + "futures", + "libc", + "network-interface", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tracing", +] + +[[package]] +name = "librqbit-lsd" +version = "0.1.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "atoi", + "bstr", + "futures", + "httparse", + "librqbit-core", + "librqbit-dualstack-sockets", + "librqbit-sha1-wrapper", + "parking_lot", + "rand 0.9.2", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "librqbit-peer-protocol" +version = "4.3.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "bitvec", + "byteorder", + "bytes", + "itertools 0.14.0", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-clone-to-owned", + "librqbit-core", + "serde", + "serde_derive", + "thiserror 2.0.17", + "tracing", +] + +[[package]] +name = "librqbit-sha1-wrapper" +version = "4.1.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "assert_cfg", + "aws-lc-rs", +] + +[[package]] +name = "librqbit-tracker-comms" +version = "3.0.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "async-stream", + "backon", + "byteorder", + "futures", + "itertools 0.14.0", + "librqbit-bencode", + "librqbit-buffers", + "librqbit-core", + "librqbit-dualstack-sockets", + "parking_lot", + "rand 0.9.2", + "reqwest", + "serde", + "serde_derive", + "serde_with", + "tokio", + "tokio-util", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "librqbit-upnp" +version = "1.0.0" +source = "git+https://github.com/StarCitizenToolBox/rqbit?tag=webseed-v0.0.1#e5ac0ac75cceffe017e247ea265dc7402c226638" +dependencies = [ + "anyhow", + "bstr", + "futures", + "httparse", + "librqbit-dualstack-sockets", + "network-interface", + "quick-xml 0.38.4", + "reqwest", + "serde", + "serde_derive", + "socket2 0.6.1", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "librqbit-utp" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bf54992c5b2c053c50782f6d14164439f46cd889fb97de25ee0bdee04fd5b33" +dependencies = [ + "bitvec", + "dontfrag", + "lazy_static", + "libc", + "librqbit-dualstack-sockets", + "metrics", + "parking_lot", + "rand 0.9.2", + "ringbuf", + "rustc-hash", + "socket2 0.6.1", + "thiserror 2.0.17", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "libz-rs-sys" version = "0.5.3" @@ -2743,12 +3404,27 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + [[package]] name = "matrixmultiply" version = "0.3.10" @@ -2775,6 +3451,15 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memmap2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744133e4a0e0a658e1374cf3bf8e415c4052a15a111acd372764c55b4177d490" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -2784,12 +3469,32 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + [[package]] name = "mime" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2891,7 +3596,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "882ed72dce9365842bf196bdeedf5055305f11fc8c03dee7bb0194a6cad34841" dependencies = [ "matrixmultiply", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-traits", "portable-atomic", @@ -2906,7 +3611,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7c9125e8f6f10c9da3aad044cc918cf8784fa34de857b1aa68038eb05a50a9" dependencies = [ "matrixmultiply", - "num-complex", + "num-complex 0.4.6", "num-integer", "num-traits", "portable-atomic", @@ -2944,6 +3649,18 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "network-interface" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07709a6d4eba90ab10ec170a0530b3aafc81cb8a2d380e4423ae41fc55fe5745" +dependencies = [ + "cc", + "libc", + "thiserror 2.0.17", + "winapi", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -2979,6 +3696,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonzero_ext" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" + [[package]] name = "notify-rust" version = "4.11.7" @@ -2993,6 +3716,38 @@ dependencies = [ "zbus", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.60.2", +] + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-complex 0.2.4", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -3017,6 +3772,28 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3284,7 +4061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969" dependencies = [ "cc", - "dashmap", + "dashmap 5.5.3", "log", ] @@ -3708,6 +4485,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi 0.11.1+wasi-snapshot-preview1", + "web-sys", + "winapi", +] + [[package]] name = "quick-xml" version = "0.37.5" @@ -3724,6 +4516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", + "serde", ] [[package]] @@ -3796,6 +4589,12 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -3906,6 +4705,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4084,21 +4892,43 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] +[[package]] +name = "ringbuf" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe47b720588c8702e34b5979cb3271a8b1842c7cb6f57408efa70c779363488c" +dependencies = [ + "crossbeam-utils", + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + [[package]] name = "rust" version = "0.1.0" dependencies = [ "anyhow", "asar", + "bytes", "crossbeam-channel", "flutter_rust_bridge", "futures", "hickory-resolver", "image", + "librqbit", "ndarray 0.17.1", "notify-rust", "once_cell", @@ -4111,6 +4941,8 @@ dependencies = [ "tao", "tokenizers", "tokio", + "tracing", + "tracing-subscriber", "unp4k_rs", "url", "uuid", @@ -4187,7 +5019,7 @@ checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" dependencies = [ "ring", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -4327,6 +5159,19 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "serde_html_form" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2f2d7ff8a2140333718bb329f5c40fc5f0865b84c426183ce14c97d2ab8154f" +dependencies = [ + "form_urlencoded", + "indexmap 2.12.1", + "itoa", + "ryu", + "serde_core", +] + [[package]] name = "serde_json" version = "1.0.145" @@ -4340,6 +5185,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + [[package]] name = "serde_repr" version = "0.1.20" @@ -4477,6 +5333,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "size_format" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed5f6ab2122c6dec69dca18c72fa4590a27e581ad20d44960fe74c032a0b23b" +dependencies = [ + "generic-array 0.12.4", + "num", +] + [[package]] name = "slab" version = "0.4.11" @@ -4552,6 +5418,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spinning_top" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96d2d1d716fb500937168cc09353ffdc7a012be8475ac7308e1bdf0e3923300" +dependencies = [ + "lock_api", +] + [[package]] name = "spm_precompiled" version = "0.1.4" @@ -4747,6 +5622,12 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.44" @@ -4994,6 +5875,30 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.69", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + [[package]] name = "tokio-util" version = "0.7.17" @@ -5169,15 +6074,33 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", + "smallvec 1.15.1", "thread_local", + "tracing", "tracing-core", + "tracing-log", ] [[package]] @@ -5192,6 +6115,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "typewit" +version = "1.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71" + [[package]] name = "uds_windows" version = "1.1.0" @@ -5203,6 +6132,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" @@ -5270,12 +6205,24 @@ dependencies = [ "zstd", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "ureq" version = "3.1.4" @@ -5318,6 +6265,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -5372,6 +6325,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "walkdir" version = "2.5.0" @@ -6345,6 +7304,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index d0a23ab..2ea2e84 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -35,6 +35,10 @@ unp4k_rs = { git = "https://github.com/StarCitizenToolBox/unp4k_rs", tag = "V0.0 uuid = { version = "1.19.0", features = ["v4"] } parking_lot = "0.12.5" crossbeam-channel = "0.5.15" +librqbit = { git = "https://github.com/StarCitizenToolBox/rqbit", tag = "webseed-v0.0.1" } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +bytes = "1.10" # WebView [target.'cfg(not(target_os = "macos"))'.dependencies] diff --git a/rust/src/api/downloader_api.rs b/rust/src/api/downloader_api.rs new file mode 100644 index 0000000..584747c --- /dev/null +++ b/rust/src/api/downloader_api.rs @@ -0,0 +1,513 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::{bail, Context, Result}; +use bytes::Bytes; +use flutter_rust_bridge::frb; +use librqbit::{ + AddTorrent, AddTorrentOptions, AddTorrentResponse, Session, SessionOptions, + TorrentStats, ManagedTorrent, TorrentStatsState, +}; +use once_cell::sync::OnceCell; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; + +// Type alias for ManagedTorrentHandle +type ManagedTorrentHandle = Arc; + +// Global session instance +static SESSION: OnceCell> = OnceCell::new(); +static SESSION_INIT_LOCK: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| Mutex::new(())); + +// Store torrent handles +static TORRENT_HANDLES: once_cell::sync::Lazy>> = + once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); + +// Store output folders for each task +static TASK_OUTPUT_FOLDERS: once_cell::sync::Lazy>> = + once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new())); + +/// Download task status +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DownloadTaskStatus { + Initializing, + Live, + Paused, + Error, + Finished, +} + +/// Download task information +#[derive(Debug, Clone)] +pub struct DownloadTaskInfo { + pub id: usize, + pub name: String, + pub status: DownloadTaskStatus, + pub total_bytes: u64, + pub downloaded_bytes: u64, + pub uploaded_bytes: u64, + pub download_speed: u64, + pub upload_speed: u64, + pub progress: f64, + pub num_peers: usize, + pub output_folder: String, +} + +/// Global statistics +#[derive(Debug, Clone, Default)] +pub struct DownloadGlobalStat { + pub download_speed: u64, + pub upload_speed: u64, + pub num_active: usize, + pub num_waiting: usize, +} + +/// Initialize the download manager session +#[frb(sync)] +pub fn downloader_init(download_dir: String) -> Result<()> { + // Already initialized + if SESSION.get().is_some() { + return Ok(()); + } + + let rt = tokio::runtime::Handle::current(); + rt.block_on(async { + let _lock = SESSION_INIT_LOCK.lock().await; + + // Double check after acquiring lock + if SESSION.get().is_some() { + return Ok(()); + } + + let output_folder = PathBuf::from(&download_dir); + std::fs::create_dir_all(&output_folder)?; + + let session = Session::new_with_opts( + output_folder, + SessionOptions { + disable_dht: false, + disable_dht_persistence: true, + persistence: None, + ..Default::default() + }, + ) + .await + .context("Failed to create rqbit session")?; + + SESSION + .set(session) + .map_err(|_| anyhow::anyhow!("Session already initialized"))?; + + Ok(()) + }) +} + +/// Check if the downloader is initialized +#[frb(sync)] +pub fn downloader_is_initialized() -> bool { + SESSION.get().is_some() +} + +/// Add a torrent from bytes (e.g., .torrent file content) +pub async fn downloader_add_torrent( + torrent_bytes: Vec, + output_folder: Option, + trackers: Option>, +) -> Result { + let session = SESSION + .get() + .context("Downloader not initialized. Call downloader_init first.")?; + + let bytes = Bytes::from(torrent_bytes); + let add_torrent = AddTorrent::from_bytes(bytes); + + let mut opts = AddTorrentOptions { + overwrite: true, + paused: false, + ..Default::default() + }; + + if let Some(ref folder) = output_folder { + opts.output_folder = Some(folder.clone()); + } + + if let Some(tracker_list) = trackers { + opts.trackers = Some(tracker_list); + } + + let response = session + .add_torrent(add_torrent, Some(opts)) + .await + .context("Failed to add torrent")?; + + match response { + AddTorrentResponse::Added(id, handle) => { + // Store output folder + if let Some(folder) = output_folder.clone() { + TASK_OUTPUT_FOLDERS.write().insert(id, folder); + } + TORRENT_HANDLES.write().insert(id, handle); + Ok(id) + } + AddTorrentResponse::AlreadyManaged(id, handle) => { + if let Some(folder) = output_folder.clone() { + TASK_OUTPUT_FOLDERS.write().insert(id, folder); + } + TORRENT_HANDLES.write().insert(id, handle); + Ok(id) + } + AddTorrentResponse::ListOnly(_) => { + bail!("Torrent was only listed, not added") + } + } +} + +/// Add a torrent from a magnet link +pub async fn downloader_add_magnet( + magnet_link: String, + output_folder: Option, + trackers: Option>, +) -> Result { + let session = SESSION + .get() + .context("Downloader not initialized. Call downloader_init first.")?; + + // Check if it's a magnet link + if !magnet_link.starts_with("magnet:") { + bail!("Invalid magnet link. Must start with 'magnet:'"); + } + + let add_torrent = AddTorrent::from_url(magnet_link); + + let mut opts = AddTorrentOptions { + overwrite: true, + paused: false, + ..Default::default() + }; + + if let Some(ref folder) = output_folder { + opts.output_folder = Some(folder.clone()); + } + + if let Some(tracker_list) = trackers { + opts.trackers = Some(tracker_list); + } + + let response = session + .add_torrent(add_torrent, Some(opts)) + .await + .context("Failed to add magnet")?; + + match response { + AddTorrentResponse::Added(id, handle) => { + if let Some(folder) = output_folder.clone() { + TASK_OUTPUT_FOLDERS.write().insert(id, folder); + } + TORRENT_HANDLES.write().insert(id, handle); + Ok(id) + } + AddTorrentResponse::AlreadyManaged(id, handle) => { + if let Some(folder) = output_folder.clone() { + TASK_OUTPUT_FOLDERS.write().insert(id, folder); + } + TORRENT_HANDLES.write().insert(id, handle); + Ok(id) + } + AddTorrentResponse::ListOnly(_) => { + bail!("Magnet was only listed, not added") + } + } +} + +/// Add a torrent from URL (HTTP download not supported, only torrent file URLs) +pub async fn downloader_add_url( + url: String, + output_folder: Option, + trackers: Option>, +) -> Result { + // Check if it's a magnet link + if url.starts_with("magnet:") { + return downloader_add_magnet(url, output_folder, trackers).await; + } + + // Check if it's a torrent file URL + if url.starts_with("http://") || url.starts_with("https://") { + // Download the torrent file first + let client = reqwest::Client::new(); + let response = client + .get(&url) + .send() + .await + .context("Failed to download torrent file")?; + + if !response.status().is_success() { + bail!("Failed to download torrent file: HTTP {}", response.status()); + } + + let bytes = response + .bytes() + .await + .context("Failed to read torrent file content")?; + + return downloader_add_torrent(bytes.to_vec(), output_folder, trackers).await; + } + + bail!("HTTP downloads are not supported. Only BitTorrent (magnet links and .torrent files) are supported.") +} + +/// Pause a download task +pub async fn downloader_pause(task_id: usize) -> Result<()> { + let session = SESSION + .get() + .context("Downloader not initialized")?; + + let handle = { + let handles = TORRENT_HANDLES.read(); + handles.get(&task_id).cloned() + }; + + if let Some(handle) = handle { + session.pause(&handle).await.context("Failed to pause torrent")?; + Ok(()) + } else { + bail!("Task not found: {}", task_id) + } +} + +/// Resume a download task +pub async fn downloader_resume(task_id: usize) -> Result<()> { + let session = SESSION + .get() + .context("Downloader not initialized")?; + + let handle = { + let handles = TORRENT_HANDLES.read(); + handles.get(&task_id).cloned() + }; + + if let Some(handle) = handle { + session.unpause(&handle).await.context("Failed to resume torrent")?; + Ok(()) + } else { + bail!("Task not found: {}", task_id) + } +} + +/// Remove a download task +pub async fn downloader_remove(task_id: usize, delete_files: bool) -> Result<()> { + let session = SESSION + .get() + .context("Downloader not initialized")?; + + session + .delete(librqbit::api::TorrentIdOrHash::Id(task_id), delete_files) + .await + .context("Failed to remove torrent")?; + + TORRENT_HANDLES.write().remove(&task_id); + + Ok(()) +} + +/// Get information about a specific task +pub async fn downloader_get_task_info(task_id: usize) -> Result { + let handle = { + let handles = TORRENT_HANDLES.read(); + handles.get(&task_id).cloned() + }; + + if let Some(handle) = handle { + let stats = handle.stats(); + let name = handle.name().unwrap_or_else(|| format!("Task {}", task_id)); + let output_folder = TASK_OUTPUT_FOLDERS + .read() + .get(&task_id) + .cloned() + .unwrap_or_default(); + + let status = get_task_status(&stats); + let progress = if stats.total_bytes > 0 { + stats.progress_bytes as f64 / stats.total_bytes as f64 + } else { + 0.0 + }; + + // Get speed from live stats + let (download_speed, upload_speed, num_peers) = if let Some(live) = &stats.live { + let down = (live.download_speed.mbps * 1024.0 * 1024.0 / 8.0) as u64; + let up = (live.upload_speed.mbps * 1024.0 * 1024.0 / 8.0) as u64; + let peers = (live.snapshot.peer_stats.queued + live.snapshot.peer_stats.connecting + live.snapshot.peer_stats.live) as usize; + (down, up, peers) + } else { + (0, 0, 0) + }; + + Ok(DownloadTaskInfo { + id: task_id, + name, + status, + total_bytes: stats.total_bytes, + downloaded_bytes: stats.progress_bytes, + uploaded_bytes: stats.uploaded_bytes, + download_speed, + upload_speed, + progress, + num_peers, + output_folder, + }) + } else { + bail!("Task not found: {}", task_id) + } +} + +fn get_task_status(stats: &TorrentStats) -> DownloadTaskStatus { + if stats.error.is_some() { + return DownloadTaskStatus::Error; + } + + if stats.finished { + return DownloadTaskStatus::Finished; + } + + match stats.state { + TorrentStatsState::Initializing => DownloadTaskStatus::Initializing, + TorrentStatsState::Live => DownloadTaskStatus::Live, + TorrentStatsState::Paused => DownloadTaskStatus::Paused, + TorrentStatsState::Error => DownloadTaskStatus::Error, + } +} + +/// Get all tasks +pub async fn downloader_get_all_tasks() -> Result> { + let session = SESSION.get(); + if session.is_none() { + return Ok(vec![]); + } + let session = session.unwrap(); + + // Use RwLock to collect tasks since with_torrents takes Fn (not FnMut) + let tasks: RwLock> = RwLock::new(Vec::new()); + + session.with_torrents(|torrents| { + for (id, handle) in torrents { + let stats = handle.stats(); + let name = handle.name().unwrap_or_else(|| format!("Task {}", id)); + let output_folder = TASK_OUTPUT_FOLDERS + .read() + .get(&id) + .cloned() + .unwrap_or_default(); + + let status = get_task_status(&stats); + let progress = if stats.total_bytes > 0 { + stats.progress_bytes as f64 / stats.total_bytes as f64 + } else { + 0.0 + }; + + // Get speed from live stats + let (download_speed, upload_speed, num_peers) = if let Some(live) = &stats.live { + let down = (live.download_speed.mbps * 1024.0 * 1024.0 / 8.0) as u64; + let up = (live.upload_speed.mbps * 1024.0 * 1024.0 / 8.0) as u64; + let peers = (live.snapshot.peer_stats.queued + live.snapshot.peer_stats.connecting + live.snapshot.peer_stats.live) as usize; + (down, up, peers) + } else { + (0, 0, 0) + }; + + tasks.write().push(DownloadTaskInfo { + id, + name, + status, + total_bytes: stats.total_bytes, + downloaded_bytes: stats.progress_bytes, + uploaded_bytes: stats.uploaded_bytes, + download_speed, + upload_speed, + progress, + num_peers, + output_folder, + }); + + // Update handles cache + TORRENT_HANDLES.write().insert(id, handle.clone()); + } + }); + + Ok(tasks.into_inner()) +} + +/// Get global statistics +pub async fn downloader_get_global_stats() -> Result { + let tasks = downloader_get_all_tasks().await?; + + let mut stat = DownloadGlobalStat::default(); + + for task in &tasks { + stat.download_speed += task.download_speed; + stat.upload_speed += task.upload_speed; + + match task.status { + DownloadTaskStatus::Live => stat.num_active += 1, + DownloadTaskStatus::Paused | DownloadTaskStatus::Initializing => stat.num_waiting += 1, + _ => {} + } + } + + Ok(stat) +} + +/// Check if a task with given name exists +pub async fn downloader_is_name_in_task(name: String) -> bool { + if let Ok(tasks) = downloader_get_all_tasks().await { + for task in tasks { + if task.name.contains(&name) { + return true; + } + } + } + false +} + +/// Pause all tasks +pub async fn downloader_pause_all() -> Result<()> { + let session = SESSION + .get() + .context("Downloader not initialized")?; + + let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect(); + + for handle in handles { + let _ = session.pause(&handle).await; + } + + Ok(()) +} + +/// Resume all tasks +pub async fn downloader_resume_all() -> Result<()> { + let session = SESSION + .get() + .context("Downloader not initialized")?; + + let handles: Vec<_> = TORRENT_HANDLES.read().values().cloned().collect(); + + for handle in handles { + let _ = session.unpause(&handle).await; + } + + Ok(()) +} + +/// Stop the downloader session +pub async fn downloader_stop() -> Result<()> { + if let Some(session) = SESSION.get() { + session.stop().await; + } + TORRENT_HANDLES.write().clear(); + TASK_OUTPUT_FOLDERS.write().clear(); + Ok(()) +} diff --git a/rust/src/api/mod.rs b/rust/src/api/mod.rs index 3f37eb7..61ebd9a 100644 --- a/rust/src/api/mod.rs +++ b/rust/src/api/mod.rs @@ -8,3 +8,4 @@ pub mod asar_api; pub mod ort_api; pub mod unp4k_api; pub mod webview_api; +pub mod downloader_api; diff --git a/rust/src/api/win32_api.rs b/rust/src/api/win32_api.rs index 744ba81..7c07fff 100644 --- a/rust/src/api/win32_api.rs +++ b/rust/src/api/win32_api.rs @@ -302,7 +302,7 @@ pub fn get_gpu_info_from_registry() -> anyhow::Result { /// Resolve shortcut (.lnk) file to get target path #[cfg(target_os = "windows")] -pub fn resolve_shortcut(lnk_path: &str) -> anyhow::Result { +pub fn resolve_shortcut(lnk_path: String) -> anyhow::Result { use windows::core::{HSTRING, Interface}; use windows::Win32::System::Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, @@ -327,7 +327,7 @@ pub fn resolve_shortcut(lnk_path: &str) -> anyhow::Result { let persist_file: IPersistFile = shell_link.cast()?; // Load the shortcut file - let lnk_path_w = HSTRING::from(lnk_path); + let lnk_path_w = HSTRING::from(&lnk_path); persist_file.Load(windows::core::PCWSTR(lnk_path_w.as_ptr()), STGM_READ)?; // Get target path @@ -351,7 +351,7 @@ pub fn resolve_shortcut(lnk_path: &str) -> anyhow::Result { } #[cfg(not(target_os = "windows"))] -pub fn resolve_shortcut(_: &str) -> anyhow::Result { +pub fn resolve_shortcut(_lnk_path: String) -> anyhow::Result { Ok(String::new()) } diff --git a/rust/src/frb_generated.rs b/rust/src/frb_generated.rs index e4d586e..2e8372a 100644 --- a/rust/src/frb_generated.rs +++ b/rust/src/frb_generated.rs @@ -37,7 +37,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueNom, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.11.1"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 1161621087; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1465039096; // Section: executor @@ -184,6 +184,407 @@ fn wire__crate__api__http_api__dns_lookup_txt_impl( }, ) } +fn wire__crate__api__downloader_api__download_global_stat_default_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "download_global_stat_default", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| { + transform_result_dco::<_, _, ()>((move || { + let output_ok = Result::<_, ()>::Ok( + crate::api::downloader_api::DownloadGlobalStat::default(), + )?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_add_magnet_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + magnet_link: impl CstDecode, + output_folder: impl CstDecode>, + trackers: impl CstDecode>>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_add_magnet", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_magnet_link = magnet_link.cst_decode(); + let api_output_folder = output_folder.cst_decode(); + let api_trackers = trackers.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_add_magnet( + api_magnet_link, + api_output_folder, + api_trackers, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_add_torrent_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + torrent_bytes: impl CstDecode>, + output_folder: impl CstDecode>, + trackers: impl CstDecode>>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_add_torrent", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_torrent_bytes = torrent_bytes.cst_decode(); + let api_output_folder = output_folder.cst_decode(); + let api_trackers = trackers.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_add_torrent( + api_torrent_bytes, + api_output_folder, + api_trackers, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_add_url_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + url: impl CstDecode, + output_folder: impl CstDecode>, + trackers: impl CstDecode>>, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_add_url", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_url = url.cst_decode(); + let api_output_folder = output_folder.cst_decode(); + let api_trackers = trackers.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_add_url( + api_url, + api_output_folder, + api_trackers, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_get_all_tasks_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_get_all_tasks", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::downloader_api::downloader_get_all_tasks().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_get_global_stats_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_get_global_stats", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::downloader_api::downloader_get_global_stats().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_get_task_info_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + task_id: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_get_task_info", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_task_id = task_id.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::downloader_api::downloader_get_task_info(api_task_id) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_init_impl( + download_dir: impl CstDecode, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_init", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + let api_download_dir = download_dir.cst_decode(); + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::api::downloader_api::downloader_init(api_download_dir)?; + Ok(output_ok) + })(), + ) + }, + ) +} +fn wire__crate__api__downloader_api__downloader_is_initialized_impl( +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_is_initialized", + port: None, + mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync, + }, + move || { + transform_result_dco::<_, _, ()>((move || { + let output_ok = + Result::<_, ()>::Ok(crate::api::downloader_api::downloader_is_initialized())?; + Ok(output_ok) + })()) + }, + ) +} +fn wire__crate__api__downloader_api__downloader_is_name_in_task_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + name: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_is_name_in_task", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_name = name.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, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_pause_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + task_id: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_pause", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_task_id = task_id.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::downloader_api::downloader_pause(api_task_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_pause_all_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_pause_all", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_pause_all().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_remove_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + task_id: impl CstDecode, + delete_files: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_remove", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_task_id = task_id.cst_decode(); + let api_delete_files = delete_files.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_remove( + api_task_id, + api_delete_files, + ) + .await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_resume_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + task_id: impl CstDecode, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_resume", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let api_task_id = task_id.cst_decode(); + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = + crate::api::downloader_api::downloader_resume(api_task_id).await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_resume_all_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_resume_all", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_resume_all().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__api__downloader_api__downloader_stop_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "downloader_stop", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + move |context| async move { + transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || async move { + let output_ok = crate::api::downloader_api::downloader_stop().await?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__api__http_api__fetch_impl( port_: flutter_rust_bridge::for_generated::MessagePort, method: impl CstDecode, @@ -688,7 +1089,7 @@ fn wire__crate__api__win32_api__remove_nvme_patch_impl( } fn wire__crate__api__win32_api__resolve_shortcut_impl( port_: flutter_rust_bridge::for_generated::MessagePort, - lnk_path: impl CstDecode, + _lnk_path: impl CstDecode, ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( flutter_rust_bridge::for_generated::TaskInfo { @@ -697,11 +1098,11 @@ fn wire__crate__api__win32_api__resolve_shortcut_impl( mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, move || { - let api_lnk_path = lnk_path.cst_decode(); + let api__lnk_path = _lnk_path.cst_decode(); move |context| { transform_result_dco::<_, _, flutter_rust_bridge::for_generated::anyhow::Error>( (move || { - let output_ok = crate::api::win32_api::resolve_shortcut(&api_lnk_path)?; + let output_ok = crate::api::win32_api::resolve_shortcut(api__lnk_path)?; Ok(output_ok) })(), ) @@ -1359,6 +1760,25 @@ impl CstDecode for bool { self } } +impl CstDecode for i32 { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::api::downloader_api::DownloadTaskStatus { + match self { + 0 => crate::api::downloader_api::DownloadTaskStatus::Initializing, + 1 => crate::api::downloader_api::DownloadTaskStatus::Live, + 2 => crate::api::downloader_api::DownloadTaskStatus::Paused, + 3 => crate::api::downloader_api::DownloadTaskStatus::Error, + 4 => crate::api::downloader_api::DownloadTaskStatus::Finished, + _ => unreachable!("Invalid variant for DownloadTaskStatus: {}", self), + } + } +} +impl CstDecode for f64 { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> f64 { + self + } +} impl CstDecode for i32 { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> i32 { @@ -1487,6 +1907,75 @@ impl SseDecode for bool { } } +impl SseDecode for crate::api::downloader_api::DownloadGlobalStat { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_downloadSpeed = ::sse_decode(deserializer); + let mut var_uploadSpeed = ::sse_decode(deserializer); + let mut var_numActive = ::sse_decode(deserializer); + let mut var_numWaiting = ::sse_decode(deserializer); + return crate::api::downloader_api::DownloadGlobalStat { + download_speed: var_downloadSpeed, + upload_speed: var_uploadSpeed, + num_active: var_numActive, + num_waiting: var_numWaiting, + }; + } +} + +impl SseDecode for crate::api::downloader_api::DownloadTaskInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_id = ::sse_decode(deserializer); + let mut var_name = ::sse_decode(deserializer); + let mut var_status = + ::sse_decode(deserializer); + let mut var_totalBytes = ::sse_decode(deserializer); + let mut var_downloadedBytes = ::sse_decode(deserializer); + let mut var_uploadedBytes = ::sse_decode(deserializer); + let mut var_downloadSpeed = ::sse_decode(deserializer); + let mut var_uploadSpeed = ::sse_decode(deserializer); + let mut var_progress = ::sse_decode(deserializer); + let mut var_numPeers = ::sse_decode(deserializer); + let mut var_outputFolder = ::sse_decode(deserializer); + return crate::api::downloader_api::DownloadTaskInfo { + id: var_id, + name: var_name, + status: var_status, + total_bytes: var_totalBytes, + downloaded_bytes: var_downloadedBytes, + uploaded_bytes: var_uploadedBytes, + download_speed: var_downloadSpeed, + upload_speed: var_uploadSpeed, + progress: var_progress, + num_peers: var_numPeers, + output_folder: var_outputFolder, + }; + } +} + +impl SseDecode for crate::api::downloader_api::DownloadTaskStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::api::downloader_api::DownloadTaskStatus::Initializing, + 1 => crate::api::downloader_api::DownloadTaskStatus::Live, + 2 => crate::api::downloader_api::DownloadTaskStatus::Paused, + 3 => crate::api::downloader_api::DownloadTaskStatus::Error, + 4 => crate::api::downloader_api::DownloadTaskStatus::Finished, + _ => unreachable!("Invalid variant for DownloadTaskStatus: {}", inner), + }; + } +} + +impl SseDecode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_f64::().unwrap() + } +} + impl SseDecode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1513,6 +2002,20 @@ impl SseDecode for Vec { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = vec![]; + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + impl SseDecode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1660,6 +2163,17 @@ impl SseDecode for Option { } } +impl SseDecode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } + } +} + impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -1938,6 +2452,83 @@ fn pde_ffi_dispatcher_sync_impl( // Section: rust2dart +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::downloader_api::DownloadGlobalStat { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.download_speed.into_into_dart().into_dart(), + self.upload_speed.into_into_dart().into_dart(), + self.num_active.into_into_dart().into_dart(), + self.num_waiting.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::downloader_api::DownloadGlobalStat +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::downloader_api::DownloadGlobalStat +{ + fn into_into_dart(self) -> crate::api::downloader_api::DownloadGlobalStat { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::downloader_api::DownloadTaskInfo { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.id.into_into_dart().into_dart(), + self.name.into_into_dart().into_dart(), + self.status.into_into_dart().into_dart(), + self.total_bytes.into_into_dart().into_dart(), + self.downloaded_bytes.into_into_dart().into_dart(), + self.uploaded_bytes.into_into_dart().into_dart(), + self.download_speed.into_into_dart().into_dart(), + self.upload_speed.into_into_dart().into_dart(), + self.progress.into_into_dart().into_dart(), + self.num_peers.into_into_dart().into_dart(), + self.output_folder.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::downloader_api::DownloadTaskInfo +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::downloader_api::DownloadTaskInfo +{ + fn into_into_dart(self) -> crate::api::downloader_api::DownloadTaskInfo { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::api::downloader_api::DownloadTaskStatus { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Initializing => 0.into_dart(), + Self::Live => 1.into_dart(), + Self::Paused => 2.into_dart(), + Self::Error => 3.into_dart(), + Self::Finished => 4.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::api::downloader_api::DownloadTaskStatus +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::api::downloader_api::DownloadTaskStatus +{ + fn into_into_dart(self) -> crate::api::downloader_api::DownloadTaskStatus { + self + } +} // Codec=Dco (DartCObject based), see doc to use other codecs impl flutter_rust_bridge::IntoDart for crate::http_package::MyHttpVersion { fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { @@ -2280,6 +2871,59 @@ impl SseEncode for bool { } } +impl SseEncode for crate::api::downloader_api::DownloadGlobalStat { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.download_speed, serializer); + ::sse_encode(self.upload_speed, serializer); + ::sse_encode(self.num_active, serializer); + ::sse_encode(self.num_waiting, serializer); + } +} + +impl SseEncode for crate::api::downloader_api::DownloadTaskInfo { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.id, serializer); + ::sse_encode(self.name, serializer); + ::sse_encode(self.status, serializer); + ::sse_encode(self.total_bytes, serializer); + ::sse_encode(self.downloaded_bytes, serializer); + ::sse_encode(self.uploaded_bytes, serializer); + ::sse_encode(self.download_speed, serializer); + ::sse_encode(self.upload_speed, serializer); + ::sse_encode(self.progress, serializer); + ::sse_encode(self.num_peers, serializer); + ::sse_encode(self.output_folder, serializer); + } +} + +impl SseEncode for crate::api::downloader_api::DownloadTaskStatus { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::api::downloader_api::DownloadTaskStatus::Initializing => 0, + crate::api::downloader_api::DownloadTaskStatus::Live => 1, + crate::api::downloader_api::DownloadTaskStatus::Paused => 2, + crate::api::downloader_api::DownloadTaskStatus::Error => 3, + crate::api::downloader_api::DownloadTaskStatus::Finished => 4, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for f64 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_f64::(self).unwrap(); + } +} + impl SseEncode for i32 { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -2304,6 +2948,16 @@ impl SseEncode for Vec { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for Vec { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -2437,6 +3091,16 @@ impl SseEncode for Option { } } +impl SseEncode for Option> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } + } +} + impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -2725,6 +3389,35 @@ mod io { CstDecode::::cst_decode(*wrap).into() } } + impl CstDecode for wire_cst_download_global_stat { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::api::downloader_api::DownloadGlobalStat { + crate::api::downloader_api::DownloadGlobalStat { + download_speed: self.download_speed.cst_decode(), + upload_speed: self.upload_speed.cst_decode(), + num_active: self.num_active.cst_decode(), + num_waiting: self.num_waiting.cst_decode(), + } + } + } + impl CstDecode for wire_cst_download_task_info { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> crate::api::downloader_api::DownloadTaskInfo { + crate::api::downloader_api::DownloadTaskInfo { + id: self.id.cst_decode(), + name: self.name.cst_decode(), + status: self.status.cst_decode(), + total_bytes: self.total_bytes.cst_decode(), + downloaded_bytes: self.downloaded_bytes.cst_decode(), + uploaded_bytes: self.uploaded_bytes.cst_decode(), + download_speed: self.download_speed.cst_decode(), + upload_speed: self.upload_speed.cst_decode(), + progress: self.progress.cst_decode(), + num_peers: self.num_peers.cst_decode(), + output_folder: self.output_folder.cst_decode(), + } + } + } impl CstDecode> for *mut wire_cst_list_String { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> Vec { @@ -2735,6 +3428,18 @@ mod io { vec.into_iter().map(CstDecode::cst_decode).collect() } } + impl CstDecode> + for *mut wire_cst_list_download_task_info + { + // Codec=Cst (C-struct based), see doc to use other codecs + fn cst_decode(self) -> Vec { + let vec = unsafe { + let wrap = flutter_rust_bridge::for_generated::box_from_leak_ptr(self); + flutter_rust_bridge::for_generated::vec_from_leak_ptr(wrap.ptr, wrap.len) + }; + vec.into_iter().map(CstDecode::cst_decode).collect() + } + } impl CstDecode> for *mut wire_cst_list_p_4_k_file_item { // Codec=Cst (C-struct based), see doc to use other codecs fn cst_decode(self) -> Vec { @@ -2933,6 +3638,43 @@ mod io { } } } + impl NewWithNullPtr for wire_cst_download_global_stat { + fn new_with_null_ptr() -> Self { + Self { + download_speed: Default::default(), + upload_speed: Default::default(), + num_active: Default::default(), + num_waiting: Default::default(), + } + } + } + impl Default for wire_cst_download_global_stat { + fn default() -> Self { + Self::new_with_null_ptr() + } + } + impl NewWithNullPtr for wire_cst_download_task_info { + fn new_with_null_ptr() -> Self { + Self { + id: Default::default(), + name: core::ptr::null_mut(), + status: Default::default(), + total_bytes: Default::default(), + downloaded_bytes: Default::default(), + uploaded_bytes: Default::default(), + download_speed: Default::default(), + upload_speed: Default::default(), + progress: Default::default(), + num_peers: Default::default(), + output_folder: core::ptr::null_mut(), + } + } + } + impl Default for wire_cst_download_task_info { + fn default() -> Self { + Self::new_with_null_ptr() + } + } impl NewWithNullPtr for wire_cst_p_4_k_file_item { fn new_with_null_ptr() -> Self { Self { @@ -3131,6 +3873,147 @@ mod io { wire__crate__api__http_api__dns_lookup_txt_impl(port_, host) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__download_global_stat_default( + port_: i64, + ) { + wire__crate__api__downloader_api__download_global_stat_default_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_magnet( + port_: i64, + magnet_link: *mut wire_cst_list_prim_u_8_strict, + output_folder: *mut wire_cst_list_prim_u_8_strict, + trackers: *mut wire_cst_list_String, + ) { + wire__crate__api__downloader_api__downloader_add_magnet_impl( + port_, + magnet_link, + output_folder, + trackers, + ) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_torrent( + port_: i64, + torrent_bytes: *mut wire_cst_list_prim_u_8_loose, + output_folder: *mut wire_cst_list_prim_u_8_strict, + trackers: *mut wire_cst_list_String, + ) { + wire__crate__api__downloader_api__downloader_add_torrent_impl( + port_, + torrent_bytes, + output_folder, + trackers, + ) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_add_url( + port_: i64, + url: *mut wire_cst_list_prim_u_8_strict, + output_folder: *mut wire_cst_list_prim_u_8_strict, + trackers: *mut wire_cst_list_String, + ) { + wire__crate__api__downloader_api__downloader_add_url_impl( + port_, + url, + output_folder, + trackers, + ) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_all_tasks( + port_: i64, + ) { + wire__crate__api__downloader_api__downloader_get_all_tasks_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_global_stats( + port_: i64, + ) { + wire__crate__api__downloader_api__downloader_get_global_stats_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_get_task_info( + port_: i64, + task_id: usize, + ) { + wire__crate__api__downloader_api__downloader_get_task_info_impl(port_, task_id) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_init( + download_dir: *mut wire_cst_list_prim_u_8_strict, + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + wire__crate__api__downloader_api__downloader_init_impl(download_dir) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_is_initialized( + ) -> flutter_rust_bridge::for_generated::WireSyncRust2DartDco { + wire__crate__api__downloader_api__downloader_is_initialized_impl() + } + + #[unsafe(no_mangle)] + 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, + ) { + wire__crate__api__downloader_api__downloader_is_name_in_task_impl(port_, name) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_pause( + port_: i64, + task_id: usize, + ) { + wire__crate__api__downloader_api__downloader_pause_impl(port_, task_id) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_pause_all( + port_: i64, + ) { + wire__crate__api__downloader_api__downloader_pause_all_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_remove( + port_: i64, + task_id: usize, + delete_files: bool, + ) { + wire__crate__api__downloader_api__downloader_remove_impl(port_, task_id, delete_files) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_resume( + port_: i64, + task_id: usize, + ) { + wire__crate__api__downloader_api__downloader_resume_impl(port_, task_id) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_resume_all( + port_: i64, + ) { + wire__crate__api__downloader_api__downloader_resume_all_impl(port_) + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__downloader_api__downloader_stop( + port_: i64, + ) { + wire__crate__api__downloader_api__downloader_stop_impl(port_) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__http_api__fetch( port_: i64, @@ -3309,9 +4192,9 @@ mod io { #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_wire__crate__api__win32_api__resolve_shortcut( port_: i64, - lnk_path: *mut wire_cst_list_prim_u_8_strict, + _lnk_path: *mut wire_cst_list_prim_u_8_strict, ) { - wire__crate__api__win32_api__resolve_shortcut_impl(port_, lnk_path) + wire__crate__api__win32_api__resolve_shortcut_impl(port_, _lnk_path) } #[unsafe(no_mangle)] @@ -3585,6 +4468,20 @@ mod io { flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) } + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_download_task_info( + len: i32, + ) -> *mut wire_cst_list_download_task_info { + let wrap = wire_cst_list_download_task_info { + ptr: flutter_rust_bridge::for_generated::new_leak_vec_ptr( + ::new_with_null_ptr(), + len, + ), + len, + }; + flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) + } + #[unsafe(no_mangle)] pub extern "C" fn frbgen_starcitizen_doctor_cst_new_list_p_4_k_file_item( len: i32, @@ -3663,6 +4560,29 @@ mod io { flutter_rust_bridge::for_generated::new_leak_box_ptr(wrap) } + #[repr(C)] + #[derive(Clone, Copy)] + pub struct wire_cst_download_global_stat { + download_speed: u64, + upload_speed: u64, + num_active: usize, + num_waiting: usize, + } + #[repr(C)] + #[derive(Clone, Copy)] + pub struct wire_cst_download_task_info { + id: usize, + name: *mut wire_cst_list_prim_u_8_strict, + status: i32, + total_bytes: u64, + downloaded_bytes: u64, + uploaded_bytes: u64, + download_speed: u64, + upload_speed: u64, + progress: f64, + num_peers: usize, + output_folder: *mut wire_cst_list_prim_u_8_strict, + } #[repr(C)] #[derive(Clone, Copy)] pub struct wire_cst_list_String { @@ -3671,6 +4591,12 @@ mod io { } #[repr(C)] #[derive(Clone, Copy)] + pub struct wire_cst_list_download_task_info { + ptr: *mut wire_cst_download_task_info, + len: i32, + } + #[repr(C)] + #[derive(Clone, Copy)] pub struct wire_cst_list_p_4_k_file_item { ptr: *mut wire_cst_p_4_k_file_item, len: i32,