From 1122073237923f651cd10b630ede26dd794f5f3c Mon Sep 17 00:00:00 2001 From: Marcos Marado Date: Mon, 7 Feb 2022 14:18:18 +0000 Subject: [PATCH 01/25] README: fix license badge image link e1f66c9c7a9c14190e91a7a49169b346a3dd561e changed the aur package name to spotube-bin, so the badge link also needs to be updated. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 34ca53ae..db3a6ebf 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ GitHub release - License + License Maintainer @@ -178,4 +178,4 @@ Bu why? You can learn about it [here](https://dev.to/krtirtho/choosing-open-sour Follow me on [Twitter](https://twitter.com/@krtirtho) for newer updates about this application -

© 2022 Spotube

\ No newline at end of file +

© 2022 Spotube

From fa65aa878c2ff1d76b2ef48d8c79be187f807486 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 8 Feb 2022 10:28:47 +0600 Subject: [PATCH 02/25] updated docs for version 1.2.0 --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34ca53ae..85cdc68a 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,11 @@ $ flatpak install flathub com.github.KRTirtho.Spotube ### AppImage: Download the [Spotube-linux-x86_64.AppImage](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-linux-x86_64.AppImage) file & double click to run it. AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed -**I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey stores or software centers or repositories** +## Mac OS +Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/download/v1.1.0/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard + +**I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey/homebrew stores or software centers or repositories** # Configuration There are some configurations that needs to be done to start using this software @@ -130,7 +133,7 @@ Also, you need a [genius](https://genius.com) account for **lyrics** & a API Cli - [x] Add support for show Lyric of currently playing track - [x] Track download - [ ] Support for playing/streaming podcasts/shows -- [ ] Artist, User & Album pages +- [x] Artist, User & Album pages # Building from source @@ -149,7 +152,6 @@ $ flutter run -d - Shows & Podcasts aren't supported as it'd require premium anyway - OS Media Controls -- Global Media Shortcuts/Keyboard Media Buttons # License From 799919d538294d7082306636526c2f3608abc336 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 8 Feb 2022 10:51:15 +0600 Subject: [PATCH 03/25] aur PKGBUILD & chocolatey VERIFICATION.txt update for version 1.2.0 --- aur-struct/.SRCINFO | 2 +- aur-struct/PKGBUILD | 5 ++++- choco-struct/tools/VERIFICATION.txt | 6 +++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/aur-struct/.SRCINFO b/aur-struct/.SRCINFO index 7f5956bc..684aaf92 100644 --- a/aur-struct/.SRCINFO +++ b/aur-struct/.SRCINFO @@ -7,6 +7,6 @@ pkgbase = spotube-bin license = BSD-4-Clause depends = libkeybinder3 source = https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-linux-x86_64.tar.xz - md5sums = 0db87627ddf753bc7f09ebbb282184ee + md5sums = f49d21ef00c7d43eb70e7e9b2a7103c1 pkgname = spotube-bin diff --git a/aur-struct/PKGBUILD b/aur-struct/PKGBUILD index 4e7ceb3e..4284e0f8 100644 --- a/aur-struct/PKGBUILD +++ b/aur-struct/PKGBUILD @@ -21,16 +21,19 @@ install= changelog= source=("https://github.com/KRTirtho/spotube/releases/download/v${pkgver}/Spotube-linux-x86_64.tar.xz") noextract=() -md5sums=(0db87627ddf753bc7f09ebbb282184ee) +md5sums=(f49d21ef00c7d43eb70e7e9b2a7103c1) validpgpkeys=() package(){ install -dm755 "${pkgdir}/usr/share/icons/${pkgname}" install -dm755 "${pkgdir}/usr/share/applications" + install -dm755 "${pkgdir}/usr/share/appdata" install -dm755 "${pkgdir}/usr/share/${pkgname}" install -dm755 "${pkgdir}/usr/bin" cp -ra ./ "${pkgdir}/usr/share/${pkgname}" cp ./spotube.desktop "${pkgdir}/usr/share/applications" cp ./spotube-logo.png "${pkgdir}/usr/share/icons/${pkgname}" + cp ./com.github.KRTirtho.Spotube.appdata.xml "${pkgdir}/usr/share/appdata/spotube.appdata.xml" + sed -i 's|com.github.KRTirtho.Spotube|spotube|' "${pkgdir}/usr/share/appdata/spotube.appdata.xml" ln -s "/usr/share/${pkgname}/spotube" "${pkgdir}/usr/bin/${pkgname}" } diff --git a/choco-struct/tools/VERIFICATION.txt b/choco-struct/tools/VERIFICATION.txt index b24d0da2..4c71818e 100644 --- a/choco-struct/tools/VERIFICATION.txt +++ b/choco-struct/tools/VERIFICATION.txt @@ -7,7 +7,7 @@ in verifying that this package's contents are trustworthy. Please go to releases page https://github.com/KRTirtho/spotube/releases -Download same version as this choco package (example for v1.1.0) +Download same version as this choco package (example for v1.2.0) https://github.com/KRTirtho/spotube/releases/tag/v1.0.1 1. get hashes. Run: @@ -15,9 +15,9 @@ powershell -command Get-FileHash tools\Spotube-windows-x86_64-setup.exe 2. The checksums should match the following: --- -Version Hashes for v1.1.0 +Version Hashes for v1.2.0 Algorithm Hash Path --------- ---- ---- -SHA256 144fb4170b424ae9ecee8941354244cb9744c0913fdc69f730a8b5e40e56753d tools\Spotube-windows-x86_64-setup.exe \ No newline at end of file +SHA256 02c032e1a2b8f60969b7a65c6a5e21df2bf5834cc8d8062cf56a2c8245a2a90e tools\Spotube-windows-x86_64-setup.exe \ No newline at end of file From 25854c843fb24af36a2cb4e6483e5eb405074c00 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 8 Feb 2022 16:23:40 +0600 Subject: [PATCH 04/25] Player._playTrack not checking currentTrack.uri validity fix --- lib/components/Player/Player.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index ef9e4d2b..a638298c 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -138,10 +138,11 @@ class _PlayerState extends State with WidgetsBindingObserver { Future _playTrack(Track currentTrack, Playback playback) async { try { if (currentTrack.id != _currentTrackId) { - if (currentTrack.uri != null) { + Uri? parsedUri = Uri.tryParse(currentTrack.uri!); + if (parsedUri != null && parsedUri.hasAbsolutePath) { await player .setAudioSource( - AudioSource.uri(Uri.parse(currentTrack.uri!)), + AudioSource.uri(parsedUri), preload: true, ) .then((value) async { From 8a1f3a3b94179b0e5c37ad00882dc0861af48268 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 8 Feb 2022 19:28:10 +0600 Subject: [PATCH 05/25] Fix wrong version download link of Mac OS binary --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a0aadedd..9448f095 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ $ flatpak install flathub com.github.KRTirtho.Spotube Download the [Spotube-linux-x86_64.AppImage](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-linux-x86_64.AppImage) file & double click to run it. AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed ## Mac OS -Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/download/v1.1.0/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard +Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard **I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey/homebrew stores or software centers or repositories** From f65ab228267ea37b73abd3de74b5bc8f34d640a0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 9 Feb 2022 09:18:07 +0600 Subject: [PATCH 06/25] README download links simplified with latest path --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9448f095..128b1d34 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ All the binaries are located in the [releases](https://github.com/krtirtho/spotu ## Windows -Download the [setup file](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-windows-x86_64-setup.exe) & follow along the installer +Download the [setup file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-windows-x86_64-setup.exe) & follow along the installer ### Chocolatey @@ -71,7 +71,7 @@ $ flatpak install flathub com.github.KRTirtho.Spotube
Download on Flathub ### Ubuntu/Debian/Linux Mint/Pop_!OS: - Download the [Spotube-linux-x86_64.deb](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-linux-x86_64.deb) then double click it or run + Download the [Spotube-linux-x86_64.deb](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.deb) then double click it or run ```bash $ sudo apt install Spotube-linux-x86_64.deb # or @@ -91,10 +91,10 @@ $ flatpak install flathub com.github.KRTirtho.Spotube ### AppImage: - Download the [Spotube-linux-x86_64.AppImage](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-linux-x86_64.AppImage) file & double click to run it. AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed + Download the [Spotube-linux-x86_64.AppImage](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-linux-x86_64.AppImage) file & double click to run it. AppImages require [appimage-launcher](https://github.com/TheAssassin/AppImageLauncher) to be installed ## Mac OS -Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/download/v1.2.0/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard +Download the [Mac OS Disk Image (.dmg) file](https://github.com/KRTirtho/spotube/releases/latest/download/Spotube-macos-x86_64.dmg) from the release & follow along the setup wizard **I'll/try to upload the package binaries to linux debian/arch/ubuntu/snap/flatpack/redhat/chocolatey/homebrew stores or software centers or repositories** From e6daba77bd6ec5f999d427719301a153a0f0d01f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 10 Feb 2022 21:37:46 +0600 Subject: [PATCH 07/25] Player._playTrack unsafe null modifier removed SpotifyDI was using ChangeNotifier as mixin & wasn't extending --- lib/components/Player/Player.dart | 2 +- lib/provider/SpotifyDI.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index a638298c..dd689c6f 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -138,7 +138,7 @@ class _PlayerState extends State with WidgetsBindingObserver { Future _playTrack(Track currentTrack, Playback playback) async { try { if (currentTrack.id != _currentTrackId) { - Uri? parsedUri = Uri.tryParse(currentTrack.uri!); + Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); if (parsedUri != null && parsedUri.hasAbsolutePath) { await player .setAudioSource( diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index fcec7a46..f8723440 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:spotify/spotify.dart'; -class SpotifyDI with ChangeNotifier { +class SpotifyDI extends ChangeNotifier { SpotifyApi _spotifyApi; SpotifyDI(this._spotifyApi); From 88b201b24b4d028af6012bf9dbc3bfaa4bc08ca8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 10 Feb 2022 22:28:48 +0600 Subject: [PATCH 08/25] using audio/mp4 for MacOS & iOS Platform for compatibilty --- lib/helpers/search-youtube.dart | 13 ++++++++++++- pubspec.lock | 2 +- pubspec.yaml | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/helpers/search-youtube.dart b/lib/helpers/search-youtube.dart index 007b1dbd..cedea1e6 100644 --- a/lib/helpers/search-youtube.dart +++ b/lib/helpers/search-youtube.dart @@ -1,3 +1,5 @@ +import 'dart:io'; +import 'package:collection/collection.dart'; import 'package:spotify/spotify.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; @@ -21,7 +23,16 @@ Future toYoutubeTrack(YoutubeExplode youtube, Track track) async { var trackManifest = await youtube.videos.streams.getManifest(ytVideo.id); - track.uri = trackManifest.audioOnly.withHighestBitrate().url.toString(); + // Since Mac OS's & IOS's CodeAudio doesn't support WebMedia + // ('audio/webm', 'video/webm' & 'image/webp') thus using 'audio/mpeg' + // codec/mimetype for those Platforms + track.uri = (Platform.isMacOS || Platform.isIOS + ? trackManifest.audioOnly + .where((info) => info.codec.mimeType == "audio/mp4") + .withHighestBitrate() + : trackManifest.audioOnly.withHighestBitrate()) + .url + .toString(); track.href = ytVideo.url; return track; } diff --git a/pubspec.lock b/pubspec.lock index 45c709eb..77b0ed0c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -121,7 +121,7 @@ packages: source: hosted version: "1.1.0" collection: - dependency: transitive + dependency: "direct main" description: name: collection url: "https://pub.dartlang.org" diff --git a/pubspec.yaml b/pubspec.yaml index 23e11b4f..49b1828a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: just_audio_libwinmedia: ^0.0.4 path: ^1.8.0 path_provider: ^2.0.8 + collection: ^1.15.0 dev_dependencies: flutter_test: From d05ec0099d9a9b09b3a30fef0fca896f4bb7a444 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 11 Feb 2022 10:44:16 +0600 Subject: [PATCH 09/25] flutter provider replaced with flutter_riverpod --- lib/components/Album/AlbumCard.dart | 10 +- lib/components/Album/AlbumView.dart | 10 +- lib/components/Artist/ArtistAlbumView.dart | 15 +- lib/components/Artist/ArtistProfile.dart | 10 +- lib/components/Category/CategoryCard.dart | 11 +- lib/components/Home.dart | 103 ++++----- lib/components/Library/UserArtists.dart | 16 +- lib/components/Library/UserPlaylists.dart | 10 +- lib/components/Login.dart | 187 ++++++++------- lib/components/Lyrics.dart | 12 +- lib/components/Player/Player.dart | 20 +- lib/components/Player/PlayerControls.dart | 8 +- lib/components/Playlist/PlaylistCard.dart | 14 +- .../Playlist/PlaylistGenreView.dart | 71 +++--- lib/components/Playlist/PlaylistView.dart | 125 +++++----- lib/components/Search/Search.dart | 12 +- lib/components/Settings.dart | 11 +- lib/components/Shared/TracksTableView.dart | 8 +- lib/helpers/oauth-login.dart | 6 +- lib/main.dart | 215 +++++++----------- lib/provider/Auth.dart | 3 + lib/provider/Playback.dart | 3 +- lib/provider/SpotifyDI.dart | 42 +++- lib/provider/UserPreferences.dart | 3 + pubspec.lock | 29 ++- pubspec.yaml | 2 +- 26 files changed, 472 insertions(+), 484 deletions(-) diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index eed381de..e5ea9423 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; @@ -9,13 +9,13 @@ import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class AlbumCard extends StatelessWidget { +class AlbumCard extends ConsumerWidget { final Album album; const AlbumCard(this.album, {Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - Playback playback = context.watch(); + Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); bool isPlaylistPlaying = playback.currentPlaylist != null && playback.currentPlaylist!.id == album.id; @@ -34,7 +34,7 @@ class AlbumCard extends StatelessWidget { )); }, onPlaybuttonPressed: () async { - SpotifyApi spotify = context.read().spotifyApi; + SpotifyApi spotify = ref.read(spotifyProvider); if (isPlaylistPlaying) return; List tracks = (await spotify.albums.getTracks(album.id!).all()) .map((track) => simpleTrackToTrack(track, album)) diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 88f74b7e..80b1b937 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; @@ -8,7 +8,7 @@ import 'package:spotube/helpers/simple-track-to-track.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class AlbumView extends StatelessWidget { +class AlbumView extends ConsumerWidget { final AlbumSimple album; const AlbumView(this.album, {Key? key}) : super(key: key); @@ -31,11 +31,11 @@ class AlbumView extends StatelessWidget { } @override - Widget build(BuildContext context) { - Playback playback = context.watch(); + Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; - SpotifyApi spotify = context.watch().spotifyApi; + SpotifyApi spotify = ref.watch(spotifyProvider); return Scaffold( body: FutureBuilder>( future: spotify.albums.getTracks(album.id!).all(), diff --git a/lib/components/Artist/ArtistAlbumView.dart b/lib/components/Artist/ArtistAlbumView.dart index 9f0906fd..fb3c150b 100644 --- a/lib/components/Artist/ArtistAlbumView.dart +++ b/lib/components/Artist/ArtistAlbumView.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart' hide Page; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:provider/provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class ArtistAlbumView extends StatefulWidget { +class ArtistAlbumView extends ConsumerStatefulWidget { final String artistId; final String artistName; const ArtistAlbumView( @@ -16,10 +16,10 @@ class ArtistAlbumView extends StatefulWidget { }) : super(key: key); @override - State createState() => _ArtistAlbumViewState(); + ConsumerState createState() => _ArtistAlbumViewState(); } -class _ArtistAlbumViewState extends State { +class _ArtistAlbumViewState extends ConsumerState { final PagingController _pagingController = PagingController(firstPageKey: 0); @@ -39,10 +39,9 @@ class _ArtistAlbumViewState extends State { _fetchPage(int pageKey) async { try { - SpotifyDI data = context.read(); - Page albums = await data.spotifyApi.artists - .albums(widget.artistId) - .getPage(8, pageKey); + SpotifyApi spotifyApi = ref.watch(spotifyProvider); + Page albums = + await spotifyApi.artists.albums(widget.artistId).getPage(8, pageKey); var items = albums.items!.toList(); diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 5eb839c2..ce5ea13f 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -1,7 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; @@ -14,7 +14,7 @@ import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class ArtistProfile extends StatefulWidget { +class ArtistProfile extends ConsumerStatefulWidget { final String artistId; const ArtistProfile(this.artistId, {Key? key}) : super(key: key); @@ -22,10 +22,10 @@ class ArtistProfile extends StatefulWidget { _ArtistProfileState createState() => _ArtistProfileState(); } -class _ArtistProfileState extends State { +class _ArtistProfileState extends ConsumerState { @override Widget build(BuildContext context) { - SpotifyApi spotify = context.watch().spotifyApi; + SpotifyApi spotify = ref.watch(spotifyProvider); return Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), @@ -134,7 +134,7 @@ class _ArtistProfileState extends State { return const Center( child: CircularProgressIndicator.adaptive()); } - Playback playback = context.watch(); + Playback playback = ref.watch(playbackProvider); var isPlaylistPlaying = playback.currentPlaylist?.id == snapshot.data?.id; playPlaylist(List tracks, {Track? currentTrack}) { diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 1788fffc..06cb0d7b 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart' hide Page; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; @@ -51,14 +51,15 @@ class _CategoryCardState extends State { ], ), ), - Consumer( - builder: (context, data, child) { + Consumer( + builder: (context, ref, child) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); return FutureBuilder>( future: widget.playlists == null ? (widget.category.id != "user-featured-playlists" - ? data.spotifyApi.playlists + ? spotifyApi.playlists .getByCategoryId(widget.category.id!) - : data.spotifyApi.playlists.featured) + : spotifyApi.playlists.featured) .getPage(4, 0) .then((value) => value.items ?? []) : Future.value(widget.playlists), diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 1a4be44d..200433f3 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -3,8 +3,8 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart' hide Page; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:oauth2/oauth2.dart' show AuthorizationException; import 'package:spotify/spotify.dart' hide Image, Player, Search; @@ -33,14 +33,14 @@ List spotifyScopes = [ "playlist-read-collaborative" ]; -class Home extends StatefulWidget { +class Home extends ConsumerStatefulWidget { const Home({Key? key}) : super(key: key); @override _HomeState createState() => _HomeState(); } -class _HomeState extends State { +class _HomeState extends ConsumerState { final PagingController _pagingController = PagingController(firstPageKey: 0); @@ -63,7 +63,7 @@ class _HomeState extends State { DateTime? expiration = expirationStr != null ? DateTime.parse(expirationStr) : null; try { - Auth authProvider = context.read(); + Auth auth = ref.read(authProvider); if (clientId != null && clientSecret != null) { SpotifyApi spotifyApi = SpotifyApi( @@ -78,7 +78,7 @@ class _HomeState extends State { ); SpotifyApiCredentials credentials = await spotifyApi.getCredentials(); if (credentials.accessToken?.isNotEmpty ?? false) { - authProvider.setAuthState( + auth.setAuthState( clientId: clientId, clientSecret: clientSecret, accessToken: @@ -91,8 +91,8 @@ class _HomeState extends State { } _pagingController.addPageRequestListener((pageKey) async { try { - SpotifyDI data = context.read(); - Page categories = await data.spotifyApi.categories + SpotifyApi spotifyApi = ref.read(spotifyProvider); + Page categories = await spotifyApi.categories .list(country: "US") .getPage(15, pageKey); @@ -113,10 +113,10 @@ class _HomeState extends State { _pagingController.error = e; } }); - } on AuthorizationException catch (e) { + } on AuthorizationException catch (_) { if (clientId != null && clientSecret != null) { oauthLogin( - context, + ref.read(authProvider), clientId: clientId, clientSecret: clientSecret, ); @@ -136,8 +136,9 @@ class _HomeState extends State { @override Widget build(BuildContext context) { - Auth authProvider = Provider.of(context); - if (!authProvider.isLoggedIn) { + Auth auth = ref.watch(authProvider); + SpotifyApi spotify = ref.watch(spotifyProvider); + if (!auth.isLoggedIn) { return const Login(); } @@ -199,49 +200,45 @@ class _HomeState extends State { style: Theme.of(context).textTheme.headline4), ]), ), - trailing: - Consumer(builder: (context, data, widget) { - return FutureBuilder( - future: data.spotifyApi.me.get(), - builder: (context, snapshot) { - var avatarImg = imageToUrlString(snapshot.data?.images, - index: (snapshot.data?.images?.length ?? 1) - 1); - return Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar( - backgroundImage: - CachedNetworkImageProvider(avatarImg), + trailing: FutureBuilder( + future: spotify.me.get(), + builder: (context, snapshot) { + var avatarImg = imageToUrlString(snapshot.data?.images, + index: (snapshot.data?.images?.length ?? 1) - 1); + return Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CircleAvatar( + backgroundImage: + CachedNetworkImageProvider(avatarImg), + ), + const SizedBox(width: 10), + Text( + snapshot.data?.displayName ?? "User's name", + style: const TextStyle( + fontWeight: FontWeight.bold, ), - const SizedBox(width: 10), - Text( - snapshot.data?.displayName ?? "User's name", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.settings_outlined), - onPressed: () { - Navigator.of(context) - .push(MaterialPageRoute( - builder: (context) { - return const Settings(); - }, - )); - }), - ], - ), - ); - }, - ); - }), + ), + ], + ), + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: () { + Navigator.of(context).push(MaterialPageRoute( + builder: (context) { + return const Settings(); + }, + )); + }), + ], + ), + ); + }, + ), ), // contents of the spotify if (_selectedIndex == 0) diff --git a/lib/components/Library/UserArtists.dart b/lib/components/Library/UserArtists.dart index 60291c33..17fae18e 100644 --- a/lib/components/Library/UserArtists.dart +++ b/lib/components/Library/UserArtists.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:provider/provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class UserArtists extends StatefulWidget { +class UserArtists extends ConsumerStatefulWidget { const UserArtists({Key? key}) : super(key: key); @override - State createState() => _UserArtistsState(); + ConsumerState createState() => _UserArtistsState(); } -class _UserArtistsState extends State { +class _UserArtistsState extends ConsumerState { final PagingController _pagingController = PagingController(firstPageKey: ""); @@ -22,8 +22,8 @@ class _UserArtistsState extends State { WidgetsBinding.instance?.addPostFrameCallback((timestamp) { _pagingController.addPageRequestListener((pageKey) async { try { - SpotifyDI data = context.read(); - CursorPage artists = await data.spotifyApi.me + SpotifyApi spotifyApi = ref.read(spotifyProvider); + CursorPage artists = await spotifyApi.me .following(FollowingType.artist) .getPage(15, pageKey); @@ -51,10 +51,10 @@ class _UserArtistsState extends State { @override Widget build(BuildContext context) { - SpotifyDI data = context.watch(); + SpotifyApi spotifyApi = ref.watch(spotifyProvider); return FutureBuilder>( - future: data.spotifyApi.me.following(FollowingType.artist).first(), + future: spotifyApi.me.following(FollowingType.artist).first(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator.adaptive()); diff --git a/lib/components/Library/UserPlaylists.dart b/lib/components/Library/UserPlaylists.dart index dbe37fbe..ce6076c0 100644 --- a/lib/components/Library/UserPlaylists.dart +++ b/lib/components/Library/UserPlaylists.dart @@ -1,18 +1,18 @@ import 'package:flutter/material.dart' hide Image; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class UserPlaylists extends StatelessWidget { +class UserPlaylists extends ConsumerWidget { const UserPlaylists({Key? key}) : super(key: key); @override - Widget build(BuildContext context) { - SpotifyDI data = context.watch(); + Widget build(BuildContext context, ref) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); return FutureBuilder>( - future: data.spotifyApi.playlists.me.all(), + future: spotifyApi.playlists.me.all(), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator.adaptive()); diff --git a/lib/components/Login.dart b/lib/components/Login.dart index e7bb4427..3c144baa 100644 --- a/lib/components/Login.dart +++ b/lib/components/Login.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -8,14 +8,14 @@ import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Login extends StatefulWidget { +class Login extends ConsumerStatefulWidget { const Login({Key? key}) : super(key: key); @override _LoginState createState() => _LoginState(); } -class _LoginState extends State { +class _LoginState extends ConsumerState { String clientId = ""; String clientSecret = ""; String accessToken = ""; @@ -28,7 +28,8 @@ class _LoginState extends State { _fieldError = true; }); } - await oauthLogin(context, clientId: clientId, clientSecret: clientSecret); + await oauthLogin(ref.read(authProvider), + clientId: clientId, clientSecret: clientSecret); } catch (e) { print("[Login.handleLogin] $e"); } @@ -36,99 +37,95 @@ class _LoginState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, authState, child) { - return Scaffold( - appBar: const PageWindowTitleBar(), - body: SingleChildScrollView( - child: Center( - child: Column( - children: [ - Image.asset( - "assets/spotube-logo.png", - width: 400, - height: 400, - ), - Text("Add your spotify credentials to get started", - style: Theme.of(context).textTheme.headline4), - const Text( - "Don't worry, any of your credentials won't be collected or shared with anyone"), - const Hyperlink("How to get these client-id & client-secret?", - "https://github.com/KRTirtho/spotube#configuration"), - const SizedBox( - height: 10, - ), - Container( - constraints: const BoxConstraints( - maxWidth: 400, - ), - child: Column( - children: [ - TextField( - decoration: const InputDecoration( - hintText: "Spotify Client ID", - label: Text("ClientID"), - ), - onChanged: (value) { - setState(() { - clientId = value; - }); - }, - ), - const SizedBox(height: 10), - TextField( - decoration: const InputDecoration( - hintText: "Spotify Client Secret", - label: Text("Client Secret"), - ), - onChanged: (value) { - setState(() { - clientSecret = value; - }); - }, - ), - const SizedBox(height: 10), - const Divider(color: Colors.grey), - const SizedBox(height: 10), - TextField( - decoration: const InputDecoration( - label: Text("Genius Access Token (optional)"), - ), - onChanged: (value) { - setState(() { - accessToken = value; - }); - }, - ), - const SizedBox( - height: 10, - ), - ElevatedButton( - onPressed: () async { - await handleLogin(authState); - UserPreferences preferences = - context.read(); - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - preferences.setGeniusAccessToken(accessToken); - await localStorage.setString( - LocalStorageKeys.geniusAccessToken, - accessToken); - setState(() { - accessToken = ""; - }); - }, - child: const Text("Submit"), - ) - ], - ), - ), - ], + Auth authState = ref.watch(authProvider); + return Scaffold( + appBar: const PageWindowTitleBar(), + body: SingleChildScrollView( + child: Center( + child: Column( + children: [ + Image.asset( + "assets/spotube-logo.png", + width: 400, + height: 400, ), - ), + Text("Add your spotify credentials to get started", + style: Theme.of(context).textTheme.headline4), + const Text( + "Don't worry, any of your credentials won't be collected or shared with anyone"), + const Hyperlink("How to get these client-id & client-secret?", + "https://github.com/KRTirtho/spotube#configuration"), + const SizedBox( + height: 10, + ), + Container( + constraints: const BoxConstraints( + maxWidth: 400, + ), + child: Column( + children: [ + TextField( + decoration: const InputDecoration( + hintText: "Spotify Client ID", + label: Text("ClientID"), + ), + onChanged: (value) { + setState(() { + clientId = value; + }); + }, + ), + const SizedBox(height: 10), + TextField( + decoration: const InputDecoration( + hintText: "Spotify Client Secret", + label: Text("Client Secret"), + ), + onChanged: (value) { + setState(() { + clientSecret = value; + }); + }, + ), + const SizedBox(height: 10), + const Divider(color: Colors.grey), + const SizedBox(height: 10), + TextField( + decoration: const InputDecoration( + label: Text("Genius Access Token (optional)"), + ), + onChanged: (value) { + setState(() { + accessToken = value; + }); + }, + ), + const SizedBox( + height: 10, + ), + ElevatedButton( + onPressed: () async { + await handleLogin(authState); + UserPreferences preferences = + ref.read(userPreferencesProvider); + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + preferences.setGeniusAccessToken(accessToken); + await localStorage.setString( + LocalStorageKeys.geniusAccessToken, accessToken); + setState(() { + accessToken = ""; + }); + }, + child: const Text("Submit"), + ) + ], + ), + ), + ], ), - ); - }, + ), + ), ); } } diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index dcc5b19a..cd09c82b 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Settings.dart'; import 'package:spotube/helpers/artist-to-string.dart'; @@ -7,20 +7,20 @@ import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Lyrics extends StatefulWidget { +class Lyrics extends ConsumerStatefulWidget { const Lyrics({Key? key}) : super(key: key); @override - State createState() => _LyricsState(); + ConsumerState createState() => _LyricsState(); } -class _LyricsState extends State { +class _LyricsState extends ConsumerState { Map _lyrics = {}; @override Widget build(BuildContext context) { - Playback playback = context.watch(); - UserPreferences userPreferences = context.watch(); + Playback playback = ref.watch(playbackProvider); + UserPreferences userPreferences = ref.watch(userPreferencesProvider); bool hasToken = (userPreferences.geniusAccessToken != null || (userPreferences.geniusAccessToken?.isNotEmpty ?? false)); diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index dd689c6f..33430389 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; @@ -10,18 +11,17 @@ import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; -class Player extends StatefulWidget { +class Player extends ConsumerStatefulWidget { const Player({Key? key}) : super(key: key); @override _PlayerState createState() => _PlayerState(); } -class _PlayerState extends State with WidgetsBindingObserver { +class _PlayerState extends ConsumerState with WidgetsBindingObserver { late AudioPlayer player; bool _isPlaying = false; bool _shuffled = false; @@ -111,7 +111,7 @@ class _PlayerState extends State with WidgetsBindingObserver { } void _movePlaylistPositionBy(int pos) { - Playback playback = context.read(); + Playback playback = ref.read(playbackProvider); if (playback.currentTrack != null && playback.currentPlaylist != null) { int index = playback.currentPlaylist!.trackIds .indexOf(playback.currentTrack!.id!) + @@ -198,8 +198,9 @@ class _PlayerState extends State with WidgetsBindingObserver { Widget build(BuildContext context) { return Container( color: Theme.of(context).backgroundColor, - child: Consumer( - builder: (context, playback, widget) { + child: Consumer( + builder: (context, ref, widget) { + Playback playback = ref.watch(playbackProvider); if (playback.currentPlaylist != null && playback.currentTrack != null) { _playTrack(playback.currentTrack!, playback); @@ -348,10 +349,11 @@ class _PlayerState extends State with WidgetsBindingObserver { DownloadTrackButton( track: playback.currentTrack, ), - Consumer(builder: (context, data, widget) { + Consumer(builder: (context, ref, widget) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); return FutureBuilder( future: playback.currentTrack?.id != null - ? data.spotifyApi.tracks.me + ? spotifyApi.tracks.me .containsOne(playback.currentTrack!.id!) : Future.value(false), initialData: false, @@ -367,7 +369,7 @@ class _PlayerState extends State with WidgetsBindingObserver { onPressed: () { if (!isLiked && playback.currentTrack?.id != null) { - data.spotifyApi.tracks.me + spotifyApi.tracks.me .saveOne( playback.currentTrack!.id!) .then((value) => setState(() {})); diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index adcefee9..38dc849e 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -1,13 +1,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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 { +class PlayerControls extends ConsumerStatefulWidget { final Stream positionStream; final bool isPlaying; final Duration duration; @@ -38,7 +38,7 @@ class PlayerControls extends StatefulWidget { _PlayerControlsState createState() => _PlayerControlsState(); } -class _PlayerControlsState extends State { +class _PlayerControlsState extends ConsumerState { StreamSubscription? _timePositionListener; late List _hotKeys = []; @@ -88,7 +88,7 @@ class _PlayerControlsState extends State { @override Widget build(BuildContext context) { - UserPreferences preferences = context.watch(); + UserPreferences preferences = ref.watch(userPreferencesProvider); _configureHotKeys(preferences); return Container( diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index c79052dc..9995b0fd 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; @@ -7,17 +7,17 @@ import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistCard extends StatefulWidget { +class PlaylistCard extends ConsumerStatefulWidget { final PlaylistSimple playlist; const PlaylistCard(this.playlist, {Key? key}) : super(key: key); @override _PlaylistCardState createState() => _PlaylistCardState(); } -class _PlaylistCardState extends State { +class _PlaylistCardState extends ConsumerState { @override Widget build(BuildContext context) { - Playback playback = context.watch(); + Playback playback = ref.watch(playbackProvider); bool isPlaylistPlaying = playback.currentPlaylist != null && playback.currentPlaylist!.id == widget.playlist.id; return PlaybuttonCard( @@ -33,13 +33,13 @@ class _PlaylistCardState extends State { }, onPlaybuttonPressed: () async { if (isPlaylistPlaying) return; - SpotifyDI data = context.read(); + SpotifyApi spotifyApi = ref.read(spotifyProvider); List tracks = (widget.playlist.id != "user-liked-tracks" - ? await data.spotifyApi.playlists + ? await spotifyApi.playlists .getTracksByPlaylistId(widget.playlist.id!) .all() - : await data.spotifyApi.tracks.me.saved + : await spotifyApi.tracks.me.saved .all() .then((tracks) => tracks.map((e) => e.track!))) .toList(); diff --git a/lib/components/Playlist/PlaylistGenreView.dart b/lib/components/Playlist/PlaylistGenreView.dart index 44597c60..4497ae7b 100644 --- a/lib/components/Playlist/PlaylistGenreView.dart +++ b/lib/components/Playlist/PlaylistGenreView.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; @@ -33,39 +33,42 @@ class _PlaylistGenreViewState extends State { style: Theme.of(context).textTheme.headline4, textAlign: TextAlign.center, ), - Consumer( - builder: (context, data, child) => Expanded( - child: SingleChildScrollView( - child: FutureBuilder>( - future: widget.playlists == null - ? (widget.genreId != "user-featured-playlists" - ? data.spotifyApi.playlists - .getByCategoryId(widget.genreId) - .all() - : data.spotifyApi.playlists.featured.all()) - : Future.value(widget.playlists), - builder: (context, snapshot) { - if (snapshot.hasError) { - return const Center(child: Text("Error occurred")); - } - if (!snapshot.hasData) { - return const CircularProgressIndicator.adaptive(); - } - return Center( - child: Wrap( - children: snapshot.data! - .map( - (playlist) => Padding( - padding: const EdgeInsets.all(8.0), - child: PlaylistCard(playlist), - ), - ) - .toList(), - ), - ); - }), - ), - ), + Consumer( + builder: (context, ref, child) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); + return Expanded( + child: SingleChildScrollView( + child: FutureBuilder>( + future: widget.playlists == null + ? (widget.genreId != "user-featured-playlists" + ? spotifyApi.playlists + .getByCategoryId(widget.genreId) + .all() + : spotifyApi.playlists.featured.all()) + : Future.value(widget.playlists), + builder: (context, snapshot) { + if (snapshot.hasError) { + return const Center(child: Text("Error occurred")); + } + if (!snapshot.hasData) { + return const CircularProgressIndicator.adaptive(); + } + return Center( + child: Wrap( + children: snapshot.data! + .map( + (playlist) => Padding( + padding: const EdgeInsets.all(8.0), + child: PlaylistCard(playlist), + ), + ) + .toList(), + ), + ); + }), + ), + ); + }, ) ], ), diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index 524a251f..5f9ef5a6 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -1,20 +1,20 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; 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/provider/Playback.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistView extends StatefulWidget { +class PlaylistView extends ConsumerStatefulWidget { final PlaylistSimple playlist; const PlaylistView(this.playlist, {Key? key}) : super(key: key); @override _PlaylistViewState createState() => _PlaylistViewState(); } -class _PlaylistViewState extends State { +class _PlaylistViewState extends ConsumerState { playPlaylist(Playback playback, List tracks, {Track? currentTrack}) { currentTrack ??= tracks.first; var isPlaylistPlaying = playback.currentPlaylist?.id != null && @@ -36,71 +36,70 @@ class _PlaylistViewState extends State { @override Widget build(BuildContext context) { - Playback playback = context.watch(); + Playback playback = ref.watch(playbackProvider); + SpotifyApi spotifyApi = ref.watch(spotifyProvider); var isPlaylistPlaying = playback.currentPlaylist?.id != null && playback.currentPlaylist?.id == widget.playlist.id; - return Consumer(builder: (_, data, __) { - return Scaffold( - body: FutureBuilder>( - future: widget.playlist.id != "user-liked-tracks" - ? data.spotifyApi.playlists - .getTracksByPlaylistId(widget.playlist.id) - .all() - : data.spotifyApi.tracks.me.saved - .all() - .then((tracks) => tracks.map((e) => e.track!)), - builder: (context, snapshot) { - List tracks = snapshot.data?.toList() ?? []; - return Column( - children: [ - PageWindowTitleBar( - leading: Row( - children: [ - // nav back - const BackButton(), - // heart playlist - IconButton( - icon: const Icon(Icons.favorite_outline_rounded), - onPressed: () {}, + return Scaffold( + body: FutureBuilder>( + future: widget.playlist.id != "user-liked-tracks" + ? spotifyApi.playlists + .getTracksByPlaylistId(widget.playlist.id) + .all() + : spotifyApi.tracks.me.saved + .all() + .then((tracks) => tracks.map((e) => e.track!)), + builder: (context, snapshot) { + List tracks = snapshot.data?.toList() ?? []; + return Column( + children: [ + PageWindowTitleBar( + leading: Row( + children: [ + // nav back + const BackButton(), + // heart playlist + IconButton( + icon: const Icon(Icons.favorite_outline_rounded), + onPressed: () {}, + ), + // play playlist + IconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, ), - // play playlist - IconButton( - icon: Icon( - isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded, - ), - onPressed: snapshot.hasData - ? () => playPlaylist(playback, tracks) - : null, - ) - ], - ), + onPressed: snapshot.hasData + ? () => playPlaylist(playback, tracks) + : null, + ) + ], ), - Center( - child: Text(widget.playlist.name!, - style: Theme.of(context).textTheme.headline4), - ), - snapshot.hasError - ? const Center(child: Text("Error occurred")) - : !snapshot.hasData - ? const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive()), - ) - : TracksTableView( + ), + Center( + child: Text(widget.playlist.name!, + style: Theme.of(context).textTheme.headline4), + ), + snapshot.hasError + ? const Center(child: Text("Error occurred")) + : !snapshot.hasData + ? const Expanded( + child: Center( + child: CircularProgressIndicator.adaptive()), + ) + : TracksTableView( + tracks, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + playback, tracks, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - playback, - tracks, - currentTrack: currentTrack, - ), + currentTrack: currentTrack, ), - ], - ); - }), - ); - }); + ), + ], + ); + }), + ); } } diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 10649e1b..e13dd643 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart' hide Page; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; @@ -11,14 +11,14 @@ import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class Search extends StatefulWidget { +class Search extends ConsumerStatefulWidget { const Search({Key? key}) : super(key: key); @override - State createState() => _SearchState(); + ConsumerState createState() => _SearchState(); } -class _SearchState extends State { +class _SearchState extends ConsumerState { late TextEditingController _controller; String searchTerm = ""; @@ -31,7 +31,7 @@ class _SearchState extends State { @override Widget build(BuildContext context) { - SpotifyApi spotify = context.watch().spotifyApi; + SpotifyApi spotify = ref.watch(spotifyProvider); return Expanded( child: Column( @@ -80,7 +80,7 @@ class _SearchState extends State { } else if (!snapshot.hasData && searchTerm.isEmpty) { return Container(); } - Playback playback = context.watch(); + Playback playback = ref.watch(playbackProvider); List albums = []; List artists = []; List tracks = []; diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 705e2059..057dfe4d 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; @@ -10,14 +9,14 @@ import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Settings extends StatefulWidget { +class Settings extends ConsumerStatefulWidget { const Settings({Key? key}) : super(key: key); @override _SettingsState createState() => _SettingsState(); } -class _SettingsState extends State { +class _SettingsState extends ConsumerState { TextEditingController? _textEditingController; String? _geniusAccessToken; @@ -40,7 +39,7 @@ class _SettingsState extends State { @override Widget build(BuildContext context) { - UserPreferences preferences = context.watch(); + UserPreferences preferences = ref.watch(userPreferencesProvider); return Scaffold( appBar: PageWindowTitleBar( @@ -151,7 +150,7 @@ class _SettingsState extends State { ), const SizedBox(height: 10), Builder(builder: (context) { - var auth = context.read(); + Auth auth = ref.watch(authProvider); return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index 6cacc577..9ae45a14 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -1,6 +1,6 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/LinkText.dart'; @@ -9,7 +9,7 @@ import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/provider/Playback.dart'; -class TracksTableView extends StatelessWidget { +class TracksTableView extends ConsumerWidget { final void Function(Track currentTrack)? onTrackPlayButtonPressed; final List tracks; const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed}) @@ -97,8 +97,8 @@ class TracksTableView extends StatelessWidget { } @override - Widget build(BuildContext context) { - Playback playback = context.watch(); + Widget build(context, ref) { + Playback playback = ref.watch(playbackProvider); TextStyle tableHeadStyle = const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); return Expanded( diff --git a/lib/helpers/oauth-login.dart b/lib/helpers/oauth-login.dart index e9bdbeb7..4ff3fb2d 100644 --- a/lib/helpers/oauth-login.dart +++ b/lib/helpers/oauth-login.dart @@ -1,5 +1,3 @@ -import 'package:flutter/cupertino.dart'; -import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Home.dart'; @@ -9,7 +7,7 @@ import 'package:spotube/provider/Auth.dart'; const redirectUri = "http://localhost:4304/auth/spotify/callback"; -Future oauthLogin(BuildContext context, +Future oauthLogin(Auth auth, {required String clientId, required String clientSecret}) async { try { String? accessToken; @@ -50,7 +48,7 @@ Future oauthLogin(BuildContext context, clientSecret, ); - Provider.of(context, listen: false).setAuthState( + auth.setAuthState( clientId: clientId, clientSecret: clientSecret, accessToken: accessToken, diff --git a/lib/main.dart b/lib/main.dart index f1bb3faa..575bc93e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,20 +1,15 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; -import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotify/spotify.dart'; import 'package:spotube/components/Home.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; -import 'package:spotube/provider/Auth.dart'; -import 'package:spotube/provider/Playback.dart'; -import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/UserPreferences.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await hotKeyManager.unregisterAll(); - runApp(MyApp()); + runApp(const ProviderScope(child: MyApp())); doWhenWindowReady(() { appWindow.minSize = const Size(900, 700); appWindow.size = const Size(900, 700); @@ -25,6 +20,8 @@ void main() async { } class MyApp extends StatefulWidget { + const MyApp({Key? key}) : super(key: key); + static _MyAppState? of(BuildContext context) => context.findAncestorStateOfType<_MyAppState>(); @override @@ -72,139 +69,95 @@ class _MyAppState extends State { @override Widget build(BuildContext context) { - return MultiProvider( - providers: [ - ChangeNotifierProvider(create: (context) => Auth()), - ChangeNotifierProvider(create: (context) { - Auth authState = Provider.of(context, listen: false); - return SpotifyDI( - SpotifyApi( - SpotifyApiCredentials( - authState.clientId, - authState.clientSecret, - accessToken: authState.accessToken, - refreshToken: authState.refreshToken, - expiration: authState.expiration, - scopes: spotifyScopes, - ), - onCredentialsRefreshed: (credentials) async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - localStorage.setString( - LocalStorageKeys.refreshToken, - credentials.refreshToken!, - ); - localStorage.setString( - LocalStorageKeys.accessToken, - credentials.accessToken!, - ); - localStorage.setString( - LocalStorageKeys.clientId, credentials.clientId!); - localStorage.setString( - LocalStorageKeys.clientSecret, - credentials.clientSecret!, - ); - }, - ), - ); - }), - ChangeNotifierProvider(create: (context) => Playback()), - ChangeNotifierProvider( - create: (context) { - return UserPreferences(); - }, + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Spotube', + theme: ThemeData( + primaryColor: Colors.green, + primarySwatch: Colors.green, + buttonTheme: const ButtonThemeData( + buttonColor: Colors.green, ), - ], - child: MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Spotube', - theme: ThemeData( - primaryColor: Colors.green, - primarySwatch: Colors.green, - buttonTheme: const ButtonThemeData( - buttonColor: Colors.green, - ), - shadowColor: Colors.grey[300], - backgroundColor: Colors.white, - textTheme: TextTheme( - bodyText1: TextStyle(color: Colors.grey[850]), - headline1: TextStyle(color: Colors.grey[850]), - headline2: TextStyle(color: Colors.grey[850]), - headline3: TextStyle(color: Colors.grey[850]), - headline4: TextStyle(color: Colors.grey[850]), - headline5: TextStyle(color: Colors.grey[850]), - headline6: TextStyle(color: Colors.grey[850]), - ), - listTileTheme: ListTileThemeData( - iconColor: Colors.grey[850], - horizontalTitleGap: 0, - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.green[400]!, - width: 2.0, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey[800]!, - ), + shadowColor: Colors.grey[300], + backgroundColor: Colors.white, + textTheme: TextTheme( + bodyText1: TextStyle(color: Colors.grey[850]), + headline1: TextStyle(color: Colors.grey[850]), + headline2: TextStyle(color: Colors.grey[850]), + headline3: TextStyle(color: Colors.grey[850]), + headline4: TextStyle(color: Colors.grey[850]), + headline5: TextStyle(color: Colors.grey[850]), + headline6: TextStyle(color: Colors.grey[850]), + ), + listTileTheme: ListTileThemeData( + iconColor: Colors.grey[850], + horizontalTitleGap: 0, + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green[400]!, + width: 2.0, ), ), - navigationRailTheme: NavigationRailThemeData( - backgroundColor: Colors.blueGrey[50], - unselectedIconTheme: - IconThemeData(color: Colors.grey[850], opacity: 1), - unselectedLabelTextStyle: TextStyle( - color: Colors.grey[850], + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey[800]!, ), ), - cardTheme: CardTheme( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - color: Colors.white, - ), ), - darkTheme: ThemeData( - brightness: Brightness.dark, - primaryColor: Colors.green, - primarySwatch: Colors.green, - backgroundColor: Colors.blueGrey[900], - scaffoldBackgroundColor: Colors.blueGrey[900], - dialogBackgroundColor: Colors.blueGrey[800], - shadowColor: Colors.black26, - buttonTheme: const ButtonThemeData( - buttonColor: Colors.green, + navigationRailTheme: NavigationRailThemeData( + backgroundColor: Colors.blueGrey[50], + unselectedIconTheme: + IconThemeData(color: Colors.grey[850], opacity: 1), + unselectedLabelTextStyle: TextStyle( + color: Colors.grey[850], ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.green[400]!, - width: 2.0, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.grey[800]!, - ), - ), - ), - navigationRailTheme: NavigationRailThemeData( - backgroundColor: Colors.blueGrey[800], - unselectedIconTheme: const IconThemeData(opacity: 1), - ), - cardTheme: CardTheme( - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), - color: Colors.blueGrey[900], - elevation: 20, - ), - canvasColor: Colors.blueGrey[900], ), - themeMode: _themeMode, - home: const Home(), + cardTheme: CardTheme( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + color: Colors.white, + ), ), + darkTheme: ThemeData( + brightness: Brightness.dark, + primaryColor: Colors.green, + primarySwatch: Colors.green, + backgroundColor: Colors.blueGrey[900], + scaffoldBackgroundColor: Colors.blueGrey[900], + dialogBackgroundColor: Colors.blueGrey[800], + shadowColor: Colors.black26, + buttonTheme: const ButtonThemeData( + buttonColor: Colors.green, + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.green[400]!, + width: 2.0, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Colors.grey[800]!, + ), + ), + ), + navigationRailTheme: NavigationRailThemeData( + backgroundColor: Colors.blueGrey[800], + unselectedIconTheme: const IconThemeData(opacity: 1), + ), + cardTheme: CardTheme( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + color: Colors.blueGrey[900], + elevation: 20, + ), + canvasColor: Colors.blueGrey[900], + ), + themeMode: _themeMode, + home: const Home(), ); } } diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index 5249b891..d5e69dc2 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; class Auth with ChangeNotifier { String? _clientId; @@ -52,3 +53,5 @@ class Auth with ChangeNotifier { notifyListeners(); } } + +var authProvider = ChangeNotifierProvider((ref) => Auth()); diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index fb20d153..b8978555 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; class CurrentPlaylist { @@ -77,4 +78,4 @@ class Playback extends ChangeNotifier { } } -var x = Playback(); +var playbackProvider = ChangeNotifierProvider((_) => Playback()); diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index f8723440..acfe96e2 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -1,10 +1,36 @@ -import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Home.dart'; +import 'package:spotube/models/LocalStorageKeys.dart'; +import 'package:spotube/provider/Auth.dart'; -class SpotifyDI extends ChangeNotifier { - SpotifyApi _spotifyApi; - - SpotifyDI(this._spotifyApi); - - SpotifyApi get spotifyApi => _spotifyApi; -} +var spotifyProvider = Provider((ref) { + Auth authState = ref.watch(authProvider); + return SpotifyApi( + SpotifyApiCredentials( + authState.clientId, + authState.clientSecret, + accessToken: authState.accessToken, + refreshToken: authState.refreshToken, + expiration: authState.expiration, + scopes: spotifyScopes, + ), + onCredentialsRefreshed: (credentials) async { + SharedPreferences localStorage = await SharedPreferences.getInstance(); + localStorage.setString( + LocalStorageKeys.refreshToken, + credentials.refreshToken!, + ); + localStorage.setString( + LocalStorageKeys.accessToken, + credentials.accessToken!, + ); + localStorage.setString(LocalStorageKeys.clientId, credentials.clientId!); + localStorage.setString( + LocalStorageKeys.clientSecret, + credentials.clientSecret!, + ); + }, + ); +}); diff --git a/lib/provider/UserPreferences.dart b/lib/provider/UserPreferences.dart index 2fc6543c..7e881377 100644 --- a/lib/provider/UserPreferences.dart +++ b/lib/provider/UserPreferences.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; @@ -110,3 +111,5 @@ class UserPreferences extends ChangeNotifier { notifyListeners(); } } + +var userPreferencesProvider = ChangeNotifierProvider((_) => UserPreferences()); diff --git a/pubspec.lock b/pubspec.lock index 77b0ed0c..dcccce79 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -195,6 +195,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.4" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" flutter_test: dependency: "direct dev" description: flutter @@ -338,13 +345,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.8.1" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0" oauth2: dependency: transitive description: @@ -457,13 +457,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" - provider: - dependency: "direct main" + riverpod: + dependency: transitive description: - name: provider + name: riverpod url: "https://pub.dartlang.org" source: hosted - version: "6.0.1" + version: "1.0.3" rxdart: dependency: transitive description: @@ -574,6 +574,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.10.0" + state_notifier: + dependency: transitive + description: + name: state_notifier + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.2+1" stream_channel: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 49b1828a..819d2389 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,7 +37,6 @@ dependencies: cached_network_image: ^3.2.0 html: ^0.15.0 http: ^0.13.4 - provider: ^6.0.1 shared_preferences: ^2.0.11 spotify: ^0.6.0 url_launcher: ^6.0.17 @@ -50,6 +49,7 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.8 collection: ^1.15.0 + flutter_riverpod: ^1.0.3 dev_dependencies: flutter_test: From 9fc155c000c4e100a24c402461defe17b6bba5c3 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 11 Feb 2022 19:29:31 +0600 Subject: [PATCH 10/25] hook support added to most of the components --- lib/components/Artist/ArtistProfile.dart | 15 +- lib/components/Category/CategoryCard.dart | 24 ++- lib/components/Library/UserLibrary.dart | 8 +- lib/components/Login.dart | 72 ++++----- lib/components/Lyrics.dart | 69 +++++---- lib/components/Player/Player.dart | 1 - lib/components/Player/PlayerControls.dart | 71 ++++----- lib/components/Playlist/PlaylistCard.dart | 29 ++-- .../Playlist/PlaylistGenreView.dart | 20 +-- lib/components/Playlist/PlaylistView.dart | 26 ++-- lib/components/Search/Search.dart | 42 ++--- lib/components/Settings.dart | 61 +++----- lib/components/Shared/AnchorButton.dart | 35 ++--- .../Shared/DownloadTrackButton.dart | 146 ++++++++---------- lib/components/Shared/RecordHotKeyDialog.dart | 19 +-- lib/main.dart | 58 ++----- lib/provider/ThemeProvider.dart | 6 + pubspec.lock | 14 ++ pubspec.yaml | 2 + 19 files changed, 297 insertions(+), 421 deletions(-) create mode 100644 lib/provider/ThemeProvider.dart diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index ce5ea13f..a947c37b 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -14,24 +14,19 @@ import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class ArtistProfile extends ConsumerStatefulWidget { +class ArtistProfile extends ConsumerWidget { final String artistId; const ArtistProfile(this.artistId, {Key? key}) : super(key: key); @override - _ArtistProfileState createState() => _ArtistProfileState(); -} - -class _ArtistProfileState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { SpotifyApi spotify = ref.watch(spotifyProvider); return Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), ), body: FutureBuilder( - future: spotify.artists.get(widget.artistId), + future: spotify.artists.get(artistId), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center(child: CircularProgressIndicator.adaptive()); @@ -222,7 +217,7 @@ class _ArtistProfileState extends ConsumerState { onPressed: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) => ArtistAlbumView( - widget.artistId, + artistId, snapshot.data?.name ?? "KRTX", ), )); @@ -260,7 +255,7 @@ class _ArtistProfileState extends ConsumerState { ), const SizedBox(height: 10), FutureBuilder>( - future: spotify.artists.getRelatedArtists(widget.artistId), + future: spotify.artists.getRelatedArtists(artistId), builder: (context, snapshot) { if (!snapshot.hasData) { return const Center( diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 06cb0d7b..19811f70 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -5,7 +5,7 @@ import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class CategoryCard extends StatefulWidget { +class CategoryCard extends StatelessWidget { final Category category; final Iterable? playlists; const CategoryCard( @@ -14,11 +14,6 @@ class CategoryCard extends StatefulWidget { this.playlists, }) : super(key: key); - @override - _CategoryCardState createState() => _CategoryCardState(); -} - -class _CategoryCardState extends State { @override Widget build(BuildContext context) { return Column( @@ -29,7 +24,7 @@ class _CategoryCardState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - widget.category.name ?? "Unknown", + category.name ?? "Unknown", style: Theme.of(context).textTheme.headline5, ), TextButton( @@ -38,9 +33,9 @@ class _CategoryCardState extends State { MaterialPageRoute( builder: (context) { return PlaylistGenreView( - widget.category.id!, - widget.category.name!, - playlists: widget.playlists, + category.id!, + category.name!, + playlists: playlists, ); }, ), @@ -55,14 +50,13 @@ class _CategoryCardState extends State { builder: (context, ref, child) { SpotifyApi spotifyApi = ref.watch(spotifyProvider); return FutureBuilder>( - future: widget.playlists == null - ? (widget.category.id != "user-featured-playlists" - ? spotifyApi.playlists - .getByCategoryId(widget.category.id!) + future: playlists == null + ? (category.id != "user-featured-playlists" + ? spotifyApi.playlists.getByCategoryId(category.id!) : spotifyApi.playlists.featured) .getPage(4, 0) .then((value) => value.items ?? []) - : Future.value(widget.playlists), + : Future.value(playlists), builder: (context, snapshot) { if (snapshot.hasError) { return const Center(child: Text("Error occurred")); diff --git a/lib/components/Library/UserLibrary.dart b/lib/components/Library/UserLibrary.dart index 415764cc..09f56a44 100644 --- a/lib/components/Library/UserLibrary.dart +++ b/lib/components/Library/UserLibrary.dart @@ -2,14 +2,8 @@ import 'package:flutter/material.dart' hide Image; import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart'; -class UserLibrary extends StatefulWidget { +class UserLibrary extends StatelessWidget { const UserLibrary({Key? key}) : super(key: key); - - @override - _UserLibraryState createState() => _UserLibraryState(); -} - -class _UserLibraryState extends State { @override Widget build(BuildContext context) { return Expanded( diff --git a/lib/components/Login.dart b/lib/components/Login.dart index 3c144baa..f5d94a1b 100644 --- a/lib/components/Login.dart +++ b/lib/components/Login.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; @@ -8,35 +9,32 @@ import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Login extends ConsumerStatefulWidget { +class Login extends HookConsumerWidget { const Login({Key? key}) : super(key: key); @override - _LoginState createState() => _LoginState(); -} + Widget build(BuildContext context, ref) { + var clientIdController = useTextEditingController(); + var clientSecretController = useTextEditingController(); + var accessTokenController = useTextEditingController(); + var fieldError = useState(false); -class _LoginState extends ConsumerState { - String clientId = ""; - String clientSecret = ""; - String accessToken = ""; - bool _fieldError = false; - - Future handleLogin(Auth authState) async { - try { - if (clientId == "" || clientSecret == "") { - return setState(() { - _fieldError = true; - }); + Future handleLogin(Auth authState) async { + try { + if (clientIdController.value.text == "" || + clientSecretController.value.text == "") { + fieldError.value = true; + } + await oauthLogin( + ref.read(authProvider), + clientId: clientIdController.value.text, + clientSecret: clientSecretController.value.text, + ); + } catch (e) { + print("[Login.handleLogin] $e"); } - await oauthLogin(ref.read(authProvider), - clientId: clientId, clientSecret: clientSecret); - } catch (e) { - print("[Login.handleLogin] $e"); } - } - @override - Widget build(BuildContext context) { Auth authState = ref.watch(authProvider); return Scaffold( appBar: const PageWindowTitleBar(), @@ -65,15 +63,11 @@ class _LoginState extends ConsumerState { child: Column( children: [ TextField( + controller: clientIdController, decoration: const InputDecoration( hintText: "Spotify Client ID", label: Text("ClientID"), ), - onChanged: (value) { - setState(() { - clientId = value; - }); - }, ), const SizedBox(height: 10), TextField( @@ -81,11 +75,7 @@ class _LoginState extends ConsumerState { hintText: "Spotify Client Secret", label: Text("Client Secret"), ), - onChanged: (value) { - setState(() { - clientSecret = value; - }); - }, + controller: clientSecretController, ), const SizedBox(height: 10), const Divider(color: Colors.grey), @@ -94,11 +84,7 @@ class _LoginState extends ConsumerState { decoration: const InputDecoration( label: Text("Genius Access Token (optional)"), ), - onChanged: (value) { - setState(() { - accessToken = value; - }); - }, + controller: accessTokenController, ), const SizedBox( height: 10, @@ -110,12 +96,12 @@ class _LoginState extends ConsumerState { ref.read(userPreferencesProvider); SharedPreferences localStorage = await SharedPreferences.getInstance(); - preferences.setGeniusAccessToken(accessToken); + preferences.setGeniusAccessToken( + accessTokenController.value.text); await localStorage.setString( - LocalStorageKeys.geniusAccessToken, accessToken); - setState(() { - accessToken = ""; - }); + LocalStorageKeys.geniusAccessToken, + accessTokenController.value.text); + accessTokenController.text = ""; }, child: const Text("Submit"), ) diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index cd09c82b..8d42e8e0 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Settings.dart'; import 'package:spotube/helpers/artist-to-string.dart'; @@ -7,48 +8,53 @@ import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Lyrics extends ConsumerStatefulWidget { +class Lyrics extends HookConsumerWidget { const Lyrics({Key? key}) : super(key: key); @override - ConsumerState createState() => _LyricsState(); -} - -class _LyricsState extends ConsumerState { - Map _lyrics = {}; - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); UserPreferences userPreferences = ref.watch(userPreferencesProvider); + var lyrics = useState({}); bool hasToken = (userPreferences.geniusAccessToken != null || (userPreferences.geniusAccessToken?.isNotEmpty ?? false)); - - if (playback.currentTrack != null && - hasToken && - playback.currentTrack!.id != _lyrics["id"]) { - getLyrics( + var lyricsFuture = useMemoized(() { + if (playback.currentTrack == null || + !hasToken || + (playback.currentTrack?.id != null && + playback.currentTrack?.id == lyrics.value["id"])) { + return null; + } + return getLyrics( playback.currentTrack!.name!, artistsToString(playback.currentTrack!.artists ?? []), apiKey: userPreferences.geniusAccessToken!, optimizeQuery: true, - ).then((lyrics) { - if (lyrics != null) { - setState(() { - _lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!}; - }); - } - }); - } + ); + }, [playback.currentTrack]); - if (_lyrics["lyrics"] != null && playback.currentTrack == null) { - setState(() { - _lyrics = {}; - }); - } + var lyricsSnapshot = useFuture(lyricsFuture); - if (_lyrics["lyrics"] == null && playback.currentTrack != null) { + useEffect(() { + if (lyricsSnapshot.hasData && lyricsSnapshot.data != null) { + lyrics.value = { + "lyrics": lyricsSnapshot.data, + "id": playback.currentTrack!.id! + }; + } + + if (lyrics.value["lyrics"] != null && playback.currentTrack == null) { + lyrics.value = {}; + } + }, [ + lyricsSnapshot.data, + lyricsSnapshot.hasData, + lyrics.value, + playback.currentTrack, + ]); + + if (lyrics.value["lyrics"] == null && playback.currentTrack != null) { if (!hasToken) { return Expanded( child: Column( @@ -99,9 +105,10 @@ class _LyricsState extends ConsumerState { child: SingleChildScrollView( child: Center( child: Text( - _lyrics["lyrics"] == null && playback.currentTrack == null + lyrics.value["lyrics"] == null && + playback.currentTrack == null ? "No Track being played currently" - : _lyrics["lyrics"]!, + : lyrics.value["lyrics"]!, style: Theme.of(context).textTheme.headline6, ), ), diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 33430389..f1a96a54 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -176,7 +176,6 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { await player.pause(); await player.seek(Duration.zero); _movePlaylistPositionBy(1); - print("ON NEXT"); } catch (e, stack) { print("[PlayerControls.onNext()] $e"); print(stack); diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 38dc849e..4583a98f 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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'; -class PlayerControls extends ConsumerStatefulWidget { +class PlayerControls extends HookConsumerWidget { final Stream positionStream; final bool isPlaying; final Duration duration; @@ -34,33 +35,21 @@ class PlayerControls extends ConsumerStatefulWidget { Key? key, }) : super(key: key); - @override - _PlayerControlsState createState() => _PlayerControlsState(); -} - -class _PlayerControlsState extends ConsumerState { - StreamSubscription? _timePositionListener; - late List _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(); + isPlaying ? await onPause?.call() : await 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 { + @override + Widget build(BuildContext context, ref) { + UserPreferences preferences = ref.watch(userPreferencesProvider); + + var _hotKeys = []; + useEffect(() { _hotKeys = [ GlobalKeyActions( HotKey(KeyCode.space, scope: HotKeyScope.inapp), @@ -68,14 +57,14 @@ class _PlayerControlsState extends ConsumerState { ), if (preferences.nextTrackHotKey != null) GlobalKeyActions( - preferences.nextTrackHotKey!, (key) => widget.onNext?.call()), + preferences.nextTrackHotKey!, (key) => onNext?.call()), if (preferences.prevTrackHotKey != null) GlobalKeyActions( - preferences.prevTrackHotKey!, (key) => widget.onPrevious?.call()), + preferences.prevTrackHotKey!, (key) => onPrevious?.call()), if (preferences.playPauseHotKey != null) GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause) ]; - await Future.wait( + Future.wait( _hotKeys.map((e) { return hotKeyManager.register( e.hotKey, @@ -83,25 +72,22 @@ class _PlayerControlsState extends ConsumerState { ); }), ); + return () { + Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey))); + }; }); - } - - @override - Widget build(BuildContext context) { - UserPreferences preferences = ref.watch(userPreferencesProvider); - _configureHotKeys(preferences); return Container( constraints: const BoxConstraints(maxWidth: 700), child: Column( children: [ StreamBuilder( - stream: widget.positionStream, + stream: positionStream, builder: (context, snapshot) { var totalMinutes = - zeroPadNumStr(widget.duration.inMinutes.remainder(60)); + zeroPadNumStr(duration.inMinutes.remainder(60)); var totalSeconds = - zeroPadNumStr(widget.duration.inSeconds.remainder(60)); + zeroPadNumStr(duration.inSeconds.remainder(60)); var currentMinutes = snapshot.hasData ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) : "00"; @@ -109,7 +95,7 @@ class _PlayerControlsState extends ConsumerState { ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) : "00"; - var sliderMax = widget.duration.inSeconds; + var sliderMax = duration.inSeconds; var sliderValue = snapshot.data?.inSeconds ?? 0; return Row( children: [ @@ -123,7 +109,7 @@ class _PlayerControlsState extends ConsumerState { : sliderValue / sliderMax, onChanged: (value) {}, onChangeEnd: (value) { - widget.onSeek?.call(value * sliderMax); + onSeek?.call(value * sliderMax); }, ), ), @@ -138,30 +124,27 @@ class _PlayerControlsState extends ConsumerState { children: [ IconButton( icon: const Icon(Icons.shuffle_rounded), - color: - widget.shuffled ? Theme.of(context).primaryColor : null, + color: shuffled ? Theme.of(context).primaryColor : null, onPressed: () { - widget.onShuffle?.call(); + onShuffle?.call(); }), IconButton( icon: const Icon(Icons.skip_previous_rounded), onPressed: () { - widget.onPrevious?.call(); + onPrevious?.call(); }), IconButton( icon: Icon( - widget.isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, + isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, ), onPressed: () => _playOrPause(null), ), IconButton( icon: const Icon(Icons.skip_next_rounded), - onPressed: () => widget.onNext?.call()), + onPressed: () => onNext?.call()), IconButton( icon: const Icon(Icons.stop_rounded), - onPressed: () => widget.onStop?.call(), + onPressed: () => onStop?.call(), ) ], ) diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 9995b0fd..862ee9fc 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; @@ -7,27 +7,22 @@ import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistCard extends ConsumerStatefulWidget { +class PlaylistCard extends ConsumerWidget { final PlaylistSimple playlist; const PlaylistCard(this.playlist, {Key? key}) : super(key: key); @override - _PlaylistCardState createState() => _PlaylistCardState(); -} - -class _PlaylistCardState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); bool isPlaylistPlaying = playback.currentPlaylist != null && - playback.currentPlaylist!.id == widget.playlist.id; + playback.currentPlaylist!.id == playlist.id; return PlaybuttonCard( - title: widget.playlist.name!, - imageUrl: widget.playlist.images![0].url!, + title: playlist.name!, + imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, onTap: () { Navigator.of(context).push(MaterialPageRoute( builder: (context) { - return PlaylistView(widget.playlist); + return PlaylistView(playlist); }, )); }, @@ -35,9 +30,9 @@ class _PlaylistCardState extends ConsumerState { if (isPlaylistPlaying) return; SpotifyApi spotifyApi = ref.read(spotifyProvider); - List tracks = (widget.playlist.id != "user-liked-tracks" + List tracks = (playlist.id != "user-liked-tracks" ? await spotifyApi.playlists - .getTracksByPlaylistId(widget.playlist.id!) + .getTracksByPlaylistId(playlist.id!) .all() : await spotifyApi.tracks.me.saved .all() @@ -48,9 +43,9 @@ class _PlaylistCardState extends ConsumerState { playback.setCurrentPlaylist = CurrentPlaylist( tracks: tracks, - id: widget.playlist.id!, - name: widget.playlist.name!, - thumbnail: imageToUrlString(widget.playlist.images), + id: playlist.id!, + name: playlist.name!, + thumbnail: imageToUrlString(playlist.images), ); playback.setCurrentTrack = tracks.first; }, diff --git a/lib/components/Playlist/PlaylistGenreView.dart b/lib/components/Playlist/PlaylistGenreView.dart index 4497ae7b..647b517e 100644 --- a/lib/components/Playlist/PlaylistGenreView.dart +++ b/lib/components/Playlist/PlaylistGenreView.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistGenreView extends StatefulWidget { +class PlaylistGenreView extends ConsumerWidget { final String genreId; final String genreName; final Iterable? playlists; @@ -15,13 +15,9 @@ class PlaylistGenreView extends StatefulWidget { this.playlists, Key? key, }) : super(key: key); - @override - _PlaylistGenreViewState createState() => _PlaylistGenreViewState(); -} -class _PlaylistGenreViewState extends State { @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { return Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), @@ -29,7 +25,7 @@ class _PlaylistGenreViewState extends State { body: Column( children: [ Text( - widget.genreName, + genreName, style: Theme.of(context).textTheme.headline4, textAlign: TextAlign.center, ), @@ -39,13 +35,13 @@ class _PlaylistGenreViewState extends State { return Expanded( child: SingleChildScrollView( child: FutureBuilder>( - future: widget.playlists == null - ? (widget.genreId != "user-featured-playlists" + future: playlists == null + ? (genreId != "user-featured-playlists" ? spotifyApi.playlists - .getByCategoryId(widget.genreId) + .getByCategoryId(genreId) .all() : spotifyApi.playlists.featured.all()) - : Future.value(widget.playlists), + : Future.value(playlists), builder: (context, snapshot) { if (snapshot.hasError) { return const Center(child: Text("Error occurred")); diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index 5f9ef5a6..c615e3cd 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -7,24 +7,20 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistView extends ConsumerStatefulWidget { +class PlaylistView extends ConsumerWidget { final PlaylistSimple playlist; const PlaylistView(this.playlist, {Key? key}) : super(key: key); - @override - _PlaylistViewState createState() => _PlaylistViewState(); -} -class _PlaylistViewState extends ConsumerState { playPlaylist(Playback playback, List tracks, {Track? currentTrack}) { currentTrack ??= tracks.first; var isPlaylistPlaying = playback.currentPlaylist?.id != null && - playback.currentPlaylist?.id == widget.playlist.id; + playback.currentPlaylist?.id == playlist.id; if (!isPlaylistPlaying) { playback.setCurrentPlaylist = CurrentPlaylist( tracks: tracks, - id: widget.playlist.id!, - name: widget.playlist.name!, - thumbnail: imageToUrlString(widget.playlist.images), + id: playlist.id!, + name: playlist.name!, + thumbnail: imageToUrlString(playlist.images), ); playback.setCurrentTrack = currentTrack; } else if (isPlaylistPlaying && @@ -35,17 +31,15 @@ class _PlaylistViewState extends ConsumerState { } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); SpotifyApi spotifyApi = ref.watch(spotifyProvider); var isPlaylistPlaying = playback.currentPlaylist?.id != null && - playback.currentPlaylist?.id == widget.playlist.id; + playback.currentPlaylist?.id == playlist.id; return Scaffold( body: FutureBuilder>( - future: widget.playlist.id != "user-liked-tracks" - ? spotifyApi.playlists - .getTracksByPlaylistId(widget.playlist.id) - .all() + future: playlist.id != "user-liked-tracks" + ? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all() : spotifyApi.tracks.me.saved .all() .then((tracks) => tracks.map((e) => e.track!)), @@ -78,7 +72,7 @@ class _PlaylistViewState extends ConsumerState { ), ), Center( - child: Text(widget.playlist.name!, + child: Text(playlist.name!, style: Theme.of(context).textTheme.headline4), ), snapshot.hasError diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index e13dd643..a70efd92 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart' hide Page; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; @@ -11,27 +12,14 @@ import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class Search extends ConsumerStatefulWidget { +class Search extends HookConsumerWidget { const Search({Key? key}) : super(key: key); @override - ConsumerState createState() => _SearchState(); -} - -class _SearchState extends ConsumerState { - late TextEditingController _controller; - - String searchTerm = ""; - - @override - void initState() { - super.initState(); - _controller = TextEditingController(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { SpotifyApi spotify = ref.watch(spotifyProvider); + var controller = useTextEditingController(); + var searchTerm = useState(""); return Expanded( child: Column( @@ -43,11 +31,9 @@ class _SearchState extends ConsumerState { Expanded( child: TextField( decoration: const InputDecoration(hintText: "Search..."), - controller: _controller, + controller: controller, onSubmitted: (value) { - setState(() { - searchTerm = _controller.value.text; - }); + searchTerm.value = controller.value.text; }, ), ), @@ -60,24 +46,22 @@ class _SearchState extends ConsumerState { textColor: Colors.white, child: const Icon(Icons.search_rounded), onPressed: () { - setState(() { - searchTerm = _controller.value.text; - }); + searchTerm.value = controller.value.text; }, ), ], ), ), FutureBuilder>( - future: searchTerm.isNotEmpty - ? spotify.search.get(searchTerm).first(10) + future: searchTerm.value.isNotEmpty + ? spotify.search.get(searchTerm.value).first(10) : null, builder: (context, snapshot) { - if (!snapshot.hasData && searchTerm.isNotEmpty) { + if (!snapshot.hasData && searchTerm.value.isNotEmpty) { return const Center( child: CircularProgressIndicator.adaptive(), ); - } else if (!snapshot.hasData && searchTerm.isEmpty) { + } else if (!snapshot.hasData && searchTerm.value.isEmpty) { return Container(); } Playback playback = ref.watch(playbackProvider); diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 057dfe4d..27ec712c 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -1,45 +1,28 @@ import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.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'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; +import 'package:spotube/provider/ThemeProvider.dart'; import 'package:spotube/provider/UserPreferences.dart'; -class Settings extends ConsumerStatefulWidget { +class Settings extends HookConsumerWidget { const Settings({Key? key}) : super(key: key); @override - _SettingsState createState() => _SettingsState(); -} - -class _SettingsState extends ConsumerState { - TextEditingController? _textEditingController; - String? _geniusAccessToken; - - @override - void initState() { - super.initState(); - _textEditingController = TextEditingController(); - _textEditingController?.addListener(() { - setState(() { - _geniusAccessToken = _textEditingController?.value.text; - }); - }); - } - - @override - void dispose() { - _textEditingController?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { UserPreferences preferences = ref.watch(userPreferencesProvider); + ThemeMode theme = ref.watch(themeProvider); + var geniusAccessToken = useState(null); + TextEditingController textEditingController = useTextEditingController(); + + textEditingController.addListener(() { + geniusAccessToken.value = textEditingController.value.text; + }); return Scaffold( appBar: PageWindowTitleBar( @@ -65,7 +48,7 @@ class _SettingsState extends ConsumerState { Expanded( flex: 1, child: TextField( - controller: _textEditingController, + controller: textEditingController, decoration: InputDecoration( hintText: preferences.geniusAccessToken, ), @@ -74,19 +57,19 @@ class _SettingsState extends ConsumerState { Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( - onPressed: _geniusAccessToken != null + onPressed: geniusAccessToken != null ? () async { SharedPreferences localStorage = await SharedPreferences.getInstance(); preferences - .setGeniusAccessToken(_geniusAccessToken); + .setGeniusAccessToken(geniusAccessToken.value); localStorage.setString( LocalStorageKeys.geniusAccessToken, - _geniusAccessToken!); - setState(() { - _geniusAccessToken = null; - }); - _textEditingController?.text = ""; + geniusAccessToken.value ?? ""); + + geniusAccessToken.value = null; + + textEditingController.text = ""; } : null, child: const Text("Save"), @@ -121,7 +104,7 @@ class _SettingsState extends ConsumerState { children: [ const Text("Theme"), DropdownButton( - value: MyApp.of(context)?.getThemeMode(), + value: theme, items: const [ DropdownMenuItem( child: Text( @@ -142,7 +125,7 @@ class _SettingsState extends ConsumerState { ], onChanged: (value) { if (value != null) { - MyApp.of(context)?.setThemeMode(value); + ref.read(themeProvider.notifier).state = value; } }, ) diff --git a/lib/components/Shared/AnchorButton.dart b/lib/components/Shared/AnchorButton.dart index c02d82fa..5af2b20b 100644 --- a/lib/components/Shared/AnchorButton.dart +++ b/lib/components/Shared/AnchorButton.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; -class AnchorButton extends StatefulWidget { +class AnchorButton extends HookWidget { final String text; final TextStyle style; final TextAlign? textAlign; @@ -16,33 +17,29 @@ class AnchorButton extends StatefulWidget { this.style = const TextStyle(), }) : super(key: key); - @override - State> createState() => _AnchorButtonState(); -} - -class _AnchorButtonState extends State> { - bool _hover = false; - bool _tap = false; - @override Widget build(BuildContext context) { + var hover = useState(false); + var tap = useState(false); + return GestureDetector( child: MouseRegion( cursor: MaterialStateMouseCursor.clickable, child: Text( - widget.text, - style: widget.style.copyWith( - decoration: _hover || _tap ? TextDecoration.underline : null, + text, + style: style.copyWith( + decoration: + hover.value || tap.value ? TextDecoration.underline : null, ), - textAlign: widget.textAlign, - overflow: widget.overflow, + textAlign: textAlign, + overflow: overflow, ), - onEnter: (event) => setState(() => _hover = true), - onExit: (event) => setState(() => _hover = false), + onEnter: (event) => hover.value = true, + onExit: (event) => hover.value = false, ), - onTapDown: (event) => setState(() => _tap = true), - onTapUp: (event) => setState(() => _tap = false), - onTap: widget.onTap, + onTapDown: (event) => tap.value = true, + onTapUp: (event) => tap.value = false, + onTap: onTap, ); } } diff --git a/lib/components/Shared/DownloadTrackButton.dart b/lib/components/Shared/DownloadTrackButton.dart index 3d8d9fcc..5be588d8 100644 --- a/lib/components/Shared/DownloadTrackButton.dart +++ b/lib/components/Shared/DownloadTrackButton.dart @@ -1,106 +1,86 @@ import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path/path.dart' as path; -class DownloadTrackButton extends StatefulWidget { +enum TrackStatus { downloading, idle, done } + +class DownloadTrackButton extends HookWidget { final Track? track; const DownloadTrackButton({Key? key, this.track}) : super(key: key); @override - _DownloadTrackButtonState createState() => _DownloadTrackButtonState(); -} + Widget build(BuildContext context) { + var status = useState(TrackStatus.idle); + YoutubeExplode yt = useMemoized(() => YoutubeExplode()); -enum TrackStatus { downloading, idle, done } + var _downloadTrack = useCallback(() async { + if (track == null) return; + StreamManifest manifest = + await yt.videos.streamsClient.getManifest(track?.href); -class _DownloadTrackButtonState extends State { - late YoutubeExplode yt; - TrackStatus status = TrackStatus.idle; + var audioStream = yt.videos.streamsClient.get( + manifest.audioOnly + .where((audio) => audio.codec.mimeType == "audio/mp4") + .withHighestBitrate(), + ); - @override - void initState() { - yt = YoutubeExplode(); - super.initState(); - } - - @override - void dispose() { - yt.close(); - super.dispose(); - } - - _downloadTrack() async { - if (widget.track == null) return; - StreamManifest manifest = - await yt.videos.streamsClient.getManifest(widget.track?.href); - - var audioStream = yt.videos.streamsClient - .get(manifest.audioOnly.withHighestBitrate()) - .asBroadcastStream(); - - var statusCb = audioStream.listen( - (event) { - if (status != TrackStatus.downloading) { - setState(() { - status = TrackStatus.downloading; - }); - } - }, - onDone: () async { - setState(() { - status = TrackStatus.done; - }); - await Future.delayed( - const Duration(seconds: 3), - () { - if (status == TrackStatus.done) { - setState(() { - status = TrackStatus.idle; - }); - } - }, - ); - }, - ); - - String downloadFolder = path.join( - (await path_provider.getDownloadsDirectory())!.path, "Spotube"); - String fileName = - "${widget.track?.name} - ${artistsToString(widget.track?.artists ?? [])}.mp3"; - File outputFile = File(path.join(downloadFolder, fileName)); - if (!outputFile.existsSync()) { - outputFile.createSync(recursive: true); - IOSink outputFileStream = outputFile.openWrite(); - await audioStream.pipe(outputFileStream); - await outputFileStream.flush(); - await outputFileStream.close().then((value) async { - if (status == TrackStatus.downloading) { - setState(() { - status = TrackStatus.done; - }); + var statusCb = audioStream.listen( + (event) { + if (status.value != TrackStatus.downloading) { + status.value = TrackStatus.downloading; + } + }, + onDone: () async { + status.value = TrackStatus.done; await Future.delayed( const Duration(seconds: 3), () { - if (status == TrackStatus.done) { - setState(() { - status = TrackStatus.idle; - }); + if (status.value == TrackStatus.done) { + status.value = TrackStatus.idle; } }, ); - } - return statusCb.cancel(); - }); - } - } + }, + ); - @override - Widget build(BuildContext context) { - if (status == TrackStatus.downloading) { + String downloadFolder = path.join( + (await path_provider.getDownloadsDirectory())!.path, "Spotube"); + String fileName = + "${track?.name} - ${artistsToString(track?.artists ?? [])}.mp3"; + File outputFile = File(path.join(downloadFolder, fileName)); + if (!outputFile.existsSync()) { + outputFile.createSync(recursive: true); + IOSink outputFileStream = outputFile.openWrite(); + await audioStream.pipe(outputFileStream); + await outputFileStream.flush(); + await outputFileStream.close().then((value) async { + if (status.value == TrackStatus.downloading) { + status.value = TrackStatus.done; + await Future.delayed( + const Duration(seconds: 3), + () { + if (status.value == TrackStatus.done) { + status.value = TrackStatus.idle; + } + }, + ); + } + return statusCb.cancel(); + }); + } + }, [track, status, yt]); + + useEffect(() { + return () => yt.close(); + }, []); + + if (status.value == TrackStatus.downloading) { return const SizedBox( child: CircularProgressIndicator.adaptive( strokeWidth: 2, @@ -108,13 +88,13 @@ class _DownloadTrackButtonState extends State { height: 20, width: 20, ); - } else if (status == TrackStatus.done) { + } else if (status.value == TrackStatus.done) { return const Icon(Icons.download_done_rounded); } return IconButton( icon: const Icon(Icons.download_rounded), - onPressed: widget.track != null && - !(widget.track!.href ?? "").startsWith("https://api.spotify.com") + onPressed: track != null && + !(track!.href ?? "").startsWith("https://api.spotify.com") ? _downloadTrack : null, ); diff --git a/lib/components/Shared/RecordHotKeyDialog.dart b/lib/components/Shared/RecordHotKeyDialog.dart index c7eba2b5..e0fde237 100644 --- a/lib/components/Shared/RecordHotKeyDialog.dart +++ b/lib/components/Shared/RecordHotKeyDialog.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; -class RecordHotKeyDialog extends StatefulWidget { +class RecordHotKeyDialog extends HookWidget { final ValueChanged onHotKeyRecorded; const RecordHotKeyDialog({ @@ -9,15 +10,9 @@ class RecordHotKeyDialog extends StatefulWidget { required this.onHotKeyRecorded, }) : super(key: key); - @override - _RecordHotKeyDialogState createState() => _RecordHotKeyDialogState(); -} - -class _RecordHotKeyDialogState extends State { - HotKey _hotKey = HotKey(null); - @override Widget build(BuildContext context) { + var _hotKey = useState(HotKey(null)); return AlertDialog( content: SingleChildScrollView( child: ListBody( @@ -58,9 +53,7 @@ class _RecordHotKeyDialogState extends State { children: [ HotKeyRecorder( onHotKeyRecorded: (hotKey) { - setState(() { - _hotKey = hotKey; - }); + _hotKey.value = hotKey; }, ), ], @@ -78,10 +71,10 @@ class _RecordHotKeyDialogState extends State { ), TextButton( child: const Text('OK'), - onPressed: !_hotKey.isSetted + onPressed: !_hotKey.value.isSetted ? null : () { - widget.onHotKeyRecorded(_hotKey); + onHotKeyRecorded(_hotKey.value); Navigator.of(context).pop(); }, ), diff --git a/lib/main.dart b/lib/main.dart index 575bc93e..c3f73d3f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,17 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Home.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; +import 'package:spotube/provider/ThemeProvider.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); await hotKeyManager.unregisterAll(); - runApp(const ProviderScope(child: MyApp())); + runApp(ProviderScope(child: MyApp())); doWhenWindowReady(() { appWindow.minSize = const Size(900, 700); appWindow.size = const Size(900, 700); @@ -19,56 +21,28 @@ void main() async { }); } -class MyApp extends StatefulWidget { - const MyApp({Key? key}) : super(key: key); - - static _MyAppState? of(BuildContext context) => - context.findAncestorStateOfType<_MyAppState>(); +class MyApp extends HookConsumerWidget { @override - State createState() => _MyAppState(); -} + Widget build(BuildContext context, ref) { + var themeMode = ref.watch(themeProvider); + useEffect(() { + SharedPreferences.getInstance().then((localStorage) { + String? themeMode = localStorage.getString(LocalStorageKeys.themeMode); + var themeNotifier = ref.read(themeProvider.notifier); -class _MyAppState extends State { - ThemeMode _themeMode = ThemeMode.system; - - @override - void initState() { - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { - SharedPreferences localStorage = await SharedPreferences.getInstance(); - String? themeMode = localStorage.getString(LocalStorageKeys.themeMode); - - setState(() { switch (themeMode) { case "light": - _themeMode = ThemeMode.light; + themeNotifier.state = ThemeMode.light; break; case "dark": - _themeMode = ThemeMode.dark; + themeNotifier.state = ThemeMode.dark; break; default: - _themeMode = ThemeMode.system; + themeNotifier.state = ThemeMode.system; } }); - }); - super.initState(); - } + }, []); - void setThemeMode(ThemeMode themeMode) { - SharedPreferences.getInstance().then((localStorage) { - localStorage.setString( - LocalStorageKeys.themeMode, themeMode.toString().split(".").last); - setState(() { - _themeMode = themeMode; - }); - }); - } - - ThemeMode getThemeMode() { - return _themeMode; - } - - @override - Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, title: 'Spotube', @@ -156,7 +130,7 @@ class _MyAppState extends State { ), canvasColor: Colors.blueGrey[900], ), - themeMode: _themeMode, + themeMode: themeMode, home: const Home(), ); } diff --git a/lib/provider/ThemeProvider.dart b/lib/provider/ThemeProvider.dart new file mode 100644 index 00000000..870c5aab --- /dev/null +++ b/lib/provider/ThemeProvider.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +var themeProvider = StateProvider((ref) { + return ThemeMode.system; +}); diff --git a/pubspec.lock b/pubspec.lock index dcccce79..4802bacb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -188,6 +188,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.3.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + url: "https://pub.dartlang.org" + source: hosted + version: "0.18.2+1" flutter_lints: dependency: "direct dev" description: @@ -219,6 +226,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.14.3" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" hotkey_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 819d2389..7ff5c832 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,8 @@ dependencies: path_provider: ^2.0.8 collection: ^1.15.0 flutter_riverpod: ^1.0.3 + flutter_hooks: ^0.18.2+1 + hooks_riverpod: ^1.0.3 dev_dependencies: flutter_test: From 7d280f92ce15edc4afd2d71d1a33fd50e9e1cc9f Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 11 Feb 2022 20:07:49 +0600 Subject: [PATCH 11/25] hooks logic implmented in Player component --- lib/components/Player/Player.dart | 289 ++++++++++++------------------ 1 file changed, 119 insertions(+), 170 deletions(-) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index f1a96a54..41cbe480 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,7 +1,8 @@ import 'dart:async'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; @@ -14,72 +15,72 @@ import 'package:flutter/material.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; -class Player extends ConsumerStatefulWidget { +class Player extends HookConsumerWidget { const Player({Key? key}) : super(key: key); @override - _PlayerState createState() => _PlayerState(); -} + Widget build(BuildContext context, ref) { + var _isPlaying = useState(false); + var _shuffled = useState(false); + var _volume = useState(0.0); + var _duration = useState(null); + var _currentTrackId = useState(null); -class _PlayerState extends ConsumerState with WidgetsBindingObserver { - late AudioPlayer player; - bool _isPlaying = false; - bool _shuffled = false; - Duration? _duration; + AudioPlayer player = useMemoized(() => AudioPlayer(), []); + YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); - String? _currentTrackId; + var _movePlaylistPositionBy = useCallback((int pos) { + Playback playback = ref.read(playbackProvider); + if (playback.currentTrack != null && playback.currentPlaylist != null) { + int index = playback.currentPlaylist!.trackIds + .indexOf(playback.currentTrack!.id!) + + pos; - double _volume = 0; + var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1 + ? 0 + : index < 0 + ? playback.currentPlaylist!.trackIds.length + : index; + Track? track = + playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex) + ? playback.currentPlaylist!.tracks.elementAt(safeIndex) + : null; + if (track != null) { + playback.setCurrentTrack = track; + _duration.value = null; + } + } + }, [_duration]); - late YoutubeExplode youtube; + useEffect(() { + _volume.value = player.volume; - @override - void initState() { - try { - super.initState(); - player = AudioPlayer(); - youtube = YoutubeExplode(); - - WidgetsBinding.instance?.addObserver(this); - WidgetsBinding.instance?.addPostFrameCallback(_init); - } catch (e, stack) { - print("[Player.initState()] $e"); - print(stack); - } - } - - _init(Duration timeStamp) async { - try { - setState(() { - _volume = player.volume; - }); - player.playingStream.listen((playing) async { - setState(() { - _isPlaying = playing; - }); + var playingStreamListener = player.playingStream.listen((playing) async { + _isPlaying.value = playing; }); - player.durationStream.listen((duration) async { + var durationStreamListener = + 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) { + if (duration != Duration.zero && duration != _duration.value) { // this line is for prev/next or already playing playlist if (player.playing) await player.pause(); await player.play(); } - setState(() { - _duration = duration; - }); + _duration.value = duration; } }); - player.processingStateStream.listen((event) async { + var processingStateStreamListener = + player.processingStateStream.listen((event) async { try { - if (event == ProcessingState.completed && _currentTrackId != null) { + if (event == ProcessingState.completed && + _currentTrackId.value != null) { _movePlaylistPositionBy(1); } } catch (e, stack) { @@ -87,117 +88,72 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { print(stack); } }); - } catch (e) { - print("[Player._init()]: $e"); - } - } + return () { + playingStreamListener.cancel(); + durationStreamListener.cancel(); + processingStateStreamListener.cancel(); + player.dispose(); + youtube.close(); + }; + }, []); - @override - void dispose() { - WidgetsBinding.instance?.removeObserver(this); - player.dispose(); - youtube.close(); - super.dispose(); - } - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.paused) { - // Release the player's resources when not in use. We use "stop" so that - // if the app resumes later, it will still remember what position to - // resume from. - player.stop(); - } - } - - void _movePlaylistPositionBy(int pos) { - Playback playback = ref.read(playbackProvider); - if (playback.currentTrack != null && playback.currentPlaylist != null) { - int index = playback.currentPlaylist!.trackIds - .indexOf(playback.currentTrack!.id!) + - pos; - - var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1 - ? 0 - : index < 0 - ? playback.currentPlaylist!.trackIds.length - : index; - Track? track = - playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex) - ? playback.currentPlaylist!.tracks.elementAt(safeIndex) - : null; - if (track != null) { - playback.setCurrentTrack = track; - setState(() { - _duration = null; - }); - } - } - } - - Future _playTrack(Track currentTrack, Playback playback) async { - try { - if (currentTrack.id != _currentTrackId) { - Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); - if (parsedUri != null && parsedUri.hasAbsolutePath) { - await player - .setAudioSource( - AudioSource.uri(parsedUri), - preload: true, - ) - .then((value) async { - setState(() { - _currentTrackId = currentTrack.id; - if (_duration != null) { - _duration = value; + var _playTrack = useCallback((Track currentTrack, Playback playback) async { + try { + if (currentTrack.id != _currentTrackId.value) { + Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); + if (parsedUri != null && parsedUri.hasAbsolutePath) { + await player + .setAudioSource( + AudioSource.uri(parsedUri), + preload: true, + ) + .then((value) async { + _currentTrackId.value = currentTrack.id; + if (_duration.value != null) { + _duration.value = value; } }); - }); - } - var ytTrack = await toYoutubeTrack(youtube, currentTrack); - if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) { - await player - .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) - .then((value) { - setState(() { - _currentTrackId = currentTrack.id; + } + var ytTrack = await toYoutubeTrack(youtube, currentTrack); + if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) { + await player + .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) + .then((value) { + _currentTrackId.value = currentTrack.id; }); - }); + } } + } catch (e, stack) { + print("[Player._playTrack()] $e"); + print(stack); } - } catch (e, stack) { - print("[Player._playTrack()] $e"); - print(stack); - } - } + }, [player, _currentTrackId, _duration]); - _onNext() async { - try { - await player.pause(); - await player.seek(Duration.zero); - _movePlaylistPositionBy(1); - } catch (e, stack) { - print("[PlayerControls.onNext()] $e"); - print(stack); - } - } + var _onNext = useCallback(() async { + try { + await player.pause(); + await player.seek(Duration.zero); + _movePlaylistPositionBy(1); + } catch (e, stack) { + print("[PlayerControls.onNext()] $e"); + print(stack); + } + }, [player]); - _onPrevious() async { - try { - await player.pause(); - await player.seek(Duration.zero); - _movePlaylistPositionBy(-1); - } catch (e, stack) { - print("[PlayerControls.onPrevious()] $e"); - print(stack); - } - } + var _onPrevious = useCallback(() async { + try { + await player.pause(); + await player.seek(Duration.zero); + _movePlaylistPositionBy(-1); + } catch (e, stack) { + print("[PlayerControls.onPrevious()] $e"); + print(stack); + } + }, [player]); - @override - Widget build(BuildContext context) { return Container( color: Theme.of(context).backgroundColor, - child: Consumer( + child: HookConsumer( builder: (context, ref, widget) { Playback playback = ref.watch(playbackProvider); if (playback.currentPlaylist != null && @@ -205,9 +161,12 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { _playTrack(playback.currentTrack!, playback); } - String? albumArt = imageToUrlString( - playback.currentTrack?.album?.images, - index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + String? albumArt = useMemoized( + () => imageToUrlString( + playback.currentTrack?.album?.images, + index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + ), + [playback.currentTrack?.album?.images], ); return Material( @@ -249,9 +208,9 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { flex: 3, child: PlayerControls( positionStream: player.positionStream, - isPlaying: _isPlaying, - duration: _duration ?? Duration.zero, - shuffled: _shuffled, + isPlaying: _isPlaying.value, + duration: _duration.value ?? Duration.zero, + shuffled: _shuffled.value, onNext: _onNext, onPrevious: _onPrevious, onPause: () async { @@ -282,16 +241,12 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { if (playback.currentTrack == null || playback.currentPlaylist == null) return; try { - if (!_shuffled) { + if (!_shuffled.value) { playback.currentPlaylist!.shuffle(); - setState(() { - _shuffled = true; - }); + _shuffled.value = true; } else { playback.currentPlaylist!.unshuffle(); - setState(() { - _shuffled = false; - }); + _shuffled.value = false; } } catch (e, stack) { print("[PlayerControls.onShuffle()] $e"); @@ -302,12 +257,10 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { try { await player.pause(); await player.seek(Duration.zero); - setState(() { - _isPlaying = false; - _currentTrackId = null; - _duration = null; - _shuffled = false; - }); + _isPlaying.value = false; + _currentTrackId.value = null; + _duration.value = null; + _shuffled.value = false; playback.reset(); } catch (e, stack) { print("[PlayerControls.onStop()] $e"); @@ -327,13 +280,11 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { height: 20, constraints: const BoxConstraints(maxWidth: 200), child: Slider.adaptive( - value: _volume, + value: _volume.value, onChanged: (value) async { try { await player.setVolume(value).then((_) { - setState(() { - _volume = value; - }); + _volume.value = value; }); } catch (e, stack) { print("[VolumeSlider.onChange()] $e"); @@ -368,10 +319,8 @@ class _PlayerState extends ConsumerState with WidgetsBindingObserver { onPressed: () { if (!isLiked && playback.currentTrack?.id != null) { - spotifyApi.tracks.me - .saveOne( - playback.currentTrack!.id!) - .then((value) => setState(() {})); + spotifyApi.tracks.me.saveOne( + playback.currentTrack!.id!); } }); }); From 639960b01443a0cc8aec437395911f5141a65385 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 21 Feb 2022 10:22:46 +0600 Subject: [PATCH 12/25] fixed AUR package compatibilty with the newer pkgname --- aur-struct/.SRCINFO | 2 +- aur-struct/PKGBUILD | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/aur-struct/.SRCINFO b/aur-struct/.SRCINFO index 684aaf92..36027837 100644 --- a/aur-struct/.SRCINFO +++ b/aur-struct/.SRCINFO @@ -1,7 +1,7 @@ pkgbase = spotube-bin pkgdesc = A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed pkgver = 1.2.0 - pkgrel = 1 + pkgrel = 2 url = https://github.com/KRTirtho/spotube/ arch = x86_64 license = BSD-4-Clause diff --git a/aur-struct/PKGBUILD b/aur-struct/PKGBUILD index 4284e0f8..3e7ee60b 100644 --- a/aur-struct/PKGBUILD +++ b/aur-struct/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kingkor Roy Tirtho pkgname=spotube-bin pkgver=1.2.0 -pkgrel=1 +pkgrel=2 epoch= pkgdesc="A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed" arch=(x86_64) @@ -25,15 +25,16 @@ md5sums=(f49d21ef00c7d43eb70e7e9b2a7103c1) validpgpkeys=() package(){ - install -dm755 "${pkgdir}/usr/share/icons/${pkgname}" + install -dm755 "${pkgdir}/usr/share/icons/spotube" install -dm755 "${pkgdir}/usr/share/applications" install -dm755 "${pkgdir}/usr/share/appdata" install -dm755 "${pkgdir}/usr/share/${pkgname}" install -dm755 "${pkgdir}/usr/bin" - cp -ra ./ "${pkgdir}/usr/share/${pkgname}" - cp ./spotube.desktop "${pkgdir}/usr/share/applications" - cp ./spotube-logo.png "${pkgdir}/usr/share/icons/${pkgname}" - cp ./com.github.KRTirtho.Spotube.appdata.xml "${pkgdir}/usr/share/appdata/spotube.appdata.xml" + + mv ./spotube.desktop "${pkgdir}/usr/share/applications" + mv ./spotube-logo.png "${pkgdir}/usr/share/icons/spotube/" + mv ./com.github.KRTirtho.Spotube.appdata.xml "${pkgdir}/usr/share/appdata/spotube.appdata.xml" + cp -ra ./data ./lib ./spotube "${pkgdir}/usr/share/${pkgname}" sed -i 's|com.github.KRTirtho.Spotube|spotube|' "${pkgdir}/usr/share/appdata/spotube.appdata.xml" - ln -s "/usr/share/${pkgname}/spotube" "${pkgdir}/usr/bin/${pkgname}" + ln -s "/usr/share/${pkgname}/spotube" "${pkgdir}/usr/bin/spotube" } From a433ed8aa52180c14c99b3e13fa346ab40027ddf Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 21 Feb 2022 10:27:25 +0600 Subject: [PATCH 13/25] fixed AUR package compatibilty with the newer pkgname --- aur-struct/.SRCINFO | 2 +- aur-struct/PKGBUILD | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/aur-struct/.SRCINFO b/aur-struct/.SRCINFO index 684aaf92..36027837 100644 --- a/aur-struct/.SRCINFO +++ b/aur-struct/.SRCINFO @@ -1,7 +1,7 @@ pkgbase = spotube-bin pkgdesc = A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed pkgver = 1.2.0 - pkgrel = 1 + pkgrel = 2 url = https://github.com/KRTirtho/spotube/ arch = x86_64 license = BSD-4-Clause diff --git a/aur-struct/PKGBUILD b/aur-struct/PKGBUILD index 4284e0f8..3e7ee60b 100644 --- a/aur-struct/PKGBUILD +++ b/aur-struct/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kingkor Roy Tirtho pkgname=spotube-bin pkgver=1.2.0 -pkgrel=1 +pkgrel=2 epoch= pkgdesc="A lightweight free Spotify desktop-client which handles playback manually, streams music using Youtube & no Spotify premium account is needed" arch=(x86_64) @@ -25,15 +25,16 @@ md5sums=(f49d21ef00c7d43eb70e7e9b2a7103c1) validpgpkeys=() package(){ - install -dm755 "${pkgdir}/usr/share/icons/${pkgname}" + install -dm755 "${pkgdir}/usr/share/icons/spotube" install -dm755 "${pkgdir}/usr/share/applications" install -dm755 "${pkgdir}/usr/share/appdata" install -dm755 "${pkgdir}/usr/share/${pkgname}" install -dm755 "${pkgdir}/usr/bin" - cp -ra ./ "${pkgdir}/usr/share/${pkgname}" - cp ./spotube.desktop "${pkgdir}/usr/share/applications" - cp ./spotube-logo.png "${pkgdir}/usr/share/icons/${pkgname}" - cp ./com.github.KRTirtho.Spotube.appdata.xml "${pkgdir}/usr/share/appdata/spotube.appdata.xml" + + mv ./spotube.desktop "${pkgdir}/usr/share/applications" + mv ./spotube-logo.png "${pkgdir}/usr/share/icons/spotube/" + mv ./com.github.KRTirtho.Spotube.appdata.xml "${pkgdir}/usr/share/appdata/spotube.appdata.xml" + cp -ra ./data ./lib ./spotube "${pkgdir}/usr/share/${pkgname}" sed -i 's|com.github.KRTirtho.Spotube|spotube|' "${pkgdir}/usr/share/appdata/spotube.appdata.xml" - ln -s "/usr/share/${pkgname}/spotube" "${pkgdir}/usr/bin/${pkgname}" + ln -s "/usr/share/${pkgname}/spotube" "${pkgdir}/usr/bin/spotube" } From 6b42c65cdb5e7ba7e9f1c314fa53fc64f290bdc8 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 24 Feb 2022 12:17:07 +0600 Subject: [PATCH 14/25] Updagraded to Flutter 2.10.2 --- pubspec.lock | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pubspec.lock b/pubspec.lock index 4802bacb..491d288a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -345,6 +345,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.12.11" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" meta: dependency: transitive description: @@ -629,7 +636,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.4.3" + version: "0.4.8" typed_data: dependency: transitive description: From a86b6bc40b1193cdd680c8da832b12c0c3bebaff Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 24 Feb 2022 13:26:21 +0600 Subject: [PATCH 15/25] persisted volume support Placeholder image instead of progress indicator in PlaybuttonCard Custom Fade Page Transition --- assets/placeholder.png | Bin 0 -> 8886 bytes lib/components/Album/AlbumCard.dart | 7 +- lib/components/Artist/ArtistCard.dart | 18 ++-- lib/components/Artist/ArtistProfile.dart | 5 +- lib/components/Category/CategoryCard.dart | 15 ++-- lib/components/Home.dart | 7 +- lib/components/Lyrics.dart | 7 +- lib/components/Player/Player.dart | 33 ++++++-- lib/components/Playlist/PlaylistCard.dart | 7 +- lib/components/Shared/PlaybuttonCard.dart | 7 +- lib/components/Shared/SpotubePageRoute.dart | 18 ++++ lib/components/Shared/TracksTableView.dart | 5 +- lib/helpers/artists-to-clickable-artists.dart | 5 +- lib/models/LocalStorageKeys.dart | 2 + pubspec.lock | 79 ++++++++---------- 15 files changed, 119 insertions(+), 96 deletions(-) create mode 100644 assets/placeholder.png create mode 100644 lib/components/Shared/SpotubePageRoute.dart diff --git a/assets/placeholder.png b/assets/placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..6f6d451f2c07a7141944461ac691b96c065f61be GIT binary patch literal 8886 zcmd6NcQo8x+paW81j(bskOa{QVbnx0>M#s4h+d-ih|XwF5-ki8HTn~y*MuN?!ie5S z52B7ih%ypwd^_iR*Ll}-&byxV{c-*{*0Ls~C@9Xf{{Fpm7JRacHm3uBn7vdCy>#8|ynHP^Y$+6M+^lTb5w4c@wmP%e*1x|FPAqJ`M@5%;+AP3D?4H| z%;Tt-MpIe2b-rUG{qvQTx-VJVi|Ujvh-AT|BmdXBUmtf0trx7vKbi3eBp=_uLh;ul zNhJyj93RCsEQLJgECrN`f*pS5PsjxdD+Y>46vZbakAdSG)z#J1^nW5(d>V>CV?OKe z7Vx#dKZoP{$DV$7{qOhucL)D^&%a*$KWY6}SpSOg-$DQPv;WZg?;-tPf&Xp5|J?cy zg8(D?ht|Ih@$X@RME|GOcVVXC6f0NGpA85II669th=^c~Qzh>m1e#YEkY%K$sY1`c z_o~sQgr&umm6b1FzU=Ss zKZ#t*pNFX^)_nZ8z? zSy>N!%j@gwYijftk~Gg*F%)T}xz|pg9PHYs^l%fI9lM0MTH5S7@%Y3*{-%(ipdd3f z1mbc!HQc?EK;CP)9pN5+xj=kF<92+OPUzI~crwKG$&)88E{SS++a0O7%It7Uk+=>q z7)+SR-*>dV{XXdQ_QAnHe1VXV5M9v{|A%%yO<6j2(#pcZ!p24^KC4D+;8OO06ur*F zhl5j7Q?s+?y1H%Ql$x^f9^zS9S;^F-qZ8tZu+W`5>l+&ztE-PG86DOvkq7+M1RPFz zkMReRorZj{O9()t`!gSObWn(s1{Z$(cu<#?AR2<5G?rbsYxC0D(Z_912 zS@TQKrekS+J^Jxud9n>(A^hbK229qkL(=-pj9Iv1I+#Q){}tbJ0&GYcBZ6 z)!#q$?Vz=fkB_5c5qNV!K?Hq1b2Ow6cQ@F|%F4@2{5$V(x>ES|a!-nByV5+frc zwHjqk(SV?diAh^q+xq%Cb=Z7mRaJ#)-SG5uPt$=oNo|_L3#7!xCe(Qw!Ex?r|NC@% zdwaT+oBpFmP^W@5*_cx5}W=9+! z86AS?wX@}BnuF3wnxYGzx~}E^Bp)9i>_%K=i+{V4Ah#947Hnu_WXTtan)vwolvt3> z!@~oiD!IwZD(Cm(+sV;SY85oLNL%|!?a>fcwWa~eFDxt!gTam&Y}-kjKR8pCwX+B8 zu_ZPg9UYvUoM1S$PiClKJS@Zr4Wy%klao670N=}UxjQ~Set1~tpsKL2@ZCF+V|xXK zXXhO<+&sLz7=p`B6gtV|=40kyuPXevqB_A46_xgx2H$129>Pez_ho3BLhe2y^RA9S z_mAC$J9qB1oxa}Cxa;v_VsdhFc9s@4ZA3<9#+#DA$LIEjall{Kb17uW_>00|ZvKY& zP(1?!4V$HjC-8CJ*{0+W?li-6-6{V$}@`xd)M`6M1V`aO9f!(*<@Yjlga&s-GCvLlL z+lj4J_jAk3`+df>eSCeHn3z(MlXD+R9C#JC%|jrNRU&a7tAXL*_G>cl8jOjd840!u z4i47P&eB7{e5XfUwcvD%}te#87VWqV4_UUl=s~0a`Y;SMd zz^uRu8sDL}eRZHV{Yy+q!b$a+{I~4WWBZ+Q{;t>{^0EJ9ZR!%Al8`HTV!}{|t$JrU z#lpwid;4=fOBf<|bm5l{m1q=|m6Icr#x1T~ zx#CE6SbOaSlBg5KLax!&)Euv{AfOc8;pXwI2qEa<_u9GPi(t#v+1cC21hsW{cRzdf ztiE3QizK;QZ|XQUHumS%T*&dS?bX%JLf>T5;GLx|-?^X;_7ObiOML?an$n=$u&uSV z1tTWo{fw_)oyW_)eSDe&4|I&rV`qK9u$-NvYyO0&&|NrtMrl@c|CCH7KYxBUTrUo+ zdAH^RJAeO!_Gl)!=2VI=C;TOf3xz@r?eEuQ`SJ+~ovf8qtS->t@p!Jq`_j_Va&qOT z`++56$TQG1ewG_IBsv`Y(tIr}vX`UB^K9c_A|e2JrDbGF#V-2yHhq8h*=1#+J=)*j zpQZUdKr8ae(Qs$N+b&&Q-QARyW0jiMSyJ=K7p)i&ZfkBc^+~HLZ^3}KT28a-oOxbh z6BF|QK91D%w*(rPnA~Vn!*JB4rKfLwt)nL-M5}9PY|S%bZ~gJs1Nd- z^S$vV=cG8q8ifP zyovM=-5?SzPrCK!g9VCjPJnHlLv9&vmy(cxs_(bgp-iS`W^OSvqgu?3jQkhB(0t*Q zsIIN`_3|pTurM|KaO_{s4j1eH@#Ed04-dc7ley-;(l$NGd-n|W_49*Iz`P*LZnEI` zg6%y#MC~5T&CRiSGO4U}_VqP+uO}Zq^!4-mwKjU^O9Uj0IoIyxWWSWS_`AS*p^OK< zcUR6S*xT75gT?tHhp4$Fq6Y}NwYvc0sbkId_V>S-1}Yl|P)0yhJP_3!648ewu1u0-!MW7`~0F?UswJ1Bx1@7FjmZRAN+YU-dehvvc12_ae z?tMa(-P5P9KEHQ#2E!`#+y3$62S7+_S}4`xty{O+8!Pc6BW-jol1b_5H$3CrD%n=3 zt;CfywX`<3w(giy!!Cq=eqS**HgXL!Sk&bC_6SmaMRP%jKZvJY%M`Y$-jFr+eVC9bHQ4J&g{p;%I0rFZH8~@H3*JC?7 z9tgF7dE?Xh=Z84vVeMXTwSxS72(uN#<%iiiwJ4B$^|Z_lG1nVn_zT%l)5i$rDg5q5>{ z-W4zxymP0dsL1lEs46HX&dwzZDvoP2PozMo3)En{s;a6+G5K4WQAX7b5eNik5QY7l z^mN_Eg{SwxqJyL&bqx#*jE#%^P|}QtXe)+-ZFEyMEJVL?VsP-2*ebtT8|~GrW_Ou5 zL5Ip<%xlCPu;MUCyQs;q!Xw6O*k5KC0rRe&9%oR8Y(#b4-QB&tC4yw3QRk)6B=3p* ze4b6y9QK%%pGU;?mnhsmafH`fwIYl39K*pzL3(i|B_-u|RM5hoG{n^#H*PSeB3Hbm zM@zh_ke8uRkX+hF!bD}&xYEI(Pf-T@KYvO|N&WtQOI3Y6V9Sic9`2Vp>{Kf&D_c9S zVeeU2boTe>VM=~pASFDP_xMg(X`)rQfm@4A`&^&yG%k$NlGp_ zF3Vf6U8klr$`6IKZ>gh5p-vzVwLE(y93MIRQ0}m`Rgpv#u@Q`DLm3#TV(74uLf0-8 z+G8r~`4|9dF;=*^x_;l>6cxqsRkFf@H@5v4go`z<+T0^?1#W-)eC|HA*gc#BeEREG zjK8%-ZxCf_DS!eP%wju74-0BJ*vDx@__*>ODp~pG^w@{5lJfvd>W{mIOzydsn^s&b zKyqT}v;i!PkR75wM@VKwTk$O_Nq~o3jrASW**RZCf|9Ko$J*XISUPwpLH!3*eZELg z##a$?Gc+bPmX(z?YcJ&fsS?JnzN}2h8`CYvIeJlozfv*!eqV2I#tsu8eyi&*hkC2u zzXuV&c;n8${d;4FU$W27?O8^X))>#JVC*<5#*Y9MPtfwa{1TNt&>?RtdUfJE&72@OzNIO6r9Ru}<*9i$gV_E3y>!VQa zEA&=$Xs?;2B^v_+CZ)5|e;_*`KhV8togG9%sHQGrYyW>(^TF>!I(9^U4fv_bvkWDBY;->q31YHBYpFXdv5B18I2IT=yWof+S` z_l%K49~dv4=YrFplrJwYk67agyI~bZ2yQyhii9SVK;q`+6IO{dm*Mu){b{$UW`i2+ zQpHV)i12Wm1cbY>g#}DTCS$No5dghh&>;>>5BigkkjS&t@8(Z&w#|NhqA$9)SCEdV zDGyaMIvh2yaA)K|0A?g%@35Dy#=G4oHK!JzoSZz{5^_3OWj``9vfa`)UF-G|l>nXZ z;u^*}reCaYX=$mh&htZ=A9TRT+do@Qg8<@@d6m^S$iEij;u~FP+q$m5ZWLQ8Tl&Q8 ze*(-8b(9+WKe2ABtPJw7@PdUl+!$ASR)pR8WgOLd#n-W^NE6kg6cqCrolIyJp z5j7~A{KT8;E#$-XU*ouuz+OsdT752b+-H3E(pF?0 z8pYst1Xylo3aC&(Nk#7l0MNaUq{9;7EaEh^-jd?tdwYAz&88+M0S7yIsFwI+x#0*N zap8-+NjV6XFM!*wCfPSMG=LW@o^~?(#h<|yl0em_JGoB$79JkXVWG}+x<}Ntc6UZ*IZGdefY66g`S?i z%aG1uHaWooEhs3+#`Y zmeaeBAJr6!fu@Gei)Jn$_O?3;Z*of9Y*oPIXOxwdDW|(fv<*#8GM}jHRMZ0=;QUdA zah~(lWteSHkSscE%ahR0$i&23L4OvS<{{qeSzK0@lrM)C26lngn(%!wu{Q1i8am^v z_)pCulsaDhoZJ|jwVpf}^EN-J7ssb#t6frBipe4@ZTrQ~ z45|9))whI}mX)RMSLK>b01yr&dPy(BWCfB0cYnM@-543%<3E%bnS%5C@R`tA?Ua20 zLN=<+HDivC*=edE5~8BM;W#=v8igMFFqaG&O>pL6Y5|E!y=ZO4K$+kZY;Dasv{hVO z97Wb?d$3xRsrg{i-WNDXJOi!|OW7HB`TYv^NG_sQ_++j#^s>B3DJe`%`=R1f;leFG zR9zRu27XQ&@1?4Hf;m;SumvnjW3@J>8Owlkkd_LH3Q|&MO%SGi=ap46HF$mp5t$Xr z7IbR|8Q;Q_{T#SBh7!eM@V*w+1W1goX!6VZx_gATfBnV&w&Tf&{WWYV9X85HaHXSg z!{tkPyDEl7tv+E@m|g&oCM$d}7e;);k(--)$O0|Hnh@gJGUtYnpNQp-uH&;3Co}F= z_BQDwFs%>(!Yt+JJ8RKPWotNce#H2Cwaw+T?I(W>kMr(5sETERI*Co)Rp9dhv{Qn= zSh2)P9M}YJ-3lsr;bJ*N-4Bk*NS)ho`WUg2U?5nZD9-h_qpk3EA2KcV@Jz{i*o+&p zx_daz1Z^JW5a!d}cpt~@RH^g^{{FDH5!WaS(S=}^+_LG zVWz{qp#0f8XP^BUqwZ)WKKcCf<2_Z5AxILolKX@PN+on0+!C&+GlVrtdW&VQUWF32X`@_oPdoJ^%dI zLGA8zSasFsoU3juo02t7U+zGuoMR87bn|#P;fpd;se<*_#vU?gWaG}!Mh#QzH7|e9b5OA_6Z+EL!SwnHTyR(U0keUq zLyor*-yyLjNT^Xjh(WHxYYQb4GsiiTcfdg^Wyw2=-hF7lzj@eF5}T&&t^@iLCneiT z@A=+1RN;|v++-|0IRO&*#JZS|B2$ZZ)Is_dkGIFYo3A0GjouDxAnyfyP=>2ic`SWD z^5qUq>sFi+s~aN0$7*+-trMNZN0qaehGaNnE<C8aWi!5tm>p=mmT84oPgFvECy?EYVbO->a|a#E7fud9~DK5MvFAMU+wtIEos zogzJnMBy|B1oMDFw5qXGNig)&;7fU-%M0f$@Xb{j6mv}c-7^e6DrDmC4+9+q=b@~)!pUquSB7P{tu42vEYuiY0ZBLU#-m4%sQIBRj_PCMh&3*ssfNzRir%<9d>BlLXElAw1qkj zbwCknjbA6N)giNONk|(j1|AaW^J9^E_9QdD;lWhofLR%_J3jwoX{q4Xxzh6THH@`$ z!QdZQ5q|h+fQGks^&?w}bWuht@tre+%-GlWD4|a02>qF2B5-FXC&2@QRiol4h$b8p zOg=UmI5PCBiw7Fbi*jyi%2)u$Cq-M#c>Q`BOmH4sIleyXpPB{o;W~z2uRC%G34x(w z;uo$J4>4HoJYqRu533NaGrgPXyk+7o!vlX?`^ShKNWfta5S+WGm%nb)C3G1$IypPH z@ruKO4s)w&2mQ-0;|lu@(fqPGEiMiYA}s;S9?D1_?To^b62RH-DcfVZFUHdVNI`mf zR`o+-HoxVvNcgk8dx}V8u-~EiBPJ$hr(pW~*ug7>-8dYMkJ{m6?&~b3ckE+S^q!-BiFt)ZmAQa|qMGNTe2Yqi_Fyw*XJfr_uVw$raRf zeB9XoBIl8m`!(!4QFtE3%guG~=JassNFfJ*+wrNXG{*Xmxm8%H0|*j$3}7PB-N`^C ztTvm^cK1%FMh@w|5p!3R^!2-g)YK#fmHo0I~@(Ft_5^gcCMTce%HLwp>@k;XIU_PpVZZmi9p9@Ww9Q_U1gJ74eYHx z&Jr9&Vy3dZLeMLZH$ixUg38UMrKQMyTPwsxEzB3)WwSaDZiL(2iy8$Z#BIMN_Tt^L zR(EPE{Ql8&o<_=EV;k_-bFK`7V})FDE7M)m$0hBPSCkh!KYvzea~r$(Nd49T>1{H26VqxeUNp zwEoS|l%#ECox_2E1D>Pkj;)GHSU>#*D55J67`>AEic1uZc*Z)AZeTkCGiP@IrphK! z{W{Pd!D}TxL#4xio7C90vEi~;o!{In*Cu=(5$!a3F4T2q>U92U$e_i~(Xp}F<{%UD zA~5)YA16JhDR*Q31_JOs#uEdsEV&?-$l@*#^{`T!#Zf}y(UmHd`+HVhNIxlB}|5h7*Ry zUvmX5Ef&)1H5#HDp`AYWWn>bucpx`5G@{R&y1Q==4ZRNC*xW2*4+@G?DHeYCVPCsb$3=RE&MH@a9h%1yRH?8C7kWwn=RU6=&SwNiDf};YUBRC!%`cC#m zYL%>-Hj|O_{sw#3lfM-|msnCd1txm>kBNJiAM(S1O3pLoRuanfe48&uLrd#Hj)C;u zqjLgkRde4Hqq)lc$JNYeOh$QaZOVEm&LKSBX%9>YLR062sq%4eR`Q1fn-pP-G5;4g zBT)f>fH#Jm23I=k7~W;p4spMC(;{$N1z>TT2Jj$uc6MMVL~R6mukNz8xsuGDAd+6! z+w&?%lhlrl1vz{+=F?YG*e$dzzKRYG7h{)Qu_OrCzQqE$Qj&kKyrZ7X$*>u0ZIp zBJtp8bH>fuT4_g*JWbfW@Iqm7o$AV!wyP;AG29JoJz3L{b6dc)hJl0W^JnM1uV!fR zn!JKs_>(#cb$EedFzkQdDELoD_y7MM&i}u;VE}F?{7;AY_d5jt=XH#~w}LANYB^uU zPQ$0a+r6Rq)9HWR*!WNR1*7=)odfof<7|p^6hFE8$`h$`hrvY@3WSQ5a=GH;=l=yk C02ti> literal 0 HcmV?d00001 diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index e5ea9423..bdc4d864 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; 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'; @@ -27,10 +28,8 @@ class AlbumCard extends ConsumerWidget { description: "Album • ${artistsToString(album.artists ?? [])}", onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - return AlbumView(album); - }, + Navigator.of(context).push(SpotubePageRoute( + child: AlbumView(album), )); }, onPlaybuttonPressed: () async { diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 0f5099e8..6f92cb59 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; class ArtistCard extends StatelessWidget { final Artist artist; @@ -9,12 +10,15 @@ class ArtistCard extends StatelessWidget { @override Widget build(BuildContext context) { + final backgroundImage = CachedNetworkImageProvider((artist + .images?.isNotEmpty ?? + false) + ? artist.images!.first.url! + : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"); return InkWell( onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - return ArtistProfile(artist.id!); - }, + Navigator.of(context).push(SpotubePageRoute( + child: ArtistProfile(artist.id!), )); }, borderRadius: BorderRadius.circular(10), @@ -38,11 +42,7 @@ class ArtistCard extends StatelessWidget { CircleAvatar( maxRadius: 80, minRadius: 20, - backgroundImage: CachedNetworkImageProvider((artist - .images?.isNotEmpty ?? - false) - ? artist.images!.first.url! - : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"), + backgroundImage: backgroundImage, ), Text( artist.name!, diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index a947c37b..8b2b0cdd 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -7,6 +7,7 @@ import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/readable-number.dart'; @@ -215,8 +216,8 @@ class ArtistProfile extends ConsumerWidget { TextButton( child: const Text("See All"), onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) => ArtistAlbumView( + Navigator.of(context).push(SpotubePageRoute( + child: ArtistAlbumView( artistId, snapshot.data?.name ?? "KRTX", ), diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 19811f70..99745883 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/provider/SpotifyDI.dart'; class CategoryCard extends StatelessWidget { @@ -30,14 +31,12 @@ class CategoryCard extends StatelessWidget { TextButton( onPressed: () { Navigator.of(context).push( - MaterialPageRoute( - builder: (context) { - return PlaylistGenreView( - category.id!, - category.name!, - playlists: playlists, - ); - }, + SpotubePageRoute( + child: PlaylistGenreView( + category.id!, + category.name!, + playlists: playlists, + ), ), ); }, diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 200433f3..2d6cfa97 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -16,6 +16,7 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; @@ -228,10 +229,8 @@ class _HomeState extends ConsumerState { IconButton( icon: const Icon(Icons.settings_outlined), onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - return const Settings(); - }, + Navigator.of(context).push(SpotubePageRoute( + child: const Settings(), )); }), ], diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 8d42e8e0..71db1357 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -3,6 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Settings.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/getLyrics.dart'; import 'package:spotube/provider/Playback.dart'; @@ -68,10 +69,8 @@ class Lyrics extends HookConsumerWidget { ), ElevatedButton( onPressed: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - return const Settings(); - }, + Navigator.of(context).push(SpotubePageRoute( + child: const Settings(), )); }, child: const Text("Add Access Token")) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 41cbe480..96c70f4f 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -4,12 +4,14 @@ import 'package:cached_network_image/cached_network_image.dart'; 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:spotify/spotify.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; 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/LocalStorageKeys.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -20,14 +22,18 @@ class Player extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { - var _isPlaying = useState(false); - var _shuffled = useState(false); - var _volume = useState(0.0); - var _duration = useState(null); - var _currentTrackId = useState(null); + final _isPlaying = useState(false); + final _shuffled = useState(false); + final _volume = useState(0.0); + final _duration = useState(null); + final _currentTrackId = useState(null); - AudioPlayer player = useMemoized(() => AudioPlayer(), []); - YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); + final AudioPlayer player = useMemoized(() => AudioPlayer(), []); + final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); + final Future future = + useMemoized(SharedPreferences.getInstance); + final AsyncSnapshot localStorage = + useFuture(future, initialData: null); var _movePlaylistPositionBy = useCallback((int pos) { Playback playback = ref.read(playbackProvider); @@ -53,8 +59,6 @@ class Player extends HookConsumerWidget { }, [_duration]); useEffect(() { - _volume.value = player.volume; - var playingStreamListener = player.playingStream.listen((playing) async { _isPlaying.value = playing; }); @@ -97,6 +101,13 @@ class Player extends HookConsumerWidget { }; }, []); + useEffect(() { + if (localStorage.hasData) { + _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? + player.volume; + } + }, [localStorage.data]); + var _playTrack = useCallback((Track currentTrack, Playback playback) async { try { if (currentTrack.id != _currentTrackId.value) { @@ -285,6 +296,10 @@ class Player extends HookConsumerWidget { try { await player.setVolume(value).then((_) { _volume.value = value; + localStorage.data?.setDouble( + LocalStorageKeys.volume, + value, + ); }); } catch (e, stack) { print("[VolumeSlider.onChange()] $e"); diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 862ee9fc..2da322ee 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -3,6 +3,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -20,10 +21,8 @@ class PlaylistCard extends ConsumerWidget { imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, onTap: () { - Navigator.of(context).push(MaterialPageRoute( - builder: (context) { - return PlaylistView(playlist); - }, + Navigator.of(context).push(SpotubePageRoute( + child: PlaylistView(playlist), )); }, onPlaybuttonPressed: () async { diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index 42b350e0..d0561b2b 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -46,11 +46,8 @@ class PlaybuttonCard extends StatelessWidget { borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( imageUrl: imageUrl, - progressIndicatorBuilder: (context, url, progress) { - return CircularProgressIndicator.adaptive( - value: progress.progress, - ); - }, + placeholder: (context, url) => + Image.asset("assets/placeholder.png"), ), ), Positioned.directional( diff --git a/lib/components/Shared/SpotubePageRoute.dart b/lib/components/Shared/SpotubePageRoute.dart new file mode 100644 index 00000000..0cc47129 --- /dev/null +++ b/lib/components/Shared/SpotubePageRoute.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class SpotubePageRoute extends PageRouteBuilder { + final Widget child; + SpotubePageRoute({required this.child}) + : super( + pageBuilder: (context, animation, secondaryAnimation) => child, + ); + + @override + Widget buildTransitions(BuildContext context, Animation animation, + Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + } +} diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index 9ae45a14..b3a6dffb 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/LinkText.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; @@ -83,8 +84,8 @@ class TracksTableView extends ConsumerWidget { Expanded( child: LinkText( track.value.album!.name!, - MaterialPageRoute( - builder: (context) => AlbumView(track.value.album!), + SpotubePageRoute( + child: AlbumView(track.value.album!), ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/helpers/artists-to-clickable-artists.dart b/lib/helpers/artists-to-clickable-artists.dart index a4b11fa5..0cb5a7f6 100644 --- a/lib/helpers/artists-to-clickable-artists.dart +++ b/lib/helpers/artists-to-clickable-artists.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Shared/LinkText.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; Widget artistsToClickableArtists( List artists, { @@ -19,8 +20,8 @@ Widget artistsToClickableArtists( (artist.key != artists.length - 1) ? "${artist.value.name}, " : artist.value.name!, - MaterialPageRoute( - builder: (context) => ArtistProfile(artist.value.id!), + SpotubePageRoute( + child: ArtistProfile(artist.value.id!), ), overflow: TextOverflow.ellipsis, ), diff --git a/lib/models/LocalStorageKeys.dart b/lib/models/LocalStorageKeys.dart index 70b5936a..1af5088c 100644 --- a/lib/models/LocalStorageKeys.dart +++ b/lib/models/LocalStorageKeys.dart @@ -10,4 +10,6 @@ abstract class LocalStorageKeys { static String nextTrackHotKey = "next_track_hot_key"; static String prevTrackHotKey = "prev_track_hot_key"; static String playPauseHotKey = "play_pause_hot_key"; + + static String volume = "volume"; } diff --git a/pubspec.lock b/pubspec.lock index 491d288a..0ad07331 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -14,7 +14,7 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "3.1.8" + version: "3.2.1" args: dependency: transitive description: @@ -180,7 +180,7 @@ packages: name: flutter_blurhash url: "https://pub.dartlang.org" source: hosted - version: "0.6.0" + version: "0.6.4" flutter_cache_manager: dependency: transitive description: @@ -225,7 +225,7 @@ packages: name: freezed_annotation url: "https://pub.dartlang.org" source: hosted - version: "0.14.3" + version: "1.1.0" hooks_riverpod: dependency: "direct main" description: @@ -267,7 +267,7 @@ packages: name: image url: "https://pub.dartlang.org" source: hosted - version: "3.1.1" + version: "3.1.3" infinite_scroll_pagination: dependency: "direct main" description: @@ -275,13 +275,6 @@ 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: @@ -302,7 +295,7 @@ packages: name: just_audio url: "https://pub.dartlang.org" source: hosted - version: "0.9.18" + version: "0.9.20" just_audio_libwinmedia: dependency: "direct main" description: @@ -316,14 +309,14 @@ packages: name: just_audio_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "4.0.0" + version: "4.1.0" just_audio_web: dependency: transitive description: name: just_audio_web url: "https://pub.dartlang.org" source: hosted - version: "0.4.2" + version: "0.4.4" libwinmedia: dependency: transitive description: @@ -365,7 +358,7 @@ packages: name: msix url: "https://pub.dartlang.org" source: hosted - version: "2.8.1" + version: "2.8.18" oauth2: dependency: transitive description: @@ -400,7 +393,7 @@ packages: name: path_provider url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.0.9" path_provider_android: dependency: transitive description: @@ -421,28 +414,28 @@ packages: name: path_provider_linux url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_macos: dependency: transitive description: name: path_provider_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" pedantic: dependency: transitive description: @@ -470,7 +463,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.1.2" process: dependency: transitive description: @@ -498,35 +491,35 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.13" shared_preferences_android: dependency: transitive description: name: shared_preferences_android url: "https://pub.dartlang.org" source: hosted - version: "2.0.9" + version: "2.0.11" shared_preferences_ios: dependency: transitive description: name: shared_preferences_ios url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.1.0" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_platform_interface: dependency: transitive description: @@ -540,14 +533,14 @@ packages: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "2.0.3" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.3" + version: "2.1.0" sky_engine: dependency: transitive description: flutter @@ -580,14 +573,14 @@ packages: name: sqflite url: "https://pub.dartlang.org" source: hosted - version: "2.0.1" + version: "2.0.2" sqflite_common: dependency: transitive description: name: sqflite_common url: "https://pub.dartlang.org" source: hosted - version: "2.1.0" + version: "2.2.0" stack_trace: dependency: transitive description: @@ -650,63 +643,63 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "6.0.17" + version: "6.0.20" url_launcher_android: dependency: transitive description: name: url_launcher_android url: "https://pub.dartlang.org" source: hosted - version: "6.0.13" + version: "6.0.15" url_launcher_ios: dependency: transitive description: name: url_launcher_ios url: "https://pub.dartlang.org" source: hosted - version: "6.0.13" + version: "6.0.15" url_launcher_linux: dependency: transitive description: name: url_launcher_linux url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.0.4" + version: "2.0.5" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.0.8" url_launcher_windows: dependency: transitive description: name: url_launcher_windows url: "https://pub.dartlang.org" source: hosted - version: "2.0.2" + version: "3.0.0" uuid: dependency: transitive description: name: uuid url: "https://pub.dartlang.org" source: hosted - version: "3.0.5" + version: "3.0.6" vector_math: dependency: transitive description: @@ -720,14 +713,14 @@ packages: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.3" + version: "2.4.1" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: transitive description: @@ -748,7 +741,7 @@ packages: name: youtube_explode_dart url: "https://pub.dartlang.org" source: hosted - version: "1.10.8" + version: "1.10.9+1" sdks: dart: ">=2.15.1 <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" From 5b389564c1be6b324f45563fc47ccedcab0ff218 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 24 Feb 2022 19:43:18 +0600 Subject: [PATCH 16/25] v2 Roadmap: Responsive navigation added for three different breakpoints --- lib/components/Home.dart | 98 +++----------- lib/components/Home/Sidebar.dart | 120 ++++++++++++++++++ lib/components/Home/SpotubeNavigationBar.dart | 30 +++++ lib/main.dart | 10 +- 4 files changed, 180 insertions(+), 78 deletions(-) create mode 100644 lib/components/Home/Sidebar.dart create mode 100644 lib/components/Home/SpotubeNavigationBar.dart diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 2d6cfa97..1d0339d5 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -1,26 +1,24 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart' hide Page; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:oauth2/oauth2.dart' show AuthorizationException; import 'package:spotify/spotify.dart' hide Image, Player, Search; + import 'package:spotube/components/Category/CategoryCard.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; +import 'package:spotube/components/Home/SpotubeNavigationBar.dart'; import 'package:spotube/components/Login.dart'; import 'package:spotube/components/Lyrics.dart'; import 'package:spotube/components/Search/Search.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Player/Player.dart'; -import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; -import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; -import 'package:spotube/models/sideBarTiles.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -135,10 +133,14 @@ class _HomeState extends ConsumerState { super.dispose(); } + _onSelectedIndexChanged(int index) => setState(() { + _selectedIndex = index; + }); + @override Widget build(BuildContext context) { Auth auth = ref.watch(authProvider); - SpotifyApi spotify = ref.watch(spotifyProvider); + final width = MediaQuery.of(context).size.width; if (!auth.isLoggedIn) { return const Login(); } @@ -153,7 +155,12 @@ class _HomeState extends ConsumerState { child: Row( children: [ Container( - constraints: const BoxConstraints(maxWidth: 256), + constraints: BoxConstraints( + maxWidth: width > 400 && width <= 700 + ? 72 + : width > 700 + ? 256 + : 0), color: Theme.of(context).navigationRailTheme.backgroundColor, child: MoveWindow(), @@ -168,76 +175,9 @@ class _HomeState extends ConsumerState { Expanded( child: Row( children: [ - NavigationRail( - destinations: sidebarTileList - .map((e) => NavigationRailDestination( - icon: Icon(e.icon), - label: Text( - e.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - )) - .toList(), + Sidebar( selectedIndex: _selectedIndex, - onDestinationSelected: (value) => setState(() { - _selectedIndex = value; - }), - extended: true, - leading: Padding( - padding: const EdgeInsets.only(left: 15), - child: Row(children: [ - Image.asset( - "assets/spotube-logo.png", - height: 50, - width: 50, - ), - const SizedBox( - width: 10, - ), - Text("Spotube", - style: Theme.of(context).textTheme.headline4), - ]), - ), - trailing: FutureBuilder( - future: spotify.me.get(), - builder: (context, snapshot) { - var avatarImg = imageToUrlString(snapshot.data?.images, - index: (snapshot.data?.images?.length ?? 1) - 1); - return Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - CircleAvatar( - backgroundImage: - CachedNetworkImageProvider(avatarImg), - ), - const SizedBox(width: 10), - Text( - snapshot.data?.displayName ?? "User's name", - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ], - ), - IconButton( - icon: const Icon(Icons.settings_outlined), - onPressed: () { - Navigator.of(context).push(SpotubePageRoute( - child: const Settings(), - )); - }), - ], - ), - ); - }, - ), + onSelectedIndexChanged: _onSelectedIndexChanged, ), // contents of the spotify if (_selectedIndex == 0) @@ -261,7 +201,11 @@ class _HomeState extends ConsumerState { ), ), // player itself - const Player() + const Player(), + SpotubeNavigationBar( + selectedIndex: _selectedIndex, + onSelectedIndexChanged: _onSelectedIndexChanged, + ), ], ), ); diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart new file mode 100644 index 00000000..f7b5be46 --- /dev/null +++ b/lib/components/Home/Sidebar.dart @@ -0,0 +1,120 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:flutter/material.dart'; +import 'package:spotify/spotify.dart' hide Image; +import 'package:spotube/components/Settings.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; +import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/provider/SpotifyDI.dart'; + +import '../../models/sideBarTiles.dart'; + +class Sidebar extends HookConsumerWidget { + final int selectedIndex; + final void Function(int) onSelectedIndexChanged; + + const Sidebar({ + required this.selectedIndex, + required this.onSelectedIndexChanged, + Key? key, + }) : super(key: key); + + Widget _buildSmallLogo() { + return Image.asset( + "assets/spotube-logo.png", + height: 50, + width: 50, + ); + } + + void _goToSettings(BuildContext context) { + Navigator.of(context).push(SpotubePageRoute( + child: const Settings(), + )); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final width = MediaQuery.of(context).size.width; + if (width <= 400) return Container(); + final extended = useState(false); + final SpotifyApi spotify = ref.watch(spotifyProvider); + useEffect(() { + if (width <= 700 && extended.value) { + extended.value = false; + } else if (width > 700 && !extended.value) { + extended.value = true; + } + }); + + return NavigationRail( + destinations: sidebarTileList + .map((e) => NavigationRailDestination( + icon: Icon(e.icon), + label: Text( + e.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + )) + .toList(), + selectedIndex: selectedIndex, + onDestinationSelected: onSelectedIndexChanged, + extended: extended.value, + leading: extended.value + ? Padding( + padding: const EdgeInsets.only(left: 15), + child: Row(children: [ + _buildSmallLogo(), + const SizedBox( + width: 10, + ), + Text("Spotube", style: Theme.of(context).textTheme.headline4), + ]), + ) + : _buildSmallLogo(), + trailing: FutureBuilder( + future: spotify.me.get(), + builder: (context, snapshot) { + var avatarImg = imageToUrlString(snapshot.data?.images, + index: (snapshot.data?.images?.length ?? 1) - 1); + return extended.value + ? Padding( + padding: const EdgeInsets.all(16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + CircleAvatar( + backgroundImage: + CachedNetworkImageProvider(avatarImg), + ), + const SizedBox(width: 10), + Text( + snapshot.data?.displayName ?? "User's name", + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + IconButton( + icon: const Icon(Icons.settings_outlined), + onPressed: () => _goToSettings(context)), + ], + )) + : InkWell( + onTap: () => _goToSettings(context), + child: CircleAvatar( + backgroundImage: CachedNetworkImageProvider(avatarImg), + ), + ); + }, + ), + ); + } +} diff --git a/lib/components/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart new file mode 100644 index 00000000..f57383ad --- /dev/null +++ b/lib/components/Home/SpotubeNavigationBar.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/models/sideBarTiles.dart'; + +class SpotubeNavigationBar extends HookWidget { + final int selectedIndex; + final void Function(int) onSelectedIndexChanged; + + const SpotubeNavigationBar({ + required this.selectedIndex, + required this.onSelectedIndexChanged, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final width = MediaQuery.of(context).size.width; + + if (width > 400) return Container(); + return NavigationBar( + destinations: sidebarTileList + .map( + (e) => NavigationDestination(icon: Icon(e.icon), label: e.title), + ) + .toList(), + selectedIndex: selectedIndex, + onDestinationSelected: onSelectedIndexChanged, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c3f73d3f..cd81052a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,7 +13,7 @@ void main() async { await hotKeyManager.unregisterAll(); runApp(ProviderScope(child: MyApp())); doWhenWindowReady(() { - appWindow.minSize = const Size(900, 700); + appWindow.minSize = const Size(280, 700); appWindow.size = const Size(900, 700); appWindow.alignment = Alignment.center; appWindow.maximize(); @@ -88,6 +88,10 @@ class MyApp extends HookConsumerWidget { color: Colors.grey[850], ), ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: Colors.blueGrey[50], + height: 55, + ), cardTheme: CardTheme( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), @@ -122,6 +126,10 @@ class MyApp extends HookConsumerWidget { backgroundColor: Colors.blueGrey[800], unselectedIconTheme: const IconThemeData(opacity: 1), ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: Colors.blueGrey[800], + height: 55, + ), cardTheme: CardTheme( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), From 584f431b04741f6699c505c5d1d964e01c1827a9 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 26 Feb 2022 10:56:32 +0600 Subject: [PATCH 17/25] Responsive Navigation for tablet & small devices Responsive design utilites created --- lib/components/Category/CategoryCard.dart | 25 ++- lib/components/Home.dart | 176 +++++++++--------- lib/components/Home/Sidebar.dart | 38 ++-- lib/components/Home/SpotubeNavigationBar.dart | 22 ++- lib/components/Playlist/PlaylistCard.dart | 1 + lib/components/Shared/PlaybuttonCard.dart | 155 +++++++-------- lib/hooks/useAsyncEffect.dart | 18 ++ lib/hooks/useBreakpointValue.dart | 17 ++ lib/hooks/useBreakpoints.dart | 99 ++++++++++ lib/hooks/usePagingController.dart | 53 ++++++ lib/hooks/useSharedPreferences.dart | 9 + lib/provider/Auth.dart | 5 + lib/provider/SpotifyDI.dart | 1 + 13 files changed, 423 insertions(+), 196 deletions(-) create mode 100644 lib/hooks/useAsyncEffect.dart create mode 100644 lib/hooks/useBreakpointValue.dart create mode 100644 lib/hooks/useBreakpoints.dart create mode 100644 lib/hooks/usePagingController.dart create mode 100644 lib/hooks/useSharedPreferences.dart diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 99745883..03b7440b 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart' hide Page; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class CategoryCard extends StatelessWidget { +class CategoryCard extends HookWidget { final Category category; final Iterable? playlists; const CategoryCard( @@ -45,9 +47,10 @@ class CategoryCard extends StatelessWidget { ], ), ), - Consumer( + HookConsumer( builder: (context, ref, child) { SpotifyApi spotifyApi = ref.watch(spotifyProvider); + final scrollController = useScrollController(); return FutureBuilder>( future: playlists == null ? (category.id != "user-featured-playlists" @@ -65,12 +68,18 @@ class CategoryCard extends StatelessWidget { child: CircularProgressIndicator.adaptive(), ); } - return Wrap( - spacing: 20, - runSpacing: 20, - children: snapshot.data! - .map((playlist) => PlaylistCard(playlist)) - .toList(), + return Scrollbar( + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: snapshot.data! + .map((playlist) => PlaylistCard(playlist)) + .toList(), + ), + ), ); }); }, diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 1d0339d5..1fc1f750 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -2,9 +2,9 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart' hide Page; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:oauth2/oauth2.dart' show AuthorizationException; import 'package:spotify/spotify.dart' hide Image, Player, Search; @@ -18,6 +18,9 @@ import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/helpers/oauth-login.dart'; +import 'package:spotube/hooks/useBreakpointValue.dart'; +import 'package:spotube/hooks/usePagingController.dart'; +import 'package:spotube/hooks/useSharedPreferences.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -32,40 +35,70 @@ List spotifyScopes = [ "playlist-read-collaborative" ]; -class Home extends ConsumerStatefulWidget { +class Home extends HookConsumerWidget { const Home({Key? key}) : super(key: key); @override - _HomeState createState() => _HomeState(); -} + Widget build(BuildContext context, ref) { + Auth auth = ref.watch(authProvider); -class _HomeState extends ConsumerState { - final PagingController _pagingController = - PagingController(firstPageKey: 0); + final pagingController = + usePagingController(firstPageKey: 0); + final int titleBarDragMaxWidth = useBreakpointValue( + md: 72, + lg: 256, + sm: 0, + xl: 0, + xxl: 0, + ); + final _selectedIndex = useState(0); + _onSelectedIndexChanged(int index) => _selectedIndex.value = index; - int _selectedIndex = 0; + final localStorage = useSharedPreferences(); - @override - void initState() { - super.initState(); - WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { - SharedPreferences localStorage = await SharedPreferences.getInstance(); - String? clientId = localStorage.getString(LocalStorageKeys.clientId); - String? clientSecret = + useEffect(() { + if (localStorage == null) return null; + final String? clientId = + localStorage.getString(LocalStorageKeys.clientId); + final String? clientSecret = localStorage.getString(LocalStorageKeys.clientSecret); - String? accessToken = + final String? accessToken = localStorage.getString(LocalStorageKeys.accessToken); - String? refreshToken = + final String? refreshToken = localStorage.getString(LocalStorageKeys.refreshToken); - String? expirationStr = + final String? expirationStr = localStorage.getString(LocalStorageKeys.expiration); - DateTime? expiration = - expirationStr != null ? DateTime.parse(expirationStr) : null; - try { - Auth auth = ref.read(authProvider); + listener(pageKey) async { + final spotify = ref.read(spotifyProvider); + try { + Page categories = + await spotify.categories.list(country: "US").getPage(15, pageKey); + var items = categories.items!.toList(); + if (pageKey == 0) { + Category category = Category(); + category.id = "user-featured-playlists"; + category.name = "Featured"; + items.insert(0, category); + } + + if (categories.isLast && categories.items != null) { + pagingController.appendLastPage(items); + } else if (categories.items != null) { + pagingController.appendPage(items, categories.nextOffset); + } + } catch (e, stack) { + pagingController.error = e; + print("[Home.pagingController.addPageRequestListener] $e"); + print(stack); + } + } + + try { + final DateTime? expiration = + expirationStr != null ? DateTime.parse(expirationStr) : null; if (clientId != null && clientSecret != null) { - SpotifyApi spotifyApi = SpotifyApi( + SpotifyApi spotify = SpotifyApi( SpotifyApiCredentials( clientId, clientSecret, @@ -75,47 +108,27 @@ class _HomeState extends ConsumerState { scopes: spotifyScopes, ), ); - SpotifyApiCredentials credentials = await spotifyApi.getCredentials(); - if (credentials.accessToken?.isNotEmpty ?? false) { - auth.setAuthState( - clientId: clientId, - clientSecret: clientSecret, - accessToken: - credentials.accessToken, // accessToken can be new/refreshed - refreshToken: refreshToken, - expiration: credentials.expiration, - isLoggedIn: true, - ); - } + spotify.getCredentials().then((credentials) { + if (credentials.accessToken?.isNotEmpty ?? false) { + auth.setAuthState( + clientId: clientId, + clientSecret: clientSecret, + accessToken: + credentials.accessToken, // accessToken can be new/refreshed + refreshToken: refreshToken, + expiration: credentials.expiration, + isLoggedIn: true, + ); + } + return null; + }).then((_) { + pagingController.addPageRequestListener(listener); + }); } - _pagingController.addPageRequestListener((pageKey) async { - try { - SpotifyApi spotifyApi = ref.read(spotifyProvider); - Page categories = await spotifyApi.categories - .list(country: "US") - .getPage(15, pageKey); - - var items = categories.items!.toList(); - if (pageKey == 0) { - Category category = Category(); - category.id = "user-featured-playlists"; - category.name = "Featured"; - items.insert(0, category); - } - - if (categories.isLast && categories.items != null) { - _pagingController.appendLastPage(items); - } else if (categories.items != null) { - _pagingController.appendPage(items, categories.nextOffset); - } - } catch (e) { - _pagingController.error = e; - } - }); } on AuthorizationException catch (_) { if (clientId != null && clientSecret != null) { oauthLogin( - ref.read(authProvider), + auth, clientId: clientId, clientSecret: clientSecret, ); @@ -124,23 +137,11 @@ class _HomeState extends ConsumerState { print("[Home.initState]: $e"); print(stack); } - }); - } + return () { + pagingController.removePageRequestListener(listener); + }; + }, [localStorage]); - @override - void dispose() { - _pagingController.dispose(); - super.dispose(); - } - - _onSelectedIndexChanged(int index) => setState(() { - _selectedIndex = index; - }); - - @override - Widget build(BuildContext context) { - Auth auth = ref.watch(authProvider); - final width = MediaQuery.of(context).size.width; if (!auth.isLoggedIn) { return const Login(); } @@ -156,11 +157,8 @@ class _HomeState extends ConsumerState { children: [ Container( constraints: BoxConstraints( - maxWidth: width > 400 && width <= 700 - ? 72 - : width > 700 - ? 256 - : 0), + maxWidth: titleBarDragMaxWidth.toDouble(), + ), color: Theme.of(context).navigationRailTheme.backgroundColor, child: MoveWindow(), @@ -176,16 +174,16 @@ class _HomeState extends ConsumerState { child: Row( children: [ Sidebar( - selectedIndex: _selectedIndex, + selectedIndex: _selectedIndex.value, onSelectedIndexChanged: _onSelectedIndexChanged, ), // contents of the spotify - if (_selectedIndex == 0) + if (_selectedIndex.value == 0) Expanded( child: Padding( padding: const EdgeInsets.all(8.0), child: PagedListView( - pagingController: _pagingController, + pagingController: pagingController, builderDelegate: PagedChildBuilderDelegate( itemBuilder: (context, item, index) { return CategoryCard(item); @@ -194,16 +192,16 @@ class _HomeState extends ConsumerState { ), ), ), - if (_selectedIndex == 1) const Search(), - if (_selectedIndex == 2) const UserLibrary(), - if (_selectedIndex == 3) const Lyrics(), + if (_selectedIndex.value == 1) const Search(), + if (_selectedIndex.value == 2) const UserLibrary(), + if (_selectedIndex.value == 3) const Lyrics(), ], ), ), // player itself const Player(), SpotubeNavigationBar( - selectedIndex: _selectedIndex, + selectedIndex: _selectedIndex.value, onSelectedIndexChanged: _onSelectedIndexChanged, ), ], diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index f7b5be46..4cb651a4 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -6,6 +6,7 @@ import 'package:spotify/spotify.dart' hide Image; import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/SpotifyDI.dart'; import '../../models/sideBarTiles.dart'; @@ -28,7 +29,7 @@ class Sidebar extends HookConsumerWidget { ); } - void _goToSettings(BuildContext context) { + static void goToSettings(BuildContext context) { Navigator.of(context).push(SpotubePageRoute( child: const Settings(), )); @@ -36,30 +37,35 @@ class Sidebar extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final width = MediaQuery.of(context).size.width; - if (width <= 400) return Container(); + final breakpoints = useBreakpoints(); + if (breakpoints.isSm) return Container(); final extended = useState(false); final SpotifyApi spotify = ref.watch(spotifyProvider); + useEffect(() { - if (width <= 700 && extended.value) { + if (breakpoints.isMd && extended.value) { extended.value = false; - } else if (width > 700 && !extended.value) { + } else if (breakpoints.isMoreThanOrEqualTo(Breakpoints.lg) && + !extended.value) { extended.value = true; } + return null; }); return NavigationRail( destinations: sidebarTileList - .map((e) => NavigationRailDestination( - icon: Icon(e.icon), - label: Text( - e.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), + .map( + (e) => NavigationRailDestination( + icon: Icon(e.icon), + label: Text( + e.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), - )) + ), + ), + ) .toList(), selectedIndex: selectedIndex, onDestinationSelected: onSelectedIndexChanged, @@ -104,11 +110,11 @@ class Sidebar extends HookConsumerWidget { ), IconButton( icon: const Icon(Icons.settings_outlined), - onPressed: () => _goToSettings(context)), + onPressed: () => goToSettings(context)), ], )) : InkWell( - onTap: () => _goToSettings(context), + onTap: () => goToSettings(context), child: CircleAvatar( backgroundImage: CachedNetworkImageProvider(avatarImg), ), diff --git a/lib/components/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart index f57383ad..54d78012 100644 --- a/lib/components/Home/SpotubeNavigationBar.dart +++ b/lib/components/Home/SpotubeNavigationBar.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:spotube/components/Home/Sidebar.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/sideBarTiles.dart'; class SpotubeNavigationBar extends HookWidget { @@ -14,17 +16,21 @@ class SpotubeNavigationBar extends HookWidget { @override Widget build(BuildContext context) { - final width = MediaQuery.of(context).size.width; + final breakpoint = useBreakpoints(); - if (width > 400) return Container(); + if (breakpoint.isMoreThan(Breakpoints.sm)) return Container(); return NavigationBar( - destinations: sidebarTileList - .map( - (e) => NavigationDestination(icon: Icon(e.icon), label: e.title), - ) - .toList(), + destinations: [ + ...sidebarTileList.map( + (e) => NavigationDestination(icon: Icon(e.icon), label: e.title), + ), + const NavigationDestination( + icon: Icon(Icons.settings_rounded), + label: "Settings", + ) + ], selectedIndex: selectedIndex, - onDestinationSelected: onSelectedIndexChanged, + onDestinationSelected: (i) => Sidebar.goToSettings(context), ); } } diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 2da322ee..8c2949e1 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -17,6 +17,7 @@ class PlaylistCard extends ConsumerWidget { bool isPlaylistPlaying = playback.currentPlaylist != null && playback.currentPlaylist!.id == playlist.id; return PlaybuttonCard( + margin: const EdgeInsets.symmetric(horizontal: 20), title: playlist.name!, imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index d0561b2b..4fcddd16 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -5,6 +5,7 @@ class PlaybuttonCard extends StatelessWidget { final void Function()? onTap; final void Function()? onPlaybuttonPressed; final String? description; + final EdgeInsetsGeometry? margin; final String imageUrl; final bool isPlaying; final String title; @@ -12,6 +13,7 @@ class PlaybuttonCard extends StatelessWidget { required this.imageUrl, required this.isPlaying, required this.title, + this.margin, this.description, this.onPlaybuttonPressed, this.onTap, @@ -20,85 +22,88 @@ class PlaybuttonCard extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), - child: Ink( - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.circular(8), - boxShadow: [ - BoxShadow( - blurRadius: 10, - offset: const Offset(0, 3), - spreadRadius: 5, - color: Theme.of(context).shadowColor) - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - // thumbnail of the playlist - Stack( - children: [ - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: imageUrl, - placeholder: (context, url) => - Image.asset("assets/placeholder.png"), - ), - ), - Positioned.directional( - textDirection: TextDirection.ltr, - bottom: 10, - end: 5, - child: Builder(builder: (context) { - return ElevatedButton( - onPressed: onPlaybuttonPressed, - child: Icon( - isPlaying - ? Icons.pause_rounded - : Icons.play_arrow_rounded, - ), - style: ButtonStyle( - shape: MaterialStateProperty.all( - const CircleBorder(), - ), - padding: MaterialStateProperty.all( - const EdgeInsets.all(16), - ), - ), - ); - }), - ) - ], - ), - const SizedBox(height: 5), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 10), - child: Column( + return Container( + margin: margin, + child: InkWell( + onTap: onTap, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Ink( + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + borderRadius: BorderRadius.circular(8), + boxShadow: [ + BoxShadow( + blurRadius: 10, + offset: const Offset(0, 3), + spreadRadius: 5, + color: Theme.of(context).shadowColor) + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // thumbnail of the playlist + Stack( children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold), + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: imageUrl, + placeholder: (context, url) => + Image.asset("assets/placeholder.png"), + ), ), - if (description != null) ...[ - const SizedBox(height: 10), - Text( - description!, - style: TextStyle( - fontSize: 13, - color: Theme.of(context).textTheme.headline4?.color, - ), - ) - ] + Positioned.directional( + textDirection: TextDirection.ltr, + bottom: 10, + end: 5, + child: Builder(builder: (context) { + return ElevatedButton( + onPressed: onPlaybuttonPressed, + child: Icon( + isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + style: ButtonStyle( + shape: MaterialStateProperty.all( + const CircleBorder(), + ), + padding: MaterialStateProperty.all( + const EdgeInsets.all(16), + ), + ), + ); + }), + ) ], ), - ) - ], + const SizedBox(height: 5), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 10), + child: Column( + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + if (description != null) ...[ + const SizedBox(height: 10), + Text( + description!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).textTheme.headline4?.color, + ), + ) + ] + ], + ), + ) + ], + ), ), ), ), diff --git a/lib/hooks/useAsyncEffect.dart b/lib/hooks/useAsyncEffect.dart new file mode 100644 index 00000000..8af25543 --- /dev/null +++ b/lib/hooks/useAsyncEffect.dart @@ -0,0 +1,18 @@ +import 'dart:async'; + +import 'package:flutter_hooks/flutter_hooks.dart'; + +void useAsyncEffect( + FutureOr Function() effect, [ + FutureOr Function()? cleanup, + List? keys, +]) { + useEffect(() { + Future.microtask(effect); + return () { + if (cleanup != null) { + Future.microtask(cleanup); + } + }; + }, keys); +} diff --git a/lib/hooks/useBreakpointValue.dart b/lib/hooks/useBreakpointValue.dart new file mode 100644 index 00000000..58240630 --- /dev/null +++ b/lib/hooks/useBreakpointValue.dart @@ -0,0 +1,17 @@ +import 'package:spotube/hooks/useBreakpoints.dart'; + +useBreakpointValue({sm, md, lg, xl, xxl}) { + final breakpoint = useBreakpoints(); + + if (breakpoint.isSm) { + return sm; + } else if (breakpoint.isMd) { + return md; + } else if (breakpoint.isXl) { + return xl; + } else if (breakpoint.isXxl) { + return xxl; + } else { + return lg; + } +} diff --git a/lib/hooks/useBreakpoints.dart b/lib/hooks/useBreakpoints.dart new file mode 100644 index 00000000..73fb0276 --- /dev/null +++ b/lib/hooks/useBreakpoints.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; + +class BreakpointUtils { + Breakpoints breakpoint; + List breakpointList = [ + Breakpoints.sm, + Breakpoints.md, + Breakpoints.lg, + Breakpoints.xl, + Breakpoints.xxl + ]; + BreakpointUtils(this.breakpoint); + + get isSm => breakpoint == Breakpoints.sm; + get isMd => breakpoint == Breakpoints.md; + get isLg => breakpoint == Breakpoints.lg; + get isXl => breakpoint == Breakpoints.xl; + get isXxl => breakpoint == Breakpoints.xxl; + + bool isMoreThanOrEqualTo(Breakpoints b) { + return breakpointList + .sublist(breakpointList.indexOf(b)) + .contains(breakpoint); + } + + bool isLessThanOrEqualTo(Breakpoints b) { + return breakpointList + .sublist(0, breakpointList.indexOf(b) + 1) + .contains(breakpoint); + } + + bool isMoreThan(Breakpoints b) { + return breakpointList + .sublist(breakpointList.indexOf(b) + 1) + .contains(breakpoint); + } + + bool isLessThan(Breakpoints b) { + return breakpointList + .sublist(0, breakpointList.indexOf(b)) + .contains(breakpoint); + } + + bool operator >(other) { + return isMoreThan(other); + } + + bool operator <(other) { + return isLessThan(other); + } + + bool operator >=(other) { + return isMoreThanOrEqualTo(other); + } + + bool operator <=(other) { + return isLessThanOrEqualTo(other); + } +} + +enum Breakpoints { sm, md, lg, xl, xxl } + +BreakpointUtils useBreakpoints() { + final context = useContext(); + final width = MediaQuery.of(context).size.width; + final breakpoint = useState(Breakpoints.lg); + final utils = BreakpointUtils(breakpoint.value); + + useEffect(() { + if (width >= 1920 && breakpoint.value != Breakpoints.xxl) { + breakpoint.value = Breakpoints.xxl; + } else if (width >= 1366 && + width < 1920 && + breakpoint.value != Breakpoints.xl) { + breakpoint.value = Breakpoints.xl; + } else if (width >= 768 && + width < 1366 && + breakpoint.value != Breakpoints.lg) { + breakpoint.value = Breakpoints.lg; + } else if (width >= 360 && + width < 768 && + breakpoint.value != Breakpoints.md) { + breakpoint.value = Breakpoints.md; + } else if (width >= 250 && + width < 360 && + breakpoint.value != Breakpoints.sm) { + breakpoint.value = Breakpoints.sm; + } + return null; + }, [width]); + + useEffect(() { + utils.breakpoint = breakpoint.value; + return null; + }, [breakpoint.value]); + + return utils; +} diff --git a/lib/hooks/usePagingController.dart b/lib/hooks/usePagingController.dart new file mode 100644 index 00000000..a32dfa0c --- /dev/null +++ b/lib/hooks/usePagingController.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; + +PagingController + usePagingController({ + required final PageKeyType firstPageKey, + final int? invisibleItemsThreshold, + List? keys, +}) { + return use( + _PagingControllerHook( + firstPageKey: firstPageKey, + invisibleItemsThreshold: invisibleItemsThreshold, + keys: keys, + ), + ); +} + +class _PagingControllerHook + extends Hook> { + const _PagingControllerHook({ + required this.firstPageKey, + this.invisibleItemsThreshold, + List? keys, + }) : super(keys: keys); + + final PageKeyType firstPageKey; + final int? invisibleItemsThreshold; + + @override + HookState, + Hook>> + createState() => _PagingControllerHookState(); +} + +class _PagingControllerHookState extends HookState< + PagingController, + _PagingControllerHook> { + late final controller = PagingController( + firstPageKey: hook.firstPageKey, + invisibleItemsThreshold: hook.invisibleItemsThreshold); + + @override + PagingController build(BuildContext context) => + controller; + + @override + void dispose() => controller.dispose(); + + @override + String get debugLabel => 'usePagingController'; +} diff --git a/lib/hooks/useSharedPreferences.dart b/lib/hooks/useSharedPreferences.dart new file mode 100644 index 00000000..922beaa6 --- /dev/null +++ b/lib/hooks/useSharedPreferences.dart @@ -0,0 +1,9 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +SharedPreferences? useSharedPreferences() { + final future = useMemoized(SharedPreferences.getInstance); + final snapshot = useFuture(future, initialData: null); + + return snapshot.data; +} diff --git a/lib/provider/Auth.dart b/lib/provider/Auth.dart index d5e69dc2..af18e84e 100644 --- a/lib/provider/Auth.dart +++ b/lib/provider/Auth.dart @@ -52,6 +52,11 @@ class Auth with ChangeNotifier { _isLoggedIn = false; notifyListeners(); } + + @override + String toString() { + return "Auth(clientId: $clientId, clientSecret: $clientSecret, accessToken: $accessToken, refreshToken: $refreshToken, expiration: $expiration, isLoggedIn: $isLoggedIn)"; + } } var authProvider = ChangeNotifierProvider((ref) => Auth()); diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index acfe96e2..6e03466e 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -7,6 +7,7 @@ import 'package:spotube/provider/Auth.dart'; var spotifyProvider = Provider((ref) { Auth authState = ref.watch(authProvider); + return SpotifyApi( SpotifyApiCredentials( authState.clientId, From b3511e49195cb1aea5e4675176a00a62bb2cd2fe Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 1 Mar 2022 10:26:20 +0600 Subject: [PATCH 18/25] Playlist TrackTile is now responsive PlaylistView/SearchAlbumView are responsive now ArtistProfile album view & tracks view are paginated now --- lib/components/Album/AlbumCard.dart | 8 +- lib/components/Artist/ArtistProfile.dart | 197 +++++++++------ lib/components/Category/CategoryCard.dart | 104 ++++---- lib/components/Home.dart | 18 +- lib/components/Home/SpotubeNavigationBar.dart | 8 +- lib/components/Playlist/PlaylistCard.dart | 8 +- lib/components/Search/Search.dart | 3 +- lib/components/Shared/PlaybuttonCard.dart | 10 +- lib/components/Shared/TracksTableView.dart | 237 ++++++++++-------- lib/helpers/artists-to-clickable-artists.dart | 2 + lib/hooks/useBreakpoints.dart | 15 +- 11 files changed, 354 insertions(+), 256 deletions(-) diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index bdc4d864..0588fd49 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; @@ -7,10 +8,11 @@ import 'package:spotube/components/Shared/SpotubePageRoute.dart'; 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/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class AlbumCard extends ConsumerWidget { +class AlbumCard extends HookConsumerWidget { final Album album; const AlbumCard(this.album, {Key? key}) : super(key: key); @@ -19,9 +21,11 @@ class AlbumCard extends ConsumerWidget { Playback playback = ref.watch(playbackProvider); bool isPlaylistPlaying = playback.currentPlaylist != null && playback.currentPlaylist!.id == album.id; - + final int marginH = + useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); return PlaybuttonCard( imageUrl: imageToUrlString(album.images), + margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), isPlaying: playback.currentPlaylist?.id != null && playback.currentPlaylist?.id == album.id, title: album.name!, diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 8b2b0cdd..3b8c1f18 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -1,7 +1,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; @@ -12,16 +13,39 @@ import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/readable-number.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/hooks/useBreakpointValue.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class ArtistProfile extends ConsumerWidget { +class ArtistProfile extends HookConsumerWidget { final String artistId; const ArtistProfile(this.artistId, {Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { SpotifyApi spotify = ref.watch(spotifyProvider); + final scrollController = useScrollController(); + final parentScrollController = useScrollController(); + final textTheme = Theme.of(context).textTheme; + final chipTextVariant = useBreakpointValue( + sm: textTheme.bodySmall, + md: textTheme.bodyMedium, + lg: textTheme.headline6, + xl: textTheme.headline6, + xxl: textTheme.headline6, + ); + + final avatarWidth = useBreakpointValue( + sm: MediaQuery.of(context).size.width * 0.50, + md: MediaQuery.of(context).size.width * 0.40, + lg: MediaQuery.of(context).size.width * 0.18, + xl: MediaQuery.of(context).size.width * 0.18, + xxl: MediaQuery.of(context).size.width * 0.18, + ); + + final breakpoint = useBreakpoints(); + return Scaffold( appBar: const PageWindowTitleBar( leading: BackButton(), @@ -34,89 +58,93 @@ class ArtistProfile extends ConsumerWidget { } return SingleChildScrollView( + controller: parentScrollController, padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, children: [ const SizedBox(width: 50), CircleAvatar( - radius: MediaQuery.of(context).size.width * 0.18, + radius: avatarWidth, backgroundImage: CachedNetworkImageProvider( imageToUrlString(snapshot.data!.images), ), ), - Flexible( - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(50)), - child: Text(snapshot.data!.type!.toUpperCase(), - style: Theme.of(context) - .textTheme - .headline6 - ?.copyWith(color: Colors.white)), - ), - Text( - snapshot.data!.name!, - style: Theme.of(context).textTheme.headline2, - ), - Text( - "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", - style: Theme.of(context).textTheme.headline5, - ), - const SizedBox(height: 20), - Row( - children: [ - // TODO: Implement check if user follows this artist - // LIMITATION: spotify-dart lib - FutureBuilder( - future: Future.value(true), - builder: (context, snapshot) { - return OutlinedButton( - onPressed: () async { - // TODO: make `follow/unfollow` artists button work - // LIMITATION: spotify-dart lib - }, - child: Text(snapshot.data == true - ? "Following" - : "Follow"), - ); - }), - IconButton( - icon: const Icon(Icons.share_rounded), - onPressed: () { - Clipboard.setData( - ClipboardData( - text: snapshot - .data?.externalUrls?.spotify), - ).then((val) { - ScaffoldMessenger.of(context) - .showSnackBar( - const SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Artist URL copied to clipboard", - textAlign: TextAlign.center, - ), + Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(50)), + child: Text(snapshot.data!.type!.toUpperCase(), + style: chipTextVariant?.copyWith( + color: Colors.white)), + ), + Text( + snapshot.data!.name!, + style: breakpoint.isSm + ? textTheme.headline4 + : textTheme.headline2, + ), + Text( + "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", + style: breakpoint.isSm + ? textTheme.bodyText1 + : textTheme.headline5, + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // TODO: Implement check if user follows this artist + // LIMITATION: spotify-dart lib + FutureBuilder( + future: Future.value(true), + builder: (context, snapshot) { + return OutlinedButton( + onPressed: () async { + // TODO: make `follow/unfollow` artists button work + // LIMITATION: spotify-dart lib + }, + child: Text(snapshot.data == true + ? "Following" + : "Follow"), + ); + }), + IconButton( + icon: const Icon(Icons.share_rounded), + onPressed: () { + Clipboard.setData( + ClipboardData( + text: snapshot + .data?.externalUrls?.spotify), + ).then((val) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Artist URL copied to clipboard", + textAlign: TextAlign.center, ), - ); - }); - }, - ) - ], - ) - ], - ), + ), + ); + }); + }, + ) + ], + ) + ], ), ), ], @@ -188,8 +216,7 @@ class ArtistProfile extends ConsumerWidget { index: (track.value.album?.images?.length ?? 1) - 1); - return TracksTableView.buildTrackTile( - context, + return TrackTile( playback, duration: duration, track: track, @@ -237,14 +264,18 @@ class ArtistProfile extends ConsumerWidget { return const Center( child: CircularProgressIndicator.adaptive()); } - return Center( - child: Wrap( - spacing: 20, - runSpacing: 20, - children: snapshot.data - ?.map((album) => AlbumCard(album)) - .toList() ?? - [], + return Scrollbar( + controller: scrollController, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: snapshot.data + ?.map((album) => AlbumCard(album)) + .toList() ?? + [], + ), ), ); }, diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 03b7440b..65225ef9 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -1,11 +1,10 @@ import 'package:flutter/material.dart' hide Page; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart'; -import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; +import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/provider/SpotifyDI.dart'; class CategoryCard extends HookWidget { @@ -24,26 +23,11 @@ class CategoryCard extends HookWidget { Padding( padding: const EdgeInsets.all(8.0), child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( category.name ?? "Unknown", style: Theme.of(context).textTheme.headline5, ), - TextButton( - onPressed: () { - Navigator.of(context).push( - SpotubePageRoute( - child: PlaylistGenreView( - category.id!, - category.name!, - playlists: playlists, - ), - ), - ); - }, - child: const Text("See all"), - ) ], ), ), @@ -51,37 +35,63 @@ class CategoryCard extends HookWidget { builder: (context, ref, child) { SpotifyApi spotifyApi = ref.watch(spotifyProvider); final scrollController = useScrollController(); - return FutureBuilder>( - future: playlists == null - ? (category.id != "user-featured-playlists" - ? spotifyApi.playlists.getByCategoryId(category.id!) - : spotifyApi.playlists.featured) - .getPage(4, 0) - .then((value) => value.items ?? []) - : Future.value(playlists), - builder: (context, snapshot) { - if (snapshot.hasError) { - return const Center(child: Text("Error occurred")); + final pagingController = + usePagingController(firstPageKey: 0); + + final _error = useState(false); + + useEffect(() { + listener(pageKey) async { + try { + if (playlists != null && playlists?.isNotEmpty == true) { + return pagingController.appendLastPage(playlists!.toList()); } - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive(), - ); + final Page page = await (category.id != + "user-featured-playlists" + ? spotifyApi.playlists.getByCategoryId(category.id!) + : spotifyApi.playlists.featured) + .getPage(3, pageKey); + + if (page.isLast && page.items != null) { + pagingController.appendLastPage(page.items!.toList()); + } else if (page.items != null) { + pagingController.appendPage( + page.items!.toList(), page.nextOffset); } - return Scrollbar( - controller: scrollController, - child: SingleChildScrollView( - controller: scrollController, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: snapshot.data! - .map((playlist) => PlaylistCard(playlist)) - .toList(), - ), - ), - ); - }); + if (_error.value) _error.value = false; + } catch (e, stack) { + if (!_error.value) _error.value = true; + pagingController.error = e; + print( + "[CategoryCard.pagingController.addPageRequestListener] $e"); + print(stack); + } + } + + pagingController.addPageRequestListener(listener); + return () { + pagingController.removePageRequestListener(listener); + }; + }, [_error]); + + if (_error.value) return const Text("Something Went Wrong"); + return SizedBox( + height: 245, + child: Scrollbar( + controller: scrollController, + child: PagedListView( + shrinkWrap: true, + pagingController: pagingController, + scrollController: scrollController, + scrollDirection: Axis.horizontal, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, playlist, index) { + return PlaylistCard(playlist); + }, + ), + ), + ), + ); }, ) ], diff --git a/lib/components/Home.dart b/lib/components/Home.dart index 1fc1f750..95d6e30b 100644 --- a/lib/components/Home.dart +++ b/lib/components/Home.dart @@ -123,16 +123,18 @@ class Home extends HookConsumerWidget { return null; }).then((_) { pagingController.addPageRequestListener(listener); + }).catchError((e, stack) { + if (e is AuthorizationException) { + oauthLogin( + auth, + clientId: clientId, + clientSecret: clientSecret, + ); + } + print("[Home.useEffect.spotify.getCredentials]: $e"); + print(stack); }); } - } on AuthorizationException catch (_) { - if (clientId != null && clientSecret != null) { - oauthLogin( - auth, - clientId: clientId, - clientSecret: clientSecret, - ); - } } catch (e, stack) { print("[Home.initState]: $e"); print(stack); diff --git a/lib/components/Home/SpotubeNavigationBar.dart b/lib/components/Home/SpotubeNavigationBar.dart index 54d78012..158aad5c 100644 --- a/lib/components/Home/SpotubeNavigationBar.dart +++ b/lib/components/Home/SpotubeNavigationBar.dart @@ -30,7 +30,13 @@ class SpotubeNavigationBar extends HookWidget { ) ], selectedIndex: selectedIndex, - onDestinationSelected: (i) => Sidebar.goToSettings(context), + onDestinationSelected: (i) { + if (i == 4) { + Sidebar.goToSettings(context); + } else { + onSelectedIndexChanged(i); + } + }, ); } } diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 8c2949e1..82c52cc2 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -5,10 +5,11 @@ import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart'; import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/hooks/useBreakpointValue.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -class PlaylistCard extends ConsumerWidget { +class PlaylistCard extends HookConsumerWidget { final PlaylistSimple playlist; const PlaylistCard(this.playlist, {Key? key}) : super(key: key); @override @@ -16,8 +17,11 @@ class PlaylistCard extends ConsumerWidget { Playback playback = ref.watch(playbackProvider); bool isPlaylistPlaying = playback.currentPlaylist != null && playback.currentPlaylist!.id == playlist.id; + + final int marginH = + useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20); return PlaybuttonCard( - margin: const EdgeInsets.symmetric(horizontal: 20), + margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()), title: playlist.name!, imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index a70efd92..07953903 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -99,8 +99,7 @@ class Search extends HookConsumerWidget { ...tracks.asMap().entries.map((track) { String duration = "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return TracksTableView.buildTrackTile( - context, + return TrackTile( playback, track: track, duration: duration, diff --git a/lib/components/Shared/PlaybuttonCard.dart b/lib/components/Shared/PlaybuttonCard.dart index 4fcddd16..7af2f678 100644 --- a/lib/components/Shared/PlaybuttonCard.dart +++ b/lib/components/Shared/PlaybuttonCard.dart @@ -85,9 +85,13 @@ class PlaybuttonCard extends StatelessWidget { const EdgeInsets.symmetric(horizontal: 8, vertical: 10), child: Column( children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold), + Tooltip( + message: title, + child: Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold), + overflow: TextOverflow.ellipsis, + ), ), if (description != null) ...[ const SizedBox(height: 10), diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index b3a6dffb..78ed7e24 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -1,6 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Shared/LinkText.dart'; @@ -8,100 +9,23 @@ import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/Playback.dart'; -class TracksTableView extends ConsumerWidget { +class TracksTableView extends HookConsumerWidget { final void Function(Track currentTrack)? onTrackPlayButtonPressed; final List tracks; const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed}) : super(key: key); - static Widget buildTrackTile( - BuildContext context, - Playback playback, { - required MapEntry track, - required String duration, - String? thumbnailUrl, - final void Function(Track currentTrack)? onTrackPlayButtonPressed, - }) { - return Row( - children: [ - SizedBox( - height: 20, - width: 25, - child: Text( - (track.key + 1).toString(), - textAlign: TextAlign.center, - ), - ), - if (thumbnailUrl != null) - Padding( - padding: const EdgeInsets.all(8.0), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(5)), - child: CachedNetworkImage( - placeholder: (context, url) { - return Container( - height: 40, - width: 40, - color: Colors.green[300], - ); - }, - imageUrl: thumbnailUrl, - maxHeightDiskCache: 40, - maxWidthDiskCache: 40, - ), - ), - ), - IconButton( - icon: Icon( - playback.currentTrack?.id != null && - playback.currentTrack?.id == track.value.id - ? Icons.pause_circle_rounded - : Icons.play_circle_rounded, - color: Theme.of(context).primaryColor, - ), - onPressed: () => onTrackPlayButtonPressed?.call( - track.value, - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - track.value.name ?? "", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 17, - ), - overflow: TextOverflow.ellipsis, - ), - artistsToClickableArtists(track.value.artists ?? []), - ], - ), - ), - Expanded( - child: LinkText( - track.value.album!.name!, - SpotubePageRoute( - child: AlbumView(track.value.album!), - ), - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 10), - Text(duration), - const SizedBox(width: 10), - ], - ); - } - @override Widget build(context, ref) { Playback playback = ref.watch(playbackProvider); TextStyle tableHeadStyle = const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); + + final breakpoint = useBreakpoints(); + return Expanded( child: Scrollbar( child: ListView( @@ -128,21 +52,25 @@ class TracksTableView extends ConsumerWidget { ), ), // used alignment of this table-head - const SizedBox(width: 100), - Expanded( - child: Row( - children: [ - Text( - "Album", - overflow: TextOverflow.ellipsis, - style: tableHeadStyle, - ), - ], - ), - ), - const SizedBox(width: 10), - Text("Time", style: tableHeadStyle), - const SizedBox(width: 10), + if (breakpoint.isMoreThan(Breakpoints.md)) ...[ + const SizedBox(width: 100), + Expanded( + child: Row( + children: [ + Text( + "Album", + overflow: TextOverflow.ellipsis, + style: tableHeadStyle, + ), + ], + ), + ) + ], + if (!breakpoint.isSm) ...[ + const SizedBox(width: 10), + Text("Time", style: tableHeadStyle), + const SizedBox(width: 10), + ] ], ), ...tracks.asMap().entries.map((track) { @@ -152,11 +80,13 @@ class TracksTableView extends ConsumerWidget { ); String duration = "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - return buildTrackTile(context, playback, - track: track, - duration: duration, - thumbnailUrl: thumbnailUrl, - onTrackPlayButtonPressed: onTrackPlayButtonPressed); + return TrackTile( + playback, + track: track, + duration: duration, + thumbnailUrl: thumbnailUrl, + onTrackPlayButtonPressed: onTrackPlayButtonPressed, + ); }).toList() ], ), @@ -164,3 +94,104 @@ class TracksTableView extends ConsumerWidget { ); } } + +class TrackTile extends HookWidget { + final Playback playback; + final MapEntry track; + final String duration; + final String? thumbnailUrl; + final void Function(Track currentTrack)? onTrackPlayButtonPressed; + const TrackTile( + this.playback, { + required this.track, + required this.duration, + this.thumbnailUrl, + this.onTrackPlayButtonPressed, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final breakpoint = useBreakpoints(); + return Row( + children: [ + SizedBox( + height: 20, + width: 25, + child: Text( + (track.key + 1).toString(), + textAlign: TextAlign.center, + ), + ), + if (thumbnailUrl != null) + Padding( + padding: EdgeInsets.symmetric( + horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0, + vertical: 8.0, + ), + child: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(5)), + child: CachedNetworkImage( + placeholder: (context, url) { + return Container( + height: 40, + width: 40, + color: Colors.green[300], + ); + }, + imageUrl: thumbnailUrl!, + maxHeightDiskCache: 40, + maxWidthDiskCache: 40, + ), + ), + ), + IconButton( + icon: Icon( + playback.currentTrack?.id != null && + playback.currentTrack?.id == track.value.id + ? Icons.pause_circle_rounded + : Icons.play_circle_rounded, + color: Theme.of(context).primaryColor, + ), + onPressed: () => onTrackPlayButtonPressed?.call( + track.value, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + track.value.name ?? "", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: breakpoint.isSm ? 14 : 17, + ), + overflow: TextOverflow.ellipsis, + ), + artistsToClickableArtists(track.value.artists ?? [], + textStyle: TextStyle( + fontSize: + breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)), + ], + ), + ), + if (breakpoint.isMoreThan(Breakpoints.md)) + Expanded( + child: LinkText( + track.value.album!.name!, + SpotubePageRoute( + child: AlbumView(track.value.album!), + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (!breakpoint.isSm) ...[ + const SizedBox(width: 10), + Text(duration), + const SizedBox(width: 10) + ], + ], + ); + } +} diff --git a/lib/helpers/artists-to-clickable-artists.dart b/lib/helpers/artists-to-clickable-artists.dart index 0cb5a7f6..030e8f3d 100644 --- a/lib/helpers/artists-to-clickable-artists.dart +++ b/lib/helpers/artists-to-clickable-artists.dart @@ -8,6 +8,7 @@ Widget artistsToClickableArtists( List artists, { CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + TextStyle textStyle = const TextStyle(), }) { return Row( crossAxisAlignment: crossAxisAlignment, @@ -24,6 +25,7 @@ Widget artistsToClickableArtists( child: ArtistProfile(artist.value.id!), ), overflow: TextOverflow.ellipsis, + style: textStyle, ), ) .toList(), diff --git a/lib/hooks/useBreakpoints.dart b/lib/hooks/useBreakpoints.dart index 73fb0276..825e1217 100644 --- a/lib/hooks/useBreakpoints.dart +++ b/lib/hooks/useBreakpoints.dart @@ -12,11 +12,11 @@ class BreakpointUtils { ]; BreakpointUtils(this.breakpoint); - get isSm => breakpoint == Breakpoints.sm; - get isMd => breakpoint == Breakpoints.md; - get isLg => breakpoint == Breakpoints.lg; - get isXl => breakpoint == Breakpoints.xl; - get isXxl => breakpoint == Breakpoints.xxl; + bool get isSm => breakpoint == Breakpoints.sm; + bool get isMd => breakpoint == Breakpoints.md; + bool get isLg => breakpoint == Breakpoints.lg; + bool get isXl => breakpoint == Breakpoints.xl; + bool get isXxl => breakpoint == Breakpoints.xxl; bool isMoreThanOrEqualTo(Breakpoints b) { return breakpointList @@ -57,6 +57,11 @@ class BreakpointUtils { bool operator <=(other) { return isLessThanOrEqualTo(other); } + + @override + String toString() { + return "BreakpointUtils($breakpoint)"; + } } enum Breakpoints { sm, md, lg, xl, xxl } From b585bf2df2e83262161932ce54ead6c7d4a76411 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 8 Mar 2022 22:25:49 +0600 Subject: [PATCH 19/25] Tweaks for responsiveness for appwindow.size Home component moved to its own subdir --- lib/components/{ => Home}/Home.dart | 0 lib/components/Shared/PageWindowTitleBar.dart | 3 ++- lib/helpers/oauth-login.dart | 2 +- lib/main.dart | 7 +++++-- lib/provider/SpotifyDI.dart | 2 +- 5 files changed, 9 insertions(+), 5 deletions(-) rename lib/components/{ => Home}/Home.dart (100%) diff --git a/lib/components/Home.dart b/lib/components/Home/Home.dart similarity index 100% rename from lib/components/Home.dart rename to lib/components/Home/Home.dart diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index 8ee4a42d..483cc4a4 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -65,7 +65,8 @@ class PageWindowTitleBar extends StatelessWidget ), if (leading != null) leading!, Expanded(child: MoveWindow(child: Center(child: center))), - if (!Platform.isMacOS) const TitleBarActionButtons() + if (!Platform.isMacOS && !Platform.isIOS && !Platform.isAndroid) + const TitleBarActionButtons() ], ), ); diff --git a/lib/helpers/oauth-login.dart b/lib/helpers/oauth-login.dart index 4ff3fb2d..32229297 100644 --- a/lib/helpers/oauth-login.dart +++ b/lib/helpers/oauth-login.dart @@ -1,6 +1,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Home.dart'; +import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/helpers/server_ipc.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; diff --git a/lib/main.dart b/lib/main.dart index cd81052a..facab79d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,12 @@ +import 'dart:io'; + import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotube/components/Home.dart'; +import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/ThemeProvider.dart'; @@ -13,7 +15,8 @@ void main() async { await hotKeyManager.unregisterAll(); runApp(ProviderScope(child: MyApp())); doWhenWindowReady(() { - appWindow.minSize = const Size(280, 700); + appWindow.minSize = + Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700); appWindow.size = const Size(900, 700); appWindow.alignment = Alignment.center; appWindow.maximize(); diff --git a/lib/provider/SpotifyDI.dart b/lib/provider/SpotifyDI.dart index 6e03466e..5d4f0597 100644 --- a/lib/provider/SpotifyDI.dart +++ b/lib/provider/SpotifyDI.dart @@ -1,7 +1,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Home.dart'; +import 'package:spotube/components/Home/Home.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Auth.dart'; From d608fa7d0209816c1c5d51407bf686d70379c0ae Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 10 Mar 2022 12:29:29 +0600 Subject: [PATCH 20/25] PlayerOverlay works as expected imageToUrlString uses uuid instead of DateTime.now() seperated parts of Player for reuse accross different sizes of screen's specific widgets integrating go_router to follow declarative route approach --- lib/components/Album/AlbumCard.dart | 5 +- lib/components/Artist/ArtistCard.dart | 5 +- lib/components/Artist/ArtistProfile.dart | 1 + lib/components/Home/Home.dart | 2 +- lib/components/Home/Sidebar.dart | 4 +- lib/components/Lyrics.dart | 4 +- lib/components/Player/Player.dart | 387 ++++++++++-------- lib/components/Player/PlayerControls.dart | 151 +++---- lib/components/Player/PlayerOverlay.dart | 55 +++ lib/components/Player/PlayerTrackDetails.dart | 71 ++++ lib/components/Playlist/PlaylistCard.dart | 11 +- lib/components/Shared/SpotubePageRoute.dart | 4 +- lib/helpers/image-to-url-string.dart | 4 +- pubspec.lock | 21 + pubspec.yaml | 2 + 15 files changed, 466 insertions(+), 261 deletions(-) create mode 100644 lib/components/Player/PlayerOverlay.dart create mode 100644 lib/components/Player/PlayerTrackDetails.dart diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index 0588fd49..8f723972 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -33,7 +33,10 @@ class AlbumCard extends HookConsumerWidget { "Album • ${artistsToString(album.artists ?? [])}", onTap: () { Navigator.of(context).push(SpotubePageRoute( - child: AlbumView(album), + child: AlbumView( + album, + key: Key("album-${album.id}"), + ), )); }, onPlaybuttonPressed: () async { diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 6f92cb59..3d844c5c 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -18,7 +18,10 @@ class ArtistCard extends StatelessWidget { return InkWell( onTap: () { Navigator.of(context).push(SpotubePageRoute( - child: ArtistProfile(artist.id!), + child: ArtistProfile( + artist.id!, + key: Key("artist-${artist.id}"), + ), )); }, borderRadius: BorderRadius.circular(10), diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 3b8c1f18..a8706689 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -247,6 +247,7 @@ class ArtistProfile extends HookConsumerWidget { child: ArtistAlbumView( artistId, snapshot.data?.name ?? "KRTX", + key: Key("artist-album-$artistId"), ), )); }, diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 95d6e30b..cdbdfdf5 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -168,7 +168,7 @@ class Home extends HookConsumerWidget { Expanded(child: MoveWindow()), if (!Platform.isMacOS) const TitleBarActionButtons(), ], - )), + )) ], ), ), diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 4cb651a4..8abb869d 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -31,7 +31,9 @@ class Sidebar extends HookConsumerWidget { static void goToSettings(BuildContext context) { Navigator.of(context).push(SpotubePageRoute( - child: const Settings(), + child: const Settings( + key: Key("settings"), + ), )); } diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 71db1357..6cb5d83c 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -70,7 +70,9 @@ class Lyrics extends HookConsumerWidget { ElevatedButton( onPressed: () { Navigator.of(context).push(SpotubePageRoute( - child: const Settings(), + child: const Settings( + key: Key("settings"), + ), )); }, child: const Text("Add Access Token")) diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 96c70f4f..1aec567c 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -4,13 +4,17 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:palette_generator/palette_generator.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotify/spotify.dart'; +import 'package:spotify/spotify.dart' hide Image; +import 'package:spotube/components/Player/PlayerOverlay.dart'; +import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; 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/hooks/useBreakpoints.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; @@ -19,15 +23,17 @@ import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class Player extends HookConsumerWidget { const Player({Key? key}) : super(key: key); - @override Widget build(BuildContext context, ref) { + Playback playback = ref.watch(playbackProvider); final _isPlaying = useState(false); final _shuffled = useState(false); final _volume = useState(0.0); final _duration = useState(null); final _currentTrackId = useState(null); + final breakpoint = useBreakpoints(); + final AudioPlayer player = useMemoized(() => AudioPlayer(), []); final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); final Future future = @@ -92,6 +98,7 @@ class Player extends HookConsumerWidget { print(stack); } }); + return () { playingStreamListener.cancel(); durationStreamListener.cancel(); @@ -106,9 +113,11 @@ class Player extends HookConsumerWidget { _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? player.volume; } + return null; }, [localStorage.data]); - var _playTrack = useCallback((Track currentTrack, Playback playback) async { + final _playTrack = + useCallback((Track currentTrack, Playback playback) async { try { if (currentTrack.id != _currentTrackId.value) { Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); @@ -140,6 +149,13 @@ class Player extends HookConsumerWidget { } }, [player, _currentTrackId, _duration]); + useEffect(() { + if (playback.currentPlaylist != null && playback.currentTrack != null) { + _playTrack(playback.currentTrack!, playback); + } + return null; + }, [playback.currentPlaylist, playback.currentTrack]); + var _onNext = useCallback(() async { try { await player.pause(); @@ -162,193 +178,202 @@ class Player extends HookConsumerWidget { } }, [player]); + String albumArt = useMemoized( + () => imageToUrlString( + playback.currentTrack?.album?.images, + index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + ), + [playback.currentTrack?.album?.images], + ); + + final entryRef = useRef(null); + + disposeOverlay() { + try { + entryRef.value?.remove(); + entryRef.value = null; + } catch (e, stack) { + if (e is! AssertionError) { + print("[Player.useEffect.cleanup] $e"); + print(stack); + } + } + } + + final controls = PlayerControls( + positionStream: player.positionStream, + isPlaying: _isPlaying.value, + duration: _duration.value ?? Duration.zero, + shuffled: _shuffled.value, + onNext: _onNext, + onPrevious: _onPrevious, + onPause: () async { + try { + await player.pause(); + } catch (e, stack) { + print("[PlayerControls.onPause()] $e"); + print(stack); + } + }, + onPlay: () async { + try { + await player.play(); + } catch (e, stack) { + print("[PlayerControls.onPlay()] $e"); + print(stack); + } + }, + onSeek: (value) async { + try { + await player.seek(Duration(seconds: value.toInt())); + } catch (e, stack) { + print("[PlayerControls.onSeek()] $e"); + print(stack); + } + }, + onShuffle: () async { + if (playback.currentTrack == null || playback.currentPlaylist == null) { + return; + } + try { + if (!_shuffled.value) { + playback.currentPlaylist!.shuffle(); + _shuffled.value = true; + } else { + playback.currentPlaylist!.unshuffle(); + _shuffled.value = false; + } + } catch (e, stack) { + print("[PlayerControls.onShuffle()] $e"); + print(stack); + } + }, + onStop: () async { + try { + await player.pause(); + await player.seek(Duration.zero); + _isPlaying.value = false; + _currentTrackId.value = null; + _duration.value = null; + _shuffled.value = false; + playback.reset(); + } catch (e, stack) { + print("[PlayerControls.onStop()] $e"); + print(stack); + } + }, + ); + + useEffect(() { + // clearing the overlay-entry as passing the already available + // entry will result in splashing while resizing the window + if (entryRef.value != null) disposeOverlay(); + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + entryRef.value = OverlayEntry( + opaque: false, + builder: (context) => PlayerOverlay( + controls: controls, + albumArt: albumArt, + ), + ); + // I can't believe useEffect doesn't run Post Frame aka + // after rendering/painting the UI + // `My disappointment is immeasurable and my day is ruined` XD + WidgetsBinding.instance?.addPostFrameCallback((time) { + Overlay.of(context)?.insert(entryRef.value!); + }); + } + return () { + disposeOverlay(); + }; + }, [breakpoint]); + + // returning an empty non spacious Container as the overlay will take + // place in the global overlay stack aka [_entries] + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + return Container(); + } + return Container( color: Theme.of(context).backgroundColor, - child: HookConsumer( - builder: (context, ref, widget) { - Playback playback = ref.watch(playbackProvider); - if (playback.currentPlaylist != null && - playback.currentTrack != null) { - _playTrack(playback.currentTrack!, playback); - } - - String? albumArt = useMemoized( - () => imageToUrlString( - playback.currentTrack?.album?.images, - index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, + child: Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded(child: PlayerTrackDetails(albumArt: albumArt)), + // controls + Flexible( + flex: 3, + child: controls, ), - [playback.currentTrack?.album?.images], - ); - - return Material( - type: MaterialType.transparency, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (albumArt != null) - CachedNetworkImage( - imageUrl: albumArt, - maxHeightDiskCache: 50, - maxWidthDiskCache: 50, - placeholder: (context, url) { - return Container( - height: 50, - width: 50, - color: Colors.green[400], - ); - }, - ), - // title of the currently playing track - Flexible( - flex: 1, - child: Column( - children: [ - Text( - playback.currentTrack?.name ?? "Not playing", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - artistsToClickableArtists( - playback.currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, - ) - ], - ), - ), - // controls - Flexible( - flex: 3, - child: PlayerControls( - positionStream: player.positionStream, - isPlaying: _isPlaying.value, - duration: _duration.value ?? Duration.zero, - shuffled: _shuffled.value, - onNext: _onNext, - onPrevious: _onPrevious, - onPause: () async { - try { - await player.pause(); - } catch (e, stack) { - print("[PlayerControls.onPause()] $e"); - print(stack); - } - }, - onPlay: () async { - try { - await player.play(); - } catch (e, stack) { - print("[PlayerControls.onPlay()] $e"); - print(stack); - } - }, - onSeek: (value) async { - try { - await player.seek(Duration(seconds: value.toInt())); - } catch (e, stack) { - print("[PlayerControls.onSeek()] $e"); - print(stack); - } - }, - onShuffle: () async { - if (playback.currentTrack == null || - playback.currentPlaylist == null) return; - try { - if (!_shuffled.value) { - playback.currentPlaylist!.shuffle(); - _shuffled.value = true; - } else { - playback.currentPlaylist!.unshuffle(); - _shuffled.value = false; + // add to saved tracks + Expanded( + flex: 1, + child: Wrap( + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + Container( + height: 20, + constraints: const BoxConstraints(maxWidth: 200), + child: Slider.adaptive( + value: _volume.value, + onChanged: (value) async { + try { + await player.setVolume(value).then((_) { + _volume.value = value; + localStorage.data?.setDouble( + LocalStorageKeys.volume, + value, + ); + }); + } catch (e, stack) { + print("[VolumeSlider.onChange()] $e"); + print(stack); } - } catch (e, stack) { - print("[PlayerControls.onShuffle()] $e"); - print(stack); - } - }, - onStop: () async { - try { - await player.pause(); - await player.seek(Duration.zero); - _isPlaying.value = false; - _currentTrackId.value = null; - _duration.value = null; - _shuffled.value = false; - playback.reset(); - } catch (e, stack) { - print("[PlayerControls.onStop()] $e"); - print(stack); - } - }, + }, + ), ), - ), - // add to saved tracks - Expanded( - flex: 1, - child: Wrap( - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, + Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Container( - height: 20, - constraints: const BoxConstraints(maxWidth: 200), - child: Slider.adaptive( - value: _volume.value, - onChanged: (value) async { - try { - await player.setVolume(value).then((_) { - _volume.value = value; - localStorage.data?.setDouble( - LocalStorageKeys.volume, - value, - ); - }); - } catch (e, stack) { - print("[VolumeSlider.onChange()] $e"); - print(stack); - } - }, - ), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - DownloadTrackButton( - track: playback.currentTrack, - ), - Consumer(builder: (context, ref, widget) { - SpotifyApi spotifyApi = ref.watch(spotifyProvider); - return FutureBuilder( - future: playback.currentTrack?.id != null - ? spotifyApi.tracks.me - .containsOne(playback.currentTrack!.id!) - : Future.value(false), - initialData: false, - builder: (context, snapshot) { - bool isLiked = snapshot.data ?? false; - return IconButton( - icon: Icon( - !isLiked - ? Icons.favorite_outline_rounded - : Icons.favorite_rounded, - color: isLiked ? Colors.green : null, - ), - onPressed: () { - if (!isLiked && - playback.currentTrack?.id != null) { - spotifyApi.tracks.me.saveOne( - playback.currentTrack!.id!); - } - }); - }); - }), - ], + DownloadTrackButton( + track: playback.currentTrack, ), + Consumer(builder: (context, ref, widget) { + SpotifyApi spotifyApi = ref.watch(spotifyProvider); + return FutureBuilder( + future: playback.currentTrack?.id != null + ? spotifyApi.tracks.me + .containsOne(playback.currentTrack!.id!) + : Future.value(false), + initialData: false, + builder: (context, snapshot) { + bool isLiked = snapshot.data ?? false; + return IconButton( + icon: Icon( + !isLiked + ? Icons.favorite_outline_rounded + : Icons.favorite_rounded, + color: isLiked ? Colors.green : null, + ), + onPressed: () { + if (!isLiked && + playback.currentTrack?.id != null) { + spotifyApi.tracks.me + .saveOne(playback.currentTrack!.id!); + } + }); + }); + }), ], ), - ) - ], - ), - ); - }, + ], + ), + ) + ], + ), ), ); } diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 4583a98f..0aac81dd 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/models/GlobalKeyActions.dart'; import 'package:spotube/provider/UserPreferences.dart'; @@ -77,79 +78,91 @@ class PlayerControls extends HookConsumerWidget { }; }); - return Container( - constraints: const BoxConstraints(maxWidth: 700), - child: Column( - children: [ - StreamBuilder( - stream: positionStream, - builder: (context, snapshot) { - var totalMinutes = - zeroPadNumStr(duration.inMinutes.remainder(60)); - var totalSeconds = - zeroPadNumStr(duration.inSeconds.remainder(60)); - var currentMinutes = snapshot.hasData - ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) - : "00"; - var currentSeconds = snapshot.hasData - ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) - : "00"; + final breakpoint = useBreakpoints(); - var sliderMax = duration.inSeconds; - var sliderValue = snapshot.data?.inSeconds ?? 0; - return Row( - children: [ - Expanded( - child: Slider.adaptive( - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: (sliderMax == 0 || sliderValue > sliderMax) - ? 0 - : sliderValue / sliderMax, - onChanged: (value) {}, - onChangeEnd: (value) { - onSeek?.call(value * sliderMax); - }, - ), - ), - Text( - "$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds", - ) - ], - ); + Widget controlButtons = Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + if (breakpoint.isMoreThan(Breakpoints.md)) + IconButton( + icon: const Icon(Icons.shuffle_rounded), + color: shuffled ? Theme.of(context).primaryColor : null, + onPressed: () { + onShuffle?.call(); + }), + IconButton( + icon: const Icon(Icons.skip_previous_rounded), + onPressed: () { + onPrevious?.call(); }), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - IconButton( - icon: const Icon(Icons.shuffle_rounded), - color: shuffled ? Theme.of(context).primaryColor : null, - onPressed: () { - onShuffle?.call(); - }), - IconButton( - icon: const Icon(Icons.skip_previous_rounded), - onPressed: () { - onPrevious?.call(); - }), - IconButton( - icon: Icon( - isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, - ), - onPressed: () => _playOrPause(null), - ), - IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext?.call()), - IconButton( - icon: const Icon(Icons.stop_rounded), - onPressed: () => onStop?.call(), - ) - ], - ) + IconButton( + icon: Icon( + isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, + ), + onPressed: () => _playOrPause(null), + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext?.call()), + if (breakpoint.isMoreThan(Breakpoints.md)) + IconButton( + icon: const Icon(Icons.stop_rounded), + onPressed: () => onStop?.call(), + ) ], ), ); + + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { + return controlButtons; + } + + return Container( + constraints: const BoxConstraints(maxWidth: 700), + child: Column( + children: [ + StreamBuilder( + stream: positionStream, + builder: (context, snapshot) { + var totalMinutes = + zeroPadNumStr(duration.inMinutes.remainder(60)); + var totalSeconds = + zeroPadNumStr(duration.inSeconds.remainder(60)); + var currentMinutes = snapshot.hasData + ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) + : "00"; + var currentSeconds = snapshot.hasData + ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) + : "00"; + + var sliderMax = duration.inSeconds; + var sliderValue = snapshot.data?.inSeconds ?? 0; + return Row( + children: [ + Expanded( + child: Slider.adaptive( + // cannot divide by zero + // there's an edge case for value being bigger + // than total duration. Keeping it resolved + value: (sliderMax == 0 || sliderValue > sliderMax) + ? 0 + : sliderValue / sliderMax, + onChanged: (value) {}, + onChangeEnd: (value) { + onSeek?.call(value * sliderMax); + }, + ), + ), + Text( + "$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds", + ) + ], + ); + }), + controlButtons, + ], + )); } } diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart new file mode 100644 index 00000000..1521fac8 --- /dev/null +++ b/lib/components/Player/PlayerOverlay.dart @@ -0,0 +1,55 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/components/Player/PlayerTrackDetails.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; + +class PlayerOverlay extends HookWidget { + final Widget controls; + final String albumArt; + const PlayerOverlay({ + required this.controls, + required this.albumArt, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final breakpoint = useBreakpoints(); + + return Positioned( + right: (breakpoint.isMd ? 10 : 5), + left: (breakpoint.isSm ? 5 : 80), + bottom: (breakpoint.isSm ? 63 : 10), + child: FutureBuilder( + future: PaletteGenerator.fromImageProvider( + CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + maxHeight: 50, + maxWidth: 50, + ), + ), + builder: (context, snapshot) { + return Container( + width: MediaQuery.of(context).size.width, + height: 50, + decoration: BoxDecoration( + color: snapshot.hasData + ? snapshot.data!.colors.first + : Colors.blueGrey[200], + borderRadius: BorderRadius.circular(5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PlayerTrackDetails(albumArt: albumArt), + controls, + ], + ), + ); + }), + ); + } +} diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart new file mode 100644 index 00000000..9d4962a1 --- /dev/null +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -0,0 +1,71 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:spotube/helpers/artists-to-clickable-artists.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/provider/Playback.dart'; + +class PlayerTrackDetails extends HookConsumerWidget { + final String? albumArt; + const PlayerTrackDetails({Key? key, this.albumArt}) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final breakpoint = useBreakpoints(); + final playback = ref.watch(playbackProvider); + + return Row( + children: [ + if (albumArt != null) + Padding( + padding: EdgeInsets.all( + breakpoint.isLessThanOrEqualTo(Breakpoints.md) ? 5.0 : 0), + child: CachedNetworkImage( + imageUrl: albumArt!, + maxHeightDiskCache: 50, + maxWidthDiskCache: 50, + cacheKey: albumArt, + placeholder: (context, url) { + return Container( + height: 50, + width: 50, + color: Colors.green[400], + ); + }, + ), + ), + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) ...[ + const SizedBox(width: 10), + Text( + playback.currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(fontWeight: FontWeight.bold), + ), + ], + // title of the currently playing track + if (breakpoint.isMoreThan(Breakpoints.md)) + Flexible( + flex: 1, + child: Column( + children: [ + Text( + playback.currentTrack?.name ?? "Not playing", + style: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(fontWeight: FontWeight.bold), + ), + artistsToClickableArtists( + playback.currentTrack?.artists ?? [], + mainAxisAlignment: MainAxisAlignment.center, + ) + ], + ), + ), + ], + ); + } +} diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 82c52cc2..5ba07808 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -26,9 +26,14 @@ class PlaylistCard extends HookConsumerWidget { imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, onTap: () { - Navigator.of(context).push(SpotubePageRoute( - child: PlaylistView(playlist), - )); + Navigator.of(context).push( + SpotubePageRoute( + child: PlaylistView( + playlist, + key: Key("playlist-${playlist.id}"), + ), + ), + ); }, onPlaybuttonPressed: () async { if (isPlaylistPlaying) return; diff --git a/lib/components/Shared/SpotubePageRoute.dart b/lib/components/Shared/SpotubePageRoute.dart index 0cc47129..69a37da5 100644 --- a/lib/components/Shared/SpotubePageRoute.dart +++ b/lib/components/Shared/SpotubePageRoute.dart @@ -4,8 +4,8 @@ class SpotubePageRoute extends PageRouteBuilder { final Widget child; SpotubePageRoute({required this.child}) : super( - pageBuilder: (context, animation, secondaryAnimation) => child, - ); + pageBuilder: (context, animation, secondaryAnimation) => child, + settings: RouteSettings(name: child.key.toString())); @override Widget buildTransitions(BuildContext context, Animation animation, diff --git a/lib/helpers/image-to-url-string.dart b/lib/helpers/image-to-url-string.dart index f9cf0938..0d4cc397 100644 --- a/lib/helpers/image-to-url-string.dart +++ b/lib/helpers/image-to-url-string.dart @@ -1,7 +1,9 @@ import 'package:spotify/spotify.dart'; +import 'package:uuid/uuid.dart' show Uuid; +const uuid = Uuid(); String imageToUrlString(List? images, {int index = 0}) { return images != null && images.isNotEmpty ? images[0].url! - : "https://avatars.dicebear.com/api/croodles-neutral/${DateTime.now().toString()}.png"; + : "https://avatars.dicebear.com/api/croodles-neutral/${uuid.v4()}.png"; } diff --git a/pubspec.lock b/pubspec.lock index 0ad07331..0d670b24 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -226,6 +226,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + go_router: + dependency: "direct main" + description: + name: go_router + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.4" hooks_riverpod: dependency: "direct main" description: @@ -331,6 +338,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" matcher: dependency: transitive description: @@ -380,6 +394,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.2" + palette_generator: + dependency: "direct main" + description: + name: palette_generator + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.3" path: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ff5c832..cfcedea6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,8 @@ dependencies: flutter_riverpod: ^1.0.3 flutter_hooks: ^0.18.2+1 hooks_riverpod: ^1.0.3 + go_router: ^3.0.4 + palette_generator: ^0.3.3 dev_dependencies: flutter_test: From aaf74b46d4c6b6958af931599c7ab387122b0d7e Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Thu, 10 Mar 2022 15:11:02 +0600 Subject: [PATCH 21/25] floating mini player works flawlessly Custom bg-color for floating player for each title track album art go_true routing integrated floating player now disappears if not on home --- lib/components/Album/AlbumCard.dart | 8 +-- lib/components/Artist/ArtistCard.dart | 10 +-- lib/components/Artist/ArtistProfile.dart | 12 ++-- lib/components/Home/Sidebar.dart | 9 +-- lib/components/Lyrics.dart | 7 +- lib/components/Player/Player.dart | 10 ++- lib/components/Player/PlayerControls.dart | 10 ++- lib/components/Player/PlayerOverlay.dart | 65 +++++++++++-------- lib/components/Player/PlayerTrackDetails.dart | 23 ++++--- lib/components/Playlist/PlaylistCard.dart | 11 ++-- lib/components/Settings.dart | 5 +- lib/components/Shared/LinkText.dart | 9 ++- lib/components/Shared/RecordHotKeyDialog.dart | 5 +- lib/components/Shared/SpotubePageRoute.dart | 18 +++++ lib/components/Shared/TracksTableView.dart | 5 +- lib/helpers/artists-to-clickable-artists.dart | 4 +- lib/helpers/image-to-url-string.dart | 2 +- lib/hooks/usePaletteColor.dart | 32 +++++++++ lib/main.dart | 64 +++++++++++++++++- 19 files changed, 211 insertions(+), 98 deletions(-) create mode 100644 lib/hooks/usePaletteColor.dart diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index 8f723972..9aa17145 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumView.dart'; @@ -32,12 +33,7 @@ class AlbumCard extends HookConsumerWidget { description: "Album • ${artistsToString(album.artists ?? [])}", onTap: () { - Navigator.of(context).push(SpotubePageRoute( - child: AlbumView( - album, - key: Key("album-${album.id}"), - ), - )); + GoRouter.of(context).push("/album/${album.id}", extra: album); }, onPlaybuttonPressed: () async { SpotifyApi spotify = ref.read(spotifyProvider); diff --git a/lib/components/Artist/ArtistCard.dart b/lib/components/Artist/ArtistCard.dart index 3d844c5c..9f2ead1a 100644 --- a/lib/components/Artist/ArtistCard.dart +++ b/lib/components/Artist/ArtistCard.dart @@ -1,8 +1,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Artist/ArtistProfile.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; class ArtistCard extends StatelessWidget { final Artist artist; @@ -17,12 +16,7 @@ class ArtistCard extends StatelessWidget { : "https://avatars.dicebear.com/api/open-peeps/${artist.id}.png?b=%231ed760&r=50&flip=1&translateX=3&translateY=-6"); return InkWell( onTap: () { - Navigator.of(context).push(SpotubePageRoute( - child: ArtistProfile( - artist.id!, - key: Key("artist-${artist.id}"), - ), - )); + GoRouter.of(context).push("/artist/${artist.id}"); }, borderRadius: BorderRadius.circular(10), child: Ink( diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index a8706689..0b3cd8b5 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -2,6 +2,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; @@ -243,13 +244,10 @@ class ArtistProfile extends HookConsumerWidget { TextButton( child: const Text("See All"), onPressed: () { - Navigator.of(context).push(SpotubePageRoute( - child: ArtistAlbumView( - artistId, - snapshot.data?.name ?? "KRTX", - key: Key("artist-album-$artistId"), - ), - )); + GoRouter.of(context).push( + "/artist-album/$artistId", + extra: snapshot.data?.name ?? "KRTX", + ); }, ) ], diff --git a/lib/components/Home/Sidebar.dart b/lib/components/Home/Sidebar.dart index 8abb869d..da77a107 100644 --- a/lib/components/Home/Sidebar.dart +++ b/lib/components/Home/Sidebar.dart @@ -1,10 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart' hide Image; -import 'package:spotube/components/Settings.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; import 'package:spotube/provider/SpotifyDI.dart'; @@ -30,11 +29,7 @@ class Sidebar extends HookConsumerWidget { } static void goToSettings(BuildContext context) { - Navigator.of(context).push(SpotubePageRoute( - child: const Settings( - key: Key("settings"), - ), - )); + GoRouter.of(context).push("/settings"); } @override diff --git a/lib/components/Lyrics.dart b/lib/components/Lyrics.dart index 6cb5d83c..16cf56f5 100644 --- a/lib/components/Lyrics.dart +++ b/lib/components/Lyrics.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Settings.dart'; @@ -69,11 +70,7 @@ class Lyrics extends HookConsumerWidget { ), ElevatedButton( onPressed: () { - Navigator.of(context).push(SpotubePageRoute( - child: const Settings( - key: Key("settings"), - ), - )); + GoRouter.of(context).push("/settings"); }, child: const Text("Add Access Token")) ], diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 1aec567c..5879172a 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -1,24 +1,23 @@ import 'dart:async'; -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:just_audio/just_audio.dart'; -import 'package:palette_generator/palette_generator.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotify/spotify.dart' hide Image; import 'package:spotube/components/Player/PlayerOverlay.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; 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/hooks/useBreakpoints.dart'; +import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/SpotifyDI.dart'; +import 'package:spotube/provider/ThemeProvider.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class Player extends HookConsumerWidget { @@ -26,6 +25,7 @@ class Player extends HookConsumerWidget { @override Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); + final _isPlaying = useState(false); final _shuffled = useState(false); final _volume = useState(0.0); @@ -200,7 +200,10 @@ class Player extends HookConsumerWidget { } } + final paletteColor = usePaletteColor(albumArt); + final controls = PlayerControls( + iconColor: paletteColor.bodyTextColor, positionStream: player.positionStream, isPlaying: _isPlaying.value, duration: _duration.value ?? Duration.zero, @@ -274,6 +277,7 @@ class Player extends HookConsumerWidget { builder: (context) => PlayerOverlay( controls: controls, albumArt: albumArt, + paletteColor: paletteColor, ), ); // I can't believe useEffect doesn't run Post Frame aka diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index 0aac81dd..c5a5e4b5 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -21,6 +21,7 @@ class PlayerControls extends HookConsumerWidget { final Function? onPrevious; final Function? onPlay; final Function? onPause; + final Color? iconColor; const PlayerControls({ required this.positionStream, required this.isPlaying, @@ -33,6 +34,7 @@ class PlayerControls extends HookConsumerWidget { this.onPrevious, this.onPlay, this.onPause, + this.iconColor, Key? key, }) : super(key: key); @@ -94,6 +96,7 @@ class PlayerControls extends HookConsumerWidget { }), IconButton( icon: const Icon(Icons.skip_previous_rounded), + color: iconColor, onPressed: () { onPrevious?.call(); }), @@ -101,11 +104,14 @@ class PlayerControls extends HookConsumerWidget { icon: Icon( isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, ), + color: iconColor, onPressed: () => _playOrPause(null), ), IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext?.call()), + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext?.call(), + color: iconColor, + ), if (breakpoint.isMoreThan(Breakpoints.md)) IconButton( icon: const Icon(Icons.stop_rounded), diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index 1521fac8..a64662bb 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -1,6 +1,6 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:palette_generator/palette_generator.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; @@ -8,48 +8,57 @@ import 'package:spotube/hooks/useBreakpoints.dart'; class PlayerOverlay extends HookWidget { final Widget controls; final String albumArt; + final PaletteColor paletteColor; const PlayerOverlay({ required this.controls, required this.albumArt, + required this.paletteColor, Key? key, }) : super(key: key); @override Widget build(BuildContext context) { final breakpoint = useBreakpoints(); + final isCurrentRoute = useState(null); + + useEffect(() { + WidgetsBinding.instance?.addPostFrameCallback((timer) { + final matches = GoRouter.of(context).location == "/"; + if (matches != isCurrentRoute.value) { + isCurrentRoute.value = matches; + } + }); + return null; + }); + + if (isCurrentRoute.value == false) { + return Container(); + } return Positioned( right: (breakpoint.isMd ? 10 : 5), left: (breakpoint.isSm ? 5 : 80), bottom: (breakpoint.isSm ? 63 : 10), - child: FutureBuilder( - future: PaletteGenerator.fromImageProvider( - CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - maxHeight: 50, - maxWidth: 50, + child: Container( + width: MediaQuery.of(context).size.width, + height: 50, + decoration: BoxDecoration( + color: paletteColor.color, + borderRadius: BorderRadius.circular(5), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: PlayerTrackDetails( + albumArt: albumArt, + color: paletteColor.bodyTextColor, + ), ), - ), - builder: (context, snapshot) { - return Container( - width: MediaQuery.of(context).size.width, - height: 50, - decoration: BoxDecoration( - color: snapshot.hasData - ? snapshot.data!.colors.first - : Colors.blueGrey[200], - borderRadius: BorderRadius.circular(5), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - PlayerTrackDetails(albumArt: albumArt), - controls, - ], - ), - ); - }), + Expanded(child: controls), + ], + ), + ), ); } } diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart index 9d4962a1..9610756c 100644 --- a/lib/components/Player/PlayerTrackDetails.dart +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -7,7 +7,9 @@ import 'package:spotube/provider/Playback.dart'; class PlayerTrackDetails extends HookConsumerWidget { final String? albumArt; - const PlayerTrackDetails({Key? key, this.albumArt}) : super(key: key); + final Color? color; + const PlayerTrackDetails({Key? key, this.albumArt, this.color}) + : super(key: key); @override Widget build(BuildContext context, ref) { @@ -36,13 +38,15 @@ class PlayerTrackDetails extends HookConsumerWidget { ), if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) ...[ const SizedBox(width: 10), - Text( - playback.currentTrack?.name ?? "Not playing", - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .bodyText1 - ?.copyWith(fontWeight: FontWeight.bold), + Flexible( + child: Text( + playback.currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyText1 + ?.copyWith(fontWeight: FontWeight.bold, color: color), + ), ), ], // title of the currently playing track @@ -53,10 +57,11 @@ class PlayerTrackDetails extends HookConsumerWidget { children: [ Text( playback.currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, style: Theme.of(context) .textTheme .bodyText1 - ?.copyWith(fontWeight: FontWeight.bold), + ?.copyWith(fontWeight: FontWeight.bold, color: color), ), artistsToClickableArtists( playback.currentTrack?.artists ?? [], diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 5ba07808..8f854c41 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; @@ -26,13 +27,9 @@ class PlaylistCard extends HookConsumerWidget { imageUrl: playlist.images![0].url!, isPlaying: isPlaylistPlaying, onTap: () { - Navigator.of(context).push( - SpotubePageRoute( - child: PlaylistView( - playlist, - key: Key("playlist-${playlist.id}"), - ), - ), + GoRouter.of(context).push( + "/playlist/${playlist.id}", + extra: playlist, ); }, onPlaybuttonPressed: () async { diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 27ec712c..32060030 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; @@ -57,7 +58,7 @@ class Settings extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton( - onPressed: geniusAccessToken != null + onPressed: geniusAccessToken.value != null ? () async { SharedPreferences localStorage = await SharedPreferences.getInstance(); @@ -148,7 +149,7 @@ class Settings extends HookConsumerWidget { await SharedPreferences.getInstance(); await localStorage.clear(); auth.logout(); - Navigator.of(context).pop(); + GoRouter.of(context).pop(); }, ), ], diff --git a/lib/components/Shared/LinkText.dart b/lib/components/Shared/LinkText.dart index 9d5d63e3..d24aaf75 100644 --- a/lib/components/Shared/LinkText.dart +++ b/lib/components/Shared/LinkText.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:spotube/components/Shared/AnchorButton.dart'; class LinkText extends StatelessWidget { @@ -6,12 +7,14 @@ class LinkText extends StatelessWidget { final TextStyle style; final TextAlign? textAlign; final TextOverflow? overflow; - final Route route; + final String route; + final T? extra; const LinkText( this.text, this.route, { Key? key, this.textAlign, + this.extra, this.overflow, this.style = const TextStyle(), }) : super(key: key); @@ -20,8 +23,8 @@ class LinkText extends StatelessWidget { Widget build(BuildContext context) { return AnchorButton( text, - onTap: () async { - await Navigator.of(context).push(route); + onTap: () { + GoRouter.of(context).push(route, extra: extra); }, key: key, overflow: overflow, diff --git a/lib/components/Shared/RecordHotKeyDialog.dart b/lib/components/Shared/RecordHotKeyDialog.dart index e0fde237..40af28e2 100644 --- a/lib/components/Shared/RecordHotKeyDialog.dart +++ b/lib/components/Shared/RecordHotKeyDialog.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; class RecordHotKeyDialog extends HookWidget { @@ -66,7 +67,7 @@ class RecordHotKeyDialog extends HookWidget { TextButton( child: const Text('Cancel'), onPressed: () { - Navigator.of(context).pop(); + GoRouter.of(context).pop(); }, ), TextButton( @@ -75,7 +76,7 @@ class RecordHotKeyDialog extends HookWidget { ? null : () { onHotKeyRecorded(_hotKey.value); - Navigator.of(context).pop(); + GoRouter.of(context).pop(); }, ), ], diff --git a/lib/components/Shared/SpotubePageRoute.dart b/lib/components/Shared/SpotubePageRoute.dart index 69a37da5..0f729873 100644 --- a/lib/components/Shared/SpotubePageRoute.dart +++ b/lib/components/Shared/SpotubePageRoute.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; class SpotubePageRoute extends PageRouteBuilder { final Widget child; @@ -16,3 +17,20 @@ class SpotubePageRoute extends PageRouteBuilder { ); } } + +class SpotubePage extends CustomTransitionPage { + SpotubePage({ + required Widget child, + }) : super( + child: child, + transitionsBuilder: (BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ); +} diff --git a/lib/components/Shared/TracksTableView.dart b/lib/components/Shared/TracksTableView.dart index 78ed7e24..dcf7f036 100644 --- a/lib/components/Shared/TracksTableView.dart +++ b/lib/components/Shared/TracksTableView.dart @@ -180,9 +180,8 @@ class TrackTile extends HookWidget { Expanded( child: LinkText( track.value.album!.name!, - SpotubePageRoute( - child: AlbumView(track.value.album!), - ), + "/album/${track.value.album?.id}", + extra: track.value.album, overflow: TextOverflow.ellipsis, ), ), diff --git a/lib/helpers/artists-to-clickable-artists.dart b/lib/helpers/artists-to-clickable-artists.dart index 030e8f3d..2ce58e98 100644 --- a/lib/helpers/artists-to-clickable-artists.dart +++ b/lib/helpers/artists-to-clickable-artists.dart @@ -21,9 +21,7 @@ Widget artistsToClickableArtists( (artist.key != artists.length - 1) ? "${artist.value.name}, " : artist.value.name!, - SpotubePageRoute( - child: ArtistProfile(artist.value.id!), - ), + "/artist/${artist.value.id}", overflow: TextOverflow.ellipsis, style: textStyle, ), diff --git a/lib/helpers/image-to-url-string.dart b/lib/helpers/image-to-url-string.dart index 0d4cc397..52731ff7 100644 --- a/lib/helpers/image-to-url-string.dart +++ b/lib/helpers/image-to-url-string.dart @@ -5,5 +5,5 @@ const uuid = Uuid(); String imageToUrlString(List? images, {int index = 0}) { return images != null && images.isNotEmpty ? images[0].url! - : "https://avatars.dicebear.com/api/croodles-neutral/${uuid.v4()}.png"; + : "https://avatars.dicebear.com/api/bottts/${uuid.v4()}.png"; } diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart new file mode 100644 index 00000000..75a21ca4 --- /dev/null +++ b/lib/hooks/usePaletteColor.dart @@ -0,0 +1,32 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:palette_generator/palette_generator.dart'; + +PaletteColor usePaletteColor(String imageUrl) { + final paletteColor = + useState(PaletteColor(Colors.grey[300]!, 0)); + + final context = useContext(); + + useEffect(() { + PaletteGenerator.fromImageProvider( + CachedNetworkImageProvider( + imageUrl, + cacheKey: imageUrl, + maxHeight: 50, + maxWidth: 50, + ), + ).then((palette) { + final color = Theme.of(context).brightness == Brightness.light + ? palette.lightMutedColor ?? palette.lightVibrantColor + : palette.darkMutedColor ?? palette.darkVibrantColor; + if (color != null) { + paletteColor.value = color; + } + }); + return null; + }, [imageUrl]); + + return paletteColor.value; +} diff --git a/lib/main.dart b/lib/main.dart index facab79d..ddf5ad8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,10 +3,18 @@ import 'dart:io'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Album/AlbumView.dart'; +import 'package:spotube/components/Artist/ArtistAlbumView.dart'; +import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; +import 'package:spotube/components/Playlist/PlaylistView.dart'; +import 'package:spotube/components/Settings.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/ThemeProvider.dart'; @@ -25,6 +33,56 @@ void main() async { } class MyApp extends HookConsumerWidget { + final GoRouter _router = GoRouter( + routes: [ + GoRoute( + path: "/", + builder: (context, state) => const Home(), + ), + GoRoute( + path: "/settings", + pageBuilder: (context, state) => SpotubePage( + child: const Settings(), + ), + ), + GoRoute( + path: "/album/:id", + pageBuilder: (context, state) { + assert(state.extra is AlbumSimple); + return SpotubePage(child: AlbumView(state.extra as AlbumSimple)); + }, + ), + GoRoute( + path: "/artist/:id", + pageBuilder: (context, state) { + assert(state.params["id"] != null); + return SpotubePage(child: ArtistProfile(state.params["id"]!)); + }, + ), + GoRoute( + path: "/artist-album/:id", + pageBuilder: (context, state) { + assert(state.params["id"] != null); + assert(state.extra is String); + return SpotubePage( + child: ArtistAlbumView( + state.params["id"]!, + state.extra as String, + ), + ); + }, + ), + GoRoute( + path: "/playlist/:id", + pageBuilder: (context, state) { + assert(state.extra is PlaylistSimple); + return SpotubePage( + child: PlaylistView(state.extra as PlaylistSimple), + ); + }, + ), + ], + ); @override Widget build(BuildContext context, ref) { var themeMode = ref.watch(themeProvider); @@ -44,9 +102,12 @@ class MyApp extends HookConsumerWidget { themeNotifier.state = ThemeMode.system; } }); + return null; }, []); - return MaterialApp( + return MaterialApp.router( + routeInformationParser: _router.routeInformationParser, + routerDelegate: _router.routerDelegate, debugShowCheckedModeBanner: false, title: 'Spotube', theme: ThemeData( @@ -142,7 +203,6 @@ class MyApp extends HookConsumerWidget { canvasColor: Colors.blueGrey[900], ), themeMode: themeMode, - home: const Home(), ); } } From 932462d77378e79efdec2c2400e1e9512c01f24a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sat, 12 Mar 2022 19:10:21 +0600 Subject: [PATCH 22/25] sperated PlayerControl from PlayerOverlay PlayerControls slider & duration are now vertical hotkey init moved to Home Player & YoutubeExplode are provided through riverpod Playback handles all things Player used to do GoRoutes are seperated from main to individual model file usePaletteColor bugfix occuring for before initilizing mount --- lib/components/Album/AlbumCard.dart | 1 + lib/components/Album/AlbumView.dart | 4 +- lib/components/Artist/ArtistProfile.dart | 6 +- lib/components/Category/CategoryCard.dart | 12 +- lib/components/Home/Home.dart | 4 + lib/components/Player/Player.dart | 218 +------------------ lib/components/Player/PlayerControls.dart | 252 +++++++++++----------- lib/components/Player/PlayerOverlay.dart | 90 +++++--- lib/components/Player/PlayerView.dart | 98 +++++++++ lib/components/Playlist/PlaylistCard.dart | 1 + lib/components/Playlist/PlaylistView.dart | 4 +- lib/components/Search/Search.dart | 1 + lib/hooks/playback.dart | 42 ++++ lib/hooks/useHotKeys.dart | 45 ++++ lib/hooks/useIsCurrentRoute.dart | 18 ++ lib/hooks/usePaletteColor.dart | 23 +- lib/main.dart | 69 +----- lib/models/GoRouteDeclarations.dart | 74 +++++++ lib/provider/AudioPlayer.dart | 6 + lib/provider/Playback.dart | 174 ++++++++++++++- lib/provider/YouTube.dart | 4 + 21 files changed, 689 insertions(+), 457 deletions(-) create mode 100644 lib/components/Player/PlayerView.dart create mode 100644 lib/hooks/playback.dart create mode 100644 lib/hooks/useHotKeys.dart create mode 100644 lib/hooks/useIsCurrentRoute.dart create mode 100644 lib/models/GoRouteDeclarations.dart create mode 100644 lib/provider/AudioPlayer.dart create mode 100644 lib/provider/YouTube.dart diff --git a/lib/components/Album/AlbumCard.dart b/lib/components/Album/AlbumCard.dart index 9aa17145..81163b1b 100644 --- a/lib/components/Album/AlbumCard.dart +++ b/lib/components/Album/AlbumCard.dart @@ -50,6 +50,7 @@ class AlbumCard extends HookConsumerWidget { thumbnail: album.images!.first.url!, ); playback.setCurrentTrack = tracks.first; + await playback.startPlaying(); }, ); } diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 80b1b937..2d1f0da8 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -12,7 +12,8 @@ class AlbumView extends ConsumerWidget { final AlbumSimple album; const AlbumView(this.album, {Key? key}) : super(key: key); - playPlaylist(Playback playback, List tracks, {Track? currentTrack}) { + playPlaylist(Playback playback, List tracks, + {Track? currentTrack}) async { currentTrack ??= tracks.first; var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; if (!isPlaylistPlaying) { @@ -28,6 +29,7 @@ class AlbumView extends ConsumerWidget { currentTrack.id != playback.currentTrack?.id) { playback.setCurrentTrack = currentTrack; } + await playback.startPlaying(); } @override diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 0b3cd8b5..2ae74acb 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -6,10 +6,8 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/components/Album/AlbumCard.dart'; -import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/readable-number.dart'; @@ -162,7 +160,8 @@ class ArtistProfile extends HookConsumerWidget { Playback playback = ref.watch(playbackProvider); var isPlaylistPlaying = playback.currentPlaylist?.id == snapshot.data?.id; - playPlaylist(List tracks, {Track? currentTrack}) { + playPlaylist(List tracks, + {Track? currentTrack}) async { currentTrack ??= tracks.first; if (!isPlaylistPlaying) { playback.setCurrentPlaylist = CurrentPlaylist( @@ -177,6 +176,7 @@ class ArtistProfile extends HookConsumerWidget { currentTrack.id != playback.currentTrack?.id) { playback.setCurrentTrack = currentTrack; } + await playback.startPlaying(); } return Column(children: [ diff --git a/lib/components/Category/CategoryCard.dart b/lib/components/Category/CategoryCard.dart index 65225ef9..fec5085b 100644 --- a/lib/components/Category/CategoryCard.dart +++ b/lib/components/Category/CategoryCard.dart @@ -39,11 +39,14 @@ class CategoryCard extends HookWidget { usePagingController(firstPageKey: 0); final _error = useState(false); + final mounted = useIsMounted(); useEffect(() { listener(pageKey) async { try { - if (playlists != null && playlists?.isNotEmpty == true) { + if (playlists != null && + playlists?.isNotEmpty == true && + mounted()) { return pagingController.appendLastPage(playlists!.toList()); } final Page page = await (category.id != @@ -52,6 +55,7 @@ class CategoryCard extends HookWidget { : spotifyApi.playlists.featured) .getPage(3, pageKey); + if (!mounted()) return; if (page.isLast && page.items != null) { pagingController.appendLastPage(page.items!.toList()); } else if (page.items != null) { @@ -60,8 +64,10 @@ class CategoryCard extends HookWidget { } if (_error.value) _error.value = false; } catch (e, stack) { - if (!_error.value) _error.value = true; - pagingController.error = e; + if (mounted()) { + if (!_error.value) _error.value = true; + pagingController.error = e; + } print( "[CategoryCard.pagingController.addPageRequestListener] $e"); print(stack); diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index cdbdfdf5..449a5bc7 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -19,6 +19,7 @@ import 'package:spotube/components/Player/Player.dart'; import 'package:spotube/components/Library/UserLibrary.dart'; import 'package:spotube/helpers/oauth-login.dart'; import 'package:spotube/hooks/useBreakpointValue.dart'; +import 'package:spotube/hooks/useHotKeys.dart'; import 'package:spotube/hooks/usePagingController.dart'; import 'package:spotube/hooks/useSharedPreferences.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; @@ -56,6 +57,9 @@ class Home extends HookConsumerWidget { final localStorage = useSharedPreferences(); + // initializing global hot keys + useHotKeys(ref); + useEffect(() { if (localStorage == null) return null; final String? clientId = diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index 5879172a..ff04d3b0 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -10,15 +10,11 @@ import 'package:spotube/components/Player/PlayerTrackDetails.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/helpers/image-to-url-string.dart'; -import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/hooks/usePaletteColor.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/provider/Playback.dart'; import 'package:flutter/material.dart'; import 'package:spotube/provider/SpotifyDI.dart'; -import 'package:spotube/provider/ThemeProvider.dart'; -import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class Player extends HookConsumerWidget { const Player({Key? key}) : super(key: key); @@ -26,88 +22,17 @@ class Player extends HookConsumerWidget { Widget build(BuildContext context, ref) { Playback playback = ref.watch(playbackProvider); - final _isPlaying = useState(false); - final _shuffled = useState(false); final _volume = useState(0.0); - final _duration = useState(null); - final _currentTrackId = useState(null); final breakpoint = useBreakpoints(); - final AudioPlayer player = useMemoized(() => AudioPlayer(), []); - final YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []); + final AudioPlayer player = playback.player; + final Future future = useMemoized(SharedPreferences.getInstance); final AsyncSnapshot localStorage = useFuture(future, initialData: null); - var _movePlaylistPositionBy = useCallback((int pos) { - Playback playback = ref.read(playbackProvider); - if (playback.currentTrack != null && playback.currentPlaylist != null) { - int index = playback.currentPlaylist!.trackIds - .indexOf(playback.currentTrack!.id!) + - pos; - - var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1 - ? 0 - : index < 0 - ? playback.currentPlaylist!.trackIds.length - : index; - Track? track = - playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex) - ? playback.currentPlaylist!.tracks.elementAt(safeIndex) - : null; - if (track != null) { - playback.setCurrentTrack = track; - _duration.value = null; - } - } - }, [_duration]); - - useEffect(() { - var playingStreamListener = player.playingStream.listen((playing) async { - _isPlaying.value = playing; - }); - - var durationStreamListener = - 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.value) { - // this line is for prev/next or already playing playlist - if (player.playing) await player.pause(); - await player.play(); - } - _duration.value = duration; - } - }); - - var processingStateStreamListener = - player.processingStateStream.listen((event) async { - try { - if (event == ProcessingState.completed && - _currentTrackId.value != null) { - _movePlaylistPositionBy(1); - } - } catch (e, stack) { - print("[PrecessingStateStreamListener] $e"); - print(stack); - } - }); - - return () { - playingStreamListener.cancel(); - durationStreamListener.cancel(); - processingStateStreamListener.cancel(); - player.dispose(); - youtube.close(); - }; - }, []); - useEffect(() { if (localStorage.hasData) { _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? @@ -116,68 +41,6 @@ class Player extends HookConsumerWidget { return null; }, [localStorage.data]); - final _playTrack = - useCallback((Track currentTrack, Playback playback) async { - try { - if (currentTrack.id != _currentTrackId.value) { - Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? ""); - if (parsedUri != null && parsedUri.hasAbsolutePath) { - await player - .setAudioSource( - AudioSource.uri(parsedUri), - preload: true, - ) - .then((value) async { - _currentTrackId.value = currentTrack.id; - if (_duration.value != null) { - _duration.value = value; - } - }); - } - var ytTrack = await toYoutubeTrack(youtube, currentTrack); - if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) { - await player - .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) - .then((value) { - _currentTrackId.value = currentTrack.id; - }); - } - } - } catch (e, stack) { - print("[Player._playTrack()] $e"); - print(stack); - } - }, [player, _currentTrackId, _duration]); - - useEffect(() { - if (playback.currentPlaylist != null && playback.currentTrack != null) { - _playTrack(playback.currentTrack!, playback); - } - return null; - }, [playback.currentPlaylist, playback.currentTrack]); - - var _onNext = useCallback(() async { - try { - await player.pause(); - await player.seek(Duration.zero); - _movePlaylistPositionBy(1); - } catch (e, stack) { - print("[PlayerControls.onNext()] $e"); - print(stack); - } - }, [player]); - - var _onPrevious = useCallback(() async { - try { - await player.pause(); - await player.seek(Duration.zero); - _movePlaylistPositionBy(-1); - } catch (e, stack) { - print("[PlayerControls.onPrevious()] $e"); - print(stack); - } - }, [player]); - String albumArt = useMemoized( () => imageToUrlString( playback.currentTrack?.album?.images, @@ -200,73 +63,6 @@ class Player extends HookConsumerWidget { } } - final paletteColor = usePaletteColor(albumArt); - - final controls = PlayerControls( - iconColor: paletteColor.bodyTextColor, - positionStream: player.positionStream, - isPlaying: _isPlaying.value, - duration: _duration.value ?? Duration.zero, - shuffled: _shuffled.value, - onNext: _onNext, - onPrevious: _onPrevious, - onPause: () async { - try { - await player.pause(); - } catch (e, stack) { - print("[PlayerControls.onPause()] $e"); - print(stack); - } - }, - onPlay: () async { - try { - await player.play(); - } catch (e, stack) { - print("[PlayerControls.onPlay()] $e"); - print(stack); - } - }, - onSeek: (value) async { - try { - await player.seek(Duration(seconds: value.toInt())); - } catch (e, stack) { - print("[PlayerControls.onSeek()] $e"); - print(stack); - } - }, - onShuffle: () async { - if (playback.currentTrack == null || playback.currentPlaylist == null) { - return; - } - try { - if (!_shuffled.value) { - playback.currentPlaylist!.shuffle(); - _shuffled.value = true; - } else { - playback.currentPlaylist!.unshuffle(); - _shuffled.value = false; - } - } catch (e, stack) { - print("[PlayerControls.onShuffle()] $e"); - print(stack); - } - }, - onStop: () async { - try { - await player.pause(); - await player.seek(Duration.zero); - _isPlaying.value = false; - _currentTrackId.value = null; - _duration.value = null; - _shuffled.value = false; - playback.reset(); - } catch (e, stack) { - print("[PlayerControls.onStop()] $e"); - print(stack); - } - }, - ); - useEffect(() { // clearing the overlay-entry as passing the already available // entry will result in splashing while resizing the window @@ -274,11 +70,7 @@ class Player extends HookConsumerWidget { if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { entryRef.value = OverlayEntry( opaque: false, - builder: (context) => PlayerOverlay( - controls: controls, - albumArt: albumArt, - paletteColor: paletteColor, - ), + builder: (context) => PlayerOverlay(albumArt: albumArt), ); // I can't believe useEffect doesn't run Post Frame aka // after rendering/painting the UI @@ -307,9 +99,9 @@ class Player extends HookConsumerWidget { children: [ Expanded(child: PlayerTrackDetails(albumArt: albumArt)), // controls - Flexible( + const Expanded( flex: 3, - child: controls, + child: PlayerControls(), ), // add to saved tracks Expanded( diff --git a/lib/components/Player/PlayerControls.dart b/lib/components/Player/PlayerControls.dart index c5a5e4b5..376e1165 100644 --- a/lib/components/Player/PlayerControls.dart +++ b/lib/components/Player/PlayerControls.dart @@ -1,173 +1,163 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:hotkey_manager/hotkey_manager.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart'; -import 'package:spotube/hooks/useBreakpoints.dart'; -import 'package:spotube/models/GlobalKeyActions.dart'; -import 'package:spotube/provider/UserPreferences.dart'; +import 'package:spotube/hooks/playback.dart'; +import 'package:spotube/provider/Playback.dart'; class PlayerControls extends HookConsumerWidget { - final Stream positionStream; - final bool isPlaying; - final Duration duration; - final bool shuffled; - final Function? onStop; - final Function? onShuffle; - final Function(double value)? onSeek; - final Function? onNext; - final Function? onPrevious; - final Function? onPlay; - final Function? onPause; final Color? iconColor; const PlayerControls({ - required this.positionStream, - required this.isPlaying, - required this.duration, - required this.shuffled, - this.onShuffle, - this.onStop, - this.onSeek, - this.onNext, - this.onPrevious, - this.onPlay, - this.onPause, this.iconColor, Key? key, }) : super(key: key); - _playOrPause(key) async { - try { - isPlaying ? await onPause?.call() : await onPlay?.call(); - } catch (e, stack) { - print("[PlayPauseShortcut] $e"); - print(stack); - } - } - @override Widget build(BuildContext context, ref) { - UserPreferences preferences = ref.watch(userPreferencesProvider); + final Playback playback = ref.watch(playbackProvider); + final AudioPlayer player = playback.player; + + final _shuffled = useState(false); + final _duration = useState(playback.duration); - var _hotKeys = []; useEffect(() { - _hotKeys = [ - GlobalKeyActions( - HotKey(KeyCode.space, scope: HotKeyScope.inapp), - _playOrPause, - ), - if (preferences.nextTrackHotKey != null) - GlobalKeyActions( - preferences.nextTrackHotKey!, (key) => onNext?.call()), - if (preferences.prevTrackHotKey != null) - GlobalKeyActions( - preferences.prevTrackHotKey!, (key) => onPrevious?.call()), - if (preferences.playPauseHotKey != null) - GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause) - ]; - Future.wait( - _hotKeys.map((e) { - return hotKeyManager.register( - e.hotKey, - keyDownHandler: e.onKeyDown, - ); - }), - ); - return () { - Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey))); - }; - }); + listener(Duration? duration) { + _duration.value = duration; + } - final breakpoint = useBreakpoints(); + playback.addDurationChangeListener(listener); - Widget controlButtons = Material( - type: MaterialType.transparency, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (breakpoint.isMoreThan(Breakpoints.md)) - IconButton( - icon: const Icon(Icons.shuffle_rounded), - color: shuffled ? Theme.of(context).primaryColor : null, - onPressed: () { - onShuffle?.call(); - }), - IconButton( - icon: const Icon(Icons.skip_previous_rounded), - color: iconColor, - onPressed: () { - onPrevious?.call(); - }), - IconButton( - icon: Icon( - isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded, - ), - color: iconColor, - onPressed: () => _playOrPause(null), - ), - IconButton( - icon: const Icon(Icons.skip_next_rounded), - onPressed: () => onNext?.call(), - color: iconColor, - ), - if (breakpoint.isMoreThan(Breakpoints.md)) - IconButton( - icon: const Icon(Icons.stop_rounded), - onPressed: () => onStop?.call(), - ) - ], - ), - ); + return () => playback.removeDurationChangeListener(listener); + }, []); - if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) { - return controlButtons; - } + final onNext = useNextTrack(playback); + + final onPrevious = usePreviousTrack(playback); + + final _playOrPause = useTogglePlayPause(playback); + + final duration = _duration.value ?? Duration.zero; return Container( constraints: const BoxConstraints(maxWidth: 700), child: Column( children: [ StreamBuilder( - stream: positionStream, + stream: player.positionStream, builder: (context, snapshot) { - var totalMinutes = + final totalMinutes = zeroPadNumStr(duration.inMinutes.remainder(60)); - var totalSeconds = + final totalSeconds = zeroPadNumStr(duration.inSeconds.remainder(60)); - var currentMinutes = snapshot.hasData + final currentMinutes = snapshot.hasData ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) : "00"; - var currentSeconds = snapshot.hasData + final currentSeconds = snapshot.hasData ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) : "00"; - var sliderMax = duration.inSeconds; - var sliderValue = snapshot.data?.inSeconds ?? 0; - return Row( + final sliderMax = duration.inSeconds; + final sliderValue = snapshot.data?.inSeconds ?? 0; + return Column( children: [ - Expanded( - child: Slider.adaptive( - // cannot divide by zero - // there's an edge case for value being bigger - // than total duration. Keeping it resolved - value: (sliderMax == 0 || sliderValue > sliderMax) - ? 0 - : sliderValue / sliderMax, - onChanged: (value) {}, - onChangeEnd: (value) { - onSeek?.call(value * sliderMax); - }, + Slider.adaptive( + // cannot divide by zero + // there's an edge case for value being bigger + // than total duration. Keeping it resolved + value: (sliderMax == 0 || sliderValue > sliderMax) + ? 0 + : sliderValue / sliderMax, + onChanged: (value) {}, + onChangeEnd: (value) { + player.seek( + Duration( + seconds: (value * sliderMax).toInt(), + ), + ); + }, + activeColor: iconColor, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "$currentMinutes:$currentSeconds", + ), + Text("$totalMinutes:$totalSeconds"), + ], ), ), - Text( - "$currentMinutes:$currentSeconds/$totalMinutes:$totalSeconds", - ) ], ); }), - controlButtons, + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + IconButton( + icon: const Icon(Icons.shuffle_rounded), + color: _shuffled.value + ? Theme.of(context).primaryColor + : iconColor, + onPressed: () { + if (playback.currentTrack == null || + playback.currentPlaylist == null) { + return; + } + try { + if (!_shuffled.value) { + playback.currentPlaylist!.shuffle(); + _shuffled.value = true; + } else { + playback.currentPlaylist!.unshuffle(); + _shuffled.value = false; + } + } catch (e, stack) { + print("[PlayerControls.onShuffle()] $e"); + print(stack); + } + }), + IconButton( + icon: const Icon(Icons.skip_previous_rounded), + color: iconColor, + onPressed: () { + onPrevious(); + }), + IconButton( + icon: Icon( + playback.isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + color: iconColor, + onPressed: _playOrPause, + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext(), + color: iconColor, + ), + IconButton( + icon: const Icon(Icons.stop_rounded), + color: iconColor, + onPressed: playback.currentTrack != null + ? () async { + try { + await player.pause(); + await player.seek(Duration.zero); + _shuffled.value = false; + playback.reset(); + } catch (e, stack) { + print("[PlayerControls.onStop()] $e"); + print(stack); + } + } + : null, + ) + ], + ), ], )); } diff --git a/lib/components/Player/PlayerOverlay.dart b/lib/components/Player/PlayerOverlay.dart index a64662bb..b3140fb7 100644 --- a/lib/components/Player/PlayerOverlay.dart +++ b/lib/components/Player/PlayerOverlay.dart @@ -1,40 +1,38 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; -import 'package:palette_generator/palette_generator.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotube/components/Player/PlayerTrackDetails.dart'; +import 'package:spotube/hooks/playback.dart'; import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/hooks/useIsCurrentRoute.dart'; +import 'package:spotube/hooks/usePaletteColor.dart'; +import 'package:spotube/provider/Playback.dart'; -class PlayerOverlay extends HookWidget { - final Widget controls; +class PlayerOverlay extends HookConsumerWidget { final String albumArt; - final PaletteColor paletteColor; + const PlayerOverlay({ - required this.controls, required this.albumArt, - required this.paletteColor, Key? key, }) : super(key: key); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, ref) { final breakpoint = useBreakpoints(); - final isCurrentRoute = useState(null); + final isCurrentRoute = useIsCurrentRoute("/"); + final paletteColor = usePaletteColor(context, albumArt); + final playback = ref.watch(playbackProvider); - useEffect(() { - WidgetsBinding.instance?.addPostFrameCallback((timer) { - final matches = GoRouter.of(context).location == "/"; - if (matches != isCurrentRoute.value) { - isCurrentRoute.value = matches; - } - }); - return null; - }); - - if (isCurrentRoute.value == false) { + if (isCurrentRoute == false) { return Container(); } + final onNext = useNextTrack(playback); + + final onPrevious = usePreviousTrack(playback); + + final _playOrPause = useTogglePlayPause(playback); + return Positioned( right: (breakpoint.isMd ? 10 : 5), left: (breakpoint.isSm ? 5 : 80), @@ -46,17 +44,49 @@ class PlayerOverlay extends HookWidget { color: paletteColor.color, borderRadius: BorderRadius.circular(5), ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: PlayerTrackDetails( - albumArt: albumArt, - color: paletteColor.bodyTextColor, + child: Material( + type: MaterialType.transparency, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: GestureDetector( + onTap: () => GoRouter.of(context).push( + "/player", + extra: paletteColor, + ), + child: PlayerTrackDetails( + albumArt: albumArt, + color: paletteColor.bodyTextColor, + ), + ), ), - ), - Expanded(child: controls), - ], + Row( + children: [ + IconButton( + icon: const Icon(Icons.skip_previous_rounded), + color: paletteColor.bodyTextColor, + onPressed: () { + onPrevious(); + }), + IconButton( + icon: Icon( + playback.isPlaying + ? Icons.pause_rounded + : Icons.play_arrow_rounded, + ), + color: paletteColor.bodyTextColor, + onPressed: _playOrPause, + ), + IconButton( + icon: const Icon(Icons.skip_next_rounded), + onPressed: () => onNext(), + color: paletteColor.bodyTextColor, + ), + ], + ), + ], + ), ), ), ); diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart new file mode 100644 index 00000000..c036bcdb --- /dev/null +++ b/lib/components/Player/PlayerView.dart @@ -0,0 +1,98 @@ +import 'dart:math'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotube/components/Player/PlayerControls.dart'; +import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; +import 'package:spotube/helpers/artists-to-clickable-artists.dart'; +import 'package:spotube/helpers/image-to-url-string.dart'; +import 'package:spotube/hooks/useBreakpoints.dart'; +import 'package:spotube/provider/Playback.dart'; + +class PlayerView extends HookConsumerWidget { + final PaletteColor paletteColor; + const PlayerView({ + required this.paletteColor, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context, ref) { + final currentTrack = ref.watch(playbackProvider.select( + (value) => value.currentTrack, + )); + final breakpoint = useBreakpoints(); + + useEffect(() { + if (breakpoint.isMoreThan(Breakpoints.md)) { + WidgetsBinding.instance?.addPostFrameCallback((_) { + GoRouter.of(context).pop(); + }); + } + return null; + }, [breakpoint]); + + String albumArt = useMemoized( + () => imageToUrlString( + currentTrack?.album?.images, + index: (currentTrack?.album?.images?.length ?? 1) - 1, + ), + [currentTrack?.album?.images], + ); + + return Scaffold( + appBar: const PageWindowTitleBar( + leading: BackButton(), + ), + backgroundColor: paletteColor.color, + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text( + currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline4?.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.titleTextColor, + ), + ), + artistsToClickableArtists( + currentTrack?.artists ?? [], + mainAxisAlignment: MainAxisAlignment.center, + textStyle: Theme.of(context).textTheme.headline6!.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.bodyTextColor, + ), + ), + ], + ), + HookBuilder(builder: (context) { + final ticker = useSingleTickerProvider(); + final controller = useAnimationController( + duration: const Duration(seconds: 10), + vsync: ticker, + )..repeat(); + return RotationTransition( + turns: Tween(begin: 0.0, end: 1.0).animate(controller), + child: CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + ), + radius: MediaQuery.of(context).size.width * + (breakpoint.isSm ? 0.4 : 0.3), + ), + ); + }), + PlayerControls(iconColor: paletteColor.bodyTextColor), + ], + ), + ); + } +} diff --git a/lib/components/Playlist/PlaylistCard.dart b/lib/components/Playlist/PlaylistCard.dart index 8f854c41..95f2c60c 100644 --- a/lib/components/Playlist/PlaylistCard.dart +++ b/lib/components/Playlist/PlaylistCard.dart @@ -54,6 +54,7 @@ class PlaylistCard extends HookConsumerWidget { thumbnail: imageToUrlString(playlist.images), ); playback.setCurrentTrack = tracks.first; + await playback.startPlaying(); }, ); } diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index c615e3cd..1c8ef708 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -11,7 +11,8 @@ class PlaylistView extends ConsumerWidget { final PlaylistSimple playlist; const PlaylistView(this.playlist, {Key? key}) : super(key: key); - playPlaylist(Playback playback, List tracks, {Track? currentTrack}) { + playPlaylist(Playback playback, List tracks, + {Track? currentTrack}) async { currentTrack ??= tracks.first; var isPlaylistPlaying = playback.currentPlaylist?.id != null && playback.currentPlaylist?.id == playlist.id; @@ -28,6 +29,7 @@ class PlaylistView extends ConsumerWidget { currentTrack.id != playback.currentTrack?.id) { playback.setCurrentTrack = currentTrack; } + await playback.startPlaying(); } @override diff --git a/lib/components/Search/Search.dart b/lib/components/Search/Search.dart index 07953903..6dd2e493 100644 --- a/lib/components/Search/Search.dart +++ b/lib/components/Search/Search.dart @@ -125,6 +125,7 @@ class Search extends HookConsumerWidget { playback.currentTrack?.id) { playback.setCurrentTrack = currentTrack; } + await playback.startPlaying(); }, ); }), diff --git a/lib/hooks/playback.dart b/lib/hooks/playback.dart new file mode 100644 index 00000000..5fe1bdce --- /dev/null +++ b/lib/hooks/playback.dart @@ -0,0 +1,42 @@ +import 'package:spotube/provider/Playback.dart'; + +Future Function() useNextTrack(Playback playback) { + return () async { + try { + await playback.player.pause(); + await playback.player.seek(Duration.zero); + playback.movePlaylistPositionBy(1); + } catch (e, stack) { + print("[PlayerControls.onNext()] $e"); + print(stack); + } + }; +} + +Future Function() usePreviousTrack(Playback playback) { + return () async { + try { + await playback.player.pause(); + await playback.player.seek(Duration.zero); + playback.movePlaylistPositionBy(-1); + } catch (e, stack) { + print("[PlayerControls.onPrevious()] $e"); + print(stack); + } + }; +} + +Future Function([dynamic]) useTogglePlayPause(Playback playback) { + return ([key]) async { + print("CLICK CLICK"); + try { + if (playback.currentTrack == null) return; + playback.isPlaying + ? await playback.player.pause() + : await playback.player.play(); + } catch (e, stack) { + print("[PlayPauseShortcut] $e"); + print(stack); + } + }; +} diff --git a/lib/hooks/useHotKeys.dart b/lib/hooks/useHotKeys.dart new file mode 100644 index 00000000..2ccfb349 --- /dev/null +++ b/lib/hooks/useHotKeys.dart @@ -0,0 +1,45 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:hotkey_manager/hotkey_manager.dart'; +import 'package:spotube/hooks/playback.dart'; +import 'package:spotube/models/GlobalKeyActions.dart'; +import 'package:spotube/provider/Playback.dart'; +import 'package:spotube/provider/UserPreferences.dart'; + +useHotKeys(WidgetRef ref) { + final playback = ref.watch(playbackProvider); + final preferences = ref.watch(userPreferencesProvider); + List _hotKeys = []; + + final onNext = useNextTrack(playback); + + final onPrevious = usePreviousTrack(playback); + + final _playOrPause = useTogglePlayPause(playback); + + useEffect(() { + _hotKeys = [ + GlobalKeyActions( + HotKey(KeyCode.space, scope: HotKeyScope.inapp), + _playOrPause, + ), + if (preferences.nextTrackHotKey != null) + GlobalKeyActions(preferences.nextTrackHotKey!, (key) => onNext()), + if (preferences.prevTrackHotKey != null) + GlobalKeyActions(preferences.prevTrackHotKey!, (key) => onPrevious()), + if (preferences.playPauseHotKey != null) + GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause) + ]; + Future.wait( + _hotKeys.map((e) { + return hotKeyManager.register( + e.hotKey, + keyDownHandler: e.onKeyDown, + ); + }), + ); + return () { + Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey))); + }; + }); +} diff --git a/lib/hooks/useIsCurrentRoute.dart b/lib/hooks/useIsCurrentRoute.dart new file mode 100644 index 00000000..eeb1ff77 --- /dev/null +++ b/lib/hooks/useIsCurrentRoute.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; + +bool? useIsCurrentRoute([String matcher = "/"]) { + final isCurrentRoute = useState(null); + final context = useContext(); + useEffect(() { + WidgetsBinding.instance?.addPostFrameCallback((timer) { + final isCurrent = GoRouter.of(context).location == matcher; + if (isCurrent != isCurrentRoute.value) { + isCurrentRoute.value = isCurrent; + } + }); + return null; + }); + return isCurrentRoute.value; +} diff --git a/lib/hooks/usePaletteColor.dart b/lib/hooks/usePaletteColor.dart index 75a21ca4..6e19c8e1 100644 --- a/lib/hooks/usePaletteColor.dart +++ b/lib/hooks/usePaletteColor.dart @@ -3,21 +3,22 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:palette_generator/palette_generator.dart'; -PaletteColor usePaletteColor(String imageUrl) { +PaletteColor usePaletteColor(BuildContext context, imageUrl) { final paletteColor = useState(PaletteColor(Colors.grey[300]!, 0)); - - final context = useContext(); + final mounted = useIsMounted(); useEffect(() { - PaletteGenerator.fromImageProvider( - CachedNetworkImageProvider( - imageUrl, - cacheKey: imageUrl, - maxHeight: 50, - maxWidth: 50, - ), - ).then((palette) { + WidgetsBinding.instance?.addPostFrameCallback((timeStamp) async { + final palette = await PaletteGenerator.fromImageProvider( + CachedNetworkImageProvider( + imageUrl, + cacheKey: imageUrl, + maxHeight: 50, + maxWidth: 50, + ), + ); + if (!mounted()) return; final color = Theme.of(context).brightness == Brightness.light ? palette.lightMutedColor ?? palette.lightVibrantColor : palette.darkMutedColor ?? palette.darkVibrantColor; diff --git a/lib/main.dart b/lib/main.dart index ddf5ad8c..a3a3c56c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,16 +7,11 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Album/AlbumView.dart'; -import 'package:spotube/components/Artist/ArtistAlbumView.dart'; -import 'package:spotube/components/Artist/ArtistProfile.dart'; -import 'package:spotube/components/Home/Home.dart'; -import 'package:spotube/components/Playlist/PlaylistView.dart'; -import 'package:spotube/components/Settings.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; +import 'package:spotube/models/GoRouteDeclarations.dart'; import 'package:spotube/models/LocalStorageKeys.dart'; +import 'package:spotube/provider/AudioPlayer.dart'; import 'package:spotube/provider/ThemeProvider.dart'; +import 'package:spotube/provider/YouTube.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -33,59 +28,12 @@ void main() async { } class MyApp extends HookConsumerWidget { - final GoRouter _router = GoRouter( - routes: [ - GoRoute( - path: "/", - builder: (context, state) => const Home(), - ), - GoRoute( - path: "/settings", - pageBuilder: (context, state) => SpotubePage( - child: const Settings(), - ), - ), - GoRoute( - path: "/album/:id", - pageBuilder: (context, state) { - assert(state.extra is AlbumSimple); - return SpotubePage(child: AlbumView(state.extra as AlbumSimple)); - }, - ), - GoRoute( - path: "/artist/:id", - pageBuilder: (context, state) { - assert(state.params["id"] != null); - return SpotubePage(child: ArtistProfile(state.params["id"]!)); - }, - ), - GoRoute( - path: "/artist-album/:id", - pageBuilder: (context, state) { - assert(state.params["id"] != null); - assert(state.extra is String); - return SpotubePage( - child: ArtistAlbumView( - state.params["id"]!, - state.extra as String, - ), - ); - }, - ), - GoRoute( - path: "/playlist/:id", - pageBuilder: (context, state) { - assert(state.extra is PlaylistSimple); - return SpotubePage( - child: PlaylistView(state.extra as PlaylistSimple), - ); - }, - ), - ], - ); + final GoRouter _router = createGoRouter(); @override Widget build(BuildContext context, ref) { var themeMode = ref.watch(themeProvider); + var player = ref.watch(audioPlayerProvider); + var youtube = ref.watch(youtubeProvider); useEffect(() { SharedPreferences.getInstance().then((localStorage) { String? themeMode = localStorage.getString(LocalStorageKeys.themeMode); @@ -102,7 +50,10 @@ class MyApp extends HookConsumerWidget { themeNotifier.state = ThemeMode.system; } }); - return null; + return () { + player.dispose(); + youtube.close(); + }; }, []); return MaterialApp.router( diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart new file mode 100644 index 00000000..926c2213 --- /dev/null +++ b/lib/models/GoRouteDeclarations.dart @@ -0,0 +1,74 @@ +import 'package:go_router/go_router.dart'; +import 'package:palette_generator/palette_generator.dart'; +import 'package:spotify/spotify.dart'; +import 'package:spotube/components/Album/AlbumView.dart'; +import 'package:spotube/components/Artist/ArtistAlbumView.dart'; +import 'package:spotube/components/Artist/ArtistProfile.dart'; +import 'package:spotube/components/Home/Home.dart'; +import 'package:spotube/components/Player/PlayerControls.dart'; +import 'package:spotube/components/Player/PlayerView.dart'; +import 'package:spotube/components/Playlist/PlaylistView.dart'; +import 'package:spotube/components/Settings.dart'; +import 'package:spotube/components/Shared/SpotubePageRoute.dart'; + +GoRouter createGoRouter() => GoRouter( + routes: [ + GoRoute( + path: "/", + builder: (context, state) => const Home(), + ), + GoRoute( + path: "/settings", + pageBuilder: (context, state) => SpotubePage( + child: const Settings(), + ), + ), + GoRoute( + path: "/album/:id", + pageBuilder: (context, state) { + assert(state.extra is AlbumSimple); + return SpotubePage(child: AlbumView(state.extra as AlbumSimple)); + }, + ), + GoRoute( + path: "/artist/:id", + pageBuilder: (context, state) { + assert(state.params["id"] != null); + return SpotubePage(child: ArtistProfile(state.params["id"]!)); + }, + ), + GoRoute( + path: "/artist-album/:id", + pageBuilder: (context, state) { + assert(state.params["id"] != null); + assert(state.extra is String); + return SpotubePage( + child: ArtistAlbumView( + state.params["id"]!, + state.extra as String, + ), + ); + }, + ), + GoRoute( + path: "/playlist/:id", + pageBuilder: (context, state) { + assert(state.extra is PlaylistSimple); + return SpotubePage( + child: PlaylistView(state.extra as PlaylistSimple), + ); + }, + ), + GoRoute( + path: "/player", + pageBuilder: (context, state) { + assert(state.extra is PaletteColor); + return SpotubePage( + child: PlayerView( + paletteColor: state.extra as PaletteColor, + ), + ); + }, + ) + ], + ); diff --git a/lib/provider/AudioPlayer.dart b/lib/provider/AudioPlayer.dart new file mode 100644 index 00000000..6aff379a --- /dev/null +++ b/lib/provider/AudioPlayer.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; + +final audioPlayerProvider = Provider((ref) { + return AudioPlayer(); +}); diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index b8978555..a94fc842 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -1,6 +1,13 @@ +import 'dart:async'; + import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:spotify/spotify.dart'; +import 'package:spotube/helpers/search-youtube.dart'; +import 'package:spotube/provider/AudioPlayer.dart'; +import 'package:spotube/provider/YouTube.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; class CurrentPlaylist { List? _tempTrack; @@ -8,6 +15,7 @@ class CurrentPlaylist { String id; String name; String thumbnail; + CurrentPlaylist({ required this.tracks, required this.id, @@ -37,13 +45,95 @@ class CurrentPlaylist { class Playback extends ChangeNotifier { CurrentPlaylist? _currentPlaylist; Track? _currentTrack; - Playback({CurrentPlaylist? currentPlaylist, Track? currentTrack}) { - _currentPlaylist = currentPlaylist; - _currentTrack = currentTrack; + + // states + bool _isPlaying = false; + Duration? _duration; + + // using custom listeners for duration as it changes super quickly + // which will cause re-renders in components that don't even need it + // thus only allowing to listen to change in duration through only + // a listener function + List _durationListeners = []; + + // listeners + StreamSubscription? _playingStreamListener; + StreamSubscription? _durationStreamListener; + StreamSubscription? _processingStateStreamListener; + + AudioPlayer player; + YoutubeExplode youtube; + Playback({ + required this.player, + required this.youtube, + CurrentPlaylist? currentPlaylist, + Track? currentTrack, + }) : _currentPlaylist = currentPlaylist, + _currentTrack = currentTrack { + _playingStreamListener = player.playingStream.listen( + (playing) { + _isPlaying = playing; + notifyListeners(); + }, + ); + + _durationStreamListener = 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 `_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(); + } + _duration = duration; + _callAllDurationListeners(duration); + // for avoiding unnecessary re-renders in other components that + // doesn't need duration + } + }); + + _processingStateStreamListener = + player.processingStateStream.listen((event) async { + try { + if (event == ProcessingState.completed && _currentTrack?.id != null) { + movePlaylistPositionBy(1); + } + } catch (e, stack) { + print("[PrecessingStateStreamListener] $e"); + print(stack); + } + }); } CurrentPlaylist? get currentPlaylist => _currentPlaylist; Track? get currentTrack => _currentTrack; + bool get isPlaying => _isPlaying; + + /// this duration field is almost static & changes occasionally + /// + /// If you want realtime duration with state-update/re-render + /// use custom state & the [addDurationChangeListener] function to do so + Duration? get duration => _duration; + + _callAllDurationListeners(Duration? arg) { + for (var listener in _durationListeners) { + listener(arg); + } + } + + void addDurationChangeListener(void Function(Duration? duration) listener) { + _durationListeners.add(listener); + } + + void removeDurationChangeListener( + void Function(Duration? duration) listener) { + _durationListeners = + _durationListeners.where((p) => p != listener).toList(); + } set setCurrentTrack(Track track) { _currentTrack = track; @@ -55,7 +145,10 @@ class Playback extends ChangeNotifier { notifyListeners(); } - reset() { + void reset() { + _isPlaying = false; + _duration = null; + _callAllDurationListeners(null); _currentPlaylist = null; _currentTrack = null; notifyListeners(); @@ -76,6 +169,77 @@ class Playback extends ChangeNotifier { return false; } } + + @override + dispose() { + _processingStateStreamListener?.cancel(); + _durationStreamListener?.cancel(); + _playingStreamListener?.cancel(); + super.dispose(); + } + + movePlaylistPositionBy(int pos) { + if (_currentTrack != null && _currentPlaylist != null) { + int index = _currentPlaylist!.trackIds.indexOf(_currentTrack!.id!) + pos; + + var safeIndex = index > _currentPlaylist!.trackIds.length - 1 + ? 0 + : index < 0 + ? _currentPlaylist!.trackIds.length + : index; + Track? track = _currentPlaylist!.tracks.asMap().containsKey(safeIndex) + ? _currentPlaylist!.tracks.elementAt(safeIndex) + : null; + if (track != null) { + _duration = null; + _callAllDurationListeners(null); + _currentTrack = track; + notifyListeners(); + // starts to play the newly entered next/prev track + startPlaying(); + } + } + } + + Future startPlaying([Track? track]) async { + try { + // the track is already playing so no need to change that + if (track != null && track.id == _currentTrack?.id) return; + track ??= _currentTrack; + if (track != null) { + Uri? parsedUri = Uri.tryParse(track.uri ?? ""); + if (parsedUri != null && parsedUri.hasAbsolutePath) { + await player + .setAudioSource( + AudioSource.uri(parsedUri), + preload: true, + ) + .then((value) async { + _currentTrack = track; + _duration = value; + _callAllDurationListeners(value); + notifyListeners(); + }); + } + final ytTrack = await toYoutubeTrack(youtube, track); + if (setTrackUriById(track.id!, ytTrack.uri!)) { + await player + .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) + .then((value) { + _currentTrack = track; + notifyListeners(); + }); + } + } + } catch (e, stack) { + print("[Playback.startPlaying] $e"); + print(stack); + } + } } -var playbackProvider = ChangeNotifierProvider((_) => Playback()); +final playbackProvider = ChangeNotifierProvider((ref) { + final player = ref.watch(audioPlayerProvider); + final youtube = ref.watch(youtubeProvider); + return Playback(player: player, youtube: youtube); +}); diff --git a/lib/provider/YouTube.dart b/lib/provider/YouTube.dart new file mode 100644 index 00000000..d96f8c1f --- /dev/null +++ b/lib/provider/YouTube.dart @@ -0,0 +1,4 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:youtube_explode_dart/youtube_explode_dart.dart'; + +final youtubeProvider = Provider((ref) => YoutubeExplode()); From 39a92a56f3bbcca449b4587f9be995d15fc3912c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 13 Mar 2022 15:08:55 +0600 Subject: [PATCH 23/25] bugfixed first played playlist/track doesn't play --- assets/warmer.mp3 | Bin 0 -> 8777 bytes lib/components/Player/Player.dart | 10 ++++++ lib/components/Player/PlayerView.dart | 43 +++++++++++++------------- lib/provider/Playback.dart | 5 ++- 4 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 assets/warmer.mp3 diff --git a/assets/warmer.mp3 b/assets/warmer.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..638976b2de9df3f45c33c21261a4b6f78109b89d GIT binary patch literal 8777 zcmeIvu@Qm*5Jb^+AkZ!>f(8i8q$f&ntbsxZ=!yXCV8fsM3vYv+*>|0vBg3BCmgJdC zYcjs)7Xo1$RP+N%36`}=Nzd~&x zI#-AmX#EPch3H%%TA=kS)E1(1g=m4+uTWcv&K05sTE9YVAv#xx7HItnwT0+hAzGmI JE7TUE^B)!Ij#2;s literal 0 HcmV?d00001 diff --git a/lib/components/Player/Player.dart b/lib/components/Player/Player.dart index ff04d3b0..d179afb4 100644 --- a/lib/components/Player/Player.dart +++ b/lib/components/Player/Player.dart @@ -33,6 +33,16 @@ class Player extends HookConsumerWidget { final AsyncSnapshot localStorage = useFuture(future, initialData: null); + useEffect(() { + /// 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 + player.setAsset("assets/warmer.mp3"); + return null; + }, []); + useEffect(() { if (localStorage.hasData) { _volume.value = localStorage.data?.getDouble(LocalStorageKeys.volume) ?? diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index c036bcdb..c6281970 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -52,25 +50,28 @@ class PlayerView extends HookConsumerWidget { body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Column( - children: [ - Text( - currentTrack?.name ?? "Not playing", - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.headline4?.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.titleTextColor, - ), - ), - artistsToClickableArtists( - currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, - textStyle: Theme.of(context).textTheme.headline6!.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.bodyTextColor, - ), - ), - ], + Padding( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + Text( + currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline4?.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.titleTextColor, + ), + ), + artistsToClickableArtists( + currentTrack?.artists ?? [], + mainAxisAlignment: MainAxisAlignment.center, + textStyle: Theme.of(context).textTheme.headline6!.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.bodyTextColor, + ), + ), + ], + ), ), HookBuilder(builder: (context) { final ticker = useSingleTickerProvider(); diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index a94fc842..4d59d981 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -224,7 +224,10 @@ class Playback extends ChangeNotifier { final ytTrack = await toYoutubeTrack(youtube, track); if (setTrackUriById(track.id!, ytTrack.uri!)) { await player - .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) + .setAudioSource( + AudioSource.uri(Uri.parse(ytTrack.uri!)), + preload: true, + ) .then((value) { _currentTrack = track; notifyListeners(); From c64f329c42926d6c1444bfe8f69ef319b95cf76a Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Tue, 15 Mar 2022 19:47:29 +0600 Subject: [PATCH 24/25] Android SafeArea issues fixed configurations for different plugin for android added adjusted platform bound opertations --- android/app/src/main/AndroidManifest.xml | 56 +- android/build.gradle | 2 +- ios/Podfile | 6 + ios/Runner/Info.plist | 95 ++-- lib/components/Album/AlbumView.dart | 112 ++-- lib/components/Artist/ArtistAlbumView.dart | 50 +- lib/components/Artist/ArtistProfile.dart | 505 +++++++++--------- lib/components/Home/Home.dart | 111 ++-- lib/components/Player/PlayerView.dart | 102 ++-- lib/components/Playlist/PlaylistView.dart | 114 ++-- lib/components/Settings.dart | 312 +++++------ lib/components/Shared/PageWindowTitleBar.dart | 15 +- lib/hooks/playback.dart | 1 - lib/hooks/useHotKeys.dart | 3 + lib/main.dart | 24 +- lib/models/GoRouteDeclarations.dart | 1 - lib/provider/Playback.dart | 15 +- pubspec.lock | 4 +- pubspec.yaml | 1 + 19 files changed, 793 insertions(+), 736 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 19246de8..591455ab 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,34 +1,30 @@ - - - - + + + + + + + + + - - - - - - - - - - + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 24047dce..4256f917 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.3.50' + ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() diff --git a/ios/Podfile b/ios/Podfile index 1e8c3c90..42542813 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -37,5 +37,11 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_ios_build_settings(target) + target.build_configurations.each do |config| + config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ + '$(inherited)', + 'AUDIO_SESSION_MICROPHONE=0' + ] + end end end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 474ff9f7..05d639bc 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -1,47 +1,54 @@ - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Sptube - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - spotube - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Sptube + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + spotube + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSAllowsArbitraryLoadsForMedia + + + + \ No newline at end of file diff --git a/lib/components/Album/AlbumView.dart b/lib/components/Album/AlbumView.dart index 2d1f0da8..2054602d 100644 --- a/lib/components/Album/AlbumView.dart +++ b/lib/components/Album/AlbumView.dart @@ -38,63 +38,65 @@ class AlbumView extends ConsumerWidget { var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; SpotifyApi spotify = ref.watch(spotifyProvider); - return Scaffold( - body: FutureBuilder>( - future: spotify.albums.getTracks(album.id!).all(), - builder: (context, snapshot) { - List tracks = snapshot.data?.map((trackSmp) { - return simpleTrackToTrack(trackSmp, album); - }).toList() ?? - []; - return Column( - children: [ - PageWindowTitleBar( - leading: Row( - children: [ - // nav back - const BackButton(), - // heart playlist - IconButton( - icon: const Icon(Icons.favorite_outline_rounded), - onPressed: () {}, - ), - // play playlist - IconButton( - icon: Icon( - isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded, + return SafeArea( + child: Scaffold( + body: FutureBuilder>( + future: spotify.albums.getTracks(album.id!).all(), + builder: (context, snapshot) { + List tracks = snapshot.data?.map((trackSmp) { + return simpleTrackToTrack(trackSmp, album); + }).toList() ?? + []; + return Column( + children: [ + PageWindowTitleBar( + leading: Row( + children: [ + // nav back + const BackButton(), + // heart playlist + IconButton( + icon: const Icon(Icons.favorite_outline_rounded), + onPressed: () {}, ), - onPressed: snapshot.hasData - ? () => playPlaylist(playback, tracks) - : null, - ) - ], - ), - ), - Center( - child: Text(album.name!, - style: Theme.of(context).textTheme.headline4), - ), - snapshot.hasError - ? const Center(child: Text("Error occurred")) - : !snapshot.hasData - ? const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive()), - ) - : TracksTableView( - tracks, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - playback, - tracks, - currentTrack: currentTrack, - ), + // play playlist + IconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, ), - ], - ); - }), + onPressed: snapshot.hasData + ? () => playPlaylist(playback, tracks) + : null, + ) + ], + ), + ), + Center( + child: Text(album.name!, + style: Theme.of(context).textTheme.headline4), + ), + snapshot.hasError + ? const Center(child: Text("Error occurred")) + : !snapshot.hasData + ? const Expanded( + child: Center( + child: CircularProgressIndicator.adaptive()), + ) + : TracksTableView( + tracks, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + playback, + tracks, + currentTrack: currentTrack, + ), + ), + ], + ); + }), + ), ); } } diff --git a/lib/components/Artist/ArtistAlbumView.dart b/lib/components/Artist/ArtistAlbumView.dart index fb3c150b..b90a78a6 100644 --- a/lib/components/Artist/ArtistAlbumView.dart +++ b/lib/components/Artist/ArtistAlbumView.dart @@ -59,32 +59,34 @@ class _ArtistAlbumViewState extends ConsumerState { @override Widget build(BuildContext context) { - return Scaffold( - appBar: const PageWindowTitleBar(leading: BackButton()), - body: Column( - children: [ - Text( - widget.artistName, - style: Theme.of(context).textTheme.headline4, - ), - Expanded( - child: PagedGridView( - pagingController: _pagingController, - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 260, - childAspectRatio: 9 / 13, - crossAxisSpacing: 20, - mainAxisSpacing: 20, - ), - padding: const EdgeInsets.all(10), - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return AlbumCard(item); - }, + return SafeArea( + child: Scaffold( + appBar: const PageWindowTitleBar(leading: BackButton()), + body: Column( + children: [ + Text( + widget.artistName, + style: Theme.of(context).textTheme.headline4, + ), + Expanded( + child: PagedGridView( + pagingController: _pagingController, + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 260, + childAspectRatio: 9 / 13, + crossAxisSpacing: 20, + mainAxisSpacing: 20, + ), + padding: const EdgeInsets.all(10), + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return AlbumCard(item); + }, + ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/components/Artist/ArtistProfile.dart b/lib/components/Artist/ArtistProfile.dart index 2ae74acb..f5f47f65 100644 --- a/lib/components/Artist/ArtistProfile.dart +++ b/lib/components/Artist/ArtistProfile.dart @@ -45,270 +45,273 @@ class ArtistProfile extends HookConsumerWidget { final breakpoint = useBreakpoints(); - return Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), - ), - body: FutureBuilder( - future: spotify.artists.get(artistId), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator.adaptive()); - } + return SafeArea( + child: Scaffold( + appBar: const PageWindowTitleBar( + leading: BackButton(), + ), + body: FutureBuilder( + future: spotify.artists.get(artistId), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center(child: CircularProgressIndicator.adaptive()); + } - return SingleChildScrollView( - controller: parentScrollController, - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - runAlignment: WrapAlignment.center, - children: [ - const SizedBox(width: 50), - CircleAvatar( - radius: avatarWidth, - backgroundImage: CachedNetworkImageProvider( - imageToUrlString(snapshot.data!.images), + return SingleChildScrollView( + controller: parentScrollController, + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + runAlignment: WrapAlignment.center, + children: [ + const SizedBox(width: 50), + CircleAvatar( + radius: avatarWidth, + backgroundImage: CachedNetworkImageProvider( + imageToUrlString(snapshot.data!.images), + ), ), - ), - Padding( - padding: const EdgeInsets.all(20), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, vertical: 5), - decoration: BoxDecoration( - color: Colors.blue, - borderRadius: BorderRadius.circular(50)), - child: Text(snapshot.data!.type!.toUpperCase(), - style: chipTextVariant?.copyWith( - color: Colors.white)), - ), - Text( - snapshot.data!.name!, - style: breakpoint.isSm - ? textTheme.headline4 - : textTheme.headline2, - ), - Text( - "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", - style: breakpoint.isSm - ? textTheme.bodyText1 - : textTheme.headline5, - ), - const SizedBox(height: 20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - // TODO: Implement check if user follows this artist - // LIMITATION: spotify-dart lib - FutureBuilder( - future: Future.value(true), - builder: (context, snapshot) { - return OutlinedButton( - onPressed: () async { - // TODO: make `follow/unfollow` artists button work - // LIMITATION: spotify-dart lib - }, - child: Text(snapshot.data == true - ? "Following" - : "Follow"), - ); - }), - IconButton( - icon: const Icon(Icons.share_rounded), - onPressed: () { - Clipboard.setData( - ClipboardData( - text: snapshot - .data?.externalUrls?.spotify), - ).then((val) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - "Artist URL copied to clipboard", - textAlign: TextAlign.center, + Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: Colors.blue, + borderRadius: BorderRadius.circular(50)), + child: Text(snapshot.data!.type!.toUpperCase(), + style: chipTextVariant?.copyWith( + color: Colors.white)), + ), + Text( + snapshot.data!.name!, + style: breakpoint.isSm + ? textTheme.headline4 + : textTheme.headline2, + ), + Text( + "${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers", + style: breakpoint.isSm + ? textTheme.bodyText1 + : textTheme.headline5, + ), + const SizedBox(height: 20), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + // TODO: Implement check if user follows this artist + // LIMITATION: spotify-dart lib + FutureBuilder( + future: Future.value(true), + builder: (context, snapshot) { + return OutlinedButton( + onPressed: () async { + // TODO: make `follow/unfollow` artists button work + // LIMITATION: spotify-dart lib + }, + child: Text(snapshot.data == true + ? "Following" + : "Follow"), + ); + }), + IconButton( + icon: const Icon(Icons.share_rounded), + onPressed: () { + Clipboard.setData( + ClipboardData( + text: snapshot + .data?.externalUrls?.spotify), + ).then((val) { + ScaffoldMessenger.of(context) + .showSnackBar( + const SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + "Artist URL copied to clipboard", + textAlign: TextAlign.center, + ), ), - ), - ); - }); - }, - ) - ], - ) - ], + ); + }); + }, + ) + ], + ) + ], + ), ), - ), - ], - ), - const SizedBox(height: 50), - FutureBuilder>( - future: - spotify.artists.getTopTracks(snapshot.data!.id!, "US"), - builder: (context, trackSnapshot) { - if (!trackSnapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - Playback playback = ref.watch(playbackProvider); - var isPlaylistPlaying = - playback.currentPlaylist?.id == snapshot.data?.id; - playPlaylist(List tracks, - {Track? currentTrack}) async { - currentTrack ??= tracks.first; - if (!isPlaylistPlaying) { - playback.setCurrentPlaylist = CurrentPlaylist( - tracks: tracks, - id: snapshot.data!.id!, - name: "${snapshot.data!.name!} To Tracks", - thumbnail: imageToUrlString(snapshot.data?.images), - ); - playback.setCurrentTrack = currentTrack; - } else if (isPlaylistPlaying && - currentTrack.id != null && - currentTrack.id != playback.currentTrack?.id) { - playback.setCurrentTrack = currentTrack; + ], + ), + const SizedBox(height: 50), + FutureBuilder>( + future: + spotify.artists.getTopTracks(snapshot.data!.id!, "US"), + builder: (context, trackSnapshot) { + if (!trackSnapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive()); + } + Playback playback = ref.watch(playbackProvider); + var isPlaylistPlaying = + playback.currentPlaylist?.id == snapshot.data?.id; + playPlaylist(List tracks, + {Track? currentTrack}) async { + currentTrack ??= tracks.first; + if (!isPlaylistPlaying) { + playback.setCurrentPlaylist = CurrentPlaylist( + tracks: tracks, + id: snapshot.data!.id!, + name: "${snapshot.data!.name!} To Tracks", + thumbnail: imageToUrlString(snapshot.data?.images), + ); + playback.setCurrentTrack = currentTrack; + } else if (isPlaylistPlaying && + currentTrack.id != null && + currentTrack.id != playback.currentTrack?.id) { + playback.setCurrentTrack = currentTrack; + } + await playback.startPlaying(); } - await playback.startPlaying(); - } - return Column(children: [ - Row( - children: [ - Text( - "Top Tracks", - style: Theme.of(context).textTheme.headline4, - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 5), - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - borderRadius: BorderRadius.circular(50), + return Column(children: [ + Row( + children: [ + Text( + "Top Tracks", + style: Theme.of(context).textTheme.headline4, ), - child: IconButton( - icon: Icon(isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded), - color: Colors.white, - onPressed: trackSnapshot.hasData - ? () => - playPlaylist(trackSnapshot.data!.toList()) - : null, - ), - ) - ], - ), - ...trackSnapshot.data - ?.toList() - .asMap() - .entries - .map((track) { - String duration = - "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; - String? thumbnailUrl = imageToUrlString( - track.value.album?.images, - index: - (track.value.album?.images?.length ?? 1) - - 1); - return TrackTile( - playback, - duration: duration, - track: track, - thumbnailUrl: thumbnailUrl, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - trackSnapshot.data!.toList(), - currentTrack: track.value, + Container( + margin: const EdgeInsets.symmetric(horizontal: 5), + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(50), ), - ); - }) ?? - [], - ]); - }, - ), - const SizedBox(height: 50), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "Albums", - style: Theme.of(context).textTheme.headline4, - ), - TextButton( - child: const Text("See All"), - onPressed: () { - GoRouter.of(context).push( - "/artist-album/$artistId", - extra: snapshot.data?.name ?? "KRTX", - ); - }, - ) - ], - ), - const SizedBox(height: 10), - FutureBuilder>( - future: spotify.artists - .albums(snapshot.data!.id!) - .getPage(5, 0) - .then((al) => al.items?.toList() ?? []), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - return Scrollbar( - controller: scrollController, - child: SingleChildScrollView( + child: IconButton( + icon: Icon(isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded), + color: Colors.white, + onPressed: trackSnapshot.hasData + ? () => playPlaylist( + trackSnapshot.data!.toList()) + : null, + ), + ) + ], + ), + ...trackSnapshot.data + ?.toList() + .asMap() + .entries + .map((track) { + String duration = + "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; + String? thumbnailUrl = imageToUrlString( + track.value.album?.images, + index: + (track.value.album?.images?.length ?? 1) - + 1); + return TrackTile( + playback, + duration: duration, + track: track, + thumbnailUrl: thumbnailUrl, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + trackSnapshot.data!.toList(), + currentTrack: track.value, + ), + ); + }) ?? + [], + ]); + }, + ), + const SizedBox(height: 50), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Albums", + style: Theme.of(context).textTheme.headline4, + ), + TextButton( + child: const Text("See All"), + onPressed: () { + GoRouter.of(context).push( + "/artist-album/$artistId", + extra: snapshot.data?.name ?? "KRTX", + ); + }, + ) + ], + ), + const SizedBox(height: 10), + FutureBuilder>( + future: spotify.artists + .albums(snapshot.data!.id!) + .getPage(5, 0) + .then((al) => al.items?.toList() ?? []), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive()); + } + return Scrollbar( controller: scrollController, - scrollDirection: Axis.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: SingleChildScrollView( + controller: scrollController, + scrollDirection: Axis.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: snapshot.data + ?.map((album) => AlbumCard(album)) + .toList() ?? + [], + ), + ), + ); + }, + ), + const SizedBox(height: 20), + Text( + "Fans also likes", + style: Theme.of(context).textTheme.headline4, + ), + const SizedBox(height: 10), + FutureBuilder>( + future: spotify.artists.getRelatedArtists(artistId), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Center( + child: CircularProgressIndicator.adaptive()); + } + + return Center( + child: Wrap( + spacing: 20, + runSpacing: 20, children: snapshot.data - ?.map((album) => AlbumCard(album)) + ?.map((artist) => ArtistCard(artist)) .toList() ?? [], ), - ), - ); - }, - ), - const SizedBox(height: 20), - Text( - "Fans also likes", - style: Theme.of(context).textTheme.headline4, - ), - const SizedBox(height: 10), - FutureBuilder>( - future: spotify.artists.getRelatedArtists(artistId), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center( - child: CircularProgressIndicator.adaptive()); - } - - return Center( - child: Wrap( - spacing: 20, - runSpacing: 20, - children: snapshot.data - ?.map((artist) => ArtistCard(artist)) - .toList() ?? - [], - ), - ); - }, - ) - ], - ), - ); - }, + ); + }, + ) + ], + ), + ); + }, + ), ), ); } diff --git a/lib/components/Home/Home.dart b/lib/components/Home/Home.dart index 449a5bc7..10fbec93 100644 --- a/lib/components/Home/Home.dart +++ b/lib/components/Home/Home.dart @@ -152,65 +152,68 @@ class Home extends HookConsumerWidget { return const Login(); } - return Scaffold( - body: Column( - children: [ - WindowTitleBarBox( - child: Row( - children: [ - Expanded( - child: Row( - children: [ - Container( - constraints: BoxConstraints( - maxWidth: titleBarDragMaxWidth.toDouble(), - ), - color: - Theme.of(context).navigationRailTheme.backgroundColor, - child: MoveWindow(), - ), - Expanded(child: MoveWindow()), - if (!Platform.isMacOS) const TitleBarActionButtons(), - ], - )) - ], - ), - ), - Expanded( - child: Row( - children: [ - Sidebar( - selectedIndex: _selectedIndex.value, - onSelectedIndexChanged: _onSelectedIndexChanged, - ), - // contents of the spotify - if (_selectedIndex.value == 0) + return SafeArea( + child: Scaffold( + body: Column( + children: [ + WindowTitleBarBox( + child: Row( + children: [ Expanded( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: PagedListView( - pagingController: pagingController, - builderDelegate: PagedChildBuilderDelegate( - itemBuilder: (context, item, index) { - return CategoryCard(item); - }, + child: Row( + children: [ + Container( + constraints: BoxConstraints( + maxWidth: titleBarDragMaxWidth.toDouble(), + ), + color: Theme.of(context) + .navigationRailTheme + .backgroundColor, + child: MoveWindow(), + ), + Expanded(child: MoveWindow()), + if (!Platform.isMacOS) const TitleBarActionButtons(), + ], + )) + ], + ), + ), + Expanded( + child: Row( + children: [ + Sidebar( + selectedIndex: _selectedIndex.value, + onSelectedIndexChanged: _onSelectedIndexChanged, + ), + // contents of the spotify + if (_selectedIndex.value == 0) + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: PagedListView( + pagingController: pagingController, + builderDelegate: PagedChildBuilderDelegate( + itemBuilder: (context, item, index) { + return CategoryCard(item); + }, + ), ), ), ), - ), - if (_selectedIndex.value == 1) const Search(), - if (_selectedIndex.value == 2) const UserLibrary(), - if (_selectedIndex.value == 3) const Lyrics(), - ], + if (_selectedIndex.value == 1) const Search(), + if (_selectedIndex.value == 2) const UserLibrary(), + if (_selectedIndex.value == 3) const Lyrics(), + ], + ), ), - ), - // player itself - const Player(), - SpotubeNavigationBar( - selectedIndex: _selectedIndex.value, - onSelectedIndexChanged: _onSelectedIndexChanged, - ), - ], + // player itself + const Player(), + SpotubeNavigationBar( + selectedIndex: _selectedIndex.value, + onSelectedIndexChanged: _onSelectedIndexChanged, + ), + ], + ), ), ); } diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index c6281970..c410d744 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -42,57 +42,59 @@ class PlayerView extends HookConsumerWidget { [currentTrack?.album?.images], ); - return Scaffold( - appBar: const PageWindowTitleBar( - leading: BackButton(), - ), - backgroundColor: paletteColor.color, - body: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Padding( - padding: const EdgeInsets.all(10), - child: Column( - children: [ - Text( - currentTrack?.name ?? "Not playing", - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.headline4?.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.titleTextColor, - ), - ), - artistsToClickableArtists( - currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, - textStyle: Theme.of(context).textTheme.headline6!.copyWith( - fontWeight: FontWeight.bold, - color: paletteColor.bodyTextColor, - ), - ), - ], - ), - ), - HookBuilder(builder: (context) { - final ticker = useSingleTickerProvider(); - final controller = useAnimationController( - duration: const Duration(seconds: 10), - vsync: ticker, - )..repeat(); - return RotationTransition( - turns: Tween(begin: 0.0, end: 1.0).animate(controller), - child: CircleAvatar( - backgroundImage: CachedNetworkImageProvider( - albumArt, - cacheKey: albumArt, - ), - radius: MediaQuery.of(context).size.width * - (breakpoint.isSm ? 0.4 : 0.3), + return SafeArea( + child: Scaffold( + appBar: const PageWindowTitleBar( + leading: BackButton(), + ), + backgroundColor: paletteColor.color, + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: Column( + children: [ + Text( + currentTrack?.name ?? "Not playing", + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.headline4?.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.titleTextColor, + ), + ), + artistsToClickableArtists( + currentTrack?.artists ?? [], + mainAxisAlignment: MainAxisAlignment.center, + textStyle: Theme.of(context).textTheme.headline6!.copyWith( + fontWeight: FontWeight.bold, + color: paletteColor.bodyTextColor, + ), + ), + ], ), - ); - }), - PlayerControls(iconColor: paletteColor.bodyTextColor), - ], + ), + HookBuilder(builder: (context) { + final ticker = useSingleTickerProvider(); + final controller = useAnimationController( + duration: const Duration(seconds: 10), + vsync: ticker, + )..repeat(); + return RotationTransition( + turns: Tween(begin: 0.0, end: 1.0).animate(controller), + child: CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + albumArt, + cacheKey: albumArt, + ), + radius: MediaQuery.of(context).size.width * + (breakpoint.isSm ? 0.4 : 0.3), + ), + ); + }), + PlayerControls(iconColor: paletteColor.bodyTextColor), + ], + ), ), ); } diff --git a/lib/components/Playlist/PlaylistView.dart b/lib/components/Playlist/PlaylistView.dart index 1c8ef708..fefced39 100644 --- a/lib/components/Playlist/PlaylistView.dart +++ b/lib/components/Playlist/PlaylistView.dart @@ -38,64 +38,66 @@ class PlaylistView extends ConsumerWidget { SpotifyApi spotifyApi = ref.watch(spotifyProvider); var isPlaylistPlaying = playback.currentPlaylist?.id != null && playback.currentPlaylist?.id == playlist.id; - return Scaffold( - body: FutureBuilder>( - future: playlist.id != "user-liked-tracks" - ? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all() - : spotifyApi.tracks.me.saved - .all() - .then((tracks) => tracks.map((e) => e.track!)), - builder: (context, snapshot) { - List tracks = snapshot.data?.toList() ?? []; - return Column( - children: [ - PageWindowTitleBar( - leading: Row( - children: [ - // nav back - const BackButton(), - // heart playlist - IconButton( - icon: const Icon(Icons.favorite_outline_rounded), - onPressed: () {}, - ), - // play playlist - IconButton( - icon: Icon( - isPlaylistPlaying - ? Icons.stop_rounded - : Icons.play_arrow_rounded, + return SafeArea( + child: Scaffold( + body: FutureBuilder>( + future: playlist.id != "user-liked-tracks" + ? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all() + : spotifyApi.tracks.me.saved + .all() + .then((tracks) => tracks.map((e) => e.track!)), + builder: (context, snapshot) { + List tracks = snapshot.data?.toList() ?? []; + return Column( + children: [ + PageWindowTitleBar( + leading: Row( + children: [ + // nav back + const BackButton(), + // heart playlist + IconButton( + icon: const Icon(Icons.favorite_outline_rounded), + onPressed: () {}, ), - onPressed: snapshot.hasData - ? () => playPlaylist(playback, tracks) - : null, - ) - ], - ), - ), - Center( - child: Text(playlist.name!, - style: Theme.of(context).textTheme.headline4), - ), - snapshot.hasError - ? const Center(child: Text("Error occurred")) - : !snapshot.hasData - ? const Expanded( - child: Center( - child: CircularProgressIndicator.adaptive()), - ) - : TracksTableView( - tracks, - onTrackPlayButtonPressed: (currentTrack) => - playPlaylist( - playback, - tracks, - currentTrack: currentTrack, - ), + // play playlist + IconButton( + icon: Icon( + isPlaylistPlaying + ? Icons.stop_rounded + : Icons.play_arrow_rounded, ), - ], - ); - }), + onPressed: snapshot.hasData + ? () => playPlaylist(playback, tracks) + : null, + ) + ], + ), + ), + Center( + child: Text(playlist.name!, + style: Theme.of(context).textTheme.headline4), + ), + snapshot.hasError + ? const Center(child: Text("Error occurred")) + : !snapshot.hasData + ? const Expanded( + child: Center( + child: CircularProgressIndicator.adaptive()), + ) + : TracksTableView( + tracks, + onTrackPlayButtonPressed: (currentTrack) => + playPlaylist( + playback, + tracks, + currentTrack: currentTrack, + ), + ), + ], + ); + }), + ), ); } } diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 32060030..117a8037 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -25,172 +25,174 @@ class Settings extends HookConsumerWidget { geniusAccessToken.value = textEditingController.value.text; }); - return Scaffold( - appBar: PageWindowTitleBar( - leading: const BackButton(), - center: Text( - "Settings", - style: Theme.of(context).textTheme.headline5, + return SafeArea( + child: Scaffold( + appBar: PageWindowTitleBar( + leading: const BackButton(), + center: Text( + "Settings", + style: Theme.of(context).textTheme.headline5, + ), ), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - Row( - children: [ - Expanded( - flex: 2, - child: Text( - "Genius Access Token", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - Expanded( - flex: 1, - child: TextField( - controller: textEditingController, - decoration: InputDecoration( - hintText: preferences.geniusAccessToken, + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + Row( + children: [ + Expanded( + flex: 2, + child: Text( + "Genius Access Token", + style: Theme.of(context).textTheme.subtitle1, ), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: ElevatedButton( - onPressed: geniusAccessToken.value != null - ? () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - preferences - .setGeniusAccessToken(geniusAccessToken.value); - localStorage.setString( - LocalStorageKeys.geniusAccessToken, - geniusAccessToken.value ?? ""); - - geniusAccessToken.value = null; - - textEditingController.text = ""; - } - : null, - child: const Text("Save"), - ), - ) - ], - ), - 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: [ - const Text("Theme"), - DropdownButton( - value: theme, - items: const [ - DropdownMenuItem( - child: Text( - "Dark", + Expanded( + flex: 1, + child: TextField( + controller: textEditingController, + decoration: InputDecoration( + hintText: preferences.geniusAccessToken, ), - value: ThemeMode.dark, ), - DropdownMenuItem( - child: Text( - "Light", - ), - value: ThemeMode.light, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: ElevatedButton( + onPressed: geniusAccessToken.value != null + ? () async { + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + preferences.setGeniusAccessToken( + geniusAccessToken.value); + localStorage.setString( + LocalStorageKeys.geniusAccessToken, + geniusAccessToken.value ?? ""); + + geniusAccessToken.value = null; + + textEditingController.text = ""; + } + : null, + child: const Text("Save"), ), - DropdownMenuItem( - child: Text("System"), - value: ThemeMode.system, - ), - ], - onChanged: (value) { - if (value != null) { - ref.read(themeProvider.notifier).state = value; - } - }, - ) - ], - ), - const SizedBox(height: 10), - Builder(builder: (context) { - Auth auth = ref.watch(authProvider); - return Row( + ) + ], + ), + 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: [ - const Text("Log out of this account"), - ElevatedButton( - child: const Text("Logout"), - style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(Colors.red), - ), - onPressed: () async { - SharedPreferences localStorage = - await SharedPreferences.getInstance(); - await localStorage.clear(); - auth.logout(); - GoRouter.of(context).pop(); + const Text("Theme"), + DropdownButton( + value: theme, + items: const [ + DropdownMenuItem( + child: Text( + "Dark", + ), + value: ThemeMode.dark, + ), + DropdownMenuItem( + child: Text( + "Light", + ), + value: ThemeMode.light, + ), + DropdownMenuItem( + child: Text("System"), + value: ThemeMode.system, + ), + ], + onChanged: (value) { + if (value != null) { + ref.read(themeProvider.notifier).state = value; + } }, + ) + ], + ), + const SizedBox(height: 10), + Builder(builder: (context) { + Auth auth = ref.watch(authProvider); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text("Log out of this account"), + ElevatedButton( + child: const Text("Logout"), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.red), + ), + onPressed: () async { + SharedPreferences localStorage = + await SharedPreferences.getInstance(); + await localStorage.clear(); + auth.logout(); + GoRouter.of(context).pop(); + }, + ), + ], + ); + }), + const SizedBox(height: 40), + const Text("Spotube v1.2.0"), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Text("Author: "), + Hyperlink( + "Kingkor Roy Tirtho", + "https://github.com/KRTirtho", ), ], - ); - }), - const SizedBox(height: 40), - const Text("Spotube v1.2.0"), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Text("Author: "), - Hyperlink( - "Kingkor Roy Tirtho", - "https://github.com/KRTirtho", - ), - ], - ), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [ - Hyperlink( - "💚 Sponsor/Donate 💚", - "https://opencollective.com/spotube", - ), - Text(" • "), - Hyperlink( - "BSD-4-Clause LICENSE", - "https://github.com/KRTirtho/spotube/blob/master/LICENSE", - ), - Text(" • "), - Hyperlink( - "Bug Report", - "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", - ), - ], - ), - const SizedBox(height: 10), - const Text("© Spotube 2022. All rights reserved") - ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Hyperlink( + "💚 Sponsor/Donate 💚", + "https://opencollective.com/spotube", + ), + Text(" • "), + Hyperlink( + "BSD-4-Clause LICENSE", + "https://github.com/KRTirtho/spotube/blob/master/LICENSE", + ), + Text(" • "), + Hyperlink( + "Bug Report", + "https://github.com/KRTirtho/spotube/issues/new?assignees=&labels=bug&template=bug_report.md&title=", + ), + ], + ), + const SizedBox(height: 10), + const Text("© Spotube 2022. All rights reserved") + ], + ), ), ), ); diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index 483cc4a4..02cc76f5 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -52,10 +52,23 @@ class PageWindowTitleBar extends StatelessWidget const PageWindowTitleBar({Key? key, this.leading, this.center}) : super(key: key); @override - Size get preferredSize => Size.fromHeight(appWindow.titleBarHeight); + Size get preferredSize => Size.fromHeight( + !Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 0, + ); @override Widget build(BuildContext context) { + if (Platform.isIOS || Platform.isAndroid) { + return PreferredSize( + preferredSize: const Size.fromHeight(70), + child: Row( + children: [ + if (leading != null) leading!, + Expanded(child: Center(child: center)), + ], + ), + ); + } return WindowTitleBarBox( child: Row( children: [ diff --git a/lib/hooks/playback.dart b/lib/hooks/playback.dart index 5fe1bdce..f113d315 100644 --- a/lib/hooks/playback.dart +++ b/lib/hooks/playback.dart @@ -28,7 +28,6 @@ Future Function() usePreviousTrack(Playback playback) { Future Function([dynamic]) useTogglePlayPause(Playback playback) { return ([key]) async { - print("CLICK CLICK"); try { if (playback.currentTrack == null) return; playback.isPlaying diff --git a/lib/hooks/useHotKeys.dart b/lib/hooks/useHotKeys.dart index 2ccfb349..94ad8f5a 100644 --- a/lib/hooks/useHotKeys.dart +++ b/lib/hooks/useHotKeys.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; @@ -18,6 +20,7 @@ useHotKeys(WidgetRef ref) { final _playOrPause = useTogglePlayPause(playback); useEffect(() { + if (Platform.isIOS || Platform.isAndroid) return null; _hotKeys = [ GlobalKeyActions( HotKey(KeyCode.space, scope: HotKeyScope.inapp), diff --git a/lib/main.dart b/lib/main.dart index a3a3c56c..25918053 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,21 +14,25 @@ import 'package:spotube/provider/ThemeProvider.dart'; import 'package:spotube/provider/YouTube.dart'; void main() async { - WidgetsFlutterBinding.ensureInitialized(); - await hotKeyManager.unregisterAll(); + if (!Platform.isAndroid && !Platform.isIOS) { + WidgetsFlutterBinding.ensureInitialized(); + await hotKeyManager.unregisterAll(); + doWhenWindowReady(() { + appWindow.minSize = + Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700); + appWindow.size = const Size(900, 700); + appWindow.alignment = Alignment.center; + appWindow.maximize(); + appWindow.show(); + }); + } runApp(ProviderScope(child: MyApp())); - doWhenWindowReady(() { - appWindow.minSize = - Size(Platform.isAndroid || Platform.isIOS ? 280 : 359, 700); - appWindow.size = const Size(900, 700); - appWindow.alignment = Alignment.center; - appWindow.maximize(); - appWindow.show(); - }); } class MyApp extends HookConsumerWidget { final GoRouter _router = createGoRouter(); + + MyApp({Key? key}) : super(key: key); @override Widget build(BuildContext context, ref) { var themeMode = ref.watch(themeProvider); diff --git a/lib/models/GoRouteDeclarations.dart b/lib/models/GoRouteDeclarations.dart index 926c2213..3a871a10 100644 --- a/lib/models/GoRouteDeclarations.dart +++ b/lib/models/GoRouteDeclarations.dart @@ -5,7 +5,6 @@ import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Home/Home.dart'; -import 'package:spotube/components/Player/PlayerControls.dart'; import 'package:spotube/components/Player/PlayerView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Settings.dart'; diff --git a/lib/provider/Playback.dart b/lib/provider/Playback.dart index 4d59d981..f87a0046 100644 --- a/lib/provider/Playback.dart +++ b/lib/provider/Playback.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:audio_session/audio_session.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:just_audio/just_audio.dart'; @@ -60,9 +61,11 @@ class Playback extends ChangeNotifier { StreamSubscription? _playingStreamListener; StreamSubscription? _durationStreamListener; StreamSubscription? _processingStateStreamListener; + StreamSubscription? _audioInterruptionEventListener; AudioPlayer player; YoutubeExplode youtube; + AudioSession? _audioSession; Playback({ required this.player, required this.youtube, @@ -107,6 +110,14 @@ class Playback extends ChangeNotifier { print(stack); } }); + + AudioSession.instance.then((session) async { + _audioSession = session; + await session.configure(const AudioSessionConfiguration.music()); + _audioInterruptionEventListener = session.interruptionEventStream.listen( + (AudioInterruptionEvent event) {}, + ); + }); } CurrentPlaylist? get currentPlaylist => _currentPlaylist; @@ -175,6 +186,8 @@ class Playback extends ChangeNotifier { _processingStateStreamListener?.cancel(); _durationStreamListener?.cancel(); _playingStreamListener?.cancel(); + _audioInterruptionEventListener?.cancel(); + _audioSession?.setActive(false); super.dispose(); } @@ -206,7 +219,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) { + if (track != null && await _audioSession?.setActive(true) == true) { Uri? parsedUri = Uri.tryParse(track.uri ?? ""); if (parsedUri != null && parsedUri.hasAbsolutePath) { await player diff --git a/pubspec.lock b/pubspec.lock index 0d670b24..5a3d5be2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -30,7 +30,7 @@ packages: source: hosted version: "2.8.2" audio_session: - dependency: transitive + dependency: "direct main" description: name: audio_session url: "https://pub.dartlang.org" @@ -323,7 +323,7 @@ packages: name: just_audio_web url: "https://pub.dartlang.org" source: hosted - version: "0.4.4" + version: "0.4.7" libwinmedia: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index cfcedea6..87dc4d5f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -54,6 +54,7 @@ dependencies: hooks_riverpod: ^1.0.3 go_router: ^3.0.4 palette_generator: ^0.3.3 + audio_session: ^0.1.6+1 dev_dependencies: flutter_test: From c4c9fd7ac27bbefbacf437a053fb63190b6f233c Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 16 Mar 2022 18:06:42 +0600 Subject: [PATCH 25/25] leading/center not working on PageWindowTitle on Android fixed artist-to-clickable-artists now responsive useBreakpoint logic updated --- lib/components/Player/PlayerTrackDetails.dart | 9 ++-- lib/components/Player/PlayerView.dart | 1 - lib/components/Settings.dart | 50 ++++++++++--------- lib/components/Shared/PageWindowTitleBar.dart | 4 +- lib/helpers/artists-to-clickable-artists.dart | 10 ++-- lib/hooks/useBreakpoints.dart | 16 +++--- 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/lib/components/Player/PlayerTrackDetails.dart b/lib/components/Player/PlayerTrackDetails.dart index 9610756c..34bd9297 100644 --- a/lib/components/Player/PlayerTrackDetails.dart +++ b/lib/components/Player/PlayerTrackDetails.dart @@ -20,8 +20,7 @@ class PlayerTrackDetails extends HookConsumerWidget { children: [ if (albumArt != null) Padding( - padding: EdgeInsets.all( - breakpoint.isLessThanOrEqualTo(Breakpoints.md) ? 5.0 : 0), + padding: const EdgeInsets.all(5.0), child: CachedNetworkImage( imageUrl: albumArt!, maxHeightDiskCache: 50, @@ -36,8 +35,7 @@ class PlayerTrackDetails extends HookConsumerWidget { }, ), ), - if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) ...[ - const SizedBox(width: 10), + if (breakpoint.isLessThanOrEqualTo(Breakpoints.md)) Flexible( child: Text( playback.currentTrack?.name ?? "Not playing", @@ -48,7 +46,7 @@ class PlayerTrackDetails extends HookConsumerWidget { ?.copyWith(fontWeight: FontWeight.bold, color: color), ), ), - ], + // title of the currently playing track if (breakpoint.isMoreThan(Breakpoints.md)) Flexible( @@ -65,7 +63,6 @@ class PlayerTrackDetails extends HookConsumerWidget { ), artistsToClickableArtists( playback.currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, ) ], ), diff --git a/lib/components/Player/PlayerView.dart b/lib/components/Player/PlayerView.dart index c410d744..1f910a2d 100644 --- a/lib/components/Player/PlayerView.dart +++ b/lib/components/Player/PlayerView.dart @@ -65,7 +65,6 @@ class PlayerView extends HookConsumerWidget { ), artistsToClickableArtists( currentTrack?.artists ?? [], - mainAxisAlignment: MainAxisAlignment.center, textStyle: Theme.of(context).textTheme.headline6!.copyWith( fontWeight: FontWeight.bold, color: paletteColor.bodyTextColor, diff --git a/lib/components/Settings.dart b/lib/components/Settings.dart index 117a8037..48d4335b 100644 --- a/lib/components/Settings.dart +++ b/lib/components/Settings.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; @@ -80,27 +82,29 @@ class Settings extends HookConsumerWidget { ], ), 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); - }, - ), + if (!Platform.isAndroid && !Platform.isIOS) ...[ + 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: [ @@ -170,8 +174,8 @@ class Settings extends HookConsumerWidget { ], ), const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, + Wrap( + alignment: WrapAlignment.center, children: const [ Hyperlink( "💚 Sponsor/Donate 💚", diff --git a/lib/components/Shared/PageWindowTitleBar.dart b/lib/components/Shared/PageWindowTitleBar.dart index 02cc76f5..c3a0038d 100644 --- a/lib/components/Shared/PageWindowTitleBar.dart +++ b/lib/components/Shared/PageWindowTitleBar.dart @@ -53,14 +53,14 @@ class PageWindowTitleBar extends StatelessWidget : super(key: key); @override Size get preferredSize => Size.fromHeight( - !Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 0, + !Platform.isIOS && !Platform.isAndroid ? appWindow.titleBarHeight : 35, ); @override Widget build(BuildContext context) { if (Platform.isIOS || Platform.isAndroid) { return PreferredSize( - preferredSize: const Size.fromHeight(70), + preferredSize: const Size.fromHeight(300), child: Row( children: [ if (leading != null) leading!, diff --git a/lib/helpers/artists-to-clickable-artists.dart b/lib/helpers/artists-to-clickable-artists.dart index 2ce58e98..aa55ab02 100644 --- a/lib/helpers/artists-to-clickable-artists.dart +++ b/lib/helpers/artists-to-clickable-artists.dart @@ -1,18 +1,16 @@ import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; -import 'package:spotube/components/Artist/ArtistProfile.dart'; import 'package:spotube/components/Shared/LinkText.dart'; -import 'package:spotube/components/Shared/SpotubePageRoute.dart'; Widget artistsToClickableArtists( List artists, { - CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, - MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, + WrapCrossAlignment crossAxisAlignment = WrapCrossAlignment.center, + WrapAlignment mainAxisAlignment = WrapAlignment.center, TextStyle textStyle = const TextStyle(), }) { - return Row( + return Wrap( crossAxisAlignment: crossAxisAlignment, - mainAxisAlignment: mainAxisAlignment, + alignment: mainAxisAlignment, children: artists .asMap() .entries diff --git a/lib/hooks/useBreakpoints.dart b/lib/hooks/useBreakpoints.dart index 825e1217..e4e73334 100644 --- a/lib/hooks/useBreakpoints.dart +++ b/lib/hooks/useBreakpoints.dart @@ -73,22 +73,22 @@ BreakpointUtils useBreakpoints() { final utils = BreakpointUtils(breakpoint.value); useEffect(() { - if (width >= 1920 && breakpoint.value != Breakpoints.xxl) { + if (width > 1920 && breakpoint.value != Breakpoints.xxl) { breakpoint.value = Breakpoints.xxl; - } else if (width >= 1366 && - width < 1920 && + } else if (width > 1366 && + width <= 1920 && breakpoint.value != Breakpoints.xl) { breakpoint.value = Breakpoints.xl; - } else if (width >= 768 && - width < 1366 && + } else if (width > 768 && + width <= 1366 && breakpoint.value != Breakpoints.lg) { breakpoint.value = Breakpoints.lg; - } else if (width >= 360 && - width < 768 && + } else if (width > 360 && + width <= 768 && breakpoint.value != Breakpoints.md) { breakpoint.value = Breakpoints.md; } else if (width >= 250 && - width < 360 && + width <= 360 && breakpoint.value != Breakpoints.sm) { breakpoint.value = Breakpoints.sm; }