added deep links for repo urls

This commit is contained in:
Schnitzel5 2025-02-12 15:48:36 +01:00
parent 011f62e157
commit f00b82ddb9
23 changed files with 353 additions and 9 deletions

View file

@ -29,6 +29,12 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter android:label="Add source repository">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mangayomi" android:host="add-repo" />
</intent-filter>
</activity>
<activity
android:name="com.linusu.flutter_web_auth_2.CallbackActivity"
@ -39,6 +45,7 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="mangayomi" />
</intent-filter>
<meta-data android:name="flutter_deeplinking_enabled" android:value="false" />
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->

View file

@ -1,6 +1,7 @@
import UIKit
import Flutter
import Libmtorrentserver
import app_links
@main
@objc class AppDelegate: FlutterAppDelegate {
@ -29,6 +30,12 @@ import Libmtorrentserver
})
GeneratedPluginRegistrant.register(with: self)
if let url = AppLinks.shared.getLink(launchOptions: launchOptions) {
AppLinks.shared.handleLink(url: url)
return true
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View file

@ -62,5 +62,7 @@
<true/>
<key>UISupportsDocumentBrowser</key>
<true/>
<key>FlutterDeepLinkingEnabled</key>
<false/>
</dict>
</plist>

View file

@ -416,5 +416,6 @@
"remove_extensions_repo": "Repository-Link entfernen",
"manage_manga_repo_url": "Verwalte Repository-Links für Manga",
"manage_anime_repo_url": "Verwalte Repository-Links für Anime",
"manage_novel_repo_url": "Verwalte Repository-Links für Novellen"
"manage_novel_repo_url": "Verwalte Repository-Links für Novellen",
"repo_added": "Erweiterungs-Repository hinzugefügt!"
}

View file

@ -428,5 +428,6 @@
"invalid_url_format": "Invalid URL format",
"clear_all_sources": "Clear all sources",
"clear_all_sources_msg": "This will completely erase all sources of the application. Are you sure you want to continue?",
"sources_cleared": "Sources cleared!!!"
"sources_cleared": "Sources cleared!!!",
"repo_added": "Source repository added!"
}

View file

