feat: use rust rqbit to replace aria2c

This commit is contained in:
xkeyC 2025-12-05 16:32:53 +08:00
parent c5de9e2252
commit 4315e36cbe
30 changed files with 4789 additions and 1052 deletions

View File

@ -82,7 +82,7 @@ final class AppGlobalModelProvider
}
}
String _$appGlobalModelHash() => r'9729c3ffb891e5899abbb3dc7d2d25ef13a442e7';
String _$appGlobalModelHash() => r'0e46d72594d94e2beb4d2ccb8616eb37facba288';
abstract class _$AppGlobalModel extends $Notifier<AppGlobalState> {
AppGlobalState build();

View File

@ -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 = <String, String>{};
static Future extractModule(List<String> modules, String workingDir) async {
for (var m in _modules.entries) {

View File

@ -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<BigInt> downloaderAddTorrent({
required List<int> torrentBytes,
String? outputFolder,
List<String>? trackers,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddTorrent(
torrentBytes: torrentBytes,
outputFolder: outputFolder,
trackers: trackers,
);
/// Add a torrent from a magnet link
Future<BigInt> downloaderAddMagnet({
required String magnetLink,
String? outputFolder,
List<String>? 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<BigInt> downloaderAddUrl({
required String url,
String? outputFolder,
List<String>? trackers,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderAddUrl(
url: url,
outputFolder: outputFolder,
trackers: trackers,
);
/// Pause a download task
Future<void> downloaderPause({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderPause(taskId: taskId);
/// Resume a download task
Future<void> downloaderResume({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderResume(taskId: taskId);
/// Remove a download task
Future<void> downloaderRemove({
required BigInt taskId,
required bool deleteFiles,
}) => RustLib.instance.api.crateApiDownloaderApiDownloaderRemove(
taskId: taskId,
deleteFiles: deleteFiles,
);
/// Get information about a specific task
Future<DownloadTaskInfo> downloaderGetTaskInfo({required BigInt taskId}) =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetTaskInfo(
taskId: taskId,
);
/// Get all tasks
Future<List<DownloadTaskInfo>> downloaderGetAllTasks() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetAllTasks();
/// Get global statistics
Future<DownloadGlobalStat> downloaderGetGlobalStats() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderGetGlobalStats();
/// Check if a task with given name exists
Future<bool> downloaderIsNameInTask({required String name}) => RustLib
.instance
.api
.crateApiDownloaderApiDownloaderIsNameInTask(name: name);
/// Pause all tasks
Future<void> downloaderPauseAll() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderPauseAll();
/// Resume all tasks
Future<void> downloaderResumeAll() =>
RustLib.instance.api.crateApiDownloaderApiDownloaderResumeAll();
/// Stop the downloader session
Future<void> 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<DownloadGlobalStat> 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 }

View File

@ -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<void> sendNotify({
@ -21,27 +20,21 @@ Future<void> sendNotify({
appId: appId,
);
/// Get system memory size in GB
Future<BigInt> getSystemMemorySizeGb() =>
RustLib.instance.api.crateApiWin32ApiGetSystemMemorySizeGb();
/// Get number of logical processors
Future<int> getNumberOfLogicalProcessors() =>
RustLib.instance.api.crateApiWin32ApiGetNumberOfLogicalProcessors();
/// Get all system information at once
Future<SystemInfo> getSystemInfo() =>
RustLib.instance.api.crateApiWin32ApiGetSystemInfo();
/// Get GPU info from registry (more accurate VRAM)
Future<String> getGpuInfoFromRegistry() =>
RustLib.instance.api.crateApiWin32ApiGetGpuInfoFromRegistry();
/// Resolve shortcut (.lnk) file to get target path
Future<String> resolveShortcut({required String lnkPath}) =>
RustLib.instance.api.crateApiWin32ApiResolveShortcut(lnkPath: lnkPath);
/// Open file explorer and select file/folder
Future<void> openDirWithExplorer({
required String path,
required bool isFile,
@ -65,19 +58,16 @@ Future<List<ProcessInfo>> getProcessListByName({required String processName}) =>
processName: processName,
);
/// Kill processes by name
Future<int> killProcessByName({required String processName}) => RustLib
.instance
.api
.crateApiWin32ApiKillProcessByName(processName: processName);
/// Get disk physical sector size for performance
Future<int> getDiskPhysicalSectorSize({required String driveLetter}) => RustLib
.instance
.api
.crateApiWin32ApiGetDiskPhysicalSectorSize(driveLetter: driveLetter);
/// Create a desktop shortcut
Future<void> createDesktopShortcut({
required String targetPath,
required String shortcutName,
@ -86,14 +76,12 @@ Future<void> createDesktopShortcut({
shortcutName: shortcutName,
);
/// Run a program with admin privileges (UAC)
Future<void> runAsAdmin({required String program, required String args}) =>
RustLib.instance.api.crateApiWin32ApiRunAsAdmin(
program: program,
args: args,
);
/// Start a program (without waiting)
Future<void> startProcess({
required String program,
required List<String> args,
@ -102,15 +90,12 @@ Future<void> startProcess({
args: args,
);
/// Check if NVME patch is applied
Future<bool> checkNvmePatchStatus() =>
RustLib.instance.api.crateApiWin32ApiCheckNvmePatchStatus();
/// Add NVME patch to registry
Future<void> addNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiAddNvmePatch();
/// Remove NVME patch from registry
Future<void> removeNvmePatch() =>
RustLib.instance.api.crateApiWin32ApiRemoveNvmePatch();

View File

@ -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<RustLibApi, RustLibApiImpl, RustLibWire> {
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<List<String>> crateApiHttpApiDnsLookupTxt({required String host});
Future<DownloadGlobalStat> crateApiDownloaderApiDownloadGlobalStatDefault();
Future<BigInt> crateApiDownloaderApiDownloaderAddMagnet({
required String magnetLink,
String? outputFolder,
List<String>? trackers,
});
Future<BigInt> crateApiDownloaderApiDownloaderAddTorrent({
required List<int> torrentBytes,
String? outputFolder,
List<String>? trackers,
});
Future<BigInt> crateApiDownloaderApiDownloaderAddUrl({
required String url,
String? outputFolder,
List<String>? trackers,
});
Future<List<DownloadTaskInfo>> crateApiDownloaderApiDownloaderGetAllTasks();
Future<DownloadGlobalStat> crateApiDownloaderApiDownloaderGetGlobalStats();
Future<DownloadTaskInfo> crateApiDownloaderApiDownloaderGetTaskInfo({
required BigInt taskId,
});
void crateApiDownloaderApiDownloaderInit({required String downloadDir});
bool crateApiDownloaderApiDownloaderIsInitialized();
Future<bool> crateApiDownloaderApiDownloaderIsNameInTask({
required String name,
});
Future<void> crateApiDownloaderApiDownloaderPause({required BigInt taskId});
Future<void> crateApiDownloaderApiDownloaderPauseAll();
Future<void> crateApiDownloaderApiDownloaderRemove({
required BigInt taskId,
required bool deleteFiles,
});
Future<void> crateApiDownloaderApiDownloaderResume({required BigInt taskId});
Future<void> crateApiDownloaderApiDownloaderResumeAll();
Future<void> crateApiDownloaderApiDownloaderStop();
Future<RustHttpResponse> 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<DownloadGlobalStat> 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<BigInt> crateApiDownloaderApiDownloaderAddMagnet({
required String magnetLink,
String? outputFolder,
List<String>? 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<BigInt> crateApiDownloaderApiDownloaderAddTorrent({
required List<int> torrentBytes,
String? outputFolder,
List<String>? 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<BigInt> crateApiDownloaderApiDownloaderAddUrl({
required String url,
String? outputFolder,
List<String>? 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<List<DownloadTaskInfo>> 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<DownloadGlobalStat> 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<DownloadTaskInfo> 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<bool> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<RustHttpResponse> 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<dynamic>;
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<dynamic>;
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<dynamic>).map(dco_decode_String).toList();
}
@protected
List<DownloadTaskInfo> dco_decode_list_download_task_info(dynamic raw) {
// Codec=Dco (DartCObject based), see doc to use other codecs
return (raw as List<dynamic>).map(dco_decode_download_task_info).toList();
}
@protected
List<P4kFileItem> 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<String>? 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<DownloadTaskInfo> 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_ = <DownloadTaskInfo>[];
for (var idx_ = 0; idx_ < len_; ++idx_) {
ans_.add(sse_decode_download_task_info(deserializer));
}
return ans_;
}
@protected
List<P4kFileItem> sse_decode_list_p_4_k_file_item(
SseDeserializer deserializer,
@ -2407,6 +3038,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
List<String>? 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<DownloadTaskInfo> 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<P4kFileItem> self,
@ -2917,6 +3629,19 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
}
}
@protected
void sse_encode_opt_list_String(
List<String>? 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,

View File

@ -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<RustLibWire> {
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<RustLibWire> {
@protected
List<String> dco_decode_list_String(dynamic raw);
@protected
List<DownloadTaskInfo> dco_decode_list_download_task_info(dynamic raw);
@protected
List<P4kFileItem> dco_decode_list_p_4_k_file_item(dynamic raw);
@ -102,6 +118,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? dco_decode_opt_box_autoadd_u_64(dynamic raw);
@protected
List<String>? 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<RustLibWire> {
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<RustLibWire> {
@protected
List<String> sse_decode_list_String(SseDeserializer deserializer);
@protected
List<DownloadTaskInfo> sse_decode_list_download_task_info(
SseDeserializer deserializer,
);
@protected
List<P4kFileItem> sse_decode_list_p_4_k_file_item(
SseDeserializer deserializer,
@ -245,6 +285,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
BigInt? sse_decode_opt_box_autoadd_u_64(SseDeserializer deserializer);
@protected
List<String>? 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<RustLibWire> {
return ans;
}
@protected
ffi.Pointer<wire_cst_list_download_task_info>
cst_encode_list_download_task_info(List<DownloadTaskInfo> 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<wire_cst_list_p_4_k_file_item> cst_encode_list_p_4_k_file_item(
List<P4kFileItem> raw,
@ -490,6 +544,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
return raw == null ? ffi.nullptr : cst_encode_box_autoadd_u_64(raw);
}
@protected
ffi.Pointer<wire_cst_list_String> cst_encode_opt_list_String(
List<String>? 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<wire_cst_list_prim_u_8_strict>
cst_encode_opt_list_prim_u_8_strict(Uint8List? raw) {
@ -525,6 +587,35 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
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<RustLibWire> {
@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<RustLibWire> {
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<RustLibWire> {
@protected
void sse_encode_list_String(List<String> self, SseSerializer serializer);
@protected
void sse_encode_list_download_task_info(
List<DownloadTaskInfo> self,
SseSerializer serializer,
);
@protected
void sse_encode_list_p_4_k_file_item(
List<P4kFileItem> self,
@ -804,6 +928,9 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
@protected
void sse_encode_opt_box_autoadd_u_64(BigInt? self, SseSerializer serializer);
@protected
void sse_encode_opt_list_String(List<String>? 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<wire_cst_list_prim_u_8_strict>)
>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
void wire__crate__api__downloader_api__downloader_add_magnet(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> magnet_link,
ffi.Pointer<wire_cst_list_prim_u_8_strict> output_folder,
ffi.Pointer<wire_cst_list_String> 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<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>
>(
'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<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>();
void wire__crate__api__downloader_api__downloader_add_torrent(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_loose> torrent_bytes,
ffi.Pointer<wire_cst_list_prim_u_8_strict> output_folder,
ffi.Pointer<wire_cst_list_String> 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<wire_cst_list_prim_u_8_loose>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>
>(
'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<wire_cst_list_prim_u_8_loose>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>();
void wire__crate__api__downloader_api__downloader_add_url(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> url,
ffi.Pointer<wire_cst_list_prim_u_8_strict> output_folder,
ffi.Pointer<wire_cst_list_String> 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<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>
>(
'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<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_prim_u_8_strict>,
ffi.Pointer<wire_cst_list_String>,
)
>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'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<void Function(int, int)>();
WireSyncRust2DartDco wire__crate__api__downloader_api__downloader_init(
ffi.Pointer<wire_cst_list_prim_u_8_strict> 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<wire_cst_list_prim_u_8_strict>,
)
>
>(
'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<wire_cst_list_prim_u_8_strict>,
)
>();
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<ffi.NativeFunction<WireSyncRust2DartDco Function()>>(
'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<WireSyncRust2DartDco Function()>();
void wire__crate__api__downloader_api__downloader_is_name_in_task(
int port_,
ffi.Pointer<wire_cst_list_prim_u_8_strict> 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<wire_cst_list_prim_u_8_strict>,
)
>
>(
'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<wire_cst_list_prim_u_8_strict>)
>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'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 Function(int, int)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
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<ffi.Void Function(ffi.Int64, ffi.UintPtr, ffi.Bool)>
>(
'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 Function(int, int, bool)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64, ffi.UintPtr)>>(
'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 Function(int, int)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
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<ffi.NativeFunction<ffi.Void Function(ffi.Int64)>>(
'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 Function(int)>();
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<wire_cst_list_prim_u_8_strict> lnk_path,
ffi.Pointer<wire_cst_list_prim_u_8_strict> _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<ffi.Pointer<wire_cst_list_String> Function(int)>();
ffi.Pointer<wire_cst_list_download_task_info> 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<wire_cst_list_download_task_info> 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<wire_cst_list_download_task_info> Function(int)
>();
ffi.Pointer<wire_cst_list_p_4_k_file_item> 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<ffi.Pointer<wire_cst_list_prim_u_8_strict>> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_list_prim_u_8_loose extends ffi.Struct {
external ffi.Pointer<ffi.Uint8> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_record_string_string extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> 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<ffi.Pointer<wire_cst_list_prim_u_8_strict>> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_rsi_launcher_asar_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> asar_path;
@ -2487,13 +2967,6 @@ final class wire_cst_rsi_launcher_asar_data extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> main_js_content;
}
final class wire_cst_list_prim_u_8_loose extends ffi.Struct {
external ffi.Pointer<ffi.Uint8> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_web_view_configuration extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> title;
@ -2514,6 +2987,46 @@ final class wire_cst_web_view_configuration extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> user_agent;
}
final class wire_cst_download_task_info extends ffi.Struct {
@ffi.UintPtr()
external int id;
external ffi.Pointer<wire_cst_list_prim_u_8_strict> 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<wire_cst_list_prim_u_8_strict> output_folder;
}
final class wire_cst_list_download_task_info extends ffi.Struct {
external ffi.Pointer<wire_cst_download_task_info> ptr;
@ffi.Int32()
external int len;
}
final class wire_cst_p_4_k_file_item extends ffi.Struct {
external ffi.Pointer<wire_cst_list_prim_u_8_strict> 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;

View File

@ -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<int> 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<void> _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<void> _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<bool> 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;
}
}

View File

@ -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>(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<Aria2cModelState> get copyWith => _$Aria2cModelStateCopyWithImpl<Aria2cModelState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@ -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<Aria2cModel, Aria2cModelState> {
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<Aria2cModelState>(value),
);
}
}
String _$aria2cModelHash() => r'17956c60a79c68ae13b8b8e700ebbafb70e93194';
abstract class _$Aria2cModel extends $Notifier<Aria2cModelState> {
Aria2cModelState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<Aria2cModelState, Aria2cModelState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Aria2cModelState, Aria2cModelState>,
Aria2cModelState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@ -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<void> 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<void> _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<int> addTorrent(List<int> torrentBytes, {String? outputFolder, List<String>? 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<int> addMagnet(String magnetLink, {String? outputFolder, List<String>? 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<int> addUrl(String url, {String? outputFolder, List<String>? trackers}) async {
await initDownloader();
final taskId = await downloader_api.downloaderAddUrl(url: url, outputFolder: outputFolder, trackers: trackers);
return taskId.toInt();
}
Future<void> pauseTask(int taskId) async {
await downloader_api.downloaderPause(taskId: BigInt.from(taskId));
}
Future<void> resumeTask(int taskId) async {
await downloader_api.downloaderResume(taskId: BigInt.from(taskId));
}
Future<void> removeTask(int taskId, {bool deleteFiles = false}) async {
await downloader_api.downloaderRemove(taskId: BigInt.from(taskId), deleteFiles: deleteFiles);
}
Future<downloader_api.DownloadTaskInfo> getTaskInfo(int taskId) async {
return await downloader_api.downloaderGetTaskInfo(taskId: BigInt.from(taskId));
}
Future<List<downloader_api.DownloadTaskInfo>> getAllTasks() async {
if (!state.isInitialized) {
return [];
}
return await downloader_api.downloaderGetAllTasks();
}
Future<bool> isNameInTask(String name) async {
if (!state.isInitialized) {
return false;
}
return await downloader_api.downloaderIsNameInTask(name: name);
}
Future<void> pauseAll() async {
await downloader_api.downloaderPauseAll();
}
Future<void> resumeAll() async {
await downloader_api.downloaderResumeAll();
}
Future<void> 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;
}
}

View File

@ -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>(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<DownloadManagerState> get copyWith => _$DownloadManagerStateCopyWithImpl<DownloadManagerState>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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 extends Object?>(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

View File

@ -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<DownloadManager, DownloadManagerState> {
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<DownloadManagerState>(value),
);
}
}
String _$downloadManagerHash() => r'adc9a147522afbfcfc8a2e16310649220a75d6a3';
abstract class _$DownloadManager extends $Notifier<DownloadManagerState> {
DownloadManagerState build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<DownloadManagerState, DownloadManagerState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<DownloadManagerState, DownloadManagerState>,
DownloadManagerState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

View File

@ -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 <MapEntry<String, IconData>, 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 <MapEntry<String, IconData>, 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,
);
}
}

View File

@ -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<Aria2Task> tasks,
@Default([]) List<Aria2Task> waitingTasks,
@Default([]) List<Aria2Task> stoppedTasks,
Aria2GlobalStat? globalStat,
@Default([]) List<DownloadTaskInfo> activeTasks,
@Default([]) List<DownloadTaskInfo> waitingTasks,
@Default([]) List<DownloadTaskInfo> 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<void> 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 = <Aria2Task>[...state.tasks, ...state.waitingTasks, ...state.stoppedTasks];
if (index >= 0 && index < state.tasks.length) {
(DownloadTaskInfo, String, bool) getTaskAndType(int index) {
final tempList = <DownloadTaskInfo>[...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<String, String> 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<String, String> 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<void> resumeTask(int taskId) async {
final downloadManager = ref.read(downloadManagerProvider.notifier);
await downloadManager.resumeTask(taskId);
}
Future<void> resumeTask(String? gid) async {
final aria2c = ref.read(aria2cModelProvider).aria2c;
if (gid != null) {
await aria2c?.unpause(gid);
}
Future<void> pauseTask(int taskId) async {
final downloadManager = ref.read(downloadManagerProvider.notifier);
await downloadManager.pauseTask(taskId);
}
Future<void> pauseTask(String? gid) async {
final aria2c = ref.read(aria2cModelProvider).aria2c;
if (gid != null) {
await aria2c?.pause(gid);
}
}
Future<void> cancelTask(BuildContext context, String? gid) async {
Future<void> 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<Aria2File> getFilesFormTask(Aria2Task task) {
List<Aria2File> 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 = <DownloadTaskInfo>[];
final waitingTasks = <DownloadTaskInfo>[];
final stoppedTasks = <DownloadTaskInfo>[];
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.");
}
}
}

View File

@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$HomeDownloaderUIState {
List<Aria2Task> get tasks; List<Aria2Task> get waitingTasks; List<Aria2Task> get stoppedTasks; Aria2GlobalStat? get globalStat;
List<DownloadTaskInfo> get activeTasks; List<DownloadTaskInfo> get waitingTasks; List<DownloadTaskInfo> 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<HomeDownloaderUIState> 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<Aria2Task> tasks, List<Aria2Task> waitingTasks, List<Aria2Task> stoppedTasks, Aria2GlobalStat? globalStat
List<DownloadTaskInfo> activeTasks, List<DownloadTaskInfo> waitingTasks, List<DownloadTaskInfo> 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<Aria2Task>,waitingTasks: null == waitingTasks ? _self.waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,stoppedTasks: null == stoppedTasks ? _self.stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,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<DownloadTaskInfo>,waitingTasks: null == waitingTasks ? _self.waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable
as List<DownloadTaskInfo>,stoppedTasks: null == stoppedTasks ? _self.stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<DownloadTaskInfo>,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 extends Object?>(TResult Function( List<Aria2Task> tasks, List<Aria2Task> waitingTasks, List<Aria2Task> stoppedTasks, Aria2GlobalStat? globalStat)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( List<DownloadTaskInfo> activeTasks, List<DownloadTaskInfo> waitingTasks, List<DownloadTaskInfo> 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 extends Object?>(TResult Function( List<Aria2Task> tasks, List<Aria2Task> waitingTasks, List<Aria2Task> stoppedTasks, Aria2GlobalStat? globalStat) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( List<DownloadTaskInfo> activeTasks, List<DownloadTaskInfo> waitingTasks, List<DownloadTaskInfo> 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 extends Object?>(TResult? Function( List<Aria2Task> tasks, List<Aria2Task> waitingTasks, List<Aria2Task> stoppedTasks, Aria2GlobalStat? globalStat)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( List<DownloadTaskInfo> activeTasks, List<DownloadTaskInfo> waitingTasks, List<DownloadTaskInfo> 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<Aria2Task> tasks = const [], final List<Aria2Task> waitingTasks = const [], final List<Aria2Task> stoppedTasks = const [], this.globalStat}): _tasks = tasks,_waitingTasks = waitingTasks,_stoppedTasks = stoppedTasks;
_HomeDownloaderUIState({final List<DownloadTaskInfo> activeTasks = const [], final List<DownloadTaskInfo> waitingTasks = const [], final List<DownloadTaskInfo> stoppedTasks = const [], this.globalStat}): _activeTasks = activeTasks,_waitingTasks = waitingTasks,_stoppedTasks = stoppedTasks;
final List<Aria2Task> _tasks;
@override@JsonKey() List<Aria2Task> get tasks {
if (_tasks is EqualUnmodifiableListView) return _tasks;
final List<DownloadTaskInfo> _activeTasks;
@override@JsonKey() List<DownloadTaskInfo> get activeTasks {
if (_activeTasks is EqualUnmodifiableListView) return _activeTasks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_tasks);
return EqualUnmodifiableListView(_activeTasks);
}
final List<Aria2Task> _waitingTasks;
@override@JsonKey() List<Aria2Task> get waitingTasks {
final List<DownloadTaskInfo> _waitingTasks;
@override@JsonKey() List<DownloadTaskInfo> get waitingTasks {
if (_waitingTasks is EqualUnmodifiableListView) return _waitingTasks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_waitingTasks);
}
final List<Aria2Task> _stoppedTasks;
@override@JsonKey() List<Aria2Task> get stoppedTasks {
final List<DownloadTaskInfo> _stoppedTasks;
@override@JsonKey() List<DownloadTaskInfo> 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<Aria2Task> tasks, List<Aria2Task> waitingTasks, List<Aria2Task> stoppedTasks, Aria2GlobalStat? globalStat
List<DownloadTaskInfo> activeTasks, List<DownloadTaskInfo> waitingTasks, List<DownloadTaskInfo> 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<Aria2Task>,waitingTasks: null == waitingTasks ? _self._waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,stoppedTasks: null == stoppedTasks ? _self._stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<Aria2Task>,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<DownloadTaskInfo>,waitingTasks: null == waitingTasks ? _self._waitingTasks : waitingTasks // ignore: cast_nullable_to_non_nullable
as List<DownloadTaskInfo>,stoppedTasks: null == stoppedTasks ? _self._stoppedTasks : stoppedTasks // ignore: cast_nullable_to_non_nullable
as List<DownloadTaskInfo>,globalStat: freezed == globalStat ? _self.globalStat : globalStat // ignore: cast_nullable_to_non_nullable
as DownloadGlobalStat?,
));
}

View File

@ -42,7 +42,7 @@ final class HomeDownloaderUIModelProvider
}
String _$homeDownloaderUIModelHash() =>
r'cb5d0973d56bbf40673afc2a734b49f5d034ab98';
r'27e2e4b7a5103eee9d489a347410131edef46be4';
abstract class _$HomeDownloaderUIModel
extends $Notifier<HomeDownloaderUIState> {

View File

@ -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<String> 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<bool> isTranslateModelDownloading() async {
final aria2cManager = ref.read(aria2cModelProvider.notifier);
return await aria2cManager.isNameInTask(_localTranslateModelName);
final downloadManager = ref.read(downloadManagerProvider.notifier);
return await downloadManager.isNameInTask(_localTranslateModelName);
}
}

View File

@ -43,7 +43,7 @@ final class InputMethodDialogUIModelProvider
}
String _$inputMethodDialogUIModelHash() =>
r'bd96c85ef2073d80de6eba71748b41adb8861e1c';
r'5c2989faf94d43bb814e5b80e10d68416c8241ec';
abstract class _$InputMethodDialogUIModel
extends $Notifier<InputMethodDialogUIState> {

View File

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

View File

@ -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<int> stepState,
WidgetRef ref,
ValueNotifier<List<String>> diagnosticLogs,
) async {
BuildContext context,
AppGlobalModel appModel,
ValueNotifier<int> stepState,
WidgetRef ref,
ValueNotifier<List<String>> 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()));
}
}
}
}

View File

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

View File

@ -41,7 +41,7 @@ final class ToolsUIModelProvider
}
}
String _$toolsUIModelHash() => r'ee1de3d555443f72b4fbb395a5728b2de1e8aaf4';
String _$toolsUIModelHash() => r'17890d6d16d8e9d98c42d06d9e6c6165965bcee0';
abstract class _$ToolsUIModel extends $Notifier<ToolsUIState> {
ToolsUIState build();

View File

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

View File

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

986
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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<ManagedTorrent>;
// Global session instance
static SESSION: OnceCell<Arc<Session>> = OnceCell::new();
static SESSION_INIT_LOCK: once_cell::sync::Lazy<Mutex<()>> =
once_cell::sync::Lazy::new(|| Mutex::new(()));
// Store torrent handles
static TORRENT_HANDLES: once_cell::sync::Lazy<RwLock<HashMap<usize, ManagedTorrentHandle>>> =
once_cell::sync::Lazy::new(|| RwLock::new(HashMap::new()));
// Store output folders for each task
static TASK_OUTPUT_FOLDERS: once_cell::sync::Lazy<RwLock<HashMap<usize, String>>> =
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<u8>,
output_folder: Option<String>,
trackers: Option<Vec<String>>,
) -> Result<usize> {
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<String>,
trackers: Option<Vec<String>>,
) -> Result<usize> {
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<String>,
trackers: Option<Vec<String>>,
) -> Result<usize> {
// 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<DownloadTaskInfo> {
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<Vec<DownloadTaskInfo>> {
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<Vec<DownloadTaskInfo>> = 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<DownloadGlobalStat> {
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(())
}

View File

@ -8,3 +8,4 @@ pub mod asar_api;
pub mod ort_api;
pub mod unp4k_api;
pub mod webview_api;
pub mod downloader_api;

View File

@ -302,7 +302,7 @@ pub fn get_gpu_info_from_registry() -> anyhow::Result<String> {
/// Resolve shortcut (.lnk) file to get target path
#[cfg(target_os = "windows")]
pub fn resolve_shortcut(lnk_path: &str) -> anyhow::Result<String> {
pub fn resolve_shortcut(lnk_path: String) -> anyhow::Result<String> {
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<String> {
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<String> {
}
#[cfg(not(target_os = "windows"))]
pub fn resolve_shortcut(_: &str) -> anyhow::Result<String> {
pub fn resolve_shortcut(_lnk_path: String) -> anyhow::Result<String> {
Ok(String::new())
}

File diff suppressed because it is too large Load Diff