mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
Global custom hotkey support for playback control
UserPreferences provider genius access token not loading on init bug fix
This commit is contained in:
parent
b378375b19
commit
e666e25ffd
@ -1,7 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/DownloadTrackButton.dart';
|
||||
@ -9,7 +8,6 @@ import 'package:spotube/components/Player/PlayerControls.dart';
|
||||
import 'package:spotube/helpers/artists-to-clickable-artists.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/search-youtube.dart';
|
||||
import 'package:spotube/models/GlobalKeyActions.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@ -35,20 +33,13 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
|
||||
late YoutubeExplode youtube;
|
||||
|
||||
late List<GlobalKeyActions> _hotKeys;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
try {
|
||||
super.initState();
|
||||
player = AudioPlayer();
|
||||
youtube = YoutubeExplode();
|
||||
_hotKeys = [
|
||||
GlobalKeyActions(
|
||||
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
|
||||
_playOrPause,
|
||||
),
|
||||
];
|
||||
|
||||
WidgetsBinding.instance?.addObserver(this);
|
||||
WidgetsBinding.instance?.addPostFrameCallback(_init);
|
||||
} catch (e, stack) {
|
||||
@ -96,15 +87,6 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
print(stack);
|
||||
}
|
||||
});
|
||||
|
||||
await Future.wait(
|
||||
_hotKeys.map((e) {
|
||||
return hotKeyManager.register(
|
||||
e.hotKey,
|
||||
keyDownHandler: e.onKeyDown,
|
||||
);
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
print("[Player._init()]: $e");
|
||||
}
|
||||
@ -115,7 +97,6 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
WidgetsBinding.instance?.removeObserver(this);
|
||||
player.dispose();
|
||||
youtube.close();
|
||||
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@ -129,15 +110,6 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
_playOrPause(key) async {
|
||||
try {
|
||||
_isPlaying ? await player.pause() : await player.play();
|
||||
} catch (e, stack) {
|
||||
print("[PlayPauseShortcut] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
void _movePlaylistPositionBy(int pos) {
|
||||
Playback playback = context.read<Playback>();
|
||||
if (playback.currentTrack != null && playback.currentPlaylist != null) {
|
||||
@ -198,6 +170,29 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
_onNext() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(1);
|
||||
print("ON NEXT");
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
_onPrevious() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
@ -256,26 +251,8 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
isPlaying: _isPlaying,
|
||||
duration: _duration ?? Duration.zero,
|
||||
shuffled: _shuffled,
|
||||
onNext: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onPrevious: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
}
|
||||
},
|
||||
onNext: _onNext,
|
||||
onPrevious: _onPrevious,
|
||||
onPause: () async {
|
||||
try {
|
||||
await player.pause();
|
||||
|
@ -1,7 +1,11 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/models/GlobalKeyActions.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PlayerControls extends StatefulWidget {
|
||||
final Stream<Duration> positionStream;
|
||||
@ -36,15 +40,57 @@ class PlayerControls extends StatefulWidget {
|
||||
|
||||
class _PlayerControlsState extends State<PlayerControls> {
|
||||
StreamSubscription? _timePositionListener;
|
||||
late List<GlobalKeyActions> _hotKeys = [];
|
||||
|
||||
@override
|
||||
void dispose() async {
|
||||
await _timePositionListener?.cancel();
|
||||
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_playOrPause(key) async {
|
||||
try {
|
||||
widget.isPlaying ? widget.onPause?.call() : await widget.onPlay?.call();
|
||||
} catch (e, stack) {
|
||||
print("[PlayPauseShortcut] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
_configureHotKeys(UserPreferences preferences) async {
|
||||
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)))
|
||||
.then((val) async {
|
||||
_hotKeys = [
|
||||
GlobalKeyActions(
|
||||
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
|
||||
_playOrPause,
|
||||
),
|
||||
if (preferences.nextTrackHotKey != null)
|
||||
GlobalKeyActions(
|
||||
preferences.nextTrackHotKey!, (key) => widget.onNext?.call()),
|
||||
if (preferences.prevTrackHotKey != null)
|
||||
GlobalKeyActions(
|
||||
preferences.prevTrackHotKey!, (key) => widget.onPrevious?.call()),
|
||||
if (preferences.playPauseHotKey != null)
|
||||
GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause)
|
||||
];
|
||||
await Future.wait(
|
||||
_hotKeys.map((e) {
|
||||
return hotKeyManager.register(
|
||||
e.hotKey,
|
||||
keyDownHandler: e.onKeyDown,
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
UserPreferences preferences = context.watch<UserPreferences>();
|
||||
_configureHotKeys(preferences);
|
||||
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 700),
|
||||
child: Column(
|
||||
@ -103,16 +149,13 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
widget.onPrevious?.call();
|
||||
}),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
widget.isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: () {
|
||||
widget.isPlaying
|
||||
? widget.onPause?.call()
|
||||
: widget.onPlay?.call();
|
||||
}),
|
||||
icon: Icon(
|
||||
widget.isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: () => _playOrPause(null),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
onPressed: () => widget.onNext?.call()),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
|
||||
import 'package:spotube/components/Shared/Hyperlink.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/main.dart';
|
||||
@ -94,6 +96,27 @@ class _SettingsState extends State<Settings> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
SettingsHotKeyTile(
|
||||
title: "Next track global shortcut",
|
||||
currentHotKey: preferences.nextTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setNextTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Prev track global shortcut",
|
||||
currentHotKey: preferences.prevTrackHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPrevTrackHotKey(value);
|
||||
},
|
||||
),
|
||||
SettingsHotKeyTile(
|
||||
title: "Play/Pause global shortcut",
|
||||
currentHotKey: preferences.playPauseHotKey,
|
||||
onHotKeyRecorded: (value) {
|
||||
preferences.setPlayPauseHotKey(value);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@ -167,7 +190,7 @@ class _SettingsState extends State<Settings> {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Hyperlink(
|
||||
"Sponsor/Donate",
|
||||
"💚 Sponsor/Donate 💚",
|
||||
"https://opencollective.com/spotube",
|
||||
),
|
||||
Text(" • "),
|
||||
|
48
lib/components/Settings/SettingsHotkeyTile.dart
Normal file
48
lib/components/Settings/SettingsHotkeyTile.dart
Normal file
@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:spotube/components/Shared/RecordHotKeyDialog.dart';
|
||||
|
||||
class SettingsHotKeyTile extends StatelessWidget {
|
||||
final String title;
|
||||
final HotKey? currentHotKey;
|
||||
final ValueChanged<HotKey> onHotKeyRecorded;
|
||||
const SettingsHotKeyTile({
|
||||
required this.onHotKeyRecorded,
|
||||
required this.title,
|
||||
Key? key,
|
||||
this.currentHotKey,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(title),
|
||||
Row(
|
||||
children: [
|
||||
if (currentHotKey != null)
|
||||
HotKeyVirtualView(hotKey: currentHotKey!),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
child: const Text("Set Shortcut"),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return RecordHotKeyDialog(
|
||||
onHotKeyRecorded: onHotKeyRecorded,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
91
lib/components/Shared/RecordHotKeyDialog.dart
Normal file
91
lib/components/Shared/RecordHotKeyDialog.dart
Normal file
@ -0,0 +1,91 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
class RecordHotKeyDialog extends StatefulWidget {
|
||||
final ValueChanged<HotKey> onHotKeyRecorded;
|
||||
|
||||
const RecordHotKeyDialog({
|
||||
Key? key,
|
||||
required this.onHotKeyRecorded,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RecordHotKeyDialogState createState() => _RecordHotKeyDialogState();
|
||||
}
|
||||
|
||||
class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
|
||||
HotKey _hotKey = HotKey(null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Press the keys you want to use',
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
const Text(
|
||||
"DO NOT Use only letters (e.g. k, g etc..)\nUse in combination with these"),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
KeyCode.control,
|
||||
KeyCode.shift,
|
||||
KeyCode.alt,
|
||||
KeyCode.superKey,
|
||||
KeyCode.meta,
|
||||
]
|
||||
.map((key) => HotKeyVirtualView(
|
||||
hotKey: HotKey(key),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
Container(
|
||||
width: 100,
|
||||
height: 60,
|
||||
margin: const EdgeInsets.only(top: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).primaryColor,
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
HotKeyRecorder(
|
||||
onHotKeyRecorded: (hotKey) {
|
||||
setState(() {
|
||||
_hotKey = hotKey;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: !_hotKey.isSetted
|
||||
? null
|
||||
: () {
|
||||
widget.onHotKeyRecorded(_hotKey);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -7,4 +7,7 @@ abstract class LocalStorageKeys {
|
||||
static String geniusAccessToken = "genius_access_token";
|
||||
|
||||
static String themeMode = "theme_mode";
|
||||
static String nextTrackHotKey = "next_track_hot_key";
|
||||
static String prevTrackHotKey = "prev_track_hot_key";
|
||||
static String playPauseHotKey = "play_pause_hot_key";
|
||||
}
|
||||
|
@ -1,25 +1,100 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
|
||||
class UserPreferences extends ChangeNotifier {
|
||||
String? _geniusAccessToken;
|
||||
UserPreferences({String? geniusAccessToken}) {
|
||||
if (geniusAccessToken == null) {
|
||||
SharedPreferences.getInstance().then((localStorage) {
|
||||
String? accessToken =
|
||||
localStorage.getString(LocalStorageKeys.geniusAccessToken);
|
||||
_geniusAccessToken ??= accessToken;
|
||||
});
|
||||
} else {
|
||||
_geniusAccessToken = geniusAccessToken;
|
||||
String? geniusAccessToken;
|
||||
HotKey? nextTrackHotKey;
|
||||
HotKey? prevTrackHotKey;
|
||||
HotKey? playPauseHotKey;
|
||||
UserPreferences({
|
||||
this.nextTrackHotKey,
|
||||
this.prevTrackHotKey,
|
||||
this.playPauseHotKey,
|
||||
this.geniusAccessToken,
|
||||
}) {
|
||||
onInit();
|
||||
}
|
||||
|
||||
Future<HotKey?> _getHotKeyFromLocalStorage(
|
||||
SharedPreferences preferences, String key) async {
|
||||
String? str = preferences.getString(key);
|
||||
if (str != null) {
|
||||
Map<String, dynamic> json = await jsonDecode(str);
|
||||
if (json.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return HotKey.fromJson(json);
|
||||
}
|
||||
}
|
||||
|
||||
String? get geniusAccessToken => _geniusAccessToken;
|
||||
onInit() async {
|
||||
try {
|
||||
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
||||
String? accessToken =
|
||||
localStorage.getString(LocalStorageKeys.geniusAccessToken);
|
||||
|
||||
geniusAccessToken ??= accessToken;
|
||||
|
||||
nextTrackHotKey ??= await _getHotKeyFromLocalStorage(
|
||||
localStorage,
|
||||
LocalStorageKeys.nextTrackHotKey,
|
||||
);
|
||||
|
||||
prevTrackHotKey ??= await _getHotKeyFromLocalStorage(
|
||||
localStorage,
|
||||
LocalStorageKeys.prevTrackHotKey,
|
||||
);
|
||||
|
||||
playPauseHotKey ??= await _getHotKeyFromLocalStorage(
|
||||
localStorage,
|
||||
LocalStorageKeys.playPauseHotKey,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e, stack) {
|
||||
print("[UserPreferences.onInit]: $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
setGeniusAccessToken(String? token) {
|
||||
_geniusAccessToken = token;
|
||||
geniusAccessToken = token;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setNextTrackHotKey(HotKey? value) {
|
||||
nextTrackHotKey = value;
|
||||
SharedPreferences.getInstance().then((preferences) {
|
||||
preferences.setString(
|
||||
LocalStorageKeys.nextTrackHotKey,
|
||||
jsonEncode(value?.toJson() ?? {}),
|
||||
);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setPrevTrackHotKey(HotKey? value) {
|
||||
prevTrackHotKey = value;
|
||||
SharedPreferences.getInstance().then((preferences) {
|
||||
preferences.setString(
|
||||
LocalStorageKeys.prevTrackHotKey,
|
||||
jsonEncode(value?.toJson() ?? {}),
|
||||
);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
setPlayPauseHotKey(HotKey? value) {
|
||||
playPauseHotKey = value;
|
||||
SharedPreferences.getInstance().then((preferences) {
|
||||
preferences.setString(
|
||||
LocalStorageKeys.playPauseHotKey,
|
||||
jsonEncode(value?.toJson() ?? {}),
|
||||
);
|
||||
});
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user