Merge branch 'dev' of github.com:KRTirtho/spotube into dev

This commit is contained in:
Kingkor Roy Tirtho 2023-12-27 19:28:20 +06:00
commit 70d5c57334
19 changed files with 273 additions and 10 deletions

View File

@ -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"
@ -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 -->

View File

@ -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);
}

View File

@ -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]);
}

View File

@ -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<void> main(List<String> rawArgs) async {
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
await registerWindowsScheme("spotify");
FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding);
MediaKit.ensureInitialized();
@ -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();
@ -193,9 +200,6 @@ class SpotubeState extends ConsumerState<Spotube> {
};
}, []);
useDisableBatteryOptimizations();
useGetStoragePermissions(ref);
final lightTheme = useMemoized(
() => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
[paletteColor, accentMaterialColor],

View File

@ -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>
@ -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);

View File

@ -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

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.
@ -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));
}

View File

@ -11,3 +11,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music
supported_mime_type:
- x-scheme-handler/spotify

View File

@ -32,3 +32,6 @@ keywords:
generic_name: Music Streaming Application
categories:
- Music
supported_mime_type:
- x-scheme-handler/spotify

View File

@ -28,3 +28,6 @@ categories:
- Music
startup_notify: true
supported_mime_type:
- x-scheme-handler/spotify

View File

@ -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"))

View File

@ -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>

View File

@ -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:

View File

@ -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

View File

@ -72,6 +72,12 @@
"explore_genres"
],
"it": [
"audio_source",
"go_to_album",
"discord_rich_presence"
],
"ja": [
"go_to_album",
"discord_rich_presence",

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
@ -20,6 +21,8 @@
#include <window_size/window_size_plugin.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
DartDiscordRpcPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DartDiscordRpcPlugin"));
FileSelectorWindowsRegisterWithRegistrar(

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
app_links
dart_discord_rpc
file_selector_windows
flutter_secure_storage_windows

View File

@ -3,6 +3,7 @@
#include <flutter_windows.h>
#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;
}

View File

@ -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_