Add support for deep linking

Deep linking capability was added to the application along with handling for custom 'wox' scheme. This change enables the application to receive and process deep links, allowing navigation to specific parts of the application from schema-formatted URLs. This will let users to directly install a new plugin from plugin store, and also improve inter-app and website-app interactions.
This commit is contained in:
qianlifeng 2024-05-28 16:29:54 +08:00
parent 25dbcae518
commit 048b1363f7
13 changed files with 169 additions and 3 deletions

View File

@ -97,4 +97,11 @@ class WoxApi {
"query": query.toJson(), "query": query.toJson(),
}); });
} }
Future<void> onProtocolUrlReceived(String command, Map<String, String> arguments) async {
await WoxHttpUtil.instance.postData("/deeplink", {
"command": command,
"arguments": arguments,
});
}
} }

View File

@ -3,9 +3,9 @@ import 'dart:io';
import 'package:chinese_font_library/chinese_font_library.dart'; import 'package:chinese_font_library/chinese_font_library.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:uuid/v4.dart'; import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
import 'package:wox/api/wox_api.dart'; import 'package:wox/api/wox_api.dart';
@ -23,6 +23,7 @@ import 'package:wox/utils/wox_websocket_msg_util.dart';
void main(List<String> arguments) async { void main(List<String> arguments) async {
await initialServices(arguments); await initialServices(arguments);
await initWindow(); await initWindow();
await initDeepLink();
runApp(const MyApp()); runApp(const MyApp());
} }
@ -58,6 +59,12 @@ Future<void> initialServices(List<String> arguments) async {
Get.put(WoxSettingController()); Get.put(WoxSettingController());
} }
Future<void> initDeepLink() async {
// Register a custom protocol
// For macOS platform needs to declare the scheme in ios/Runner/Info.plist
await protocolHandler.register('wox');
}
Future<void> initWindow() async { Future<void> initWindow() async {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
await Window.initialize(); await Window.initialize();
@ -99,11 +106,13 @@ class WoxApp extends StatefulWidget {
State<WoxApp> createState() => _WoxAppState(); State<WoxApp> createState() => _WoxAppState();
} }
class _WoxAppState extends State<WoxApp> with WindowListener { class _WoxAppState extends State<WoxApp> with WindowListener, ProtocolListener {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
protocolHandler.addListener(this);
setAcrylicEffect(); setAcrylicEffect();
var launcherController = Get.find<WoxLauncherController>(); var launcherController = Get.find<WoxLauncherController>();
@ -144,8 +153,28 @@ class _WoxAppState extends State<WoxApp> with WindowListener {
} }
} }
@override
void onProtocolUrlReceived(String url) {
Logger.instance.info(const UuidV4().generate(), "deep link received: $url");
//replace %20 with space in the url
url = url.replaceAll("%20", " ");
// split the command and argument
// wox://command?argument=value&argument2=value2
var command = url.split("?")[0].split("//")[1];
var arguments = url.split("?")[1].split("&");
var argumentMap = <String, String>{};
for (var argument in arguments) {
var key = argument.split("=")[0];
var value = argument.split("=")[1];
argumentMap[key] = value;
}
WoxApi.instance.onProtocolUrlReceived(command, argumentMap);
}
@override @override
void dispose() { void dispose() {
protocolHandler.removeListener(this);
windowManager.removeListener(this); windowManager.removeListener(this);
super.dispose(); super.dispose();
} }

View File

@ -10,6 +10,7 @@ import device_info_plus
import hotkey_manager_macos import hotkey_manager_macos
import macos_window_utils import macos_window_utils
import path_provider_foundation import path_provider_foundation
import protocol_handler_macos
import screen_retriever import screen_retriever
import syncfusion_pdfviewer_macos import syncfusion_pdfviewer_macos
import url_launcher_macos import url_launcher_macos
@ -21,6 +22,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin")) HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
ProtocolHandlerMacosPlugin.register(with: registry.registrar(forPlugin: "ProtocolHandlerMacosPlugin"))
ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin"))
SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin")) SyncfusionFlutterPdfViewerPlugin.register(with: registry.registrar(forPlugin: "SyncfusionFlutterPdfViewerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))

View File

