diff --git a/lib/components/Player.dart b/lib/components/Player.dart index 1239999a..325065bc 100644 --- a/lib/components/Player.dart +++ b/lib/components/Player.dart @@ -2,13 +2,14 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:spotube/components/PlayerControls.dart'; import 'package:spotube/helpers/artist-to-string.dart'; +import 'package:spotube/models/GlobalKeyActions.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:mpv_dart/mpv_dart.dart'; import 'package:provider/provider.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/provider/PlayerDI.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -27,6 +28,9 @@ class _PlayerState extends State { String? _currentPlaylistId; double _volume = 0; + + List _hotKeys = []; + @override void initState() { WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { @@ -79,6 +83,45 @@ class _PlayerState extends State { _duration = data["value"]; }); } + playOrPause(key) async { + _isPlaying ? await player.pause() : await player.play(); + } + + List keyWithActions = [ + GlobalKeyActions( + HotKey(KeyCode.space, scope: HotKeyScope.inapp), + playOrPause, + ), + GlobalKeyActions( + HotKey(KeyCode.mediaPlayPause), + playOrPause, + ), + GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async { + await player.next(); + }), + GlobalKeyActions(HotKey(KeyCode.mediaTrackPrevious), (key) async { + await player.prev(); + }), + GlobalKeyActions(HotKey(KeyCode.mediaStop), (key) async { + await player.stop(); + setState(() { + _isPlaying = false; + _currentPlaylistId = null; + _duration = 0; + _shuffled = false; + }); + playback.reset(); + }) + ]; + + await Future.wait( + keyWithActions.map((e) { + return hotKeyManager.register( + e.hotKey, + keyDownHandler: e.onKeyDown, + ); + }), + ); }); } catch (e) { if (kDebugMode) { @@ -90,11 +133,12 @@ class _PlayerState extends State { } @override - void dispose() { + void dispose() async { MPVPlayer player = context.read().player; player.removeAllByEvent(MPVEvents.paused); player.removeAllByEvent(MPVEvents.resumed); player.removeAllByEvent(MPVEvents.status); + await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e))); super.dispose(); } diff --git a/lib/main.dart b/lib/main.dart index eb0b795c..25d0508d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:mpv_dart/mpv_dart.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -12,7 +13,11 @@ import 'package:spotube/provider/PlayerDI.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/UserPreferences.dart'; -void main() { +void main() async { + // Must add this line. + WidgetsFlutterBinding.ensureInitialized(); + // For hot reload, `unregisterAll()` needs to be called. + await hotKeyManager.unregisterAll(); runApp(MyApp()); doWhenWindowReady(() { appWindow.minSize = const Size(900, 700); diff --git a/lib/models/GlobalKeyActions.dart b/lib/models/GlobalKeyActions.dart new file mode 100644 index 00000000..b4348539 --- /dev/null +++ b/lib/models/GlobalKeyActions.dart @@ -0,0 +1,7 @@ +import 'package:hotkey_manager/hotkey_manager.dart'; + +class GlobalKeyActions { + late final HotKey hotKey; + late final Function(HotKey hotKey) onKeyDown; + GlobalKeyActions(this.hotKey, this.onKeyDown); +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index bb3345d3..0d193ebd 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,12 +7,16 @@ #include "generated_plugin_registrant.h" #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) bitsdojo_window_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "BitsdojoWindowPlugin"); bitsdojo_window_plugin_register_with_registrar(bitsdojo_window_linux_registrar); + g_autoptr(FlPluginRegistrar) hotkey_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "HotkeyManagerPlugin"); + hotkey_manager_plugin_register_with_registrar(hotkey_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1d233e73..2e370f4b 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_linux + hotkey_manager url_launcher_linux ) diff --git a/pubspec.lock b/pubspec.lock index 9701cd21..92abc763 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,27 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.8" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" async: dependency: transitive description: @@ -191,6 +212,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.14.3" + hotkey_manager: + dependency: "direct main" + description: + name: hotkey_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" html: dependency: "direct main" description: @@ -212,6 +240,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.0" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" infinite_scroll_pagination: dependency: "direct main" description: @@ -219,6 +254,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.1.0" + injector: + dependency: transitive + description: + name: injector + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" js: dependency: transitive description: @@ -261,6 +303,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1" + msix: + dependency: "direct dev" + description: + name: msix + url: "https://pub.dartlang.org" + source: hosted + version: "2.8.0" nested: dependency: transitive description: @@ -282,6 +331,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" path: dependency: transitive description: @@ -623,6 +679,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "5.3.1" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" youtube_explode_dart: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 82bdf4b3..26d192e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,10 +45,12 @@ dependencies: mpv_dart: ^0.0.1 infinite_scroll_pagination: ^3.1.0 bitsdojo_window: ^0.1.1+1 + hotkey_manager: ^0.1.6 dev_dependencies: flutter_test: sdk: flutter + msix: ^2.8.0 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is