Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: discord rpc for macOS and arm64 #1713

Merged
merged 3 commits into from
Jul 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import 'dart:async';

import 'package:dart_discord_rpc/dart_discord_rpc.dart';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:hive/hive.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:local_notifier/local_notifier.dart';
import 'package:media_kit/media_kit.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/initializers.dart';
import 'package:spotube/collections/routes.dart';
import 'package:spotube/collections/intents.dart';
Expand All @@ -37,6 +38,7 @@ import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/migrations/hive.dart';
import 'package:spotube/utils/migrations/sandbox.dart';
import 'package:spotube/utils/platform.dart';
import 'package:system_theme/system_theme.dart';
import 'package:path_provider/path_provider.dart';
Expand Down Expand Up @@ -67,6 +69,8 @@ Future<void> main(List<String> rawArgs) async {

MediaKit.ensureInitialized();

await migrateMacOsFromSandboxToNoSandbox();

// force High Refresh Rate on some Android devices (like One Plus)
if (kIsAndroid) {
await FlutterDisplayMode.setHighRefreshRate();
Expand All @@ -82,8 +86,8 @@ Future<void> main(List<String> rawArgs) async {
MetadataGod.initialize();
}

if (kIsWindows || kIsLinux) {
DiscordRPC.initialize();
if (kIsDesktop) {
await FlutterDiscordRPC.initialize(Env.discordAppId);
}

await KVStoreService.initialize();
Expand All @@ -108,6 +112,9 @@ Future<void> main(List<String> rawArgs) async {
overrides: [
databaseProvider.overrideWith((ref) => database),
],
observers: const [
AppLoggerProviderObserver(),
],
child: const Spotube(),
),
);
Expand Down
6 changes: 5 additions & 1 deletion lib/pages/home/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/modules/home/sections/featured.dart';
import 'package:spotube/modules/home/sections/feed.dart';
Expand All @@ -15,6 +16,7 @@ import 'package:spotube/modules/home/sections/recent.dart';
import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';

Expand All @@ -26,6 +28,8 @@ class HomePage extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final controller = useScrollController();
final mediaQuery = MediaQuery.of(context);
final layoutMode =
ref.watch(userPreferencesProvider.select((s) => s.layoutMode));

return SafeArea(
bottom: false,
Expand All @@ -34,7 +38,7 @@ class HomePage extends HookConsumerWidget {
body: CustomScrollView(
controller: controller,
slivers: [
if (mediaQuery.smAndDown)
if (mediaQuery.smAndDown || layoutMode == LayoutMode.compact)
SliverAppBar(
floating: true,
title: Assets.spotubeLogoPng.image(height: 45),
Expand Down
15 changes: 6 additions & 9 deletions lib/pages/settings/sections/desktop.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ import 'package:spotube/components/adaptive/adaptive_select_tile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';

import 'package:spotube/utils/platform.dart';

class SettingsDesktopSection extends HookConsumerWidget {
const SettingsDesktopSection({super.key});

Expand Down Expand Up @@ -54,13 +52,12 @@ class SettingsDesktopSection extends HookConsumerWidget {
value: preferences.systemTitleBar,
onChanged: preferencesNotifier.setSystemTitleBar,
),
if (!kIsMacOS)
SwitchListTile(
secondary: const Icon(SpotubeIcons.discord),
title: Text(context.l10n.discord_rich_presence),
value: preferences.discordPresence,
onChanged: preferencesNotifier.setDiscordPresence,
),
SwitchListTile(
secondary: const Icon(SpotubeIcons.discord),
title: Text(context.l10n.discord_rich_presence),
value: preferences.discordPresence,
onChanged: preferencesNotifier.setDiscordPresence,
),
],
);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/provider/audio_player/audio_player_streams.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class AudioPlayerStreamListeners {

ScrobblerNotifier get scrobbler => ref.read(scrobblerProvider.notifier);
UserPreferences get preferences => ref.read(userPreferencesProvider);
Discord get discord => ref.read(discordProvider);
DiscordNotifier get discord => ref.read(discordProvider.notifier);
AudioPlayerState get audioPlayerState => ref.read(audioPlayerProvider);
PlaybackHistoryActions get history =>
ref.read(playbackHistoryActionsProvider);
Expand Down
101 changes: 55 additions & 46 deletions lib/provider/discord_provider.dart
Original file line number Diff line number Diff line change
@@ -1,67 +1,76 @@
import 'package:dart_discord_rpc/dart_discord_rpc.dart';
import 'package:flutter/foundation.dart';
import 'dart:async';

import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/extensions/artist_simple.dart';
import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';

class Discord extends ChangeNotifier {
final DiscordRPC? discordRPC;
final bool isEnabled;
class DiscordNotifier extends AsyncNotifier<void> {
@override
FutureOr<void> build() async {
final enabled = ref.watch(
userPreferencesProvider.select((s) => s.discordPresence && kIsDesktop));
final playback = ref.read(audioPlayerProvider);

final subscription =
FlutterDiscordRPC.instance.isConnectedStream.listen((connected) async {
if (connected && playback.activeTrack != null) {
await updatePresence(playback.activeTrack!);
}
});

Discord(this.isEnabled)
: discordRPC = (kIsWindows || kIsLinux) && isEnabled
? DiscordRPC(applicationId: Env.discordAppId)
: null {
discordRPC?.start(autoRegister: true);
ref.onDispose(() async {
subscription.cancel();
await close();
await FlutterDiscordRPC.instance.dispose();
});

if (!enabled && FlutterDiscordRPC.instance.isConnected) {
await clear();
await close();
} else {
await FlutterDiscordRPC.instance.connect(autoRetry: true);
}
}

void updatePresence(Track track) {
clear();
Future<void> updatePresence(Track track) async {
await clear();
final artistNames = track.artists?.asString() ?? "";
discordRPC?.updatePresence(
DiscordPresence(
details: "Song: ${track.name} by $artistNames",
await FlutterDiscordRPC.instance.setActivity(
activity: RPCActivity(
details: "${track.name} by $artistNames",
state: "Vibing in Music",
startTimeStamp: DateTime.now().millisecondsSinceEpoch,
largeImageKey: "spotube-logo-foreground",
largeImageText: "Spotube",
smallImageKey: "spotube-logo-foreground",
smallImageText: "Spotube",
assets: const RPCAssets(
largeImage: "spotube-logo-foreground",
largeText: "Spotube",
smallImage: "spotube-logo-foreground",
smallText: "Spotube",
),
buttons: [
RPCButton(
label: "Listen on Spotify",
url: track.externalUrls?.spotify ??
"https://open.spotify.com/tracks/${track.id}",
),
],
timestamps: RPCTimestamps(
start: DateTime.now().millisecondsSinceEpoch,
),
),
);
}

void clear() {
discordRPC?.clearPresence();
Future<void> clear() async {
await FlutterDiscordRPC.instance.clearActivity();
}

void shutdown() {
discordRPC?.shutDown();
}

@override
void dispose() {
clear();
shutdown();
super.dispose();
Future<void> close() async {
await FlutterDiscordRPC.instance.disconnect();
}
}

final discordProvider = ChangeNotifierProvider(
(ref) {
final isEnabled =
ref.watch(userPreferencesProvider.select((s) => s.discordPresence));
final playback = ref.read(audioPlayerProvider);
final discord = Discord(isEnabled);

if (playback.activeTrack != null) {
discord.updatePresence(playback.activeTrack!);
}

return discord;
},
);
final discordProvider =
AsyncNotifierProvider<DiscordNotifier, void>(() => DiscordNotifier());
15 changes: 15 additions & 0 deletions lib/services/logger/logger.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:isolate';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:logger/logger.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
Expand Down Expand Up @@ -88,3 +89,17 @@ class AppLogger {
}
}
}

class AppLoggerProviderObserver extends ProviderObserver {
const AppLoggerProviderObserver();

@override
void providerDidFail(
ProviderBase<Object?> provider,
Object error,
StackTrace stackTrace,
ProviderContainer container,
) {
AppLogger.reportError(error, stackTrace);
}
}
58 changes: 58 additions & 0 deletions lib/utils/migrations/sandbox.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:io';

import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/platform.dart';

/// Migrates sandbox files on macOS to non-sandbox directories
Future<void> migrateMacOsFromSandboxToNoSandbox() async {
if (!kIsMacOS) return;

try {
final sandboxApplicationSupportDir = Directory(
"/Users/${Platform.environment["USER"]}/Library/Containers/oss.krtirtho.spotube/Data/Library/Application Support/oss.krtirtho.spotube",
);

if (!await sandboxApplicationSupportDir.exists()) {
stdout.writeln("🔵 Sandbox directory not found, skipping migration");
return;
}

const fileExts = [".db", ".lock", ".hive"];

final supportDir = await getApplicationSupportDirectory()
..create(recursive: true);

final supportFiles = await supportDir.list().toList();
final oldSupportFiles = await sandboxApplicationSupportDir.list().toList();

if (oldSupportFiles.isEmpty) {
stdout.writeln(
"🔵 No files found in sandboxed directory, skipping migration",
);
return;
} else if (supportFiles.any(
(file) => file is File && fileExts.contains(extension(file.path)))) {
stdout.writeln(
"🔵 Non-sandbox directory is not empty, skipping migration",
);
return;
}

for (final oldSupportFile in oldSupportFiles) {
if (oldSupportFile is File &&
fileExts.contains(extension(oldSupportFile.path))) {
final newPath = join(supportDir.path, basename(oldSupportFile.path));
await oldSupportFile.copy(newPath);
}
}

stdout.writeln("✅ Migrated sandboxed files to non-sandboxed directory");
} catch (e, stack) {
stdout.writeln(
"❌ Error migrating sandboxed files to non-sandboxed directory",
);
AppLogger.reportError(e, stack);
}
}
4 changes: 0 additions & 4 deletions linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

#include "generated_plugin_registrant.h"

#include <dart_discord_rpc/dart_discord_rpc_plugin.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
Expand All @@ -22,9 +21,6 @@
#include <window_manager/window_manager_plugin.h>

void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) dart_discord_rpc_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DartDiscordRpcPlugin");
dart_discord_rpc_plugin_register_with_registrar(dart_discord_rpc_registrar);
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);
Expand Down
2 changes: 1 addition & 1 deletion linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
#

list(APPEND FLUTTER_PLUGIN_LIST
dart_discord_rpc
desktop_webview_window
file_selector_linux
flutter_secure_storage_linux
Expand All @@ -20,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)

list(APPEND FLUTTER_FFI_PLUGIN_LIST
flutter_discord_rpc
media_kit_native_event_loop
metadata_god
)
Expand Down
Loading