mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'dev' of github.com:KRTirtho/spotube into dev
This commit is contained in:
commit
70d5c57334
@ -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 -->
|
||||
|
25
lib/collections/initializers.dart
Normal file
25
lib/collections/initializers.dart
Normal 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);
|
||||
}
|
93
lib/hooks/configurators/use_deep_linking.dart
Normal file
93
lib/hooks/configurators/use_deep_linking.dart
Normal 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]);
|
||||
}
|
@ -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],
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -11,3 +11,6 @@ keywords:
|
||||
generic_name: Music Streaming Application
|
||||
categories:
|
||||
- Music
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
@ -32,3 +32,6 @@ keywords:
|
||||
generic_name: Music Streaming Application
|
||||
categories:
|
||||
- Music
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
@ -28,3 +28,6 @@ categories:
|
||||
- Music
|
||||
|
||||
startup_notify: true
|
||||
|
||||
supported_mime_type:
|
||||
- x-scheme-handler/spotify
|
||||
|
@ -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"))
|
||||
|
@ -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>
|
||||
|
30
pubspec.lock
30
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:
|
||||
|
@ -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
|
||||
|
@ -72,6 +72,12 @@
|
||||
"explore_genres"
|
||||
],
|
||||
|
||||
"it": [
|
||||
"audio_source",
|
||||
"go_to_album",
|
||||
"discord_rich_presence"
|
||||
],
|
||||
|
||||
"ja": [
|
||||
"go_to_album",
|
||||
"discord_rich_presence",
|
||||
|
@ -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(
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
dart_discord_rpc
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
|
@ -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;
|
||||
}
|
@ -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_
|
||||
|
Loading…
Reference in New Issue
Block a user