Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: torrent scheme deep link and file associations #866

Merged
merged 11 commits into from
Jan 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion internal/protocol/bt/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions internal/protocol/bt/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
gohttp "net/http"
"net/url"
"os"
"path/filepath"
"reflect"
"testing"
)
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions internal/protocol/http/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion pkg/util/url.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
Expand Down
2 changes: 2 additions & 0 deletions ui/flutter/lib/api/model/downloader_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class ExtraConfig {
String locale;
bool lastDeleteTaskKeep;
bool defaultDirectDownload;
bool defaultBtClient;

ExtraConfigBt bt = ExtraConfigBt();

Expand All @@ -88,6 +89,7 @@ class ExtraConfig {
this.locale = '',
this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false,
this.defaultBtClient = true,
});

factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
Expand Down
34 changes: 23 additions & 11 deletions ui/flutter/lib/app/modules/app/controllers/app_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -160,22 +161,21 @@ class AppController extends GetxController with WindowListener, TrayListener {
}

Future<void> _initDeepLinks() async {
// currently only support android
if (!Util.isAndroid()) {
if (Util.isWeb()) {
return;
}

_appLinks = AppLinks();

// 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);
}
}

Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 22 additions & 24 deletions ui/flutter/lib/app/modules/create/views/create_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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(),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import 'package:get/get.dart';

class RedirectController extends GetxController {}
23 changes: 23 additions & 0 deletions ui/flutter/lib/app/modules/redirect/views/redirect_view.dart
Original file line number Diff line number Diff line change
@@ -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();
}
}
42 changes: 38 additions & 4 deletions ui/flutter/lib/app/modules/setting/views/setting_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -947,6 +980,7 @@ class SettingView extends GetView<SettingController> {
buildBtTrackerSubscribeUrls(),
buildBtTrackers(),
buildBtSeedConfig(),
buildBtDefaultClientConfig(),
]),
)),
Text('ui'.tr),
Expand Down
11 changes: 9 additions & 2 deletions ui/flutter/lib/app/routes/app_pages.dart
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
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';
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';

Expand Down Expand Up @@ -68,6 +70,11 @@ class AppPages {
page: () => CreateView(),
binding: CreateBinding(),
),
GetPage(
name: _Paths.REDIRECT,
page: () => const RedirectView(),
binding: RedirectBinding(),
),
]),
];
}
4 changes: 4 additions & 0 deletions ui/flutter/lib/app/routes/app_routes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,26 @@ part of 'app_pages.dart';

abstract class Routes {
Routes._();

static const ROOT = _Paths.ROOT;
static const HOME = _Paths.HOME;
static const CREATE = _Paths.CREATE;
static const TASK = _Paths.HOME + _Paths.TASK;
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';
static const TASK = '/task';
static const TASK_FILES = '/files';
static const EXTENSION = '/extension';
static const SETTING = '/setting';
static const REDIRECT = '/redirect';
}
Loading
Loading