diff --git a/lib/common/helper/system_helper.dart b/lib/common/helper/system_helper.dart index 7006d8a..eb69ccc 100644 --- a/lib/common/helper/system_helper.dart +++ b/lib/common/helper/system_helper.dart @@ -93,7 +93,10 @@ class SystemHelper { static Future> getPID(String name) async { final r = await Process.run("powershell", ["(ps $name).Id"]); - return r.stdout.toString().trim().split("\n"); + final str = r.stdout.toString().trim(); + dPrint(str); + if (str.isEmpty) return []; + return str.split("\n"); } static checkAndLaunchRSILauncher(String path) async { diff --git a/lib/ui/tools/downloader/dio_range_download.dart b/lib/ui/tools/downloader/dio_range_download.dart new file mode 100644 index 0000000..20f7674 --- /dev/null +++ b/lib/ui/tools/downloader/dio_range_download.dart @@ -0,0 +1,163 @@ +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:starcitizen_doctor/base/ui.dart'; + +/// https://github.com/qiaoshouqing/dio_range_download/blob/master/lib/dio_range_download.dart + +class RangeDownload { + static Future downloadWithChunks( + url, + savePath, { + bool isRangeDownload = true, + ProgressCallback? onReceiveProgress, + int maxChunk = 6, + Dio? dio, + CancelToken? cancelToken, + }) async { + const firstChunkSize = 102; + + int total = 0; + if (dio == null) { + dio = Dio(); + dio.options.connectTimeout = const Duration(seconds: 10); + } + var progress = []; + var progressInit = []; + + Future mergeTempFiles(chunk) async { + File f = File(savePath + "temp0"); + IOSink ioSink = f.openWrite(mode: FileMode.writeOnlyAppend); + for (int i = 1; i < chunk; ++i) { + File f0 = File(savePath + "temp$i"); + await ioSink.addStream(f0.openRead()); + await f0.delete(); + } + await ioSink.close(); + await f.rename(savePath); + } + + Future mergeFiles(file1, file2, targetFile) async { + File f1 = File(file1); + File f2 = File(file2); + IOSink ioSink = f1.openWrite(mode: FileMode.writeOnlyAppend); + await ioSink.addStream(f2.openRead()); + await f2.delete(); + await ioSink.close(); + await f1.rename(targetFile); + } + + createCallback(no) { + return (int received, rangeTotal) async { + if (received >= rangeTotal) { + var path = savePath + "temp$no"; + var oldPath = savePath + "temp${no}_pre"; + File oldFile = File(oldPath); + if (oldFile.existsSync()) { + await mergeFiles(oldPath, path, path); + } + } + try { + progress[no] = progressInit[no] + received; + } catch (e) { + dPrint(e); + } + if (onReceiveProgress != null && total != 0) { + onReceiveProgress(progress.reduce((a, b) => a + b), total); + } + }; + } + + Future downloadChunk(url, start, end, no, + {isMerge = true}) async { + int initLength = 0; + --end; + var path = savePath + "temp$no"; + File targetFile = File(path); + if (await targetFile.exists() && isMerge) { + dPrint("good job start:$start length:${File(path).lengthSync()}"); + if (start + await targetFile.length() < end) { + initLength = await targetFile.length(); + start += initLength; + var preFile = File(path + "_pre"); + if (await preFile.exists()) { + initLength += await preFile.length(); + start += await preFile.length(); + await mergeFiles(preFile.path, targetFile.path, preFile.path); + } else { + await targetFile.rename(preFile.path); + } + } else { + await targetFile.delete(); + } + } + progress.add(initLength); + progressInit.add(initLength); + return dio!.download( + url, + path, + deleteOnError: false, + onReceiveProgress: createCallback(no), + options: Options( + headers: {"range": "bytes=$start-$end"}, + ), + cancelToken: cancelToken, + ); + } + + if (isRangeDownload) { + Response response = + await downloadChunk(url, 0, firstChunkSize, 0, isMerge: false); + if (response.statusCode == 206) { + dPrint("This http protocol support range download"); + total = int.parse(response.headers + .value(HttpHeaders.contentRangeHeader)! + .split("/") + .last); + int reserved = total - + int.parse(response.headers.value(HttpHeaders.contentLengthHeader)!); + int chunk = (reserved / firstChunkSize).ceil() + 1; + if (chunk > 1) { + int chunkSize = firstChunkSize; + if (chunk > maxChunk + 1) { + chunk = maxChunk + 1; + chunkSize = (reserved / maxChunk).ceil(); + } + var futures = []; + for (int i = 0; i < maxChunk; ++i) { + int start = firstChunkSize + i * chunkSize; + int end; + if (i == maxChunk - 1) { + end = total; + } else { + end = start + chunkSize; + } + futures.add(downloadChunk(url, start, end, i + 1)); + } + await Future.wait(futures); + } + await mergeTempFiles(chunk); + return Response( + statusCode: 200, + statusMessage: "Download success.", + data: "Download success.", + requestOptions: RequestOptions(), + ); + } else if (response.statusCode == 200) { + dPrint( + "The protocol does not support resumed downloads, and regular downloads will be used."); + return dio.download(url, savePath, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + deleteOnError: false); + } else { + dPrint("The request encountered a problem, please handle it yourself"); + return response; + } + } else { + return dio.download(url, savePath, + onReceiveProgress: onReceiveProgress, + cancelToken: cancelToken, + deleteOnError: false); + } + } +} diff --git a/lib/ui/tools/downloader/downloader_dialog_ui_model.dart b/lib/ui/tools/downloader/downloader_dialog_ui_model.dart index a1b16a5..20b6bba 100644 --- a/lib/ui/tools/downloader/downloader_dialog_ui_model.dart +++ b/lib/ui/tools/downloader/downloader_dialog_ui_model.dart @@ -1,9 +1,11 @@ import 'dart:io'; +import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:hyper_thread_downloader/hyper_thread_downloader.dart'; import 'package:starcitizen_doctor/base/ui_model.dart'; +import 'dio_range_download.dart'; + class DownloaderDialogUIModel extends BaseUIModel { final String fileName; String savePath; @@ -14,15 +16,16 @@ class DownloaderDialogUIModel extends BaseUIModel { DownloaderDialogUIModel(this.fileName, this.savePath, this.downloadUrl, {this.showChangeSavePathDialog = false, this.threadCount = 1}); - final downloader = HyperDownload(); + CancelToken? downloadCancelToken; int? downloadTaskId; bool isInMerging = false; double? progress; - double? speed; - double? remainTime; + int? speed; + DateTime? lastUpdateTime; + int? lastUpdateCount; int? count; int? total; @@ -53,48 +56,44 @@ class DownloaderDialogUIModel extends BaseUIModel { savePath = "$savePath/$fileName"; } // start download - downloader.startDownload( - url: downloadUrl, - savePath: savePath, - threadCount: threadCount, - prepareWorking: (bool done) {}, - workingMerge: (bool done) { + try { + downloadCancelToken = CancelToken(); + final r = await RangeDownload.downloadWithChunks(downloadUrl, savePath, + maxChunk: 10, + cancelToken: downloadCancelToken, + isRangeDownload: true, onReceiveProgress: (int count, int total) { + lastUpdateTime ??= DateTime.now(); + if ((DateTime.now().difference(lastUpdateTime ?? DateTime.now())) + .inSeconds >= + 1) { + lastUpdateTime = DateTime.now(); + speed = (count - (lastUpdateCount ?? 0)); + lastUpdateCount = count; + notifyListeners(); + } + this.count = count; + this.total = total; + progress = count / total * 100; + if (count == total) { isInMerging = true; - progress = null; - notifyListeners(); - }, - downloadProgress: ({ - required double progress, - required double speed, - required double remainTime, - required int count, - required int total, - }) { - this.progress = ((progress) * 100); - this.speed = speed; - this.remainTime = remainTime; - this.count = count; - this.total = total; - notifyListeners(); - }, - downloadComplete: () { - notifyListeners(); - Navigator.pop(context!, savePath); - }, - downloadFailed: (String reason) { - notifyListeners(); - showToast(context!, "下载失败! $reason"); - }, - downloadTaskId: (int id) { - downloadTaskId = id; - }, - downloadingLog: (String log) {}); + } + notifyListeners(); + }); + if (r.statusCode == 200) { + Navigator.pop(context!, savePath); + } + } catch (e) { + if (e is DioException && e.type != DioExceptionType.cancel) { + if (mounted) showToast(context!, "下载失败:$e"); + } + } } doCancel() { - if (downloadTaskId != null) { - downloader.stopDownload(id: downloadTaskId!); - } + try { + downloadCancelToken?.cancel(); + downloadCancelToken = null; + } catch (_) {} Navigator.pop(context!, "cancel"); } } diff --git a/lib/ui/tools/tools_ui_model.dart b/lib/ui/tools/tools_ui_model.dart index 1ee3fa5..c11c10c 100644 --- a/lib/ui/tools/tools_ui_model.dart +++ b/lib/ui/tools/tools_ui_model.dart @@ -308,10 +308,15 @@ class ToolsUIModel extends BaseUIModel { showToast(context!, "该功能维护中,请稍后再试!"); return; } + if ((await SystemHelper.getPID("RSI Launcher.exe")).isNotEmpty) { + showToast(context!, "RSI启动器正在运行,请手动退出启动器再使用此功能。"); + return; + } await showToast( context!, "P4k 是星际公民的核心游戏文件,高达近 100GB,盒子提供的离线下载是为了帮助一些p4k文件下载超级慢的用户。" "\n\n接下来会弹窗询问您保存位置(可以选择星际公民文件夹也可以选择别处),下载完成后请确保 P4K 文件夹位于 LIVE 文件夹内,之后使用星际公民启动器校验更新即可。"); + final r = await showDialog( context: context!, dismissWithEsc: false, diff --git a/pubspec.yaml b/pubspec.yaml index d1f04f3..b8b3c3e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,7 +43,6 @@ dependencies: dio: ^5.3.3 markdown_widget: ^2.2.0 extended_image: ^8.1.1 - hyper_thread_downloader: ^1.0.5 device_info_plus: ^9.0.3 file_picker: ^5.5.0 file_sizes: ^1.0.6