From d05ec0099d9a9b09b3a30fef0fca896f4bb7a444 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 11 Feb 2022 10:44:16 +0600 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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: