From 46d9b656f9ef5519ac8ff4e622ba77b86da05806 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 30 May 2022 16:59:47 +0600 Subject: [PATCH 1/2] Update Checker implemented private playlists of current user aren't shown fix (https://github.com/KRTirtho/spotube/issues/92) --- lib/components/Home/Home.dart | 4 + .../Playlist/PlaylistCreateDialog.dart | 18 ++-- lib/components/Settings/Settings.dart | 17 ++++ lib/hooks/useUpdateChecker.dart | 90 +++++++++++++++++++ lib/provider/UserPreferences.dart | 11 +++ pubspec.lock | 7 ++ pubspec.yaml | 1 + 7 files changed, 141 insertions(+), 7 deletions(-) create mode 100644 lib/hooks/useUpdateChecker.dart diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 26f628dc..63f849b3 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -19,12 +19,14 @@ import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/usePaginatedFutureProvider.dart'; +import 'package:spotube/hooks/useUpdateChecker.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/SpotifyRequests.dart'; List spotifyScopes = [ "playlist-modify-public", "playlist-modify-private", + "playlist-read-private", "user-library-read", "user-library-modify", "user-read-private", @@ -52,6 +54,8 @@ class Home extends HookConsumerWidget { // initializing global hot keys useHotKeys(ref); + // checks for latest version of the application + useUpdateChecker(ref); final titleBarContents = Container( color: Theme.of(context).scaffoldBackgroundColor, diff --git a/lib/components/Playlist/PlaylistCreateDialog.dart b/lib/components/Playlist/PlaylistCreateDialog.dart index 7fef9060..ff3f4ea8 100644 --- a/lib/components/Playlist/PlaylistCreateDialog.dart +++ b/lib/components/Playlist/PlaylistCreateDialog.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/SpotifyRequests.dart'; class PlaylistCreateDialog extends HookConsumerWidget { const PlaylistCreateDialog({Key? key}) : super(key: key); @@ -43,13 +44,16 @@ class PlaylistCreateDialog extends HookConsumerWidget { final me = await spotify.me.get(); await spotify.playlists .createPlaylist( - me.id!, - playlistName.text, - collaborative: collaborative.value, - public: public.value, - description: description.text, - ) - .then((_) => Navigator.pop(context)); + me.id!, + playlistName.text, + collaborative: collaborative.value, + public: public.value, + description: description.text, + ) + .then((_) { + ref.refresh(currentUserPlaylistsQuery); + Navigator.pop(context); + }); }, ) ], diff --git a/lib/components/Settings/Settings.dart b/lib/components/Settings/Settings.dart index 9747eb92..72f86dac 100644 --- a/lib/components/Settings/Settings.dart +++ b/lib/components/Settings/Settings.dart @@ -217,6 +217,7 @@ class Settings extends HookConsumerWidget { children: [ const Text("Download lyrics along with the Track"), Switch.adaptive( + activeColor: Theme.of(context).primaryColor, value: preferences.saveTrackLyrics, onChanged: (state) { preferences.setSaveTrackLyrics(state); @@ -265,6 +266,22 @@ class Settings extends HookConsumerWidget { ) ], ), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Expanded( + flex: 2, + child: Text("Check for Update)"), + ), + Switch.adaptive( + activeColor: Theme.of(context).primaryColor, + value: preferences.checkUpdate, + onChanged: (checked) => + preferences.setCheckUpdate(checked), + ) + ], + ), if (auth.isLoggedIn) Builder(builder: (context) { Auth auth = ref.watch(authProvider); diff --git a/lib/hooks/useUpdateChecker.dart b/lib/hooks/useUpdateChecker.dart new file mode 100644 index 00000000..ca5b7e69 --- /dev/null +++ b/lib/hooks/useUpdateChecker.dart @@ -0,0 +1,90 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:http/http.dart' as http; +import 'package:spotube/components/Shared/AnchorButton.dart'; +import 'package:spotube/hooks/usePackageInfo.dart'; +import 'package:spotube/provider/UserPreferences.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:version/version.dart'; + +void useUpdateChecker(WidgetRef ref) { + final isCheckUpdateEnabled = + ref.watch(userPreferencesProvider.select((s) => s.checkUpdate)); + final packageInfo = usePackageInfo( + appName: 'Spotube', + packageName: 'spotube', + ); + final Future> Function() checkUpdate = useCallback( + () async { + final value = await http.get( + Uri.parse( + "https://api.github.com/repos/KRTirtho/spotube/releases/latest"), + ); + final tagName = + (jsonDecode(value.body)["tag_name"] as String).replaceAll("v", ""); + final currentVersion = packageInfo.version == "Unknown" + ? null + : Version.parse( + packageInfo.version, + ); + final latestVersion = Version.parse(tagName); + return [currentVersion, latestVersion]; + }, + [packageInfo.version], + ); + + final context = useContext(); + + download(String url) => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ); + + useEffect(() { + if (!isCheckUpdateEnabled) return null; + checkUpdate().then((value) { + if (value.first == null) return; + if (value.first! <= value.last) return; + showDialog( + context: context, + builder: (context) { + final url = + "https://github.com/KRTirtho/spotube/releases/tag/v${value.last}"; + return AlertDialog( + title: const Text("Spotube has an update"), + actions: [ + ElevatedButton( + child: const Text("Download Now"), + onPressed: () => download(url), + ), + ], + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("Spotube v${value.last} has been released"), + Row( + children: [ + const Text("Read the latest "), + AnchorButton( + "release notes", + style: const TextStyle(color: Colors.blue), + onTap: () => launchUrlString( + url, + mode: LaunchMode.externalApplication, + ), + ), + ], + ), + ], + ), + ); + }); + }); + return null; + }, [packageInfo, isCheckUpdateEnabled]); +} diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 6ffd7c83..cb0bde6b 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -20,6 +20,8 @@ class UserPreferences extends PersistedChangeNotifier { HotKey? prevTrackHotKey; HotKey? playPauseHotKey; + bool checkUpdate; + MaterialColor accentColorScheme; MaterialColor backgroundColorScheme; UserPreferences({ @@ -33,6 +35,7 @@ class UserPreferences extends PersistedChangeNotifier { this.nextTrackHotKey, this.prevTrackHotKey, this.playPauseHotKey, + this.checkUpdate = true, }) : super(); void setThemeMode(ThemeMode mode) { @@ -95,10 +98,17 @@ class UserPreferences extends PersistedChangeNotifier { updatePersistence(); } + void setCheckUpdate(bool check) { + checkUpdate = check; + notifyListeners(); + updatePersistence(); + } + @override FutureOr loadFromLocal(Map map) { saveTrackLyrics = map["saveTrackLyrics"] ?? false; recommendationMarket = map["recommendationMarket"] ?? recommendationMarket; + checkUpdate = map["checkUpdate"] ?? checkUpdate; geniusAccessToken = map["geniusAccessToken"] ?? getRandomElement(lyricsSecrets); nextTrackHotKey = map["nextTrackHotKey"] != null @@ -133,6 +143,7 @@ class UserPreferences extends PersistedChangeNotifier { "themeMode": themeMode.index, "backgroundColorScheme": backgroundColorScheme.value, "accentColorScheme": accentColorScheme.value, + "checkUpdate": checkUpdate, }; } } diff --git a/pubspec.lock b/pubspec.lock index b6f97602..98aa82f4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -870,6 +870,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + version: + dependency: "direct main" + description: + name: version + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" win32: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c7e48629..a2abee56 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,6 +61,7 @@ dependencies: marquee: ^2.2.1 scroll_to_index: ^2.1.1 package_info_plus: ^1.4.2 + version: ^2.0.0 dev_dependencies: flutter_test: From 8d7e5b0c9d5b060da1df4e6deed92707ed117776 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 2 Jun 2022 18:46:34 +0600 Subject: [PATCH 2/2] [android] Next/Prev track from notification/lockscreen support added (https://github.com/KRTirtho/spotube/issues/91) --- README.md | 1 - lib/components/Album/AlbumCard.dart | 1 + lib/components/Album/AlbumView.dart | 1 + lib/components/Artist/ArtistProfile.dart | 1 + lib/components/Player/Player.dart | 27 ++--- lib/components/Player/PlayerControls.dart | 8 +- lib/components/Playlist/PlaylistCard.dart | 1 + lib/components/Playlist/PlaylistView.dart | 1 + lib/components/Search/Search.dart | 1 + lib/hooks/useSyncedLyrics.dart | 10 +- lib/main.dart | 44 ++++++-- lib/models/CurrentPlaylist.dart | 38 +++++++ lib/provider/Playback.dart | 119 ++++++---------------- lib/utils/AudioPlayerHandler.dart | 83 +++++++++++++++ pubspec.lock | 9 +- pubspec.yaml | 3 +- test/widget_test.dart | 2 +- 17 files changed, 210 insertions(+), 140 deletions(-) create mode 100644 lib/models/CurrentPlaylist.dart create mode 100644 lib/utils/AudioPlayerHandler.dart diff --git a/README.md b/README.md index abdd72d8..8b2bbe17 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,6 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour - [hooks_riverpod](https://riverpod.dev/) - Riverpod with hooks - [go_router](https://github.com/flutter/packages/tree/main/packages/go_router) - A declarative router for Flutter based on Navigation 2 supporting deep linking, data-driven routes and more - [palette_generator](https://github.com/flutter/packages/tree/main/packages/palette_generator) - Flutter package for generating palette colors from a source image. -- [audio_session](https://github.com/ryanheise/audio_session) - Sets the iOS audio session category and Android audio attributes for your app, and manages your app's audio focus, mixing and ducking behaviour. - [logger](https://github.com/leisim/logger) - Small, easy to use and extensible logger which prints beautiful logs - [flutter_launcher_icons](https://github.com/fluttercommunity/flutter_launcher_icons) - A package which simplifies the task of updating your Flutter app's launcher icon. - [permission_handler](https://github.com/baseflow/flutter-permission-handler) - Permission plugin for Flutter. This plugin provides a cross-platform (iOS, Android) API to request and check permissions. diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index 287c3738..5e838e0c 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -7,6 +7,7 @@ import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 68ffef6c..489bd713 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -7,6 +7,7 @@ import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 5d70f457..e25a3139 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -15,6 +15,7 @@ import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 0d7b9cdf..0d7fc3ba 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,9 +1,7 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio/just_audio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Player/PlayerActions.dart'; import 'package:spotube/components/Player/PlayerOverlay.dart'; @@ -15,6 +13,7 @@ import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; +import 'package:spotube/utils/AudioPlayerHandler.dart'; class Player extends HookConsumerWidget { Player({Key? key}) : super(key: key); @@ -28,7 +27,7 @@ class Player extends HookConsumerWidget { final breakpoint = useBreakpoints(); - final AudioPlayer player = playback.player; + final AudioPlayerHandler player = playback.player; final Future future = useMemoized(SharedPreferences.getInstance); @@ -36,33 +35,19 @@ class Player extends HookConsumerWidget { useFuture(future, initialData: null); useEffect(() { - // registering all the stream subscription listeners of player - playback.register(); - /// warm up the audio player before playing actual audio /// It's for resolving unresolved issue related to just_audio's /// [disposeAllPlayers] method which is throwing /// [UnimplementedException] in the [PlatformInterface] /// implementation - if (Platform.isAndroid || Platform.isIOS) { - playback.audioSession - ?.setActive(true) - .then((_) => player.setAsset("assets/warmer.mp3")) - .catchError((e) { - logger.e("useEffect", e, StackTrace.current); - }); - } else { - player.setAsset("assets/warmer.mp3"); - } - return () { - playback.dispose(); - }; + player.core.setAsset("assets/warmer.mp3"); + return null; }, []); useEffect(() { if (localStorage.hasData) { _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? - player.volume; + player.core.volume; } return null; }, [localStorage.data]); @@ -154,7 +139,7 @@ class Player extends HookConsumerWidget { value: _volume.value, onChanged: (value) async { try { - await player.setVolume(value).then((_) { + await player.core.setVolume(value).then((_) { _volume.value = value; localStorage.data?.setDouble( LocalStorageKeys.volume, diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 15d99e21..d6f4d6ff 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:just_audio/just_audio.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/hooks/playback.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/utils/AudioPlayerHandler.dart'; class PlayerControls extends HookConsumerWidget { final Color? iconColor; @@ -18,7 +18,7 @@ class PlayerControls extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { final Playback playback = ref.watch(playbackProvider); - final AudioPlayer player = playback.player; + final AudioPlayerHandler player = playback.player; final onNext = useNextTrack(playback); @@ -33,9 +33,7 @@ class PlayerControls extends HookConsumerWidget { child: Column( children: [ StreamBuilder( - stream: player.positionStream.isBroadcast - ? player.positionStream - : player.positionStream.asBroadcastStream(), + stream: player.core.positionStream, builder: (context, snapshot) { final totalMinutes = zeroPadNumStr(duration.inMinutes.remainder(60)); diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index c631cdd0..a7ac5132 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -5,6 +5,7 @@ import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index d54f21e2..5f7c2597 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -5,6 +5,7 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/hooks/useForceUpdate.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 40355366..acc0b724 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -11,6 +11,7 @@ import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/simple-album-to-album.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; diff --git a/lib/hooks/useSyncedLyrics.dart b/lib/hooks/useSyncedLyrics.dart index 2d6456f3..f48686ab 100644 --- a/lib/hooks/useSyncedLyrics.dart +++ b/lib/hooks/useSyncedLyrics.dart @@ -1,12 +1,12 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:spotube/provider/AudioPlayer.dart'; +import 'package:spotube/provider/Playback.dart'; useSyncedLyrics(WidgetRef ref, Map lyricsMap) { - final player = ref.watch(audioPlayerProvider); - final stream = player.positionStream.isBroadcast - ? player.positionStream - : player.positionStream.asBroadcastStream(); + final player = ref.watch(playbackProvider.select( + (value) => (value.player), + )); + final stream = player.core.positionStream; final currentTime = useState(0); diff --git a/lib/main.dart b/lib/main.dart index 747ec653..e4380d8d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:audio_service/audio_service.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -9,20 +10,28 @@ import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/Logger.dart'; import 'package:spotube/provider/AudioPlayer.dart'; +import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/YouTube.dart'; -import 'package:just_audio_background/just_audio_background.dart'; import 'package:spotube/themes/dark-theme.dart'; import 'package:spotube/themes/light-theme.dart'; +import 'package:spotube/utils/AudioPlayerHandler.dart'; void main() async { - if (Platform.isAndroid || Platform.isIOS) { - await JustAudioBackground.init( - androidNotificationChannelId: 'oss.krtirtho.Spotube', + // await JustAudioBackground.init( + // androidNotificationChannelId: 'oss.krtirtho.Spotube', + // androidNotificationChannelName: 'Spotube', + // androidNotificationOngoing: true, + // ); + AudioPlayerHandler audioPlayerHandler = await AudioService.init( + builder: () => AudioPlayerHandler(), + config: const AudioServiceConfig( + androidNotificationChannelId: 'com.krtirtho.Spotube', androidNotificationChannelName: 'Spotube', androidNotificationOngoing: true, - ); - } else { + ), + ); + if (!Platform.isAndroid && !Platform.isIOS) { WidgetsFlutterBinding.ensureInitialized(); await hotKeyManager.unregisterAll(); doWhenWindowReady(() { @@ -35,14 +44,29 @@ void main() async { appWindow.show(); }); } - runApp(ProviderScope(child: MyApp())); + runApp(ProviderScope( + child: Spotube(), + overrides: [ + playbackProvider.overrideWithProvider(ChangeNotifierProvider( + (ref) { + final youtube = ref.watch(youtubeProvider); + final preferences = ref.watch(userPreferencesProvider); + return Playback( + player: audioPlayerHandler, + youtube: youtube, + preferences: preferences, + ); + }, + )) + ], + )); } -class MyApp extends HookConsumerWidget { +class Spotube extends HookConsumerWidget { final GoRouter _router = createGoRouter(); - final logger = getLogger(MyApp); + final logger = getLogger(Spotube); - MyApp({Key? key}) : super(key: key); + Spotube({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { final themeMode = diff --git a/lib/models/CurrentPlaylist.dart b/lib/models/CurrentPlaylist.dart new file mode 100644 index 00000000..11523220 --- /dev/null +++ b/lib/models/CurrentPlaylist.dart @@ -0,0 +1,38 @@ +import 'package:spotify/spotify.dart'; + +class CurrentPlaylist { + List? _tempTrack; + List tracks; + String id; + String name; + String thumbnail; + + CurrentPlaylist({ + required this.tracks, + required this.id, + required this.name, + required this.thumbnail, + }); + + List get trackIds => tracks.map((e) => e.id!).toList(); + + bool shuffle() { + // won't shuffle if already shuffled + if (_tempTrack == null) { + _tempTrack = [...tracks]; + tracks.shuffle(); + return true; + } + return false; + } + + bool unshuffle() { + // without _tempTracks unshuffling can't be done + if (_tempTrack != null) { + tracks = [..._tempTrack!]; + _tempTrack = null; + return true; + } + return false; + } +} diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 8fe1cf1f..de15ae6f 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -1,59 +1,21 @@ import 'dart:async'; -import 'package:audio_session/audio_session.dart'; +import 'package:audio_service/audio_service.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:just_audio_background/just_audio_background.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/search-youtube.dart'; +import 'package:spotube/models/CurrentPlaylist.dart'; import 'package:spotube/models/Logger.dart'; -import 'package:spotube/provider/AudioPlayer.dart'; import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/YouTube.dart'; +import 'package:spotube/utils/AudioPlayerHandler.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; -class CurrentPlaylist { - List? _tempTrack; - List tracks; - String id; - String name; - String thumbnail; - - CurrentPlaylist({ - required this.tracks, - required this.id, - required this.name, - required this.thumbnail, - }); - - List get trackIds => tracks.map((e) => e.id!).toList(); - - bool shuffle() { - // won't shuffle if already shuffled - if (_tempTrack == null) { - _tempTrack = [...tracks]; - tracks.shuffle(); - return true; - } - return false; - } - - bool unshuffle() { - // without _tempTracks unshuffling can't be done - if (_tempTrack != null) { - tracks = [..._tempTrack!]; - _tempTrack = null; - return true; - } - return false; - } -} - class Playback extends ChangeNotifier { - ChangeNotifierProviderRef ref; AudioSource? _currentAudioSource; final _logger = getLogger(Playback); CurrentPlaylist? _currentPlaylist; @@ -63,36 +25,38 @@ class Playback extends ChangeNotifier { bool _isPlaying = false; Duration? duration; - // listeners - StreamSubscription? _playingStreamListener; - StreamSubscription? _durationStreamListener; - StreamSubscription? _audioInterruptionEventListener; - StreamSubscription? _positionStreamListener; - Duration _prevPosition = Duration.zero; bool _shuffled = false; - AudioPlayer player; + AudioPlayerHandler player; YoutubeExplode youtube; - AudioSession? _audioSession; + UserPreferences preferences; Playback({ required this.player, required this.youtube, - required this.ref, + required this.preferences, CurrentPlaylist? currentPlaylist, Track? currentTrack, }) : _currentPlaylist = currentPlaylist, - _currentTrack = currentTrack; + _currentTrack = currentTrack { + player.onNextRequest = () { + movePlaylistPositionBy(1); + }; + player.onPreviousRequest = () { + movePlaylistPositionBy(-1); + }; + _init(); + } - void register() { - _playingStreamListener = player.playingStream.listen( + void _init() { + player.core.playingStream.listen( (playing) { _isPlaying = playing; notifyListeners(); }, ); - _durationStreamListener = player.durationStream.listen((event) async { + player.core.durationStream.listen((event) async { if (event != null) { // Actually things doesn't work all the time as they were // described. So instead of listening to a `_ready` @@ -101,7 +65,7 @@ class Playback extends ChangeNotifier { // been loaded thus indicating buffering started if (event != Duration.zero && event != duration) { // this line is for prev/next or already playing playlist - if (player.playing) await player.pause(); + if (player.core.playing) await player.pause(); await player.play(); } duration = event; @@ -109,8 +73,7 @@ class Playback extends ChangeNotifier { } }); - _positionStreamListener = - player.createPositionStream().listen((position) async { + player.core.createPositionStream().listen((position) async { // detecting multiple same call if (_prevPosition.inSeconds == position.inSeconds) return; _prevPosition = position; @@ -126,28 +89,18 @@ class Playback extends ChangeNotifier { await player.pause(); movePlaylistPositionBy(1); } else { - await audioSession?.setActive(false); _isPlaying = false; duration = null; notifyListeners(); } } }); - - AudioSession.instance.then((session) async { - _audioSession = session; - await session.configure(const AudioSessionConfiguration.music()); - _audioInterruptionEventListener = session.interruptionEventStream.listen( - (AudioInterruptionEvent event) {}, - ); - }); } bool get shuffled => _shuffled; CurrentPlaylist? get currentPlaylist => _currentPlaylist; Track? get currentTrack => _currentTrack; bool get isPlaying => _isPlaying; - AudioSession? get audioSession => _audioSession; set setCurrentTrack(Track track) { _logger.v("[Setting Current Track] ${track.name} - ${track.id}"); @@ -168,7 +121,6 @@ class Playback extends ChangeNotifier { duration = null; _currentPlaylist = null; _currentTrack = null; - _audioSession?.setActive(false); notifyListeners(); } @@ -188,16 +140,6 @@ class Playback extends ChangeNotifier { } } - @override - dispose() { - _durationStreamListener?.cancel(); - _playingStreamListener?.cancel(); - _audioInterruptionEventListener?.cancel(); - _positionStreamListener?.cancel(); - _audioSession?.setActive(false); - super.dispose(); - } - void movePlaylistPositionBy(int pos) { _logger.v("[Playlist Position Move] $pos"); if (_currentTrack != null && _currentPlaylist != null) { @@ -227,7 +169,7 @@ class Playback extends ChangeNotifier { // the track is already playing so no need to change that if (track != null && track.id == _currentTrack?.id) return; track ??= _currentTrack; - if (track != null && await _audioSession?.setActive(true) == true) { + if (track != null) { Uri? parsedUri = Uri.tryParse(track.uri ?? ""); final tag = MediaItem( id: track.id!, @@ -236,9 +178,10 @@ class Playback extends ChangeNotifier { artist: artistsToString(track.artists ?? []), artUri: Uri.parse(imageToUrlString(track.album?.images)), ); + player.addItem(tag); if (parsedUri != null && parsedUri.hasAbsolutePath) { - _currentAudioSource = AudioSource.uri(parsedUri, tag: tag); - await player + _currentAudioSource = AudioSource.uri(parsedUri); + await player.core .setAudioSource( _currentAudioSource!, preload: true, @@ -247,17 +190,17 @@ class Playback extends ChangeNotifier { _currentTrack = track; notifyListeners(); }); + // await player.play(); + return; } - final preferences = ref.read(userPreferencesProvider); final spotubeTrack = await toSpotubeTrack( youtube, track, preferences.ytSearchFormat, ); if (setTrackUriById(track.id!, spotubeTrack.ytUri)) { - _currentAudioSource = - AudioSource.uri(Uri.parse(spotubeTrack.ytUri), tag: tag); - await player + _currentAudioSource = AudioSource.uri(Uri.parse(spotubeTrack.ytUri)); + await player.core .setAudioSource( _currentAudioSource!, preload: true, @@ -266,6 +209,7 @@ class Playback extends ChangeNotifier { _currentTrack = spotubeTrack; notifyListeners(); }); + // await player.play(); } } } catch (e, stack) { @@ -289,11 +233,12 @@ class Playback extends ChangeNotifier { } final playbackProvider = ChangeNotifierProvider((ref) { - final player = ref.watch(audioPlayerProvider); + final player = AudioPlayerHandler(); final youtube = ref.watch(youtubeProvider); + final preferences = ref.watch(userPreferencesProvider); return Playback( player: player, youtube: youtube, - ref: ref, + preferences: preferences, ); }); diff --git a/lib/utils/AudioPlayerHandler.dart b/lib/utils/AudioPlayerHandler.dart new file mode 100644 index 00000000..bdab99a1 --- /dev/null +++ b/lib/utils/AudioPlayerHandler.dart @@ -0,0 +1,83 @@ +import 'dart:async'; + +import 'package:audio_service/audio_service.dart'; +import 'package:just_audio/just_audio.dart'; + +/// An [AudioHandler] for playing a single item. +class AudioPlayerHandler extends BaseAudioHandler { + final _player = AudioPlayer(); + + FutureOr Function()? onNextRequest; + FutureOr Function()? onPreviousRequest; + + /// Initialise our audio handler. + AudioPlayerHandler() { + // So that our clients (the Flutter UI and the system notification) know + // what state to display, here we set up our audio handler to broadcast all + // playback state changes as they happen via playbackState... + _player.playbackEventStream.map(_transformEvent).pipe(playbackState); + } + + AudioPlayer get core => _player; + + void addItem(MediaItem item) { + mediaItem.add(item); + } + + // In this simple example, we handle only 4 actions: play, pause, seek and + // stop. Any button press from the Flutter UI, notification, lock screen or + // headset will be routed through to these 4 methods so that you can handle + // your audio playback logic in one place. + + @override + Future play() => _player.play(); + + @override + Future pause() => _player.pause(); + + @override + Future seek(Duration position) => _player.seek(position); + + @override + Future stop() => _player.stop(); + + @override + Future skipToNext() async { + await onNextRequest?.call(); + await super.skipToNext(); + } + + @override + Future skipToPrevious() async { + await onPreviousRequest?.call(); + await super.skipToPrevious(); + } + + /// Transform a just_audio event into an audio_service state. + /// + /// This method is used from the constructor. Every event received from the + /// just_audio player will be transformed into an audio_service state so that + /// it can be broadcast to audio_service clients. + PlaybackState _transformEvent(PlaybackEvent event) { + return PlaybackState( + controls: [ + MediaControl.skipToPrevious, + if (_player.playing) MediaControl.pause else MediaControl.play, + MediaControl.skipToNext, + ], + androidCompactActionIndices: const [0, 1, 2], + processingState: const { + ProcessingState.idle: AudioProcessingState.idle, + ProcessingState.loading: AudioProcessingState.loading, + ProcessingState.buffering: AudioProcessingState.buffering, + ProcessingState.ready: AudioProcessingState.ready, + ProcessingState.completed: AudioProcessingState.completed, + }[_player.processingState]!, + playing: _player.playing, + updatePosition: _player.position, + bufferedPosition: _player.bufferedPosition, + speed: _player.speed, + queueIndex: event.currentIndex, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 98aa82f4..c7f4c92f 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,7 +30,7 @@ packages: source: hosted version: "2.8.2" audio_service: - dependency: transitive + dependency: "direct main" description: name: audio_service url: "https://pub.dartlang.org" @@ -338,13 +338,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.9.21" - just_audio_background: - dependency: "direct main" - description: - name: just_audio_background - url: "https://pub.dartlang.org" - source: hosted - version: "0.0.1-beta.5" just_audio_libwinmedia: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index a2abee56..c6bf3531 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,14 +54,13 @@ dependencies: hooks_riverpod: ^1.0.3 go_router: ^3.0.4 palette_generator: ^0.3.3 - audio_session: ^0.1.6+1 - just_audio_background: ^0.0.1-beta.5 logger: ^1.1.0 permission_handler: ^9.2.0 marquee: ^2.2.1 scroll_to_index: ^2.1.1 package_info_plus: ^1.4.2 version: ^2.0.0 + audio_service: ^0.18.4 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart index dc084ddc..b18f4962 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:spotube/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); + await tester.pumpWidget(Spotube()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);