mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-15 04:43:41 +00:00
feat: torrent scheme deep link and file associations (#866)
This commit is contained in:
30 files changed
+464
-79
No files matched your search
@@ -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<String, dynamic>? json) =>
|
||||
|
||||
@@ -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<void> _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<void> _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<void> _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() {
|
||||
|
||||
@@ -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 = <int>[].obs;
|
||||
|
||||
@@ -61,31 +61,29 @@ class CreateView extends GetView<CreateController> {
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../controllers/redirect_controller.dart';
|
||||
|
||||
class RedirectBinding extends Bindings {
|
||||
@override
|
||||
void dependencies() {
|
||||
Get.lazyPut<RedirectController>(
|
||||
() => RedirectController(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class RedirectController extends GetxController {}
|
||||
@@ -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<RedirectController> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<SettingController> {
|
||||
});
|
||||
|
||||
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<SettingController> {
|
||||
// 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<SettingController> {
|
||||
],
|
||||
);
|
||||
});
|
||||
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<SettingController> {
|
||||
].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<SettingController> {
|
||||
buildBtTrackerSubscribeUrls(),
|
||||
buildBtTrackers(),
|
||||
buildBtSeedConfig(),
|
||||
buildBtDefaultClientConfig(),
|
||||
]),
|
||||
)),
|
||||
Text('ui'.tr),
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -103,6 +103,7 @@ const zhCN = {
|
||||
'seedKeep': '持续做种直到手动停止',
|
||||
'seedRatio': '做种分享率',
|
||||
'seedTime': '做种时间(分钟)',
|
||||
'setAsDefaultBtClient': '设为系统默认 BT 客户端',
|
||||
'taskDetail': '任务详情',
|
||||
'taskName': '任务名称',
|
||||
'taskUrl': '任务链接',
|
||||
|
||||
@@ -103,6 +103,7 @@ const zhTW = {
|
||||
'seedKeep': '持續做種直到手動停止',
|
||||
'seedRatio': '做種分享率',
|
||||
'seedTime': '做種時間(分鐘)',
|
||||
'setAsDefaultBtClient': '設為系統預設 BT 客戶端',
|
||||
'taskDetail': '任務詳情',
|
||||
'taskName': '任務名稱',
|
||||
'taskUrl': '任務連結',
|
||||
|
||||
@@ -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<void> 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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
doRegisterUrlScheme(String scheme) => throw UnimplementedError();
|
||||
|
||||
doUnregisterUrlScheme(String scheme) => throw UnimplementedError();
|
||||
|
||||
doRegisterDefaultTorrentClient() => throw UnimplementedError();
|
||||
|
||||
doUnregisterDefaultTorrentClient() => throw UnimplementedError();
|
||||
Reference in new issue
Block a user