diff --git a/internal/protocol/bt/fetcher.go b/internal/protocol/bt/fetcher.go index 3244be1a7..8f34b06cb 100644 --- a/internal/protocol/bt/fetcher.go +++ b/internal/protocol/bt/fetcher.go @@ -363,7 +363,14 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { f.torrent, err = client.AddMagnet(req.URL) } else { var reader io.Reader - if schema == "DATA" { + if schema == "FILE" { + fileUrl, _ := url.Parse(req.URL) + filePath := fileUrl.Path[1:] + reader, err = os.Open(filePath) + if err != nil { + return + } + } else if schema == "DATA" { _, data := util.ParseDataUri(req.URL) reader = bytes.NewBuffer(data) } else { diff --git a/internal/protocol/bt/fetcher_test.go b/internal/protocol/bt/fetcher_test.go index 250f4c4c9..9e09856d6 100644 --- a/internal/protocol/bt/fetcher_test.go +++ b/internal/protocol/bt/fetcher_test.go @@ -11,6 +11,7 @@ import ( gohttp "net/http" "net/url" "os" + "path/filepath" "reflect" "testing" ) @@ -149,6 +150,16 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) { } }) + t.Run("Resolve file scheme Torrent", func(t *testing.T) { + file, _ := filepath.Abs("./testdata/test.unclean.torrent") + uri := "file:///" + file + err := fetcher.Resolve(&base.Request{ + URL: uri, + }) + if err != nil { + t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err) + } + }) } func TestFetcherManager_ParseName(t *testing.T) { diff --git a/internal/protocol/http/fetcher.go b/internal/protocol/http/fetcher.go index d40be4525..dd2576168 100644 --- a/internal/protocol/http/fetcher.go +++ b/internal/protocol/http/fetcher.go @@ -107,9 +107,9 @@ func (f *Fetcher) Resolve(req *base.Request) error { if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) { // response 206 status code, support breakpoint continuation res.Range = true - // parse content length from Content-Range header, eg: bytes 0-1000/1001 + // parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/* contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) - if contentTotal != "" { + if contentTotal != "" && contentTotal != "*" { parse, err := strconv.ParseInt(contentTotal, 10, 64) if err != nil { return err diff --git a/pkg/util/url.go b/pkg/util/url.go index 0f785d0ab..07c87be5d 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -10,7 +10,6 @@ import ( func ParseSchema(url string) string { index := strings.Index(url, ":") - // if no schema or windows path like C:\a.txt, return FILE if index == -1 || index == 1 { return "" } diff --git a/ui/flutter/lib/api/model/downloader_config.dart b/ui/flutter/lib/api/model/downloader_config.dart index 0a5e58fca..e05ac3b47 100644 --- a/ui/flutter/lib/api/model/downloader_config.dart +++ b/ui/flutter/lib/api/model/downloader_config.dart @@ -80,6 +80,7 @@ class ExtraConfig { String locale; bool lastDeleteTaskKeep; bool defaultDirectDownload; + bool defaultBtClient; ExtraConfigBt bt = ExtraConfigBt(); @@ -88,6 +89,7 @@ class ExtraConfig { this.locale = '', this.lastDeleteTaskKeep = false, this.defaultDirectDownload = false, + this.defaultBtClient = true, }); factory ExtraConfig.fromJson(Map? json) => 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 4b05f2435..0b04170cf 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -27,6 +27,7 @@ import '../../../../util/log_util.dart'; import '../../../../util/package_info.dart'; import '../../../../util/util.dart'; import '../../../routes/app_pages.dart'; +import '../../redirect/views/redirect_view.dart'; const unixSocketPath = 'gopeed.sock'; @@ -160,8 +161,7 @@ class AppController extends GetxController with WindowListener, TrayListener { } Future _initDeepLinks() async { - // currently only support android - if (!Util.isAndroid()) { + if (Util.isWeb()) { return; } @@ -169,13 +169,13 @@ class AppController extends GetxController with WindowListener, TrayListener { // Handle link when app is in warm state (front or background) _linkSubscription = _appLinks.uriLinkStream.listen((uri) async { - await _toCreate(uri); + await _handleDeepLink(uri); }); // Check initial link if app was in cold state (terminated) final uri = await _appLinks.getInitialLink(); if (uri != null) { - await _toCreate(uri); + await _handleDeepLink(uri); } } @@ -304,13 +304,25 @@ class AppController extends GetxController with WindowListener, TrayListener { } } - Future _toCreate(Uri uri) async { - final path = (uri.scheme == "magnet" || - uri.scheme == "http" || - uri.scheme == "https") - ? uri.toString() - : (await toFile(uri.toString())).path; - await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path); + Future _handleDeepLink(Uri uri) async { + // Wake up application only + if (uri.scheme == "gopeed") { + Get.rootDelegate.offAndToNamed(Routes.HOME); + return; + } + + String path; + if (uri.scheme == "magnet" || + uri.scheme == "http" || + uri.scheme == "https") { + path = uri.toString(); + } else if (uri.scheme == "file") { + path = Util.isWindows() ? uri.path.substring(1) : uri.path; + } else { + path = (await toFile(uri.toString())).path; + } + Get.rootDelegate.offAndToNamed(Routes.REDIRECT, + arguments: RedirectArgs(Routes.CREATE, arguments: path)); } String runningAddress() { diff --git a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart index 9b22d904d..7b5550bdc 100644 --- a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart +++ b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart @@ -9,7 +9,6 @@ import '../../app/controllers/app_controller.dart'; class CreateController extends GetxController with GetSingleTickerProviderStateMixin { - // final files = [].obs; final RxList fileInfos = [].obs; final RxList openedFolders = [].obs; final selectedIndexes = [].obs; 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 bde8f0076..f7848321a 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -61,31 +61,29 @@ class CreateView extends GetView { } final String? filePath = Get.rootDelegate.arguments(); - if (_urlController.text.isEmpty) { - if (filePath?.isNotEmpty ?? false) { - // get file path from route arguments - _urlController.text = filePath!; - _urlController.selection = TextSelection.fromPosition( - TextPosition(offset: _urlController.text.length)); - } else { - // read clipboard - Clipboard.getData('text/plain').then((value) { - if (value?.text?.isNotEmpty ?? false) { - if (_availableSchemes - .where((e) => - value!.text!.startsWith(e) || - value.text!.startsWith(e.toUpperCase())) - .isNotEmpty) { - _urlController.text = value!.text!; - _urlController.selection = TextSelection.fromPosition( - TextPosition(offset: _urlController.text.length)); - return; - } - - recognizeMagnetUri(value!.text!); + if (filePath?.isNotEmpty ?? false) { + // get file path from route arguments + _urlController.text = filePath!; + _urlController.selection = TextSelection.fromPosition( + TextPosition(offset: _urlController.text.length)); + } else if (_urlController.text.isEmpty) { + // read clipboard + Clipboard.getData('text/plain').then((value) { + if (value?.text?.isNotEmpty ?? false) { + if (_availableSchemes + .where((e) => + value!.text!.startsWith(e) || + value.text!.startsWith(e.toUpperCase())) + .isNotEmpty) { + _urlController.text = value!.text!; + _urlController.selection = TextSelection.fromPosition( + TextPosition(offset: _urlController.text.length)); + return; } - }); - } + + recognizeMagnetUri(value!.text!); + } + }); } return Scaffold( diff --git a/ui/flutter/lib/app/modules/redirect/bindings/redirect_binding.dart b/ui/flutter/lib/app/modules/redirect/bindings/redirect_binding.dart new file mode 100644 index 000000000..f16fe0864 --- /dev/null +++ b/ui/flutter/lib/app/modules/redirect/bindings/redirect_binding.dart @@ -0,0 +1,12 @@ +import 'package:get/get.dart'; + +import '../controllers/redirect_controller.dart'; + +class RedirectBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut( + () => RedirectController(), + ); + } +} diff --git a/ui/flutter/lib/app/modules/redirect/controllers/redirect_controller.dart b/ui/flutter/lib/app/modules/redirect/controllers/redirect_controller.dart new file mode 100644 index 000000000..5300fec9c --- /dev/null +++ b/ui/flutter/lib/app/modules/redirect/controllers/redirect_controller.dart @@ -0,0 +1,3 @@ +import 'package:get/get.dart'; + +class RedirectController extends GetxController {} diff --git a/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart b/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart new file mode 100644 index 000000000..3455945ca --- /dev/null +++ b/ui/flutter/lib/app/modules/redirect/views/redirect_view.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../controllers/redirect_controller.dart'; + +class RedirectArgs { + final String page; + final dynamic arguments; + + RedirectArgs(this.page, {this.arguments}); +} + +class RedirectView extends GetView { + const RedirectView({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs; + Get.rootDelegate + .offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments); + return const SizedBox(); + } +} diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index afa7e231d..e85147102 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -17,6 +17,7 @@ import '../../../../util/locale_manager.dart'; import '../../../../util/log_util.dart'; import '../../../../util/message.dart'; import '../../../../util/package_info.dart'; +import '../../../../util/scheme_register/scheme_register.dart'; import '../../../../util/util.dart'; import '../../../views/check_list_view.dart'; import '../../../views/directory_selector.dart'; @@ -111,7 +112,7 @@ class SettingView extends GetView { }); final buildDefaultDirectDownload = - _buildConfigItem('defaultDirectDownload'.tr, () { + _buildConfigItem('defaultDirectDownload', () { return appController.downloaderConfig.value.extra.defaultDirectDownload ? 'on'.tr : 'off'.tr; @@ -161,7 +162,7 @@ class SettingView extends GetView { // Currently auto startup only support Windows and Linux final buildAutoStartup = !Util.isWindows() && !Util.isLinux() ? () => null - : _buildConfigItem('launchAtStartup'.tr, () { + : _buildConfigItem('launchAtStartup', () { return appController.autoStartup.value ? 'on'.tr : 'off'.tr; }, (Key key) { return Container( @@ -229,8 +230,9 @@ class SettingView extends GetView { ], ); }); - final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr, - () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) { + final buildHttpUseServerCtime = _buildConfigItem( + 'useServerCtime', () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, + (Key key) { return Container( alignment: Alignment.centerLeft, child: Switch( @@ -420,6 +422,37 @@ class SettingView extends GetView { ].where((e) => e != null).map((e) => e!).toList(), ); }); + final buildBtDefaultClientConfig = !Util.isWindows() + ? () => null + : _buildConfigItem('setAsDefaultBtClient', () { + return appController.downloaderConfig.value.extra.defaultBtClient + ? 'on'.tr + : 'off'.tr; + }, (Key key) { + return Container( + alignment: Alignment.centerLeft, + child: Switch( + value: + appController.downloaderConfig.value.extra.defaultBtClient, + onChanged: (bool value) async { + try { + if (value) { + registerDefaultTorrentClient(); + } else { + unregisterDefaultTorrentClient(); + } + appController.downloaderConfig.update((val) { + val!.extra.defaultBtClient = value; + }); + await debounceSave(); + } catch (e) { + showErrorMessage(e); + logger.e('register default torrent client fail', e); + } + }, + ), + ); + }); // ui config items start final buildTheme = _buildConfigItem( @@ -947,6 +980,7 @@ class SettingView extends GetView { buildBtTrackerSubscribeUrls(), buildBtTrackers(), buildBtSeedConfig(), + buildBtDefaultClientConfig(), ]), )), Text('ui'.tr), diff --git a/ui/flutter/lib/app/routes/app_pages.dart b/ui/flutter/lib/app/routes/app_pages.dart index 1ce7cf070..4773bb339 100644 --- a/ui/flutter/lib/app/routes/app_pages.dart +++ b/ui/flutter/lib/app/routes/app_pages.dart @@ -1,6 +1,4 @@ import 'package:get/get.dart'; -import 'package:gopeed/app/modules/task/views/task_files_view.dart'; -import 'package:gopeed/app/modules/task/views/task_view.dart'; import '../modules/create/bindings/create_binding.dart'; import '../modules/create/views/create_view.dart'; @@ -8,12 +6,16 @@ import '../modules/extension/bindings/extension_binding.dart'; import '../modules/extension/views/extension_view.dart'; import '../modules/home/bindings/home_binding.dart'; import '../modules/home/views/home_view.dart'; +import '../modules/redirect/bindings/redirect_binding.dart'; +import '../modules/redirect/views/redirect_view.dart'; import '../modules/root/bindings/root_binding.dart'; import '../modules/root/views/root_view.dart'; import '../modules/setting/bindings/setting_binding.dart'; import '../modules/setting/views/setting_view.dart'; import '../modules/task/bindings/task_binding.dart'; import '../modules/task/bindings/task_files_binding.dart'; +import '../modules/task/views/task_files_view.dart'; +import '../modules/task/views/task_view.dart'; part 'app_routes.dart'; @@ -68,6 +70,11 @@ class AppPages { page: () => CreateView(), binding: CreateBinding(), ), + GetPage( + name: _Paths.REDIRECT, + page: () => const RedirectView(), + binding: RedirectBinding(), + ), ]), ]; } diff --git a/ui/flutter/lib/app/routes/app_routes.dart b/ui/flutter/lib/app/routes/app_routes.dart index 31aa1853a..95faa3f8b 100644 --- a/ui/flutter/lib/app/routes/app_routes.dart +++ b/ui/flutter/lib/app/routes/app_routes.dart @@ -3,6 +3,7 @@ part of 'app_pages.dart'; abstract class Routes { Routes._(); + static const ROOT = _Paths.ROOT; static const HOME = _Paths.HOME; static const CREATE = _Paths.CREATE; @@ -10,10 +11,12 @@ abstract class Routes { static const TASK_FILES = TASK + _Paths.TASK_FILES; static const EXTENSION = _Paths.HOME + _Paths.EXTENSION; static const SETTING = _Paths.HOME + _Paths.SETTING; + static const REDIRECT = _Paths.REDIRECT; } abstract class _Paths { _Paths._(); + static const ROOT = '/'; static const HOME = '/home'; static const CREATE = '/create'; @@ -21,4 +24,5 @@ abstract class _Paths { static const TASK_FILES = '/files'; static const EXTENSION = '/extension'; static const SETTING = '/setting'; + static const REDIRECT = '/redirect'; } diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index 4c968cd7b..ac92b68df 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -105,6 +105,7 @@ const enUS = { 'seedKeep': 'Keep seeding until manually stopped', 'seedRatio': 'Seed ratio', 'seedTime': 'Seed time (minutes)', + 'setAsDefaultBtClient': 'Set as the default BT client', 'taskDetail': 'Task Detail', 'taskName': 'Task Name', 'taskUrl': 'Task URL', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index 0a8ffa661..eedc74cc4 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -103,6 +103,7 @@ const zhCN = { 'seedKeep': '持续做种直到手动停止', 'seedRatio': '做种分享率', 'seedTime': '做种时间(分钟)', + 'setAsDefaultBtClient': '设为系统默认 BT 客户端', 'taskDetail': '任务详情', 'taskName': '任务名称', 'taskUrl': '任务链接', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index cf8bf0748..818b846f5 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -103,6 +103,7 @@ const zhTW = { 'seedKeep': '持續做種直到手動停止', 'seedRatio': '做種分享率', 'seedTime': '做種時間(分鐘)', + 'setAsDefaultBtClient': '設為系統預設 BT 客戶端', 'taskDetail': '任務詳情', 'taskName': '任務名稱', 'taskUrl': '任務連結', diff --git a/ui/flutter/lib/main.dart b/ui/flutter/lib/main.dart index 06b14f4f8..245eb63bb 100644 --- a/ui/flutter/lib/main.dart +++ b/ui/flutter/lib/main.dart @@ -14,6 +14,7 @@ import 'util/locale_manager.dart'; import 'util/log_util.dart'; import 'util/mac_secure_util.dart'; import 'util/package_info.dart'; +import 'util/scheme_register/scheme_register.dart'; import 'util/util.dart'; class Args { @@ -81,6 +82,15 @@ Future init(Args args) async { logger.e("libgopeed init fail", e); } + try { + registerUrlScheme("gopeed"); + if (controller.downloaderConfig.value.extra.defaultBtClient) { + registerDefaultTorrentClient(); + } + } catch (e) { + logger.e("register scheme fail", e); + } + try { await controller.loadDownloaderConfig(); MacSecureUtil.loadBookmark(); 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 new file mode 100644 index 000000000..97945854d --- /dev/null +++ b/ui/flutter/lib/util/scheme_register/entry/scheme_register_native.dart @@ -0,0 +1,92 @@ +import 'dart:io'; + +import 'package:gopeed/util/util.dart'; +import 'package:win32_registry/win32_registry.dart'; + +doRegisterUrlScheme(String scheme) { + if (Util.isWindows()) { + final schemeKey = 'Software\\Classes\\$scheme'; + final appPath = Platform.resolvedExecutable; + + _upsertRegistry( + schemeKey, + 'URL Protocol', + '', + ); + _upsertRegistry( + '$schemeKey\\shell\\open\\command', + '', + '"$appPath" "%1"', + ); + } +} + +doUnregisterUrlScheme(String scheme) { + if (Util.isWindows()) { + Registry.currentUser + .deleteKey('Software\\Classes\\$scheme', recursive: true); + } +} + +const _torrentRegKey = 'Software\\Classes\\.torrent'; +const _torrentRegValue = 'Gopeed_torrent'; +const _torrentAppRegKey = 'Software\\Classes\\$_torrentRegValue'; + +/// Register as the system's default torrent client +/// 1. Register the scheme "magnet" +/// 2. Register the file type ".torrent" +doRegisterDefaultTorrentClient() { + if (Util.isWindows()) { + doRegisterUrlScheme("magnet"); + + final appPath = Platform.resolvedExecutable; + final iconPath = + '${File(appPath).parent.path}\\data\\flutter_assets\\assets\\tray_icon\\icon.ico'; + _upsertRegistry( + _torrentRegKey, + '', + _torrentRegValue, + ); + _upsertRegistry( + _torrentAppRegKey, + '', + 'Torrent file', + ); + _upsertRegistry( + '$_torrentAppRegKey\\DefaultIcon', + '', + iconPath, + ); + _upsertRegistry( + '$_torrentAppRegKey\\shell\\open\\command', + '', + '"$appPath" "file:///%1"', + ); + } +} + +doUnregisterDefaultTorrentClient() { + if (Util.isWindows()) { + doUnregisterUrlScheme("magnet"); + + Registry.currentUser.deleteKey(_torrentRegKey, recursive: true); + Registry.currentUser.deleteKey(_torrentAppRegKey, recursive: true); + } +} + +// Add Windows registry key and value if not exists +_upsertRegistry(String keyPath, String valueName, String value) { + RegistryKey regKey; + try { + regKey = Registry.openPath(RegistryHive.currentUser, + path: keyPath, desiredAccessRights: AccessRights.allAccess); + } catch (e) { + regKey = Registry.currentUser.createKey(keyPath); + } + + if (regKey.getValueAsString(valueName) != value) { + regKey + .createValue(RegistryValue(valueName, RegistryValueType.string, value)); + } + regKey.close(); +} diff --git a/ui/flutter/lib/util/scheme_register/scheme_register.dart b/ui/flutter/lib/util/scheme_register/scheme_register.dart new file mode 100644 index 000000000..5fe3e2ff0 --- /dev/null +++ b/ui/flutter/lib/util/scheme_register/scheme_register.dart @@ -0,0 +1,11 @@ +import 'scheme_register_stub.dart' + if (dart.library.io) 'entry/scheme_register_native.dart'; + +registerUrlScheme(String scheme) => doRegisterUrlScheme(scheme); + +unregisterUrlScheme(String scheme) => doUnregisterUrlScheme(scheme); + +registerDefaultTorrentClient() => doRegisterDefaultTorrentClient(); + +unregisterDefaultTorrentClient() => doUnregisterDefaultTorrentClient(); + diff --git a/ui/flutter/lib/util/scheme_register/scheme_register_stub.dart b/ui/flutter/lib/util/scheme_register/scheme_register_stub.dart new file mode 100644 index 000000000..d4ea1623a --- /dev/null +++ b/ui/flutter/lib/util/scheme_register/scheme_register_stub.dart @@ -0,0 +1,7 @@ +doRegisterUrlScheme(String scheme) => throw UnimplementedError(); + +doUnregisterUrlScheme(String scheme) => throw UnimplementedError(); + +doRegisterDefaultTorrentClient() => throw UnimplementedError(); + +doUnregisterDefaultTorrentClient() => throw UnimplementedError(); diff --git a/ui/flutter/linux/assets/com.gopeed.Gopeed.desktop b/ui/flutter/linux/assets/com.gopeed.Gopeed.desktop index 621762ba8..b11187a2e 100644 --- a/ui/flutter/linux/assets/com.gopeed.Gopeed.desktop +++ b/ui/flutter/linux/assets/com.gopeed.Gopeed.desktop @@ -11,4 +11,5 @@ Exec=gopeed Icon=com.gopeed.Gopeed Type=Application Categories=Utility;Internet -Keywords=Bittorrent;Downloader; \ No newline at end of file +Keywords=Bittorrent;Downloader; +MimeType=x-scheme-handler/gopeed;x-scheme-handler/magnet;application/x-bittorrent; \ No newline at end of file diff --git a/ui/flutter/linux/my_application.cc b/ui/flutter/linux/my_application.cc index 89ac28af3..8cf0c6988 100644 --- a/ui/flutter/linux/my_application.cc +++ b/ui/flutter/linux/my_application.cc @@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -79,7 +86,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GObject::dispose. @@ -100,6 +107,6 @@ static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/ui/flutter/linux/packaging/appimage/make_config.yaml b/ui/flutter/linux/packaging/appimage/make_config.yaml index 3f1af7e78..56ced5838 100644 --- a/ui/flutter/linux/packaging/appimage/make_config.yaml +++ b/ui/flutter/linux/packaging/appimage/make_config.yaml @@ -14,4 +14,9 @@ categories: - Network - Utility +supported_mime_type: + - x-scheme-handler/gopeed + - x-scheme-handler/magnet + - application/x-bittorrent + startup_notify: true \ No newline at end of file diff --git a/ui/flutter/linux/packaging/deb/make_config.yaml b/ui/flutter/linux/packaging/deb/make_config.yaml index da58a5da9..91f54ae04 100644 --- a/ui/flutter/linux/packaging/deb/make_config.yaml +++ b/ui/flutter/linux/packaging/deb/make_config.yaml @@ -28,4 +28,9 @@ categories: - Network - Utility +supported_mime_type: + - x-scheme-handler/gopeed + - x-scheme-handler/magnet + - application/x-bittorrent + startup_notify: true diff --git a/ui/flutter/macos/Runner/AppDelegate.swift b/ui/flutter/macos/Runner/AppDelegate.swift index ccfb9a19c..6fde117bc 100644 --- a/ui/flutter/macos/Runner/AppDelegate.swift +++ b/ui/flutter/macos/Runner/AppDelegate.swift @@ -1,8 +1,22 @@ import Cocoa import FlutterMacOS +import app_links @main class AppDelegate: FlutterAppDelegate { + public override func application(_ application: NSApplication, + continue userActivity: NSUserActivity, + restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void) -> Bool { + + guard let url = AppLinks.shared.getUniversalLink(userActivity) else { + return false + } + + AppLinks.shared.handleLink(link: url.absoluteString) + + return false // Returning true will stop the propagation to other packages + } + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return false } @@ -19,4 +33,18 @@ class AppDelegate: FlutterAppDelegate { } return true } + + override func application(_ sender: NSApplication, openFile filename: String) -> Bool { + let url = URL(fileURLWithPath: filename) + AppLinks.shared.handleLink(link: url.absoluteString) + return false; + } + + override func application(_ application: NSApplication, open urls: [URL]) { + // Only handle the first file + if(urls.isEmpty) { + return + } + AppLinks.shared.handleLink(link: urls.first!.absoluteString) + } } diff --git a/ui/flutter/macos/Runner/Info.plist b/ui/flutter/macos/Runner/Info.plist index 4789daa6a..7eb8b3b0d 100644 --- a/ui/flutter/macos/Runner/Info.plist +++ b/ui/flutter/macos/Runner/Info.plist @@ -1,32 +1,96 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + CFBundleURLTypes + + + CFBundleURLName + gopeed + CFBundleURLSchemes + + gopeed + + + + CFBundleURLName + magnet + CFBundleURLSchemes + + magnet + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + torrent + + CFBundleTypeIconFile + icon + CFBundleTypeName + BitTorrent Document + CFBundleTypeRole + Viewer + LSTypeIsPackage + + LSHandlerRank + Owner + LSItemContentTypes + + org.bittorrent.torrent + + + + UTExportedTypeDeclarations + + + UTTypeIdentifier + org.bittorrent.torrent + UTTypeDescription + Torrent File + UTTypeConformsTo + + public.data + + UTTypeTagSpecification + + public.filename-extension + + torrent + + public.mime-type + application/x-bittorrent + + + + + \ No newline at end of file diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index bc8ad52a0..4553c7c8e 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -1313,7 +1313,7 @@ packages: source: hosted version: "5.6.1" win32_registry: - dependency: transitive + dependency: "direct main" description: name: win32_registry sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 2efb62eff..ab72856db 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: device_info_plus: ^11.1.0 checkable_treeview: ^1.3.1 contextmenu_plus: ^1.0.1 + win32_registry: ^1.1.5 dependency_overrides: permission_handler_windows: git: diff --git a/ui/flutter/windows/runner/main.cpp b/ui/flutter/windows/runner/main.cpp index 61de3867e..657a00709 100644 --- a/ui/flutter/windows/runner/main.cpp +++ b/ui/flutter/windows/runner/main.cpp @@ -4,9 +4,49 @@ #include "flutter_window.h" #include "utils.h" +#include "app_links/app_links_plugin_c_api.h" + +bool SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END (Optional) Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { + if (SendAppLinkToInstance(L"gopeed")) { + return EXIT_SUCCESS; + } + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"gopeed"); if (hwnd != NULL) { ::ShowWindow(hwnd, SW_NORMAL);