From 97ad12ef0fcd5fbfbdd1e591b81a50de4353d579 Mon Sep 17 00:00:00 2001 From: lollipopkit Date: Tue, 7 May 2024 17:36:31 +0800 Subject: [PATCH] new: migrate `app_links` --- README.md | 2 +- README_zh.md | 2 +- lib/core/util/platform/win.dart | 28 +++++++++ lib/main.dart | 4 +- lib/view/page/home/home.dart | 11 ++-- linux/flutter/generated_plugin_registrant.cc | 4 ++ linux/flutter/generated_plugins.cmake | 1 + linux/my_application.cc | 12 +++- macos/Flutter/GeneratedPluginRegistrant.swift | 4 +- pubspec.lock | 62 +++++++------------ pubspec.yaml | 4 +- .../flutter/generated_plugin_registrant.cc | 6 +- windows/flutter/generated_plugins.cmake | 2 +- windows/runner/main.cpp | 43 +++++++++++-- 14 files changed, 121 insertions(+), 64 deletions(-) create mode 100644 lib/core/util/platform/win.dart diff --git a/README.md b/README.md index 39f5657..156ebc7 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Please don't use it in production environment, and don't use it for important da - Text / Image / Audio chat. (Web only support text) - Share chat by picture. - Render code block / latex formula. -- Uni-Link, eg: `lk-gptbox://chat/new?msg=hello` (except Linux) +- Url Scheme, eg: `lk-gptbox://chat/new?msg=hello` - All platforms support. - Sync with WebDAV / iCloud. diff --git a/README_zh.md b/README_zh.md index 2aecc18..38d621d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -24,7 +24,7 @@ - 从 [ChatGPT Next Web 备份](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web) / [OpenAI导出文件](https://chat.openai.com) 恢复 - 文本 / 图片 / 音频聊天(网页版仅支持文本) - 以图片形式分享聊天 -- Uni-Link,例如:`lk-gptbox://chat/new?msg=你好` (Linux 除外) +- Url Scheme,例如:`lk-gptbox://chat/new?msg=你好` - 与 WebDAV / iCloud 同步 - 全平台支持 - 渲染 代码块 / LaTeX 公式 diff --git a/lib/core/util/platform/win.dart b/lib/core/util/platform/win.dart new file mode 100644 index 0000000..beaa0b6 --- /dev/null +++ b/lib/core/util/platform/win.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:flutter_chatgpt/core/util/platform/base.dart'; +import 'package:win32_registry/win32_registry.dart'; + +abstract final class Win32 { + static Future registerProtocol(String scheme) async { + if (isWindows) return; + + String appPath = Platform.resolvedExecutable; + String protocolRegKey = 'Software\\Classes\\$scheme'; + RegistryValue protocolRegValue = const RegistryValue( + 'URL Protocol', + RegistryValueType.string, + '', + ); + String protocolCmdRegKey = 'shell\\open\\command'; + RegistryValue protocolCmdRegValue = RegistryValue( + '', + RegistryValueType.string, + '"$appPath" "%1"', + ); + + final regKey = Registry.currentUser.createKey(protocolRegKey); + regKey.createValue(protocolRegValue); + regKey.createKey(protocolCmdRegKey).createValue(protocolCmdRegValue); + } +} diff --git a/lib/main.dart b/lib/main.dart index ed7f725..9bf61e2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter_chatgpt/core/build_mode.dart'; import 'package:flutter_chatgpt/core/logger.dart'; import 'package:flutter_chatgpt/core/util/datetime.dart'; import 'package:flutter_chatgpt/core/util/platform/base.dart'; +import 'package:flutter_chatgpt/core/util/platform/win.dart'; import 'package:flutter_chatgpt/core/util/sync/base.dart'; import 'package:flutter_chatgpt/data/model/chat/config.dart'; import 'package:flutter_chatgpt/data/model/chat/history.dart'; @@ -20,7 +21,6 @@ import 'package:flutter_chatgpt/data/store/all.dart'; import 'package:flutter_chatgpt/view/widget/appbar.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:logging/logging.dart'; -import 'package:uni_links_desktop/uni_links_desktop.dart'; import 'package:window_manager/window_manager.dart'; Future main() async { @@ -65,7 +65,7 @@ Future _initApp() async { SyncService.sync(force: true); if (isWindows) { - registerProtocol('lk-gptbox'); + Win32.registerProtocol('lk-gptbox'); } Paths.createAll(); diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart index 2b9d1c9..a4aa59c 100644 --- a/lib/view/page/home/home.dart +++ b/lib/view/page/home/home.dart @@ -3,6 +3,7 @@ import 'dart:collection'; import 'dart:io'; import 'package:after_layout/after_layout.dart'; +import 'package:app_links/app_links.dart'; import 'package:dart_openai/dart_openai.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -58,7 +59,6 @@ import 'package:screenshot/screenshot.dart'; import 'package:share_plus/share_plus.dart'; import 'package:shortid/shortid.dart'; import 'package:tiktoken/tiktoken.dart'; -import 'package:uni_links/uni_links.dart'; part 'chat.dart'; part 'history.dart'; @@ -89,6 +89,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State with AfterLayoutMixin, TickerProviderStateMixin { Timer? _refreshTimeTimer; + final _appLink = AppLinks(); @override void initState() { @@ -100,7 +101,7 @@ class _HomePageState extends State if (mounted) _timeRN.build(); }, ); - _initUniLinks(); + _initUrlScheme(); AudioCard.listenAudioPlayer(); } @@ -233,13 +234,13 @@ class _HomePageState extends State } } - Future _initUniLinks() async { + Future _initUrlScheme() async { if (isWeb) { - final uri = await getInitialUri(); + final uri = await _appLink.getInitialLink(); if (uri == null) return; AppLink.handle(context, uri); } else { - uriLinkStream.listen((Uri? uri) { + _appLink.uriLinkStream.listen((Uri? uri) { if (uri == null) return; if (!mounted) return; AppLink.handle(context, uri); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index b6bb4c2..98945ac 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -19,6 +20,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); g_autoptr(FlPluginRegistrar) screen_retriever_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 67e919f..99c0614 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST audioplayers_linux file_selector_linux + gtk screen_retriever url_launcher_linux window_manager diff --git a/linux/my_application.cc b/linux/my_application.cc index 05b99b1..9fa0835 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -17,6 +17,14 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. static void my_application_activate(GApplication* application) { MyApplication* self = MY_APPLICATION(application); + + // For registering URL schemes + GList* windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + GtkWindow* window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); @@ -78,7 +86,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GObject::dispose. @@ -99,6 +107,6 @@ static void my_application_init(MyApplication* self) {} MyApplication* my_application_new() { return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 673dd11..571862f 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,24 +5,24 @@ import FlutterMacOS import Foundation +import app_links import audioplayers_darwin import file_selector_macos import icloud_storage import path_provider_foundation import screen_retriever import share_plus -import uni_links_desktop import url_launcher_macos import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) IcloudStoragePlugin.register(with: registry.registrar(forPlugin: "IcloudStoragePlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) - UniLinksDesktopPlugin.register(with: registry.registrar(forPlugin: "UniLinksDesktopPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 3c5d628..a06683e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "1c2b9e9c56d80d17610bcbd111b37187875c5d0ded8654caa1bda14ea753d001" + url: "https://pub.dev" + source: hosted + version: "6.0.1" archive: dependency: transitive description: @@ -333,10 +341,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -510,6 +518,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" highlight: dependency: transitive description: @@ -1115,38 +1131,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uni_links: - dependency: "direct main" - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_desktop: - dependency: "direct main" - description: - name: uni_links_desktop - sha256: "692de81efc32ef72df56d428902afb5216d5f9e43d71c7b315d360acd7a1e115" - url: "https://pub.dev" - source: hosted - version: "0.1.7" - uni_links_platform_interface: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" universal_html: dependency: "direct main" description: @@ -1312,18 +1296,18 @@ packages: dependency: transitive description: name: win32 - sha256: b0f37db61ba2f2e9b7a78a1caece0052564d1bc70668156cf3a29d676fe4e574 + sha256: "0eaf06e3446824099858367950a813472af675116bf63f008a4c2a75ae13e9cb" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.5.0" win32_registry: - dependency: transitive + dependency: "direct main" description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.3" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c81f8cb..0fcd5c8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,14 +50,14 @@ dependencies: screenshot: ^2.3.0 after_layout: ^1.2.0 # flutter_math_fork: ^0.7.2 - uni_links: ^0.5.1 - uni_links_desktop: ^0.1.7 + app_links: ^6.0.1 image_picker: ^1.0.7 audioplayers: ^6.0.0 cross_file: ^0.3.4 shortid: ^0.1.2 icons_plus: ^5.0.0 tiktoken: ^1.0.3 + win32_registry: ^1.1.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2a8c423..250f078 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,15 +6,17 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include -#include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); FileSelectorWindowsRegisterWithRegistrar( @@ -23,8 +25,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); - UniLinksDesktopPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UniLinksDesktopPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); WindowManagerPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 6822529..352f3f9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,11 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links audioplayers_windows file_selector_windows screen_retriever share_plus - uni_links_desktop url_launcher_windows window_manager ) diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 47d25e7..36b4805 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -7,14 +7,45 @@ #include +bool SendAppLinkToInstance(const std::wstring& title) { + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) }; + GetWindowPlacement(hwnd, &place); + + switch(place.showCmd) { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, _In_ wchar_t *command_line, _In_ int show_command) { - HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"GPT Box"); - if (hwnd != NULL) { - DispatchToUniLinksDesktop(hwnd); - ::ShowWindow(hwnd, SW_NORMAL); - ::SetForegroundWindow(hwnd); - return EXIT_FAILURE; + if (SendAppLinkToInstance(L"GPT Box")) { + return EXIT_SUCCESS; } // Attach to console when present (e.g., 'flutter run') or create a