@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'package:app_links/app_links.dart';
import 'package:bot_toast/bot_toast.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
@ -10,8 +12,12 @@ import 'package:google_fonts/google_fonts.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/eval/model/m_bridge.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/app_font_family.dart';
import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart';
import 'package:mangayomi/providers/l10n_providers.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/router/router.dart';
@ -21,6 +27,7 @@ import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_
import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:mangayomi/src/rust/frb_generated.dart';
import 'package:mangayomi/utils/url_protocol/api.dart';
import 'package:media_kit/media_kit.dart';
import 'package:path_provider/path_provider.dart';
import 'package:window_manager/window_manager.dart';
@ -40,6 +47,9 @@ void main(List<String> args) async {
if (!(Platform.isAndroid || Platform.isIOS)) {
await windowManager.ensureInitialized();
}
if (Platform.isWindows) {
registerProtocolHandler("mangayomi");
}
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) {
final availableVersion = await WebViewEnvironment.getAvailableVersion();
if (availableVersion != null) {
@ -73,9 +83,13 @@ class MyApp extends ConsumerStatefulWidget {
}
class _MyAppState extends ConsumerState<MyApp> {
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
@override
void initState() {
iniDateFormatting();
initDeepLinks();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (ref.read(clearChapterCacheOnAppLaunchStateProvider)) {
ref
@ -145,4 +159,55 @@ class _MyAppState extends ConsumerState<MyApp> {
title: 'MangaYomi',
);
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
Future<void> initDeepLinks() async {
final l10n = l10nLocalizations(context);
_appLinks = AppLinks();
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
switch (uri.host) {
case "add-repo":
final repoName = uri.queryParameters["repo_name"];
final repoUrl = uri.queryParameters["repo_url"];
final mangaRepoUrls = uri.queryParametersAll["manga_url"];
final animeRepoUrls = uri.queryParametersAll["anime_url"];
final novelRepoUrls = uri.queryParametersAll["novel_url"];
if (mangaRepoUrls != null) {
final mangaRepos =
ref.read(extensionsRepoStateProvider(ItemType.manga)).toList();
mangaRepos.addAll(mangaRepoUrls.map(
(e) => Repo(name: repoName, jsonUrl: e, website: repoUrl)));
ref
.read(extensionsRepoStateProvider(ItemType.manga).notifier)
.set(mangaRepos);
}
if (animeRepoUrls != null) {
final animeRepos =
ref.read(extensionsRepoStateProvider(ItemType.anime)).toList();
animeRepos.addAll(animeRepoUrls.map(
(e) => Repo(name: repoName, jsonUrl: e, website: repoUrl)));
ref
.read(extensionsRepoStateProvider(ItemType.anime).notifier)
.set(animeRepos);
}
if (novelRepoUrls != null) {
final novelRepos =
ref.read(extensionsRepoStateProvider(ItemType.novel)).toList();
novelRepos.addAll(novelRepoUrls.map(
(e) => Repo(name: repoName, jsonUrl: e, website: repoUrl)));
ref
.read(extensionsRepoStateProvider(ItemType.novel).notifier)
.set(novelRepos);
}
botToast(l10n.repo_added);
break;
default:
}
});
}
}

View file

@ -1,5 +1,6 @@
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/history.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/source.dart';
@ -12,6 +13,8 @@ part 'migration.g.dart';
Future<void> migration(Ref ref) async {
final mangas =
isar.mangas.filter().idIsNotNull().isMangaIsNotNull().findAllSync();
final categories =
isar.categorys.filter().idIsNotNull().forMangaIsNotNull().findAllSync();
final histories = isar.historys
.filter()
@ -43,6 +46,9 @@ Future<void> migration(Ref ref) async {
for (var manga in mangas) {
isar.mangas.putSync(manga..itemType = _convertToItemType(manga.isManga!));
}
for (var category in categories) {
isar.categorys.putSync(category..forItemType = _convertToItemType(category.forManga!));
}
});
}

View file

@ -203,7 +203,7 @@ ItemType _convertToItemType(Map<String, dynamic> backup) {
ItemType _convertToItemTypeCategory(Map<String, dynamic> backup) {
final forManga = backup['forManga'];
return forManga == null
? ItemType.values[backup['itemType'] ?? 0]
? ItemType.values[backup['forItemType'] ?? 0]
: forManga
? ItemType.manga
: ItemType.anime;

View file

@ -0,0 +1,42 @@
import 'windows_protocol.dart'
if (dart.library.js_interop) 'web_url_protocol.dart';
/// Registers a protocol by [scheme] to allow for links in the form `<scheme>://...`
/// to be processed by this application. By default, opening a link will open
/// the executable that was used to register the scheme with the URL as the first
/// argument passed to the executable.
///
/// If a protocol is already registered for the given scheme, this function will
/// attempt to overwrite the previous handler with the current executable information.
/// However, note that depending on process permissions, this operation may be
/// disallowed by the underlying platform.
///
/// You may pass an [executable] to override the path to the executable to run
/// when accessing the URL.
///
/// [arguments] is a list of arguments to be used when running the executable.
/// If passed, the list must contain at least one element, and at least one of
/// those elements must contain the literal value `%s` to denote the URL to open.
/// Quoting arguments is not necessary, as this will be handled for you.
/// Escaping the `%s` as an unprocessed literal is currently unsupported.
void registerProtocolHandler(
String scheme, {
String? executable,
List<String>? arguments,
}) {
WindowsProtocolHandler().register(
scheme,
executable: executable,
arguments: arguments,
);
}
/// Unregisters the protocol handler with the underlying platform. The provided
/// [scheme] will no longer be used in links.
///
/// Note that this will unregister a protocol by scheme regardless of which process
/// had registered it. Unregistering a scheme that was not registered by this
/// application is undefined and depends on platform-specific restrictions.
void unregisterProtocolHandler(String scheme) {
WindowsProtocolHandler().unregister(scheme);
}

View file

@ -0,0 +1,15 @@
abstract class ProtocolHandler {
void register(String scheme, {String? executable, List<String>? arguments});
void unregister(String scheme);
List<String> getArguments(List<String>? arguments) {
if (arguments == null) return ['%s'];
if (arguments.isEmpty && !arguments.any((e) => e.contains('%s'))) {
throw ArgumentError('arguments must contain at least 1 instance of "%s"');
}
return arguments;
}
}

View file

@ -0,0 +1,9 @@
import './protocol.dart';
class WindowsProtocolHandler extends ProtocolHandler {
@override
void register(String scheme, {String? executable, List<String>? arguments}) {}
@override
void unregister(String scheme) {}
}

View file

@ -0,0 +1,65 @@
import 'dart:io';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:win32/win32.dart';
import './protocol.dart';
const _hive = HKEY_CURRENT_USER;
class WindowsProtocolHandler extends ProtocolHandler {
@override
void register(String scheme, {String? executable, List<String>? arguments}) {
if (defaultTargetPlatform != TargetPlatform.windows) return;
final prefix = _regPrefix(scheme);
final capitalized = scheme[0].toUpperCase() + scheme.substring(1);
final args = getArguments(arguments).map((a) => _sanitize(a));
final cmd =
'${executable ?? Platform.resolvedExecutable} ${args.join(' ')}';
_regCreateStringKey(_hive, prefix, '', 'URL:$capitalized');
_regCreateStringKey(_hive, prefix, 'URL Protocol', '');
_regCreateStringKey(_hive, '$prefix\\shell\\open\\command', '', cmd);
}
@override
void unregister(String scheme) {
if (defaultTargetPlatform != TargetPlatform.windows) return;
final txtKey = TEXT(_regPrefix(scheme));
try {
RegDeleteTree(HKEY_CURRENT_USER, txtKey);
} finally {
free(txtKey);
}
}
String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme';
int _regCreateStringKey(int hKey, String key, String valueName, String data) {
final txtKey = TEXT(key);
final txtValue = TEXT(valueName);
final txtData = TEXT(data);
try {
return RegSetKeyValue(
hKey,
txtKey,
txtValue,
REG_VALUE_TYPE.REG_SZ,
txtData,
txtData.length * 2 + 2,
);
} finally {
free(txtKey);
free(txtValue);
free(txtData);
}
}
String _sanitize(String value) {
value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"');
return '"$value"';
}
}

View file

@ -8,6 +8,7 @@
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
#include <gtk/gtk_plugin.h>
#include <isar_flutter_libs/isar_flutter_libs_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h>
@ -23,6 +24,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_qjs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterQjsPlugin");
flutter_qjs_plugin_register_with_registrar(flutter_qjs_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) isar_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "IsarFlutterLibsPlugin");
isar_flutter_libs_plugin_register_with_registrar(isar_flutter_libs_registrar);

View file

@ -5,6 +5,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
desktop_webview_window
flutter_qjs
gtk
isar_flutter_libs
media_kit_libs_linux
media_kit_video

View file

@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
// Implements GApplication::activate.
static void my_application_activate(GApplication* application) {
MyApplication* self = MY_APPLICATION(application);
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 +85,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 +106,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));
}

