Skip to content

Commit

Permalink
feat: Deep link support (#950)
Browse files Browse the repository at this point in the history
* feat: add deep link support

* feat(android): add intent share support

* chore: untranslated msg for it locale
  • Loading branch information
KRTirtho authored Dec 18, 2023
1 parent 05f9ae6 commit 4050f55
Show file tree
Hide file tree
Showing 19 changed files with 273 additions and 10 deletions.
26 changes: 25 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<activity
android:name="com.ryanheise.audioservice.AudioServiceActivity"
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
Expand All @@ -48,6 +48,30 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="open.spotify.com"
/>
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<!-- Accepts URIs that begin with "spotify:// -->
<data android:scheme="spotify" />
</intent-filter>

</activity>

<!-- AudioService Config -->
Expand Down
25 changes: 25 additions & 0 deletions lib/collections/initializers.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import 'dart:io';
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:win32_registry/win32_registry.dart';

Future<void> 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);
}
93 changes: 93 additions & 0 deletions lib/hooks/configurators/use_deep_linking.dart
Original file line number Diff line number Diff line change
@@ -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<SharedFile> 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, dynamic>(
"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, dynamic>(
"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, dynamic>(
"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, dynamic>(
"playlist/$endSegment",
() => spotify.playlists.get(endSegment),
),
);
break;
default:
break;
}
});

return () {
mediaStream.cancel();
subscription.cancel();
};
}, [spotify, queryClient]);
}
10 changes: 7 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -41,6 +43,8 @@ Future<void> main(List<String> rawArgs) async {

final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();

await registerWindowsScheme("spotify");

FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);

MediaKit.ensureInitialized();
Expand Down Expand Up @@ -181,8 +185,11 @@ class SpotubeState extends ConsumerState<Spotube> {
final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));

useDisableBatteryOptimizations();
useInitSysTray(ref);
useDeepLinking(ref);
useCloseBehavior(ref);
useGetStoragePermissions(ref);

useEffect(() {
FlutterNativeSplash.remove();
Expand All @@ -193,9 +200,6 @@ class SpotubeState extends ConsumerState<Spotube> {
};
}, []);

useDisableBatteryOptimizations();
useGetStoragePermissions(ref);

final lightTheme = useMemoized(
() => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
[paletteColor, accentMaterialColor],
Expand Down
4 changes: 4 additions & 0 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
#include <gtk/gtk_plugin.h>
#include <local_notifier/local_notifier_plugin.h>
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <screen_retriever/screen_retriever_plugin.h>
Expand All @@ -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);
Expand Down
1 change: 1 addition & 0 deletions linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions linux/my_application.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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)));

Expand Down Expand Up @@ -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.
Expand All @@ -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));
}
3 changes: 3 additions & 0 deletions linux/packaging/appimage/make_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music

supported_mime_type:
- x-scheme-handler/spotify
3 changes: 3 additions & 0 deletions linux/packaging/deb/make_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music

supported_mime_type:
- x-scheme-handler/spotify
3 changes: 3 additions & 0 deletions linux/packaging/rpm/make_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ categories:
- Music

startup_notify: true

supported_mime_type:
- x-scheme-handler/spotify
2 changes: 2 additions & 0 deletions macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import FlutterMacOS
import Foundation

import app_links
import audio_service
import audio_session
import device_info_plus
Expand All @@ -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"))
Expand Down
13 changes: 13 additions & 0 deletions macos/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<!-- abstract name for this URL type (you can leave it blank) -->
<string>Spotify</string>
<key>CFBundleURLSchemes</key>
<array>
<!-- your schemes -->
<string>spotify</string>
</array>
</dict>
</array>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
Expand Down
30 changes: 27 additions & 3 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 4050f55

Please sign in to comment.