diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json new file mode 100644 index 00000000..2248d357 --- /dev/null +++ b/.vscode/c_cpp_properties.json @@ -0,0 +1,21 @@ +{ + "configurations": [ + { + "name": "Win32", + "includePath": [ + "${workspaceFolder}/**" + ], + "defines": [ + "_DEBUG", + "UNICODE", + "_UNICODE" + ], + "windowsSdkVersion": "10.0.19041.0", + "compilerPath": "C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Tools\\MSVC\\14.29.30133\\bin\\Hostx64\\x64\\cl.exe", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "windows-msvc-x64" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 9e26dfee..cad7657d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1 +1,3 @@ -{} \ No newline at end of file +{ + "cmake.configureOnOpen": false +} \ No newline at end of file diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index e6f3ebfc..7d44b354 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:spotube/components/Settings.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/provider/Playback.dart'; @@ -20,13 +21,16 @@ class _LyricsState extends State { Playback playback = context.watch(); UserPreferences userPreferences = context.watch(); + bool hasToken = (userPreferences.geniusAccessToken != null || + (userPreferences.geniusAccessToken?.isNotEmpty ?? false)); + if (playback.currentTrack != null && - userPreferences.geniusAccessToken != null && + hasToken && playback.currentTrack!.id != _lyrics["id"]) { getLyrics( playback.currentTrack!.name!, artistsToString(playback.currentTrack!.artists ?? []), - apiKey: userPreferences.geniusAccessToken, + apiKey: userPreferences.geniusAccessToken!, optimizeQuery: true, ).then((lyrics) { if (lyrics != null) { @@ -44,12 +48,32 @@ class _LyricsState extends State { } if (_lyrics["lyrics"] == null && playback.currentTrack != null) { + if (!hasToken) { + return Expanded( + child: Center( + child: Column( + children: [ + const Text("Genius lyrics API access token isn't set"), + ElevatedButton( + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) { + return const Settings(); + }, + )); + }, + child: const Text("Add Access Token")) + ], + ), + )); + } return const Expanded( child: Center( child: CircularProgressIndicator.adaptive(), ), ); } + return Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/components/Player.dart b/lib/components/Player.dart index b1cbf516..821c603b 100644 --- a/lib/components/Player.dart +++ b/lib/components/Player.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -30,70 +31,24 @@ class _PlayerState extends State with WidgetsBindingObserver { double _volume = 0; - final List _hotKeys = []; + late List _hotKeys; @override void initState() { - super.initState(); - player = AudioPlayer(); - WidgetsBinding.instance?.addObserver(this); - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { - try { - setState(() { - _volume = player.volume; - }); - player.playingStream.listen((playing) async { - setState(() { - _isPlaying = playing; - }); - }); - - player.durationStream.listen((duration) async { - if (duration != null) { - // Actually things doesn't work all the time as they were - // described. So instead of listening to a `playback.ready` - // stream, it has to listen to duration stream since duration - // is always added to the Stream sink after all icyMetadata has - // been loaded thus indicating buffering started - if (duration != Duration.zero && duration != _duration) { - // this line is for prev/next or already playing playlist - if (player.playing) await player.pause(); - await player.play(); - } - setState(() { - _duration = duration; - }); - } - }); - - player.processingStateStream.listen((event) async { - try { - if (event == ProcessingState.completed && _currentTrackId != null) { - _movePlaylistPositionBy(1); - } - } catch (e, stack) { - print("[PrecessingStateStreamListener] $e"); - print(stack); - } - }); - - playOrPause(key) async { - try { - _isPlaying ? await player.pause() : await player.play(); - } catch (e, stack) { - print("[PlayPauseShortcut] $e"); - print(stack); - } - } - - List keyWithActions = [ - GlobalKeyActions( - HotKey(KeyCode.space, scope: HotKeyScope.inapp), - playOrPause, - ), + try { + super.initState(); + player = AudioPlayer(); + _hotKeys = [ + GlobalKeyActions( + HotKey(KeyCode.space, scope: HotKeyScope.inapp), + _playOrPause, + ), + // causaes crash in Windows for aquiring global hotkey of + // keyboard media buttons + if (!Platform.isWindows) ...[ GlobalKeyActions( HotKey(KeyCode.mediaPlayPause), - playOrPause, + _playOrPause, ), GlobalKeyActions(HotKey(KeyCode.mediaTrackNext), (key) async { _movePlaylistPositionBy(1); @@ -111,27 +66,74 @@ class _PlayerState extends State with WidgetsBindingObserver { }); playback.reset(); }) - ]; + ] + ]; + WidgetsBinding.instance?.addObserver(this); + WidgetsBinding.instance?.addPostFrameCallback(_init); + } catch (e, stack) { + print("[Player.initState()] $e"); + print(stack); + } + } - await Future.wait( - keyWithActions.map((e) { - return hotKeyManager.register( - e.hotKey, - keyDownHandler: e.onKeyDown, - ); - }), - ); - } catch (e) { - print("[Player.initState()]: $e"); - } - }); + _init(Duration timeStamp) async { + try { + setState(() { + _volume = player.volume; + }); + player.playingStream.listen((playing) async { + setState(() { + _isPlaying = playing; + }); + }); + + player.durationStream.listen((duration) async { + if (duration != null) { + // Actually things doesn't work all the time as they were + // described. So instead of listening to a `playback.ready` + // stream, it has to listen to duration stream since duration + // is always added to the Stream sink after all icyMetadata has + // been loaded thus indicating buffering started + if (duration != Duration.zero && duration != _duration) { + // this line is for prev/next or already playing playlist + if (player.playing) await player.pause(); + await player.play(); + } + setState(() { + _duration = duration; + }); + } + }); + + player.processingStateStream.listen((event) async { + try { + if (event == ProcessingState.completed && _currentTrackId != null) { + _movePlaylistPositionBy(1); + } + } catch (e, stack) { + print("[PrecessingStateStreamListener] $e"); + print(stack); + } + }); + + await Future.wait( + _hotKeys.map((e) { + return hotKeyManager.register( + e.hotKey, + keyDownHandler: e.onKeyDown, + ); + }), + ); + } catch (e) { + print("[Player._init()]: $e"); + } } @override void dispose() { WidgetsBinding.instance?.removeObserver(this); player.dispose(); - Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e))); + Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey))); super.dispose(); } @@ -145,6 +147,15 @@ class _PlayerState extends State 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(); if (playback.currentTrack != null && playback.currentPlaylist != null) { @@ -303,19 +314,19 @@ class _PlayerState extends State with WidgetsBindingObserver { } }, onShuffle: () async { + if (playback.currentTrack == null || + playback.currentPlaylist == null) return; try { if (!_shuffled) { - await player.setShuffleModeEnabled(true).then( - (value) => setState(() { - _shuffled = true; - }), - ); + playback.currentPlaylist!.shuffle(); + setState(() { + _shuffled = true; + }); } else { - await player.setShuffleModeEnabled(false).then( - (value) => setState(() { - _shuffled = false; - }), - ); + playback.currentPlaylist!.unshuffle(); + setState(() { + _shuffled = false; + }); } } catch (e, stack) { print("[PlayerControls.onShuffle()] $e"); diff --git a/lib/helpers/server_ipc.dart b/lib/helpers/server_ipc.dart index 8694a064..33db359c 100644 --- a/lib/helpers/server_ipc.dart +++ b/lib/helpers/server_ipc.dart @@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart'; Future connectIpc(String authUri, String redirectUri) async { try { if (await canLaunch(authUri)) { + print("[Launching]: $authUri"); await launch(authUri); } diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 81868621..fb20d153 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -2,18 +2,35 @@ import 'package:flutter/cupertino.dart'; import 'package:spotify/spotify.dart'; class CurrentPlaylist { + List? _tempTrack; List tracks; String id; String name; String thumbnail; CurrentPlaylist({ - required List this.tracks, - required String this.id, - required String this.name, - required String this.thumbnail, + required this.tracks, + required this.id, + required this.name, + required this.thumbnail, }); List get trackIds => tracks.map((e) => e.id!).toList(); + + void shuffle() { + // won't shuffle if already shuffled + if (_tempTrack == null) { + _tempTrack = [...tracks]; + tracks.shuffle(); + } + } + + void unshuffle() { + // without _tempTracks unshuffling can't be done + if (_tempTrack != null) { + tracks = [..._tempTrack!]; + _tempTrack = null; + } + } } class Playback extends ChangeNotifier { diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 0fe10c5e..dc504710 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -16,7 +16,7 @@ class UserPreferences extends ChangeNotifier { } } - get geniusAccessToken => _geniusAccessToken; + String? get geniusAccessToken => _geniusAccessToken; setGeniusAccessToken(String? token) { _geniusAccessToken = token;