From 83febe0a00f424b1f88c4a7f6297d281cc531d06 Mon Sep 17 00:00:00 2001 From: Levi Date: Thu, 16 Jan 2025 18:49:32 +0800 Subject: [PATCH] feat: support creating tasks using gopeed scheme (#876) --- .../lib/api/model/downloader_config.g.dart | 2 + ui/flutter/lib/api/model/options.dart | 14 ++-- ui/flutter/lib/api/model/options.g.dart | 13 ++-- ui/flutter/lib/api/model/request.dart | 45 ++++++++---- ui/flutter/lib/api/model/request.g.dart | 35 +++++---- ui/flutter/lib/api/model/result.g.dart | 2 +- .../app/controllers/app_controller.dart | 20 ++++- .../create/dto/create_router_params.dart | 24 ++++++ .../create/dto/create_router_params.g.dart | 31 ++++++++ .../app/modules/create/views/create_view.dart | 73 ++++++++++++++++++- .../modules/redirect/views/redirect_view.dart | 8 +- .../lib/core/common/start_config.g.dart | 2 +- .../entry/scheme_register_native.dart | 3 +- ui/flutter/pubspec.yaml | 2 +- 14 files changed, 222 insertions(+), 52 deletions(-) create mode 100644 ui/flutter/lib/app/modules/create/dto/create_router_params.dart create mode 100644 ui/flutter/lib/app/modules/create/dto/create_router_params.g.dart diff --git a/ui/flutter/lib/api/model/downloader_config.g.dart b/ui/flutter/lib/api/model/downloader_config.g.dart index ee78f1b4e..b7f2aa525 100644 --- a/ui/flutter/lib/api/model/downloader_config.g.dart +++ b/ui/flutter/lib/api/model/downloader_config.g.dart @@ -73,6 +73,7 @@ ExtraConfig _$ExtraConfigFromJson(Map json) => ExtraConfig( locale: json['locale'] as String? ?? '', lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false, defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false, + defaultBtClient: json['defaultBtClient'] as bool? ?? true, )..bt = ExtraConfigBt.fromJson(json['bt'] as Map); Map _$ExtraConfigToJson(ExtraConfig instance) => @@ -81,6 +82,7 @@ Map _$ExtraConfigToJson(ExtraConfig instance) => 'locale': instance.locale, 'lastDeleteTaskKeep': instance.lastDeleteTaskKeep, 'defaultDirectDownload': instance.defaultDirectDownload, + 'defaultBtClient': instance.defaultBtClient, 'bt': instance.bt.toJson(), }; diff --git a/ui/flutter/lib/api/model/options.dart b/ui/flutter/lib/api/model/options.dart index fdce5ac42..2c5fac452 100644 --- a/ui/flutter/lib/api/model/options.dart +++ b/ui/flutter/lib/api/model/options.dart @@ -10,8 +10,8 @@ class Options { Object? extra; Options({ - required this.name, - required this.path, + this.name = '', + this.path = '', this.selectFiles = const [], this.extra, }); @@ -24,12 +24,16 @@ class Options { @JsonSerializable() class OptsExtraHttp { - int connections = 0; - bool autoTorrent = false; + int connections; + bool autoTorrent; - OptsExtraHttp(); + OptsExtraHttp({ + this.connections = 0, + this.autoTorrent = false, + }); factory OptsExtraHttp.fromJson(Map json) => _$OptsExtraHttpFromJson(json); + Map toJson() => _$OptsExtraHttpToJson(this); } diff --git a/ui/flutter/lib/api/model/options.g.dart b/ui/flutter/lib/api/model/options.g.dart index b496e9fc6..8cbad4471 100644 --- a/ui/flutter/lib/api/model/options.g.dart +++ b/ui/flutter/lib/api/model/options.g.dart @@ -7,10 +7,10 @@ part of 'options.dart'; // ************************************************************************** Options _$OptionsFromJson(Map json) => Options( - name: json['name'] as String, - path: json['path'] as String, + name: json['name'] as String? ?? '', + path: json['path'] as String? ?? '', selectFiles: (json['selectFiles'] as List?) - ?.map((e) => e as int) + ?.map((e) => (e as num).toInt()) .toList() ?? const [], extra: json['extra'], @@ -34,9 +34,10 @@ Map _$OptionsToJson(Options instance) { } OptsExtraHttp _$OptsExtraHttpFromJson(Map json) => - OptsExtraHttp() - ..connections = json['connections'] as int - ..autoTorrent = json['autoTorrent'] as bool; + OptsExtraHttp( + connections: (json['connections'] as num?)?.toInt() ?? 0, + autoTorrent: json['autoTorrent'] as bool? ?? false, + ); Map _$OptsExtraHttpToJson(OptsExtraHttp instance) => { diff --git a/ui/flutter/lib/api/model/request.dart b/ui/flutter/lib/api/model/request.dart index fd9a7e063..289f1e26a 100644 --- a/ui/flutter/lib/api/model/request.dart +++ b/ui/flutter/lib/api/model/request.dart @@ -6,9 +6,9 @@ part 'request.g.dart'; class Request { String url; Object? extra; - Map? labels = {}; + Map? labels; RequestProxy? proxy; - bool skipVerifyCert = false; + bool skipVerifyCert; Request({ required this.url, @@ -26,11 +26,15 @@ class Request { @JsonSerializable() class ReqExtraHttp { - String method = 'GET'; - Map header = {}; - String body = ''; - - ReqExtraHttp(); + String method; + Map header; + String body; + + ReqExtraHttp({ + this.method = 'GET', + this.header = const {}, + this.body = '', + }); factory ReqExtraHttp.fromJson(Map json) => _$ReqExtraHttpFromJson(json); @@ -40,9 +44,11 @@ class ReqExtraHttp { @JsonSerializable() class ReqExtraBt { - List trackers = []; + List trackers; - ReqExtraBt(); + ReqExtraBt({ + this.trackers = const [], + }); factory ReqExtraBt.fromJson(Map json) => _$ReqExtraBtFromJson(json); @@ -58,15 +64,22 @@ enum RequestProxyMode { @JsonSerializable() class RequestProxy { - RequestProxyMode mode = RequestProxyMode.follow; - String scheme = 'http'; - String host = ''; - String usr = ''; - String pwd = ''; - - RequestProxy(); + RequestProxyMode mode; + String scheme; + String host; + String usr; + String pwd; + + RequestProxy({ + this.mode = RequestProxyMode.follow, + this.scheme = 'http', + this.host = '', + this.usr = '', + this.pwd = '', + }); factory RequestProxy.fromJson(Map json) => _$RequestProxyFromJson(json); + Map toJson() => _$RequestProxyToJson(this); } diff --git a/ui/flutter/lib/api/model/request.g.dart b/ui/flutter/lib/api/model/request.g.dart index 5acbda52a..bd2638dfc 100644 --- a/ui/flutter/lib/api/model/request.g.dart +++ b/ui/flutter/lib/api/model/request.g.dart @@ -36,10 +36,14 @@ Map _$RequestToJson(Request instance) { return val; } -ReqExtraHttp _$ReqExtraHttpFromJson(Map json) => ReqExtraHttp() - ..method = json['method'] as String - ..header = Map.from(json['header'] as Map) - ..body = json['body'] as String; +ReqExtraHttp _$ReqExtraHttpFromJson(Map json) => ReqExtraHttp( + method: json['method'] as String? ?? 'GET', + header: (json['header'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ) ?? + const {}, + body: json['body'] as String? ?? '', + ); Map _$ReqExtraHttpToJson(ReqExtraHttp instance) => { @@ -48,21 +52,26 @@ Map _$ReqExtraHttpToJson(ReqExtraHttp instance) => 'body': instance.body, }; -ReqExtraBt _$ReqExtraBtFromJson(Map json) => ReqExtraBt() - ..trackers = - (json['trackers'] as List).map((e) => e as String).toList(); +ReqExtraBt _$ReqExtraBtFromJson(Map json) => ReqExtraBt( + trackers: (json['trackers'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); Map _$ReqExtraBtToJson(ReqExtraBt instance) => { 'trackers': instance.trackers, }; -RequestProxy _$RequestProxyFromJson(Map json) => RequestProxy() - ..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode']) - ..scheme = json['scheme'] as String - ..host = json['host'] as String - ..usr = json['usr'] as String - ..pwd = json['pwd'] as String; +RequestProxy _$RequestProxyFromJson(Map json) => RequestProxy( + mode: $enumDecodeNullable(_$RequestProxyModeEnumMap, json['mode']) ?? + RequestProxyMode.follow, + scheme: json['scheme'] as String? ?? 'http', + host: json['host'] as String? ?? '', + usr: json['usr'] as String? ?? '', + pwd: json['pwd'] as String? ?? '', + ); Map _$RequestProxyToJson(RequestProxy instance) => { diff --git a/ui/flutter/lib/api/model/result.g.dart b/ui/flutter/lib/api/model/result.g.dart index 452a7c7ca..03d4f734e 100644 --- a/ui/flutter/lib/api/model/result.g.dart +++ b/ui/flutter/lib/api/model/result.g.dart @@ -11,7 +11,7 @@ Result _$ResultFromJson( T Function(Object? json) fromJsonT, ) => Result( - code: json['code'] as int, + code: (json['code'] as num).toInt(), msg: json['msg'] as String?, data: _$nullableGenericFromJson(json['data'], fromJsonT), ); diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index 0b04170cf..0efd070c1 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -16,6 +16,7 @@ import 'package:window_manager/window_manager.dart'; import '../../../../api/api.dart'; import '../../../../api/model/downloader_config.dart'; +import '../../../../api/model/request.dart'; import '../../../../core/common/start_config.dart'; import '../../../../core/libgopeed_boot.dart'; import '../../../../database/database.dart'; @@ -27,6 +28,8 @@ import '../../../../util/log_util.dart'; import '../../../../util/package_info.dart'; import '../../../../util/util.dart'; import '../../../routes/app_pages.dart'; +import '../../create/controllers/create_controller.dart'; +import '../../create/dto/create_router_params.dart'; import '../../redirect/views/redirect_view.dart'; const unixSocketPath = 'gopeed.sock'; @@ -305,8 +308,20 @@ class AppController extends GetxController with WindowListener, TrayListener { } Future _handleDeepLink(Uri uri) async { - // Wake up application only if (uri.scheme == "gopeed") { + if (uri.path == "/create") { + final params = uri.queryParameters["params"]; + if (params?.isNotEmpty == true) { + final paramsJson = String.fromCharCodes(base64Decode(params!)); + Get.rootDelegate.offAndToNamed(Routes.REDIRECT, + arguments: RedirectArgs(Routes.CREATE, + arguments: + CreateRouterParams.fromJson(jsonDecode(paramsJson)))); + return; + } + Get.rootDelegate.offAndToNamed(Routes.CREATE); + return; + } Get.rootDelegate.offAndToNamed(Routes.HOME); return; } @@ -322,7 +337,8 @@ class AppController extends GetxController with WindowListener, TrayListener { path = (await toFile(uri.toString())).path; } Get.rootDelegate.offAndToNamed(Routes.REDIRECT, - arguments: RedirectArgs(Routes.CREATE, arguments: path)); + arguments: RedirectArgs(Routes.CREATE, + arguments: CreateRouterParams(req: Request(url: path)))); } String runningAddress() { diff --git a/ui/flutter/lib/app/modules/create/dto/create_router_params.dart b/ui/flutter/lib/app/modules/create/dto/create_router_params.dart new file mode 100644 index 000000000..63f1ea0e1 --- /dev/null +++ b/ui/flutter/lib/app/modules/create/dto/create_router_params.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +import '../../../../api/model/options.dart'; +import '../../../../api/model/request.dart'; + +part 'create_router_params.g.dart'; + +@JsonSerializable(explicitToJson: true) +class CreateRouterParams { + Request? req; + Options? opt; + + CreateRouterParams({ + this.req, + this.opt, + }); + + factory CreateRouterParams.fromJson( + Map json, + ) => + _$CreateRouterParamsFromJson(json); + + Map toJson() => _$CreateRouterParamsToJson(this); +} diff --git a/ui/flutter/lib/app/modules/create/dto/create_router_params.g.dart b/ui/flutter/lib/app/modules/create/dto/create_router_params.g.dart new file mode 100644 index 000000000..50fc659f0 --- /dev/null +++ b/ui/flutter/lib/app/modules/create/dto/create_router_params.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'create_router_params.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CreateRouterParams _$CreateRouterParamsFromJson(Map json) => + CreateRouterParams( + req: json['req'] == null + ? null + : Request.fromJson(json['req'] as Map), + opt: json['opt'] == null + ? null + : Options.fromJson(json['opt'] as Map), + ); + +Map _$CreateRouterParamsToJson(CreateRouterParams instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('req', instance.req?.toJson()); + writeNotNull('opt', instance.opt?.toJson()); + return val; +} diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index e5898118e..80c0232dc 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:contentsize_tabbarview/contentsize_tabbarview.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:file_picker/file_picker.dart'; @@ -12,6 +14,7 @@ import '../../../../api/model/create_task.dart'; import '../../../../api/model/options.dart'; import '../../../../api/model/request.dart'; import '../../../../api/model/resolve_result.dart'; +import '../../../../api/model/task.dart'; import '../../../../database/database.dart'; import '../../../../util/input_formatter.dart'; import '../../../../util/message.dart'; @@ -23,6 +26,7 @@ import '../../../views/file_tree_view.dart'; import '../../app/controllers/app_controller.dart'; import '../../history/views/history_view.dart'; import '../controllers/create_controller.dart'; +import '../dto/create_router_params.dart'; class CreateView extends GetView { final _confirmFormKey = GlobalKey(); @@ -71,12 +75,73 @@ class CreateView extends GetView { _pathController.text = appController.downloaderConfig.value.downloadDir; } - final String? filePath = Get.rootDelegate.arguments(); - if (filePath?.isNotEmpty ?? false) { - // get file path from route arguments - _urlController.text = filePath!; + final CreateRouterParams? routerParams = Get.rootDelegate.arguments(); + if (routerParams?.req?.url.isNotEmpty ?? false) { + // get url from route arguments + final url = routerParams!.req!.url; + _urlController.text = url; _urlController.selection = TextSelection.fromPosition( TextPosition(offset: _urlController.text.length)); + final uppercaseUrl = url.toUpperCase(); + Protocol? protocol; + if (uppercaseUrl.startsWith("HTTP:") || + uppercaseUrl.startsWith("HTTPS:")) { + protocol = Protocol.http; + } + if (uppercaseUrl.startsWith("MAGNET:") || + uppercaseUrl.endsWith(".TORRENT")) { + protocol = Protocol.bt; + } + if (protocol != null) { + final extraHandlers = { + Protocol.http: () { + final reqExtra = ReqExtraHttp.fromJson( + jsonDecode(jsonEncode(routerParams.req!.extra))); + _httpHeaderControllers.clear(); + reqExtra.header.forEach((key, value) { + _httpHeaderControllers.add( + ( + name: TextEditingController(text: key), + value: TextEditingController(text: value), + ), + ); + }); + _skipVerifyCertController.value = routerParams.req!.skipVerifyCert; + }, + Protocol.bt: () { + final reqExtra = ReqExtraBt.fromJson( + jsonDecode(jsonEncode(routerParams.req!.extra))); + _btTrackerController.text = reqExtra.trackers.join("\n"); + }, + }; + if (routerParams.req?.extra != null) { + extraHandlers[protocol]?.call(); + } + + // handle options + if (routerParams.opt != null) { + _renameController.text = routerParams.opt!.name; + _pathController.text = routerParams.opt!.path; + + final optionsHandlers = { + Protocol.http: () { + final opt = routerParams.opt!; + _renameController.text = opt.name; + _pathController.text = opt.path; + if (opt.extra != null) { + final optsExtraHttp = + OptsExtraHttp.fromJson(jsonDecode(jsonEncode(opt.extra))); + _connectionsController.text = + optsExtraHttp.connections.toString(); + } + }, + Protocol.bt: null, + }; + if (routerParams.opt?.extra != null) { + optionsHandlers[protocol]?.call(); + } + } + } } else if (_urlController.text.isEmpty) { // read clipboard Clipboard.getData('text/plain').then((value) { diff --git a/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart b/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart index 3455945ca..943e05a0b 100644 --- a/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart +++ b/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart @@ -16,8 +16,12 @@ class RedirectView extends GetView { @override Widget build(BuildContext context) { final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs; - Get.rootDelegate - .offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments); + // Waiting for previous page controller to delete, avoid deleting controller that route page after redirect + WidgetsBinding.instance.addPostFrameCallback((_) async { + await Future.delayed(const Duration(milliseconds: 350)); + Get.rootDelegate + .offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments); + }); return const SizedBox(); } } diff --git a/ui/flutter/lib/core/common/start_config.g.dart b/ui/flutter/lib/core/common/start_config.g.dart index 8732accb0..85e67b856 100644 --- a/ui/flutter/lib/core/common/start_config.g.dart +++ b/ui/flutter/lib/core/common/start_config.g.dart @@ -11,7 +11,7 @@ StartConfig _$StartConfigFromJson(Map json) => StartConfig() ..address = json['address'] as String ..storage = json['storage'] as String ..storageDir = json['storageDir'] as String - ..refreshInterval = json['refreshInterval'] as int + ..refreshInterval = (json['refreshInterval'] as num).toInt() ..apiToken = json['apiToken'] as String; Map _$StartConfigToJson(StartConfig instance) => diff --git a/ui/flutter/lib/util/scheme_register/entry/scheme_register_native.dart b/ui/flutter/lib/util/scheme_register/entry/scheme_register_native.dart index 97945854d..0ccf2e747 100644 --- a/ui/flutter/lib/util/scheme_register/entry/scheme_register_native.dart +++ b/ui/flutter/lib/util/scheme_register/entry/scheme_register_native.dart @@ -1,8 +1,9 @@ import 'dart:io'; -import 'package:gopeed/util/util.dart'; import 'package:win32_registry/win32_registry.dart'; +import '../../util.dart'; + doRegisterUrlScheme(String scheme) { if (Util.isWindows()) { final schemeKey = 'Software\\Classes\\$scheme'; diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 4b4a343d2..c87dcd905 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -35,7 +35,7 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - get: ^4.6.5 + get: ^4.6.6 styled_widget: ^0.4.0+3 context_menus: ^1.2.0 json_annotation: ^4.8.1