diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a3f1390a3..5ab7a0b51 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -27,7 +27,7 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/collections/initializers.dart b/lib/collections/initializers.dart
new file mode 100644
index 000000000..9627de1c3
--- /dev/null
+++ b/lib/collections/initializers.dart
@@ -0,0 +1,25 @@
+import 'dart:io';
+import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
+import 'package:win32_registry/win32_registry.dart';
+
+Future registerWindowsScheme(String scheme) async {
+ if (!DesktopTools.platform.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/hooks/configurators/use_deep_linking.dart b/lib/hooks/configurators/use_deep_linking.dart
new file mode 100644
index 000000000..546ab2e8b
--- /dev/null
+++ b/lib/hooks/configurators/use_deep_linking.dart
@@ -0,0 +1,93 @@
+import 'package:app_links/app_links.dart';
+import 'package:fl_query_hooks/fl_query_hooks.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:spotify/spotify.dart';
+import 'package:spotube/collections/routes.dart';
+import 'package:spotube/provider/spotify_provider.dart';
+import 'package:flutter_sharing_intent/flutter_sharing_intent.dart';
+import 'package:flutter_sharing_intent/model/sharing_file.dart';
+
+void useDeepLinking(WidgetRef ref) {
+ // single instance no worries
+ final appLinks = AppLinks();
+ final spotify = ref.watch(spotifyProvider);
+ final queryClient = useQueryClient();
+
+ useEffect(() {
+ void uriListener(List files) async {
+ for (final file in files) {
+ if (file.type != SharedMediaType.URL) continue;
+ final url = Uri.parse(file.value!);
+ if (url.pathSegments.length != 2) continue;
+
+ switch (url.pathSegments.first) {
+ case "album":
+ router.push(
+ "/album/${url.pathSegments.last}",
+ extra: await queryClient.fetchQuery(
+ "album/${url.pathSegments.last}",
+ () => spotify.albums.get(url.pathSegments.last),
+ ),
+ );
+ break;
+ case "artist":
+ router.push("/artist/${url.pathSegments.last}");
+ break;
+ case "playlist":
+ router.push(
+ "/playlist/${url.pathSegments.last}",
+ extra: await queryClient.fetchQuery(
+ "playlist/${url.pathSegments.last}",
+ () => spotify.playlists.get(url.pathSegments.last),
+ ),
+ );
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ FlutterSharingIntent.instance.getInitialSharing().then(uriListener);
+
+ final mediaStream =
+ FlutterSharingIntent.instance.getMediaStream().listen(uriListener);
+
+ final subscription = appLinks.allStringLinkStream.listen((uri) async {
+ final startSegment = uri.split(":").take(2).join(":");
+ final endSegment = uri.split(":").last;
+
+ switch (startSegment) {
+ case "spotify:album":
+ await router.push(
+ "/album/$endSegment",
+ extra: await queryClient.fetchQuery(
+ "album/$endSegment",
+ () => spotify.albums.get(endSegment),
+ ),
+ );
+ break;
+ case "spotify:artist":
+ await router.push("/artist/$endSegment");
+ break;
+ case "spotify:playlist":
+ await router.push(
+ "/playlist/$endSegment",
+ extra: await queryClient.fetchQuery(
+ "playlist/$endSegment",
+ () => spotify.playlists.get(endSegment),
+ ),
+ );
+ break;
+ default:
+ break;
+ }
+ });
+
+ return () {
+ mediaStream.cancel();
+ subscription.cancel();
+ };
+ }, [spotify, queryClient]);
+}
diff --git a/lib/main.dart b/lib/main.dart
index 91ec789d9..052e68092 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -13,9 +13,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:media_kit/media_kit.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:shared_preferences/shared_preferences.dart';
+import 'package:spotube/collections/initializers.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/intents.dart';
import 'package:spotube/hooks/configurators/use_close_behavior.dart';
+import 'package:spotube/hooks/configurators/use_deep_linking.dart';
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
import 'package:spotube/l10n/l10n.dart';
@@ -41,6 +43,8 @@ Future main(List rawArgs) async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
+ await registerWindowsScheme("spotify");
+
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
MediaKit.ensureInitialized();
@@ -181,8 +185,11 @@ class SpotubeState extends ConsumerState {
final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
+ useDisableBatteryOptimizations();
useInitSysTray(ref);
+ useDeepLinking(ref);
useCloseBehavior(ref);
+ useGetStoragePermissions(ref);
useEffect(() {
FlutterNativeSplash.remove();
@@ -193,9 +200,6 @@ class SpotubeState extends ConsumerState {
};
}, []);
- useDisableBatteryOptimizations();
- useGetStoragePermissions(ref);
-
final lightTheme = useMemoized(
() => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
[paletteColor, accentMaterialColor],
diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc
index a07f7f9b4..c69c17c01 100644
--- a/linux/flutter/generated_plugin_registrant.cc
+++ b/linux/flutter/generated_plugin_registrant.cc
@@ -9,6 +9,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -28,6 +29,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin");
flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_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) local_notifier_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "LocalNotifierPlugin");
local_notifier_plugin_register_with_registrar(local_notifier_registrar);
diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake
index 97d541b3d..a4487f4d0 100644
--- a/linux/flutter/generated_plugins.cmake
+++ b/linux/flutter/generated_plugins.cmake
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
dart_discord_rpc
file_selector_linux
flutter_secure_storage_linux
+ gtk
local_notifier
media_kit_libs_linux
screen_retriever
diff --git a/linux/my_application.cc b/linux/my_application.cc
index 759285af3..d1ac5d124 100644
--- a/linux/my_application.cc
+++ b/linux/my_application.cc
@@ -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.
@@ -98,7 +105,7 @@ 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,
+ "com.github.KRTirtho.Spotube", APPLICATION_ID,
+ "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
nullptr));
}
diff --git a/linux/packaging/appimage/make_config.yaml b/linux/packaging/appimage/make_config.yaml
index 68e36df72..c7332ea28 100644
--- a/linux/packaging/appimage/make_config.yaml
+++ b/linux/packaging/appimage/make_config.yaml
@@ -11,3 +11,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music
+
+supported_mime_type:
+ - x-scheme-handler/spotify
diff --git a/linux/packaging/deb/make_config.yaml b/linux/packaging/deb/make_config.yaml
index 46493122d..f4c279b49 100644
--- a/linux/packaging/deb/make_config.yaml
+++ b/linux/packaging/deb/make_config.yaml
@@ -32,3 +32,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music
+
+supported_mime_type:
+ - x-scheme-handler/spotify
diff --git a/linux/packaging/rpm/make_config.yaml b/linux/packaging/rpm/make_config.yaml
index 00f4c20ef..1f952d0e5 100644
--- a/linux/packaging/rpm/make_config.yaml
+++ b/linux/packaging/rpm/make_config.yaml
@@ -28,3 +28,6 @@ categories:
- Music
startup_notify: true
+
+supported_mime_type:
+ - x-scheme-handler/spotify
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 270e62614..a7965e14c 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation
+import app_links
import audio_service
import audio_session
import device_info_plus
@@ -24,6 +25,7 @@ import window_manager
import window_size
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist
index 19f1c02a2..1a8bb655f 100644
--- a/macos/Runner/Info.plist
+++ b/macos/Runner/Info.plist
@@ -2,6 +2,19 @@
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+
+ Spotify
+ CFBundleURLSchemes
+
+
+ spotify
+
+
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
diff --git a/pubspec.lock b/pubspec.lock
index 8921f8a7d..526898d50 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.13.0"
+ app_links:
+ dependency: "direct main"
+ description:
+ name: app_links
+ sha256: "4e392b5eba997df356ca6021f28431ce1cfeb16758699553a94b13add874a3bb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.5.0"
app_package_maker:
dependency: transitive
description:
@@ -907,6 +915,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
+ flutter_sharing_intent:
+ dependency: "direct main"
+ description:
+ name: flutter_sharing_intent
+ sha256: "6eb896e6523b735e8230eeb206fd3b9f220f11ce879c2400a90b443147036ff9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.0"
flutter_svg:
dependency: "direct main"
description:
@@ -1018,6 +1034,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.8"
+ gtk:
+ dependency: transitive
+ description:
+ name: gtk
+ sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
hive:
dependency: "direct main"
description:
@@ -2246,13 +2270,13 @@ packages:
source: hosted
version: "5.0.7"
win32_registry:
- dependency: transitive
+ dependency: "direct main"
description:
name: win32_registry
- sha256: e4506d60b7244251bc59df15656a3093501c37fb5af02105a944d73eb95be4c9
+ sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
url: "https://pub.dev"
source: hosted
- version: "1.1.1"
+ version: "1.1.2"
window_manager:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 77a26911c..267ab17fe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -119,6 +119,9 @@ dependencies:
html_unescape: ^2.0.0
wikipedia_api: ^0.1.0
skeletonizer: ^0.8.0
+ app_links: ^3.5.0
+ win32_registry: ^1.1.2
+ flutter_sharing_intent: ^1.1.0
dev_dependencies:
build_runner: ^2.3.2
diff --git a/untranslated_messages.json b/untranslated_messages.json
index 66d8c7b22..84d6b95f6 100644
--- a/untranslated_messages.json
+++ b/untranslated_messages.json
@@ -72,6 +72,12 @@
"explore_genres"
],
+ "it": [
+ "audio_source",
+ "go_to_album",
+ "discord_rich_presence"
+ ],
+
"ja": [
"go_to_album",
"discord_rich_presence",
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index b9c6a4816..fcf9927e0 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
+#include
#include
#include
#include
@@ -20,6 +21,8 @@
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ AppLinksPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DartDiscordRpcPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartDiscordRpcPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 5cd55ff37..0fe6e076b 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ app_links
dart_discord_rpc
file_selector_windows
flutter_secure_storage_windows
diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp
index d5c04f23e..9823151cd 100644
--- a/windows/runner/win32_window.cpp
+++ b/windows/runner/win32_window.cpp
@@ -3,6 +3,7 @@
#include
#include "resource.h"
+#include "app_links/app_links_plugin_c_api.h"
namespace {
@@ -105,6 +106,9 @@ Win32Window::~Win32Window() {
bool Win32Window::CreateAndShow(const std::wstring& title,
const Point& origin,
const Size& size) {
+ if (SendAppLinkToInstance(title)) {
+ return false;
+ }
Destroy();
const wchar_t* window_class =
@@ -244,3 +248,39 @@ bool Win32Window::OnCreate() {
void Win32Window::OnDestroy() {
// No-op; provided for subclasses.
}
+
+// app_links
+bool Win32Window::SendAppLinkToInstance(const std::wstring& title) {
+ // Find our exact window
+ HWND hwnd = ::FindWindow(kWindowClassName, 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;
+}
\ No newline at end of file
diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h
index 17ba43112..1d817bd2f 100644
--- a/windows/runner/win32_window.h
+++ b/windows/runner/win32_window.h
@@ -93,6 +93,10 @@ class Win32Window {
// window handle for hosted content.
HWND child_content_ = nullptr;
+ // Dispatches link if any.
+ // This method enables our app to be with a single instance too.
+ // This is mandatory if you want to catch further links in same app.
+ bool SendAppLinkToInstance(const std::wstring& title);
};
#endif // RUNNER_WIN32_WINDOW_H_