@ -13,6 +13,8 @@ PODS:
- path_provider_foundation (0.0.1): - path_provider_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
- protocol_handler_macos (0.0.1):
- FlutterMacOS
- screen_retriever (0.0.1): - screen_retriever (0.0.1):
- FlutterMacOS - FlutterMacOS
- syncfusion_pdfviewer_macos (0.0.1): - syncfusion_pdfviewer_macos (0.0.1):
@ -29,6 +31,7 @@ DEPENDENCIES:
- hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`) - hotkey_manager_macos (from `Flutter/ephemeral/.symlinks/plugins/hotkey_manager_macos/macos`)
- macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`) - macos_window_utils (from `Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos`)
- path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
- protocol_handler_macos (from `Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos`)
- screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`)
- syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`) - syncfusion_pdfviewer_macos (from `Flutter/ephemeral/.symlinks/plugins/syncfusion_pdfviewer_macos/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@ -51,6 +54,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos :path: Flutter/ephemeral/.symlinks/plugins/macos_window_utils/macos
path_provider_foundation: path_provider_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
protocol_handler_macos:
:path: Flutter/ephemeral/.symlinks/plugins/protocol_handler_macos/macos
screen_retriever: screen_retriever:
:path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos
syncfusion_pdfviewer_macos: syncfusion_pdfviewer_macos:
@ -68,6 +73,7 @@ SPEC CHECKSUMS:
hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160 hotkey_manager_macos: 1e2edb0c7ae4fe67108af44a9d3445de41404160
macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663 macos_window_utils: 933f91f64805e2eb91a5bd057cf97cd097276663
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
protocol_handler_macos: d10a6c01d6373389ffd2278013ab4c47ed6d6daa
screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38
syncfusion_pdfviewer_macos: e9194851581cad04b28b53913d0636d39a4ed4b2 syncfusion_pdfviewer_macos: e9194851581cad04b28b53913d0636d39a4ed4b2
url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95

View File

@ -26,6 +26,19 @@
<string>$(PRODUCT_COPYRIGHT)</string> <string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key> <key>NSMainNibFile</key>
<string>MainMenu</string> <string>MainMenu</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string></string>
<key>CFBundleURLSchemes</key>
<array>
<string>wox</string>
</array>
</dict>
</array>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
</dict> </dict>

View File

@ -517,6 +517,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.8" version: "2.1.8"
protocol_handler:
dependency: "direct main"
description:
name: protocol_handler
sha256: dc2e2dcb1e0e313c3f43827ec3fa6d98adee6e17edc0c3923ac67efee87479a9
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_android:
dependency: transitive
description:
name: protocol_handler_android
sha256: "82eb860ca42149e400328f54b85140329a1766d982e94705b68271f6ca73895c"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_ios:
dependency: transitive
description:
name: protocol_handler_ios
sha256: "0d3a56b8c1926002cb1e32b46b56874759f4dcc8183d389b670864ac041b6ec2"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_macos:
dependency: transitive
description:
name: protocol_handler_macos
sha256: "6eb8687a84e7da3afbc5660ce046f29d7ecf7976db45a9dadeae6c87147dd710"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_platform_interface:
dependency: transitive
description:
name: protocol_handler_platform_interface
sha256: "53776b10526fdc25efdf1abcf68baf57fdfdb75342f4101051db521c9e3f3e5b"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
protocol_handler_windows:
dependency: transitive
description:
name: protocol_handler_windows
sha256: d8f3a58938386aca2c76292757392f4d059d09f11439d6d896d876ebe997f2c4
url: "https://pub.dev"
source: hosted
version: "0.2.0"
recase: recase:
dependency: transitive dependency: transitive
description: description:

View File

@ -56,6 +56,7 @@ dependencies:
url_launcher: ^6.2.5 url_launcher: ^6.2.5
flutter_image_slideshow: ^0.1.6 flutter_image_slideshow: ^0.1.6
dynamic_tabbar: ^1.0.6 dynamic_tabbar: ^1.0.6
protocol_handler: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View File

@ -9,6 +9,7 @@
#include <desktop_drop/desktop_drop_plugin.h> #include <desktop_drop/desktop_drop_plugin.h>
#include <flutter_acrylic/flutter_acrylic_plugin.h> #include <flutter_acrylic/flutter_acrylic_plugin.h>
#include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h> #include <hotkey_manager_windows/hotkey_manager_windows_plugin_c_api.h>
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
#include <screen_retriever/screen_retriever_plugin.h> #include <screen_retriever/screen_retriever_plugin.h>
#include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h> #include <syncfusion_pdfviewer_windows/syncfusion_pdfviewer_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); registry->GetRegistrarForPlugin("FlutterAcrylicPlugin"));
HotkeyManagerWindowsPluginCApiRegisterWithRegistrar( HotkeyManagerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi")); registry->GetRegistrarForPlugin("HotkeyManagerWindowsPluginCApi"));
ProtocolHandlerWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ProtocolHandlerWindowsPluginCApi"));
ScreenRetrieverPluginRegisterWithRegistrar( ScreenRetrieverPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar( SyncfusionPdfviewerWindowsPluginRegisterWithRegistrar(

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
desktop_drop desktop_drop
flutter_acrylic flutter_acrylic
hotkey_manager_windows hotkey_manager_windows
protocol_handler_windows
screen_retriever screen_retriever
syncfusion_pdfviewer_windows syncfusion_pdfviewer_windows
url_launcher_windows url_launcher_windows

View File

@ -5,8 +5,21 @@
#include "flutter_window.h" #include "flutter_window.h"
#include "utils.h" #include "utils.h"
#include <protocol_handler_windows/protocol_handler_windows_plugin_c_api.h>
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) { _In_ wchar_t *command_line, _In_ int show_command) {
// Replace protocol_handler_example with your_window_title.
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"wox");
if (hwnd != NULL) {
DispatchToProtocolHandler(hwnd);
::ShowWindow(hwnd, SW_NORMAL);
::SetForegroundWindow(hwnd);
return EXIT_FAILURE;
}
// Attach to console when present (e.g., 'flutter run') or create a // Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger. // new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {

View File

@ -479,3 +479,18 @@ func (m *Manager) SetActiveWindowName(name string) {
func (m *Manager) GetActiveWindowName() string { func (m *Manager) GetActiveWindowName() string {
return m.activeWindowName return m.activeWindowName
} }
func (m *Manager) PostDeeplink(ctx context.Context, command string, arguments map[string]string) {
logger.Info(ctx, fmt.Sprintf("deeplink: %s, %v", command, arguments))
if command == "query" {
query := arguments["q"]
if query != "" {
m.ui.ChangeQuery(ctx, share.PlainQuery{
QueryType: plugin.QueryTypeInput,
QueryText: query,
})
m.ui.ShowApp(ctx, share.ShowContext{SelectAll: false})
}
}
}

View File

@ -62,6 +62,7 @@ var routers = map[string]func(w http.ResponseWriter, r *http.Request){
"/backup/all": handleBackupAll, "/backup/all": handleBackupAll,
"/hotkey/available": handleHotkeyAvailable, "/hotkey/available": handleHotkeyAvailable,
"/query/icon": handleQueryIcon, "/query/icon": handleQueryIcon,
"/deeplink": handleDeeplink,
} }
func handleHome(w http.ResponseWriter, r *http.Request) { func handleHome(w http.ResponseWriter, r *http.Request) {
@ -718,3 +719,29 @@ func handleQueryIcon(w http.ResponseWriter, r *http.Request) {
iconImage := plugin.ConvertIcon(ctx, iconImg, pluginInstance.PluginDirectory) iconImage := plugin.ConvertIcon(ctx, iconImg, pluginInstance.PluginDirectory)
writeSuccessResponse(w, iconImage) writeSuccessResponse(w, iconImage)
} }
func handleDeeplink(w http.ResponseWriter, r *http.Request) {
ctx := util.NewTraceContext()
body, _ := io.ReadAll(r.Body)
commandResult := gjson.GetBytes(body, "command")
if !commandResult.Exists() {
writeErrorResponse(w, "command is empty")
return
}
// arguments is map[string]string
argumentsResult := gjson.GetBytes(body, "arguments")
var arguments = make(map[string]string)
if argumentsResult.Exists() {
err := json.Unmarshal([]byte(argumentsResult.String()), &arguments)
if err != nil {
writeErrorResponse(w, err.Error())
return
}
}
GetUIManager().PostDeeplink(ctx, commandResult.String(), arguments)
writeSuccessResponse(w, "")
}

View File

@ -11,7 +11,7 @@ fetch('https://raw.githubusercontent.com/Wox-launcher/Wox/v2/plugin-store.json')
let thead = document.createElement('thead'); let thead = document.createElement('thead');
let headerRow = document.createElement('tr'); let headerRow = document.createElement('tr');
let headers = ['Icon', 'Name', 'Description', 'Author', 'Version']; let headers = ['Icon', 'Name', 'Description', 'Author', 'Version', 'Install'];
headers.forEach(header => { headers.forEach(header => {
let th = document.createElement('th'); let th = document.createElement('th');
if (header === 'Icon') { if (header === 'Icon') {
@ -45,6 +45,7 @@ fetch('https://raw.githubusercontent.com/Wox-launcher/Wox/v2/plugin-store.json')
plugin.Description, plugin.Description,
plugin.Author, plugin.Author,
`v${plugin.Version}`, `v${plugin.Version}`,
`<a href="wox://query?q=wpm install ${plugin.Name}" target="_blank">Install</a>`
]; ];
cells.forEach(cell => { cells.forEach(cell => {
let td = document.createElement('td'); let td = document.createElement('td');