View file

@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
import app_links
import audio_session
import connectivity_plus
import flutter_inappwebview_macos
@ -28,6 +29,7 @@ import window_manager
import window_to_front
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin"))

View file

@ -1,8 +1,19 @@
import Cocoa
import FlutterMacOS
import app_links
@main
class AppDelegate: FlutterAppDelegate {
public override func application(_ application: NSApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void) -> Bool {
guard let url = AppLinks.shared.getUniversalLink(userActivity) else {
return false
}
AppLinks.shared.handleLink(link: url.absoluteString)
return false
}
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true
}

View file

@ -33,5 +33,16 @@
<string>MainMenu</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>mangayomi</string>
<key>CFBundleURLSchemes</key>
<array>
<string>mangayomi</string>
</array>
</dict>
</array>
</dict>
</plist>

View file

@ -38,6 +38,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.13.2"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
app_links_linux:
dependency: transitive
description:
name: app_links_linux
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
url: "https://pub.dev"
source: hosted
version: "1.0.3"
app_links_platform_interface:
dependency: transitive
description:
name: app_links_platform_interface
sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
app_links_web:
dependency: transitive
description:
name: app_links_web
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
url: "https://pub.dev"
source: hosted
version: "1.0.4"
archive:
dependency: "direct main"
description:
@ -796,6 +828,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
gtk:
dependency: transitive
description:
name: gtk
sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
url: "https://pub.dev"
source: hosted
version: "2.1.0"
html:
dependency: "direct main"
description:
@ -2060,13 +2100,13 @@ packages:
source: hosted
version: "3.16.3"
win32:
dependency: transitive
dependency: "direct main"
description:
name: win32
sha256: "154360849a56b7b67331c21f09a386562d88903f90a1099c5987afc1912e1f29"
sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e
url: "https://pub.dev"
source: hosted
version: "5.10.0"
version: "5.10.1"
window_manager:
dependency: "direct main"
description:

View file

@ -80,6 +80,8 @@ dependencies:
flutter_widget_from_html: ^0.15.3
convert: ^3.1.2
connectivity_plus: ^6.1.2
app_links: ^6.3.3
win32: ^5.10.1
dependency_overrides:
http: ^1.2.2

View file

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <connectivity_plus/connectivity_plus_windows_plugin.h>
#include <flutter_inappwebview_windows/flutter_inappwebview_windows_plugin_c_api.h>
#include <flutter_qjs/flutter_qjs_plugin.h>
@ -21,6 +22,8 @@
#include <window_to_front/window_to_front_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
ConnectivityPlusWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin"));
FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar(

View file

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
connectivity_plus
flutter_inappwebview_windows
flutter_qjs

View file

@ -4,9 +4,51 @@
#include "flutter_window.h"
#include "utils.h"
#include "app_links/app_links_plugin_c_api.h"
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 (Optional) 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) {
// Replace "example" with the generated title found as parameter of `window.Create` in this file.
// You may ignore the result if you need to create another window.
if (SendAppLinkToInstance(L"Mangayomi")) {
return EXIT_SUCCESS;
}
// 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()) {
@ -40,4 +82,4 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
::CoUninitialize();
return EXIT_SUCCESS;
}
}