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: