feat: enhance deep link handling for install extensions (#1249)

This commit is contained in:
Levi authored and GitHub committed 2026-01-21 20:23:04 +08:00
1 parent d90af0b9d1
commit af1c8b2719
7 files changed
+77 -51

No files matched your search

+14 -10
View File
@@ -3,6 +3,14 @@ package download
import (
"encoding/json"
"fmt"
"io"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download/engine"
@@ -12,13 +20,6 @@ import (
"github.com/dop251/goja"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport"
"io"
"os"
"path"
"path/filepath"
"strconv"
"strings"
"time"
)
var (
@@ -200,10 +201,14 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
subPath = pathArr[1]
}
tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, projectPath)
if err := util.RmAndMkDirAll(tempExtDir); err != nil {
// Use unique suffix to avoid concurrent conflicts when multiple git operations
// target the same extension (e.g., install and update check happening simultaneously)
tempExtDir := filepath.Join(d.cfg.StorageDir, tempExtensionsDir, fmt.Sprintf("%s_%d", projectPath, time.Now().UnixNano()))
if err := os.MkdirAll(tempExtDir, 0755); err != nil {
return nil, err
}
defer os.RemoveAll(tempExtDir)
proxyOptions := transport.ProxyOptions{}
proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl()
if proxyUrl != nil {
@@ -220,7 +225,6 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
}); err != nil {
return nil, err
}
defer os.RemoveAll(tempExtDir)
return handler(filepath.Join(tempExtDir, subPath), false)
}
-11
View File
@@ -141,17 +141,6 @@ func CopyDir(source string, target string, excludeDir ...string) error {
return nil
}
// RmAndMkDirAll remove and create directory, if the directory already exists, it will be overwritten.
func RmAndMkDirAll(path string) error {
if err := os.RemoveAll(path); err != nil {
return err
}
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
return nil
}
// copy file, if the target file already exists, it will be overwritten.
func copyForce(source string, target string) error {
sourceFile, err := os.Open(source)
@@ -20,6 +20,7 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart';
import '../../../../api/model/create_task.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../api/model/install_extension.dart';
import '../../../../api/model/request.dart';
import '../../../../api/model/result.dart';
import '../../../../core/common/start_config.dart';
@@ -293,7 +294,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
final jsonMeta = jsonDecode(meta);
final silent = jsonMeta['silent'] as bool? ?? false;
final params = await ctx.readText();
final createTaskParams = _decodeToCreatTaskParams(params);
final createTaskParams = CreateTask.fromJson(_decodeParams(params));
if (!silent) {
await windowManager.show();
_handleToCreate0(createTaskParams);
@@ -382,6 +383,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
Future<void> _handleDeepLink(Uri uri) async {
if (uri.scheme == "gopeed") {
// gopeed:///create?params=eyJyZXEiOnsidXJsIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9maWxlLnR4dCJ9fQ==
if (uri.path == "/create") {
final params = uri.queryParameters["params"];
if (params?.isNotEmpty == true) {
@@ -391,6 +393,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
Get.rootDelegate.offAndToNamed(Routes.CREATE);
return;
}
// gopeed:///extension?params=eyJ1cmwiOiJodHRwczovL2dpdGh1Yi5jb20vbW9ua2V5V2llL2dvcGVlZC1leHRlbnNpb24tYmlsaWJpbGkiLCJkZXZNb2RlIjpmYWxzZX0=
if (uri.path == "/extension") {
final params = uri.queryParameters["params"];
if (params?.isNotEmpty == true) {
_handleToExtension(params!);
return;
}
Get.rootDelegate.offAndToNamed(Routes.EXTENSION);
return;
}
Get.rootDelegate.offAndToNamed(Routes.HOME);
return;
}
@@ -650,15 +662,15 @@ class AppController extends GetxController with WindowListener, TrayListener {
await putConfig(downloaderConfig.value);
}
CreateTask _decodeToCreatTaskParams(String params) {
Map<String, dynamic> _decodeParams(String params) {
final safeParams = params.replaceAll('"', "").replaceAll(" ", "+");
final paramsJson =
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
return CreateTask.fromJson(jsonDecode(paramsJson));
return jsonDecode(paramsJson);
}
_handleToCreate(String params) {
final createTaskParams = _decodeToCreatTaskParams(params);
final createTaskParams = CreateTask.fromJson(_decodeParams(params));
_handleToCreate0(createTaskParams);
}
@@ -666,4 +678,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE, arguments: createTaskParams));
}
_handleToExtension(String params) {
final installExtension = InstallExtension.fromJson(_decodeParams(params));
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.EXTENSION, arguments: installExtension));
}
}
@@ -20,6 +20,9 @@ class CreateController extends GetxController
final oldUrl = "".obs;
final fileDataUri = "".obs;
/// Flag to prevent handling pending create task from deep link multiple times
bool pendingCreateHandled = false;
@override
void onInit() {
super.onInit();
@@ -82,17 +82,16 @@ class CreateView extends GetView<CreateController> {
_pathController.text = renderPathPlaceholders(downloadDir);
}
// Initialize archive settings from global config if not already set
if (_autoExtractController.value == null) {
_autoExtractController.value =
appController.downloaderConfig.value.archive.autoExtract;
}
if (_deleteAfterExtractController.value == null) {
_deleteAfterExtractController.value =
appController.downloaderConfig.value.archive.deleteAfterExtract;
}
_autoExtractController.value ??=
appController.downloaderConfig.value.archive.autoExtract;
_deleteAfterExtractController.value ??=
appController.downloaderConfig.value.archive.deleteAfterExtract;
// Handle pending create task from deep link
final CreateTask? routerParams = Get.rootDelegate.arguments();
if (routerParams?.req?.url.isNotEmpty ?? false) {
if ((routerParams?.req?.url.isNotEmpty ?? false) &&
!controller.pendingCreateHandled) {
controller.pendingCreateHandled = true;
// get url from route arguments
final url = routerParams!.req!.url;
_urlController.text = url;
@@ -9,6 +9,9 @@ class ExtensionController extends GetxController {
final devMode = false.obs;
var _devModeCount = 0;
/// Flag to prevent handling pending install multiple times
bool pendingInstallHandled = false;
@override
void onInit() async {
super.onInit();
@@ -30,8 +30,34 @@ class ExtensionView extends GetView<ExtensionController> {
final _installUrlController = TextEditingController();
final _installBtnController = IconButtonLoadingController();
Future<void> _doInstall() async {
if (_installUrlController.text.isEmpty) {
controller.tryOpenDevMode();
return;
}
_installBtnController.start();
try {
await installExtension(InstallExtension(url: _installUrlController.text));
Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr);
await controller.load();
} catch (e) {
showErrorMessage(e);
} finally {
_installBtnController.stop();
}
}
@override
Widget build(BuildContext context) {
// Handle pending install extension from deep link
final args = Get.rootDelegate.arguments();
if (args is InstallExtension && !controller.pendingInstallHandled) {
controller.pendingInstallHandled = true;
_installUrlController.text = args.url;
WidgetsBinding.instance.addPostFrameCallback((_) {
_doInstall();
});
}
return Scaffold(
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -71,23 +97,7 @@ class ExtensionView extends GetView<ExtensionController> {
const SizedBox(width: 10),
IconButtonLoading(
controller: _installBtnController,
onPressed: () async {
if (_installUrlController.text.isEmpty) {
controller.tryOpenDevMode();
return;
}
_installBtnController.start();
try {
await installExtension(InstallExtension(
url: _installUrlController.text));
Get.snackbar('tip'.tr, 'extensionInstallSuccess'.tr);
await controller.load();
} catch (e) {
showErrorMessage(e);
} finally {
_installBtnController.stop();
}
},
onPressed: _doInstall,
icon: const Icon(Icons.download)),
controller.devMode.value && Util.isDesktop()
? IconButton(