Skip to content

Commit

Permalink
feat: discord rpc for macOS, windows-arm64 and linux-arm64 (#1713)
Browse files Browse the repository at this point in the history
* feat: add discord rpc support for macos, windows arm64 and linux arm64

* chore: discord rpc not clearing activity after close/setting rpc to false

* chore: add migration script to move from files from macos sandbox to non-sandbox directories
  • Loading branch information
KRTirtho authored Jul 14, 2024
1 parent a6e13ff commit 6a50073
Show file tree
Hide file tree
Showing 17 changed files with 230 additions and 136 deletions.
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

0 comments on commit 6a50073

Please sign in to comment.