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(),
});
}
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:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_acrylic/flutter_acrylic.dart';
import 'package:get/get.dart';
import 'package:protocol_handler/protocol_handler.dart';
import 'package:uuid/v4.dart';
import 'package:window_manager/window_manager.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 {
await initialServices(arguments);
await initWindow();
await initDeepLink();
runApp(const MyApp());
}
@ -58,6 +59,12 @@ Future<void> initialServices(List<String> arguments) async {
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 {
await windowManager.ensureInitialized();
await Window.initialize();
@ -99,11 +106,13 @@ class WoxApp extends StatefulWidget {
State<WoxApp> createState() => _WoxAppState();
}
class _WoxAppState extends State<WoxApp> with WindowListener {
class _WoxAppState extends State<WoxApp> with WindowListener, ProtocolListener {
@override
void initState() {
super.initState();
protocolHandler.addListener(this);
setAcrylicEffect();
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
void dispose() {
protocolHandler.removeListener(this);
windowManager.removeListener(this);
super.dispose();
}

View File

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

View File

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

View File

@ -26,6 +26,19 @@
<string>$(PRODUCT_COPYRIGHT)</string>
<key>NSMainNibFile</key>
<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>
<string>NSApplication</string>
</dict>

View File

@ -517,6 +517,54 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

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

View File

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

View File

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

View File

@ -5,8 +5,21 @@
#include "flutter_window.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,
_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
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {

View File

@ -479,3 +479,18 @@ func (m *Manager) SetActiveWindowName(name string) {
func (m *Manager) GetActiveWindowName() string {
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,
"/hotkey/available": handleHotkeyAvailable,
"/query/icon": handleQueryIcon,
"/deeplink": handleDeeplink,
}
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)
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 headerRow = document.createElement('tr');
let headers = ['Icon', 'Name', 'Description', 'Author', 'Version'];
let headers = ['Icon', 'Name', 'Description', 'Author', 'Version', 'Install'];
headers.forEach(header => {
let th = document.createElement('th');
if (header === 'Icon') {
@ -45,6 +45,7 @@ fetch('https://raw.githubusercontent.com/Wox-launcher/Wox/v2/plugin-store.json')
plugin.Description,
plugin.Author,
`v${plugin.Version}`,
`<a href="wox://query?q=wpm install ${plugin.Name}" target="_blank">Install</a>`
];
cells.forEach(cell => {
let td = document.createElement('td');