From e0ca3377accfa97d0beaf59583fec4238da68a8f Mon Sep 17 00:00:00 2001 From: xkeyC <3334969096@qq.com> Date: Sat, 24 Feb 2024 14:42:33 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E7=AE=A1=E7=90=86=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/helper/system_helper.dart | 6 + lib/common/io/aria2c.dart | 7 +- lib/ui/home/downloads/downloads_ui.dart | 172 ++++++++++++++++++ lib/ui/home/downloads/downloads_ui_model.dart | 138 ++++++++++++++ .../game_doctor/game_doctor_ui_model.dart | 9 +- lib/ui/index_ui_model.dart | 8 +- lib/ui/tools/tools_ui_model.dart | 52 ++++-- pubspec.yaml | 2 + 8 files changed, 363 insertions(+), 31 deletions(-) create mode 100644 lib/ui/home/downloads/downloads_ui.dart create mode 100644 lib/ui/home/downloads/downloads_ui_model.dart diff --git a/lib/common/helper/system_helper.dart b/lib/common/helper/system_helper.dart index 300606b..9da4e68 100644 --- a/lib/common/helper/system_helper.dart +++ b/lib/common/helper/system_helper.dart @@ -251,4 +251,10 @@ foreach ($adapter in $adapterMemory) { .padLeft(hexDigits, '0') .toUpperCase(); } + + static Future openDir(path) async { + dPrint("SystemHelper.openDir path === $path"); + await Process.run( + SystemHelper.powershellPath, ["explorer.exe", "/select,\"$path\""]); + } } diff --git a/lib/common/io/aria2c.dart b/lib/common/io/aria2c.dart index 715f348..8c5ad8b 100644 --- a/lib/common/io/aria2c.dart +++ b/lib/common/io/aria2c.dart @@ -62,12 +62,13 @@ class Aria2cManager { "--save-session=${sessionFile.absolute.path.trim()}", "--save-session-interval=60", "--file-allocation=trunc", + // TODO for debug + "--max-overall-download-limit=100k" ], - workingDirectory: _aria2cDir, - runInShell: false); + workingDirectory: _aria2cDir); p.stdout.transform(utf8.decoder).listen((event) { if (event.trim().isEmpty) return; - dPrint("[aria2c]: $event"); + dPrint("[aria2c]: ${event.trim()}"); if (event.contains("IPv4 RPC: listening on TCP port")) { _isDaemonRunning = true; aria2c; diff --git a/lib/ui/home/downloads/downloads_ui.dart b/lib/ui/home/downloads/downloads_ui.dart new file mode 100644 index 0000000..a200860 --- /dev/null +++ b/lib/ui/home/downloads/downloads_ui.dart @@ -0,0 +1,172 @@ +import 'package:file_sizes/file_sizes.dart'; +import 'package:intl/intl.dart'; +import 'package:starcitizen_doctor/base/ui.dart'; +import 'package:starcitizen_doctor/base/ui_model.dart'; + +import 'downloads_ui_model.dart'; + +class DownloadsUI extends BaseUI { + final DateFormat formatter = DateFormat('yyyy-MM-dd HH:mm:ss'); + + @override + Widget? buildBody(BuildContext context, DownloadsUIModel model) { + return makeDefaultPage(context, model, + content: Column( + children: [ + const SizedBox(height: 12), + Expanded( + child: ListView.builder( + itemBuilder: (BuildContext context, int index) { + final (task, type, isFirstType) = model.getTaskAndType(index); + final nt = model.getTaskTypeAndName(task); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isFirstType) + Column( + children: [ + Container( + padding: const EdgeInsets.only( + left: 24, right: 24, top: 12, bottom: 12), + margin: const EdgeInsets.only(top: 6, bottom: 6), + child: Row( + children: [ + Expanded( + child: 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), + decoration: BoxDecoration( + color: + FluentTheme.of(context).cardColor.withOpacity(.03), + borderRadius: BorderRadius.circular(7), + ), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + nt.value, + style: const TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 6), + Row( + children: [ + Text( + "总大小:${FileSize.getSize(task.totalLength ?? 0)}", + style: const TextStyle(fontSize: 14), + ), + const SizedBox(width: 12), + if (nt.key == "torrent" && + task.verifiedLength != null && + task.verifiedLength != 0) + Text( + "校验中...(${FileSize.getSize(task.verifiedLength)})", + style: const TextStyle(fontSize: 14), + ) + else if (task.status == "active") + Text( + "下载中... (${((task.completedLength ?? 0) / (task.totalLength ?? 1)).toStringAsFixed(4)}%)") + else + Text( + "状态:${model.statusMap[task.status]}", + style: const TextStyle(fontSize: 14), + ), + const SizedBox(width: 24), + if (task.status == "active" && + task.verifiedLength == null) + Text( + "ETA: ${formatter.format(DateTime.now().add(Duration(seconds: model.getETA(task))))}"), + ], + ), + ], + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "已上传:${FileSize.getSize(task.uploadLength)}"), + Text( + "已下载:${FileSize.getSize(task.completedLength)}"), + ], + ), + const SizedBox(width: 18), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("↑:${FileSize.getSize(task.uploadSpeed)}/s"), + Text( + "↓:${FileSize.getSize(task.downloadSpeed)}/s"), + ], + ), + const SizedBox(width: 32), + if (type != "stopped") + DropDownButton( + closeAfterClick: false, + title: const Padding( + padding: EdgeInsets.all(3), + child: Text('选项'), + ), + items: [ + if (task.status == "paused") + MenuFlyoutItem( + leading: const Icon(FluentIcons.download), + text: const Text('继续下载'), + onPressed: () => + model.resumeTask(task.gid)) + else if (task.status == "active") + MenuFlyoutItem( + leading: const Icon(FluentIcons.pause), + text: const Text('暂停下载'), + onPressed: () => + model.pauseTask(task.gid)), + const MenuFlyoutSeparator(), + MenuFlyoutItem( + leading: const Icon( + FluentIcons.chrome_close, + size: 14, + ), + text: const Text('取消下载'), + onPressed: () => + model.cancelTask(task.gid)), + MenuFlyoutItem( + leading: const Icon( + FluentIcons.folder_open, + size: 14, + ), + text: const Text('打开文件夹'), + onPressed: () => + model.openFolder(task)), + ], + ), + const SizedBox(width: 12), + ], + ), + ), + ], + ); + }, + itemCount: model.getTasksLen(), + )) + ], + )); + } + + @override + String getUITitle(BuildContext context, DownloadsUIModel model) => "下载管理"; +} diff --git a/lib/ui/home/downloads/downloads_ui_model.dart b/lib/ui/home/downloads/downloads_ui_model.dart new file mode 100644 index 0000000..3877eb4 --- /dev/null +++ b/lib/ui/home/downloads/downloads_ui_model.dart @@ -0,0 +1,138 @@ +import 'dart:io'; + +import 'package:aria2/aria2.dart'; +import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'package:starcitizen_doctor/common/helper/system_helper.dart'; +import 'package:starcitizen_doctor/common/io/aria2c.dart'; + +class DownloadsUIModel extends BaseUIModel { + List tasks = []; + List waitingTasks = []; + List stoppedTasks = []; + + final statusMap = { + "active": "下载中...", + "waiting": "等待中", + "paused": "已暂停", + "error": "下载失败", + "complete": "下载完成", + "removed": "已删除", + }; + + final listHeaderStatusMap = { + "active": "下载中", + "waiting": "等待中", + "stopped": "已结束", + }; + + @override + initModel() { + super.initModel(); + _listenDownloader(); + } + + onTapButton(String key) {} + + _listenDownloader() async { + try { + while (true) { + if (!mounted) return; + tasks.clear(); + tasks = await Aria2cManager.aria2c.tellActive(); + waitingTasks = await Aria2cManager.aria2c.tellWaiting(0, 1000000); + stoppedTasks = await Aria2cManager.aria2c.tellStopped(0, 1000000); + notifyListeners(); + await Future.delayed(const Duration(seconds: 1)); + } + } catch (e) { + dPrint("[DownloadsUIModel]._listenDownloader Error: $e"); + } + } + + int getTasksLen() { + return tasks.length + waitingTasks.length + stoppedTasks.length; + } + + (Aria2Task, String, bool) getTaskAndType(int index) { + final tempList = [...tasks, ...waitingTasks, ...stoppedTasks]; + if (index >= 0 && index < tasks.length) { + return (tempList[index], "active", index == 0); + } + if (index >= tasks.length && index < tasks.length + waitingTasks.length) { + return (tempList[index], "waiting", index == tasks.length); + } + if (index >= tasks.length + waitingTasks.length && + index < tempList.length) { + return ( + tempList[index], + "stopped", + index == tasks.length + waitingTasks.length + ); + } + throw Exception("Index out of range or element is null"); + } + + MapEntry getTaskTypeAndName(Aria2Task task) { + if (task.bittorrent == null) { + String uri = task.files?[0]['uris'][0]['uri'] as String; + return MapEntry("url", uri.split('/').last); + } else if (task.bittorrent != null) { + if (task.bittorrent!.containsKey('info')) { + var btName = task.bittorrent?["info"]["name"]; + return MapEntry("torrent", btName ?? 'torrent'); + } else { + return MapEntry("magnet", '[METADATA]${task.infoHash}'); + } + } else { + return const MapEntry("metaLink", '==========metaLink============'); + } + } + + List getFilesFormTask(Aria2Task task) { + List l = []; + if (task.files != null) { + for (var element in task.files!) { + final f = Aria2File.fromJson(element); + l.add(f); + } + } + return l; + } + + int getETA(Aria2Task task) { + if (task.downloadSpeed == null || task.downloadSpeed == 0) return 0; + final remainingBytes = + (task.totalLength ?? 0) - (task.completedLength ?? 0); + return remainingBytes ~/ (task.downloadSpeed!); + } + + Future resumeTask(String? gid) async { + if (gid != null) { + await Aria2cManager.aria2c.unpause(gid); + } + } + + Future pauseTask(String? gid) async { + if (gid != null) { + await Aria2cManager.aria2c.pause(gid); + } + } + + Future cancelTask(String? gid) async { + await Future.delayed(const Duration(milliseconds: 300)); + if (gid != null) { + final ok = await showConfirmDialogs( + context!, "确认取消下载?", const Text("你可能需要手动删除下载文件。")); + if (ok == true) { + await Aria2cManager.aria2c.remove(gid); + } + } + } + + openFolder(Aria2Task task) { + final f = getFilesFormTask(task).firstOrNull; + if (f != null) { + SystemHelper.openDir(File(f.path!).absolute.path.replaceAll("/", "\\")); + } + } +} diff --git a/lib/ui/home/game_doctor/game_doctor_ui_model.dart b/lib/ui/home/game_doctor/game_doctor_ui_model.dart index 9bddd39..00183ab 100644 --- a/lib/ui/home/game_doctor/game_doctor_ui_model.dart +++ b/lib/ui/home/game_doctor/game_doctor_ui_model.dart @@ -245,20 +245,15 @@ class GameDoctorUIModel extends BaseUIModel { case "rsi_log": final path = await SCLoggerHelper.getLogFilePath(); if (path == null) return; - openDir(path); + SystemHelper.openDir(path); return; case "game_log": if (scInstalledPath == "not_install") { showToast(context!, "请在首页选择游戏安装目录。"); return; } - openDir("$scInstalledPath\\Game.log"); + SystemHelper.openDir("$scInstalledPath\\Game.log"); return; } } - - openDir(path) async { - await Process.run( - SystemHelper.powershellPath, ["explorer.exe", "/select,\"$path\""]); - } } diff --git a/lib/ui/index_ui_model.dart b/lib/ui/index_ui_model.dart index 61c34d5..34452b6 100644 --- a/lib/ui/index_ui_model.dart +++ b/lib/ui/index_ui_model.dart @@ -7,6 +7,8 @@ import 'package:starcitizen_doctor/common/helper/system_helper.dart'; import 'package:starcitizen_doctor/common/io/aria2c.dart'; import 'package:starcitizen_doctor/global_ui_model.dart'; import 'package:starcitizen_doctor/ui/about/about_ui_model.dart'; +import 'package:starcitizen_doctor/ui/home/downloads/downloads_ui.dart'; +import 'package:starcitizen_doctor/ui/home/downloads/downloads_ui_model.dart'; import 'package:starcitizen_doctor/ui/home/home_ui_model.dart'; import 'package:starcitizen_doctor/ui/settings/settings_ui_model.dart'; import 'package:starcitizen_doctor/ui/tools/tools_ui_model.dart'; @@ -98,8 +100,10 @@ class IndexUIModel extends BaseUIModel { } } - void goDownloader() { - + Future goDownloader() async { + await BaseUIContainer( + uiCreate: () => DownloadsUI(), + modelCreate: () => DownloadsUIModel()).push(context!); } void _listenAria2c() async { diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index d7f4a92..2b6c498 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -9,8 +9,12 @@ import 'package:starcitizen_doctor/common/conf/app_conf.dart'; import 'package:starcitizen_doctor/common/helper/log_helper.dart'; import 'package:starcitizen_doctor/common/helper/system_helper.dart'; import 'package:starcitizen_doctor/common/io/aria2c.dart'; +import 'package:starcitizen_doctor/common/io/rs_http.dart'; +import 'package:starcitizen_doctor/ui/home/downloads/downloads_ui.dart'; import 'package:xml/xml.dart'; +import '../home/downloads/downloads_ui_model.dart'; + class ToolsUIModel extends BaseUIModel { bool _working = false; @@ -50,7 +54,7 @@ class ToolsUIModel extends BaseUIModel { ), _ToolsItemData( "p4k_downloader", - "P4K 分流下载", + "P4K 分流下载 / 修复", "使用星际公民中文百科提供的分流下载服务。 \n\n资源有限,请勿滥用。请确保您的硬盘拥有至少大于 200G 的可用空间。", const Icon(FontAwesomeIcons.download, size: 28), onTap: _downloadP4k, @@ -340,11 +344,6 @@ class ToolsUIModel extends BaseUIModel { String savePath = scInstalledPath; String fileName = "Data.p4k"; - var downloadUrl = AppConf.networkVersionData?.p4kDownloadUrl; - if (downloadUrl == null || downloadUrl.isEmpty) { - showToast(context!, "该功能维护中,请稍后再试!"); - return; - } if ((await SystemHelper.getPID("\"RSI Launcher\"")).isNotEmpty) { showToast(context!, "RSI启动器正在运行!请先关闭启动器再使用此功能!", constraints: BoxConstraints( @@ -354,20 +353,20 @@ class ToolsUIModel extends BaseUIModel { await showToast( context!, - "P4k 是星际公民的核心游戏文件,高达 100GB+,盒子提供的离线下载是为了帮助一些p4k文件下载超级慢的用户。" + "P4k 是星际公民的核心游戏文件,高达 100GB+,盒子提供的离线下载是为了帮助一些p4k文件下载超级慢的用户 或用于修复官方启动器无法修复的 p4k 文件。" "\n\n接下来会弹窗询问您保存位置(可以选择星际公民文件夹也可以选择别处),下载完成后请确保 P4K 文件夹位于 LIVE 文件夹内,之后使用星际公民启动器校验更新即可。"); // AnalyticsApi.touch("p4k_download"); final userSelect = await FilePicker.platform.saveFile( initialDirectory: savePath, fileName: fileName, lockParentWindow: true); if (userSelect == null) { - Navigator.pop(context!); return; } - final f = File(userSelect); - if (await f.exists()) { - await f.delete(); - } + // 不再需要删除旧 p4k,直接在其基础上校验 + // final f = File(userSelect); + // if (await f.exists()) { + // await f.delete(); + // } savePath = userSelect; dPrint(savePath); notifyListeners(); @@ -375,14 +374,29 @@ class ToolsUIModel extends BaseUIModel { savePath = savePath.substring(0, savePath.length - fileName.length - 1); } - final gid = await Aria2cManager.aria2c.addUri( - ["https://release.scbox.org/data_3.22.0A-LIVE.9035564.p4k.torrent"], - extraParams: {"dir": savePath}); - - dPrint("Aria2cManager.aria2c.addUri resp === $gid"); - - showToast(context!, "添加下载任务成功!请留意盒子右上角的下载管理器。"); + _working = true; + final btData = await handleError(() => RSHttp.get( + "https://p4k.42kit.com/3.22.1-LIVE.9072370/Data.p4k.torrent")); + if (btData == null || btData.data == null) { + _working = false; + return; + } + try { + final b64Str = base64Encode(btData.data!); + dPrint(b64Str); + final gid = await Aria2cManager.aria2c + .addTorrent(b64Str, extraParams: {"dir": savePath}); + _working = false; + dPrint("Aria2cManager.aria2c.addUri resp === $gid"); + await Aria2cManager.aria2c.saveSession(); + await showToast(context!, "创建下载任务成功!"); + BaseUIContainer( + uiCreate: () => DownloadsUI(), + modelCreate: () => DownloadsUIModel()).push(context!); + } catch (e) { + showToast(context!, "初始化失败!"); + } // launchUrlString("https://citizenwiki.cn/SC%E6%B1%89%E5%8C%96%E7%9B%92%E5%AD%90#%E5%88%86%E6%B5%81%E4%B8%8B%E8%BD%BD%E6%95%99%E7%A8%8B"); } diff --git a/pubspec.yaml b/pubspec.yaml index 3362edb..2946cd1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -73,7 +73,9 @@ dependencies: rust_builder: path: rust_builder aria2: + #git: https://github.com/xkeyC/dart_aria2_rpc.git path: ../../xkeyC/dart_aria2_rpc + intl: ^0.18.0 dependency_overrides: http: ^1.1.2