From 73a9160161570d1ae4742f99ea515c96623c6057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=9A=E5=AD=90?= <40852301+uiYzzi@users.noreply.github.com> Date: Mon, 19 Feb 2024 01:05:13 +0800 Subject: [PATCH] :sparkles: Add Torrent Download --- lib/generated/intl/messages_en.dart | 2 + lib/generated/intl/messages_zh_CN.dart | 1 + lib/generated/l10n.dart | 10 + lib/l10n/intl_en.arb | 3 +- lib/l10n/intl_zh_CN.arb | 3 +- lib/utils/ariar2_http_utils.dart | 19 + lib/widgets/new_download_dialog.dart | 327 ++++++++++-------- lib/widgets/upload_torrent.dart | 22 ++ linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 13 files changed, 262 insertions(+), 138 deletions(-) create mode 100644 lib/widgets/upload_torrent.dart diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index daba5c3..4778e88 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -62,6 +62,8 @@ class MessageLookup extends MessageLookupByLibrary { "downloadPathInfo": MessageLookupByLibrary.simpleMessage("Set default download path"), "downloading": MessageLookupByLibrary.simpleMessage("Downloading"), + "dropTorrent": MessageLookupByLibrary.simpleMessage( + "Drag the torrent file here or click here to open it"), "exitApp": MessageLookupByLibrary.simpleMessage("Exit App"), "general": MessageLookupByLibrary.simpleMessage("General"), "language": MessageLookupByLibrary.simpleMessage("Language"), diff --git a/lib/generated/intl/messages_zh_CN.dart b/lib/generated/intl/messages_zh_CN.dart index 112bbec..891db4e 100644 --- a/lib/generated/intl/messages_zh_CN.dart +++ b/lib/generated/intl/messages_zh_CN.dart @@ -53,6 +53,7 @@ class MessageLookup extends MessageLookupByLibrary { "downloadPath": MessageLookupByLibrary.simpleMessage("下载路径"), "downloadPathInfo": MessageLookupByLibrary.simpleMessage("设置默认下载路径"), "downloading": MessageLookupByLibrary.simpleMessage("下载中"), + "dropTorrent": MessageLookupByLibrary.simpleMessage("拖动种子文件到此处或点击打开"), "exitApp": MessageLookupByLibrary.simpleMessage("退出程序"), "general": MessageLookupByLibrary.simpleMessage("常规"), "language": MessageLookupByLibrary.simpleMessage("语言"), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index a79294b..4d6d455 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -839,6 +839,16 @@ class S { args: [], ); } + + /// `Drag the torrent file here or click here to open it` + String get dropTorrent { + return Intl.message( + 'Drag the torrent file here or click here to open it', + name: 'dropTorrent', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 146b2db..d102a38 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -77,5 +77,6 @@ "showWindow":"Show Window", "exitApp":"Exit App", "openDirectory":"Open Directory", - "openFile":"Open File" + "openFile":"Open File", + "dropTorrent":"Drag the torrent file here or click here to open it" } \ No newline at end of file diff --git a/lib/l10n/intl_zh_CN.arb b/lib/l10n/intl_zh_CN.arb index ff2073d..876bf8c 100644 --- a/lib/l10n/intl_zh_CN.arb +++ b/lib/l10n/intl_zh_CN.arb @@ -77,5 +77,6 @@ "showWindow":"显示窗口", "exitApp":"退出程序", "openDirectory":"打开目录", - "openFile":"打开文件" + "openFile":"打开文件", + "dropTorrent":"拖动种子文件到此处或点击打开" } \ No newline at end of file diff --git a/lib/utils/ariar2_http_utils.dart b/lib/utils/ariar2_http_utils.dart index acc1aea..7514c3a 100644 --- a/lib/utils/ariar2_http_utils.dart +++ b/lib/utils/ariar2_http_utils.dart @@ -67,6 +67,25 @@ addUrl(params, aria2url) async { } } +addTorrent(params, aria2url) async { + try { + var rpcSecret = Global.rpcSecret; + var res = await http.post(Uri.parse(aria2url), + body: json.encode({ + "jsonrpc": "2.0", + "method": "aria2.addTorrent", + "id": uuid.v4(), + "params": ['token:$rpcSecret', ...params] + })); + if (res.statusCode == 200) { + var resJson = json.decode(res.body); + return resJson['result']; + } + } catch (e) { + Log.e(e); + } +} + tellStopped(String aria2url) async { try { var rpcSecret = Global.rpcSecret; diff --git a/lib/widgets/new_download_dialog.dart b/lib/widgets/new_download_dialog.dart index a9c8730..9fb5c0f 100644 --- a/lib/widgets/new_download_dialog.dart +++ b/lib/widgets/new_download_dialog.dart @@ -1,10 +1,16 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_selector/file_selector.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:path/path.dart'; import 'package:yolx/common/global.dart'; import 'package:yolx/generated/l10n.dart'; import 'package:yolx/utils/ariar2_http_utils.dart' as Aria2Http; import 'package:yolx/utils/file_utils.dart'; import 'package:yolx/utils/url_utils.dart'; +import 'package:yolx/widgets/upload_torrent.dart'; class NewDownloadDialog extends StatefulWidget { const NewDownloadDialog({super.key}); @@ -18,15 +24,32 @@ class _NewDownloadDialogState extends State { late TextEditingController _urlEditingController; late TextEditingController _downloadPathEditingController; late TextEditingController _downloadLimitEditingController; + late ValueNotifier _filePathNotifier; + late ValueNotifier _currentIndexNotifier; + bool _isFilledButtonEnabled = false; @override void initState() { super.initState(); _urlEditingController = TextEditingController(text: ""); + _urlEditingController.addListener(_updateButtonState); + _currentIndexNotifier = ValueNotifier(0); + _currentIndexNotifier.addListener(_updateButtonState); + _filePathNotifier = ValueNotifier(''); + _filePathNotifier.addListener(_updateButtonState); _downloadPathEditingController = TextEditingController(text: Global.downloadPath); _downloadLimitEditingController = TextEditingController(text: ""); } + void _updateButtonState() { + setState(() { + _isFilledButtonEnabled = _currentIndexNotifier.value == 0 && + _urlEditingController.text.isNotEmpty || + _currentIndexNotifier.value == 1 && + _filePathNotifier.value.isNotEmpty; + }); + } + @override void dispose() { _urlEditingController.dispose(); @@ -34,151 +57,185 @@ class _NewDownloadDialogState extends State { super.dispose(); } - int currentIndex = 0; // Initialize currentIndex with default value - @override Widget build(BuildContext context) { return ContentDialog( - actions: [ - Button( - child: Text(S.of(context).cancel), - onPressed: () { - Navigator.pop(context, 'cancel'); - }, - ), - FilledButton( - child: Text(S.of(context).submit), - onPressed: () async { - if (currentIndex == 0) { - String downloadPath; - var params = {}; - var urls = _urlEditingController.text.split("\n"); - if (_downloadLimitEditingController.text.isNotEmpty) { - params['max-download-limit'] = - (double.parse(_downloadLimitEditingController.text) * - 1048576) - .toInt() - .toString(); - } - if (Global.classificationSaving && - _downloadPathEditingController.text == Global.downloadPath) { - createClassificationFolder(); - List ruleNames = [ - S.current.compressedFiles, - S.current.documents, - S.current.music, - S.current.programs, - S.current.videos, - S.current.general - ]; - for (int i = 0; i < urls.length; i++) { - if (urls[i].startsWith("thunder://")) { - urls[i] = getURLFromThunder(urls[i]); - } - var j = getDownloadDirectory(await getFileType(urls[i])); - params["dir"] = - '${Global.downloadPath}${Global.pathSeparator}${ruleNames[j]}'; - await Aria2Http.addUrl([ - [urls[i]], - params - ], Global.rpcUrl); - } - } else { - downloadPath = _downloadPathEditingController.text; - params["dir"] = downloadPath; - for (int i = 0; i < urls.length; i++) { - if (urls[i].isNotEmpty) { - if (urls[i].startsWith("thunder://")) { - urls[i] = getURLFromThunder(urls[i]); + actions: [ + Button( + child: Text(S.of(context).cancel), + onPressed: () { + Navigator.pop(context, 'cancel'); + }, + ), + FilledButton( + onPressed: !_isFilledButtonEnabled + ? null + : () async { + var params = {}; + if (_downloadLimitEditingController.text.isNotEmpty) { + params['max-download-limit'] = + (double.parse(_downloadLimitEditingController.text) * + 1048576) + .toInt() + .toString(); } - await Aria2Http.addUrl([ - [urls[i]], - params - ], Global.rpcUrl); - } - } - } - } - - // ignore: use_build_context_synchronously - Navigator.pop(context, 'ok'); - }, - ), - ], - content: SizedBox( - height: 400, - child: TabView( - tabs: [ - Tab( - text: Text(S.current.URL), - icon: const Icon(FluentIcons.link), - semanticLabel: S.current.URL, - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextBox( + if (_currentIndexNotifier.value == 0) { + String downloadPath; + var urls = _urlEditingController.text.split("\n"); + if (Global.classificationSaving && + _downloadPathEditingController.text == + Global.downloadPath) { + createClassificationFolder(); + List ruleNames = [ + S.current.compressedFiles, + S.current.documents, + S.current.music, + S.current.programs, + S.current.videos, + S.current.general + ]; + for (int i = 0; i < urls.length; i++) { + if (urls[i].startsWith("thunder://")) { + urls[i] = getURLFromThunder(urls[i]); + } + var j = + getDownloadDirectory(await getFileType(urls[i])); + params["dir"] = + '${Global.downloadPath}${Global.pathSeparator}${ruleNames[j]}'; + await Aria2Http.addUrl([ + [urls[i]], + params + ], Global.rpcUrl); + } + } else { + downloadPath = _downloadPathEditingController.text; + params["dir"] = downloadPath; + for (int i = 0; i < urls.length; i++) { + if (urls[i].isNotEmpty) { + if (urls[i].startsWith("thunder://")) { + urls[i] = getURLFromThunder(urls[i]); + } + await Aria2Http.addUrl([ + [urls[i]], + params + ], Global.rpcUrl); + } + } + } + } else if (_currentIndexNotifier.value == 1) { + params["dir"] = _downloadPathEditingController.text; + List fileBytes = + File(_filePathNotifier.value).readAsBytesSync(); + String base64Encoded = base64Encode(fileBytes); + await Aria2Http.addTorrent( + [base64Encoded, [], params], Global.rpcUrl); + } + // ignore: use_build_context_synchronously + Navigator.pop(context, 'ok'); + }, + child: Text(S.of(context).submit), + ), + ], + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + height: 160, + child: TabView( + tabs: [ + Tab( + text: Text(S.current.URL), + icon: const Icon(FluentIcons.link), + semanticLabel: S.current.URL, + body: TextBox( placeholder: S.current.URLTextBox, placeholderStyle: const TextStyle( overflow: TextOverflow.visible, ), controller: _urlEditingController, - minLines: 3, - maxLines: 4, - ), - const SizedBox(height: 4), - Text(S.of(context).path), - Row( - children: [ - Expanded( - child: TextBox( - enabled: false, - controller: _downloadPathEditingController, - expands: false, - ), - ), - IconButton( - icon: const Icon(FluentIcons.fabric_folder, - size: 20.0), - onPressed: () async { - final String? path = await getDirectoryPath(); - if (path != null) { - setState(() { - _downloadPathEditingController.text = path; - }); - } - }) - ], + minLines: 6, + maxLines: null, ), - const SizedBox(height: 4), - Text(S.of(context).speedLimit), - Row( - children: [ - Expanded( - child: TextBox( - placeholder: S.current.maxDownloadLimit, - controller: _downloadLimitEditingController, - ), - ), - const Text("MB/s"), - ], - ), - ], - )), - Tab( - text: Text(S.current.torrent), - icon: const Icon(FluentIcons.classroom_logo), - semanticLabel: S.current.torrent, - body: Text(S.current.torrent)), + ), + Tab( + text: Text(S.current.torrent), + icon: const Icon(FluentIcons.classroom_logo), + semanticLabel: S.current.torrent, + body: DropTarget( + onDragDone: (DropDoneDetails details) { + final filePath = details.files.last.path; + if (filePath.endsWith('.torrent')) { + setState(() { + _filePathNotifier.value = filePath; + }); + } + }, + child: GestureDetector( + onTap: () async { + const xType = + XTypeGroup(extensions: ['torrent']); + final XFile? file = + await openFile(acceptedTypeGroups: [xType]); + + if (file != null) { + setState(() { + _filePathNotifier.value = file.path; + }); + } + }, + child: uploadTorrent( + context, + (_filePathNotifier.value.isNotEmpty + ? basename(_filePathNotifier.value) + : S.of(context).dropTorrent))))), + ], + closeButtonVisibility: CloseButtonVisibilityMode.never, + currentIndex: _currentIndexNotifier.value, + onChanged: (index) { + setState(() { + _currentIndexNotifier.value = + index; // Update the _currentIndexNotifier.value using setState + }); + }, + ), + ), + const SizedBox(height: 4), + Text(S.of(context).path), + Row( + children: [ + Expanded( + child: TextBox( + enabled: false, + controller: _downloadPathEditingController, + expands: false, + ), + ), + IconButton( + icon: const Icon(FluentIcons.fabric_folder, size: 20.0), + onPressed: () async { + final String? path = await getDirectoryPath(); + if (path != null) { + setState(() { + _downloadPathEditingController.text = path; + }); + } + }) + ], + ), + const SizedBox(height: 4), + Text(S.of(context).speedLimit), + Row( + children: [ + Expanded( + child: TextBox( + placeholder: S.current.maxDownloadLimit, + controller: _downloadLimitEditingController, + ), + ), + const Text("MB/s"), + ], + ), ], - closeButtonVisibility: CloseButtonVisibilityMode.never, - currentIndex: currentIndex, - onChanged: (index) { - setState(() { - currentIndex = index; // Update the currentIndex using setState - }); - }, - ), - ), - ); + )); } } diff --git a/lib/widgets/upload_torrent.dart b/lib/widgets/upload_torrent.dart new file mode 100644 index 0000000..712fdcf --- /dev/null +++ b/lib/widgets/upload_torrent.dart @@ -0,0 +1,22 @@ +import 'package:dotted_decoration/dotted_decoration.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +Widget uploadTorrent(BuildContext context, String text) { + return Container( + decoration: DottedDecoration( + shape: Shape.box, + borderRadius: const BorderRadius.all(Radius.circular(6)), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(FluentIcons.bulk_upload, size: 40), + const SizedBox(height: 10), + Text( + text, + style: const TextStyle(fontSize: 16), + ), + ], + ), + ); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 7d1a95a..604d302 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -15,6 +16,9 @@ #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_drop_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopDropPlugin"); + desktop_drop_plugin_register_with_registrar(desktop_drop_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1fcf55f..a414814 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop file_selector_linux handy_window screen_retriever diff --git a/pubspec.yaml b/pubspec.yaml index 51fced0..01d1ccb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,9 @@ dependencies: shared_preferences: ^2.2.2 gtk_window: ^0.1.1 tray_manager: ^0.2.1 - + desktop_drop: ^0.4.4 + dotted_decoration: ^2.0.0 + dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b44a627..12b74f2 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -14,6 +15,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DesktopDropPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopDropPlugin")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); ScreenRetrieverPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 8761497..08c3f6f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_drop file_selector_windows screen_retriever system_theme