Merge branch 'riverpod-&-hooks'

This commit is contained in:
Kingkor Roy Tirtho 2022-02-24 12:19:11 +06:00
commit 5a9a988d26
31 changed files with 829 additions and 1009 deletions

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.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:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.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/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class AlbumCard extends StatelessWidget { class AlbumCard extends ConsumerWidget {
final Album album; final Album album;
const AlbumCard(this.album, {Key? key}) : super(key: key); const AlbumCard(this.album, {Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null && bool isPlaylistPlaying = playback.currentPlaylist != null &&
playback.currentPlaylist!.id == album.id; playback.currentPlaylist!.id == album.id;
@ -34,7 +34,7 @@ class AlbumCard extends StatelessWidget {
)); ));
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {
SpotifyApi spotify = context.read<SpotifyDI>().spotifyApi; SpotifyApi spotify = ref.read(spotifyProvider);
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;
List<Track> tracks = (await spotify.albums.getTracks(album.id!).all()) List<Track> tracks = (await spotify.albums.getTracks(album.id!).all())
.map((track) => simpleTrackToTrack(track, album)) .map((track) => simpleTrackToTrack(track, album))

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.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:spotify/spotify.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.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/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class AlbumView extends StatelessWidget { class AlbumView extends ConsumerWidget {
final AlbumSimple album; final AlbumSimple album;
const AlbumView(this.album, {Key? key}) : super(key: key); const AlbumView(this.album, {Key? key}) : super(key: key);
@ -31,11 +31,11 @@ class AlbumView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id; var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi; SpotifyApi spotify = ref.watch(spotifyProvider);
return Scaffold( return Scaffold(
body: FutureBuilder<Iterable<TrackSimple>>( body: FutureBuilder<Iterable<TrackSimple>>(
future: spotify.albums.getTracks(album.id!).all(), future: spotify.albums.getTracks(album.id!).all(),

View File

@ -1,12 +1,12 @@
import 'package:flutter/material.dart' hide Page; 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class ArtistAlbumView extends StatefulWidget { class ArtistAlbumView extends ConsumerStatefulWidget {
final String artistId; final String artistId;
final String artistName; final String artistName;
const ArtistAlbumView( const ArtistAlbumView(
@ -16,10 +16,10 @@ class ArtistAlbumView extends StatefulWidget {
}) : super(key: key); }) : super(key: key);
@override @override
State<ArtistAlbumView> createState() => _ArtistAlbumViewState(); ConsumerState<ArtistAlbumView> createState() => _ArtistAlbumViewState();
} }
class _ArtistAlbumViewState extends State<ArtistAlbumView> { class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
final PagingController<int, Album> _pagingController = final PagingController<int, Album> _pagingController =
PagingController<int, Album>(firstPageKey: 0); PagingController<int, Album>(firstPageKey: 0);
@ -39,10 +39,9 @@ class _ArtistAlbumViewState extends State<ArtistAlbumView> {
_fetchPage(int pageKey) async { _fetchPage(int pageKey) async {
try { try {
SpotifyDI data = context.read<SpotifyDI>(); SpotifyApi spotifyApi = ref.watch(spotifyProvider);
Page<Album> albums = await data.spotifyApi.artists Page<Album> albums =
.albums(widget.artistId) await spotifyApi.artists.albums(widget.artistId).getPage(8, pageKey);
.getPage(8, pageKey);
var items = albums.items!.toList(); var items = albums.items!.toList();

View File

@ -1,7 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.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:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart'; import 'package:spotube/components/Artist/ArtistAlbumView.dart';
@ -14,24 +14,19 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class ArtistProfile extends StatefulWidget { class ArtistProfile extends ConsumerWidget {
final String artistId; final String artistId;
const ArtistProfile(this.artistId, {Key? key}) : super(key: key); const ArtistProfile(this.artistId, {Key? key}) : super(key: key);
@override @override
_ArtistProfileState createState() => _ArtistProfileState(); Widget build(BuildContext context, ref) {
} SpotifyApi spotify = ref.watch(spotifyProvider);
class _ArtistProfileState extends State<ArtistProfile> {
@override
Widget build(BuildContext context) {
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
leading: BackButton(), leading: BackButton(),
), ),
body: FutureBuilder<Artist>( body: FutureBuilder<Artist>(
future: spotify.artists.get(widget.artistId), future: spotify.artists.get(artistId),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());
@ -134,7 +129,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive()); child: CircularProgressIndicator.adaptive());
} }
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
var isPlaylistPlaying = var isPlaylistPlaying =
playback.currentPlaylist?.id == snapshot.data?.id; playback.currentPlaylist?.id == snapshot.data?.id;
playPlaylist(List<Track> tracks, {Track? currentTrack}) { playPlaylist(List<Track> tracks, {Track? currentTrack}) {
@ -222,7 +217,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
onPressed: () { onPressed: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ArtistAlbumView( builder: (context) => ArtistAlbumView(
widget.artistId, artistId,
snapshot.data?.name ?? "KRTX", snapshot.data?.name ?? "KRTX",
), ),
)); ));
@ -260,7 +255,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
FutureBuilder<Iterable<Artist>>( FutureBuilder<Iterable<Artist>>(
future: spotify.artists.getRelatedArtists(widget.artistId), future: spotify.artists.getRelatedArtists(artistId),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center( return const Center(

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart' hide Page; 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:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Playlist/PlaylistGenreView.dart'; import 'package:spotube/components/Playlist/PlaylistGenreView.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends StatefulWidget { class CategoryCard extends StatelessWidget {
final Category category; final Category category;
final Iterable<PlaylistSimple>? playlists; final Iterable<PlaylistSimple>? playlists;
const CategoryCard( const CategoryCard(
@ -14,11 +14,6 @@ class CategoryCard extends StatefulWidget {
this.playlists, this.playlists,
}) : super(key: key); }) : super(key: key);
@override
_CategoryCardState createState() => _CategoryCardState();
}
class _CategoryCardState extends State<CategoryCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return Column(
@ -29,7 +24,7 @@ class _CategoryCardState extends State<CategoryCard> {
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
widget.category.name ?? "Unknown", category.name ?? "Unknown",
style: Theme.of(context).textTheme.headline5, style: Theme.of(context).textTheme.headline5,
), ),
TextButton( TextButton(
@ -38,9 +33,9 @@ class _CategoryCardState extends State<CategoryCard> {
MaterialPageRoute( MaterialPageRoute(
builder: (context) { builder: (context) {
return PlaylistGenreView( return PlaylistGenreView(
widget.category.id!, category.id!,
widget.category.name!, category.name!,
playlists: widget.playlists, playlists: playlists,
); );
}, },
), ),
@ -51,17 +46,17 @@ class _CategoryCardState extends State<CategoryCard> {
], ],
), ),
), ),
Consumer<SpotifyDI>( Consumer(
builder: (context, data, child) { builder: (context, ref, child) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<Iterable<PlaylistSimple>>( return FutureBuilder<Iterable<PlaylistSimple>>(
future: widget.playlists == null future: playlists == null
? (widget.category.id != "user-featured-playlists" ? (category.id != "user-featured-playlists"
? data.spotifyApi.playlists ? spotifyApi.playlists.getByCategoryId(category.id!)
.getByCategoryId(widget.category.id!) : spotifyApi.playlists.featured)
: data.spotifyApi.playlists.featured)
.getPage(4, 0) .getPage(4, 0)
.then((value) => value.items ?? []) .then((value) => value.items ?? [])
: Future.value(widget.playlists), : Future.value(playlists),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
return const Center(child: Text("Error occurred")); return const Center(child: Text("Error occurred"));

View File

@ -3,8 +3,8 @@ import 'dart:io';
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart' hide Page; 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:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:oauth2/oauth2.dart' show AuthorizationException; import 'package:oauth2/oauth2.dart' show AuthorizationException;
import 'package:spotify/spotify.dart' hide Image, Player, Search; import 'package:spotify/spotify.dart' hide Image, Player, Search;
@ -33,14 +33,14 @@ List<String> spotifyScopes = [
"playlist-read-collaborative" "playlist-read-collaborative"
]; ];
class Home extends StatefulWidget { class Home extends ConsumerStatefulWidget {
const Home({Key? key}) : super(key: key); const Home({Key? key}) : super(key: key);
@override @override
_HomeState createState() => _HomeState(); _HomeState createState() => _HomeState();
} }
class _HomeState extends State<Home> { class _HomeState extends ConsumerState<Home> {
final PagingController<int, Category> _pagingController = final PagingController<int, Category> _pagingController =
PagingController(firstPageKey: 0); PagingController(firstPageKey: 0);
@ -63,7 +63,7 @@ class _HomeState extends State<Home> {
DateTime? expiration = DateTime? expiration =
expirationStr != null ? DateTime.parse(expirationStr) : null; expirationStr != null ? DateTime.parse(expirationStr) : null;
try { try {
Auth authProvider = context.read<Auth>(); Auth auth = ref.read(authProvider);
if (clientId != null && clientSecret != null) { if (clientId != null && clientSecret != null) {
SpotifyApi spotifyApi = SpotifyApi( SpotifyApi spotifyApi = SpotifyApi(
@ -78,7 +78,7 @@ class _HomeState extends State<Home> {
); );
SpotifyApiCredentials credentials = await spotifyApi.getCredentials(); SpotifyApiCredentials credentials = await spotifyApi.getCredentials();
if (credentials.accessToken?.isNotEmpty ?? false) { if (credentials.accessToken?.isNotEmpty ?? false) {
authProvider.setAuthState( auth.setAuthState(
clientId: clientId, clientId: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
accessToken: accessToken:
@ -91,8 +91,8 @@ class _HomeState extends State<Home> {
} }
_pagingController.addPageRequestListener((pageKey) async { _pagingController.addPageRequestListener((pageKey) async {
try { try {
SpotifyDI data = context.read<SpotifyDI>(); SpotifyApi spotifyApi = ref.read(spotifyProvider);
Page<Category> categories = await data.spotifyApi.categories Page<Category> categories = await spotifyApi.categories
.list(country: "US") .list(country: "US")
.getPage(15, pageKey); .getPage(15, pageKey);
@ -113,10 +113,10 @@ class _HomeState extends State<Home> {
_pagingController.error = e; _pagingController.error = e;
} }
}); });
} on AuthorizationException catch (e) { } on AuthorizationException catch (_) {
if (clientId != null && clientSecret != null) { if (clientId != null && clientSecret != null) {
oauthLogin( oauthLogin(
context, ref.read(authProvider),
clientId: clientId, clientId: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
); );
@ -136,8 +136,9 @@ class _HomeState extends State<Home> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Auth authProvider = Provider.of<Auth>(context); Auth auth = ref.watch(authProvider);
if (!authProvider.isLoggedIn) { SpotifyApi spotify = ref.watch(spotifyProvider);
if (!auth.isLoggedIn) {
return const Login(); return const Login();
} }
@ -199,49 +200,45 @@ class _HomeState extends State<Home> {
style: Theme.of(context).textTheme.headline4), style: Theme.of(context).textTheme.headline4),
]), ]),
), ),
trailing: trailing: FutureBuilder<User>(
Consumer<SpotifyDI>(builder: (context, data, widget) { future: spotify.me.get(),
return FutureBuilder<User>( builder: (context, snapshot) {
future: data.spotifyApi.me.get(), var avatarImg = imageToUrlString(snapshot.data?.images,
builder: (context, snapshot) { index: (snapshot.data?.images?.length ?? 1) - 1);
var avatarImg = imageToUrlString(snapshot.data?.images, return Padding(
index: (snapshot.data?.images?.length ?? 1) - 1); padding: const EdgeInsets.all(16),
return Padding( child: Row(
padding: const EdgeInsets.all(16), mainAxisAlignment: MainAxisAlignment.spaceBetween,
child: Row( children: [
mainAxisAlignment: MainAxisAlignment.spaceBetween, Row(
children: [ children: [
Row( CircleAvatar(
children: [ backgroundImage:
CircleAvatar( CachedNetworkImageProvider(avatarImg),
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( IconButton(
fontWeight: FontWeight.bold, 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 // contents of the spotify
if (_selectedIndex == 0) if (_selectedIndex == 0)

View File

@ -1,18 +1,18 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart'; import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Artist/ArtistCard.dart'; import 'package:spotube/components/Artist/ArtistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class UserArtists extends StatefulWidget { class UserArtists extends ConsumerStatefulWidget {
const UserArtists({Key? key}) : super(key: key); const UserArtists({Key? key}) : super(key: key);
@override @override
State<UserArtists> createState() => _UserArtistsState(); ConsumerState<UserArtists> createState() => _UserArtistsState();
} }
class _UserArtistsState extends State<UserArtists> { class _UserArtistsState extends ConsumerState<UserArtists> {
final PagingController<String, Artist> _pagingController = final PagingController<String, Artist> _pagingController =
PagingController(firstPageKey: ""); PagingController(firstPageKey: "");
@ -22,8 +22,8 @@ class _UserArtistsState extends State<UserArtists> {
WidgetsBinding.instance?.addPostFrameCallback((timestamp) { WidgetsBinding.instance?.addPostFrameCallback((timestamp) {
_pagingController.addPageRequestListener((pageKey) async { _pagingController.addPageRequestListener((pageKey) async {
try { try {
SpotifyDI data = context.read<SpotifyDI>(); SpotifyApi spotifyApi = ref.read(spotifyProvider);
CursorPage<Artist> artists = await data.spotifyApi.me CursorPage<Artist> artists = await spotifyApi.me
.following(FollowingType.artist) .following(FollowingType.artist)
.getPage(15, pageKey); .getPage(15, pageKey);
@ -51,10 +51,10 @@ class _UserArtistsState extends State<UserArtists> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
SpotifyDI data = context.watch<SpotifyDI>(); SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<CursorPage<Artist>>( return FutureBuilder<CursorPage<Artist>>(
future: data.spotifyApi.me.following(FollowingType.artist).first(), future: spotifyApi.me.following(FollowingType.artist).first(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());

View File

@ -2,14 +2,8 @@ import 'package:flutter/material.dart' hide Image;
import 'package:spotube/components/Library/UserArtists.dart'; import 'package:spotube/components/Library/UserArtists.dart';
import 'package:spotube/components/Library/UserPlaylists.dart'; import 'package:spotube/components/Library/UserPlaylists.dart';
class UserLibrary extends StatefulWidget { class UserLibrary extends StatelessWidget {
const UserLibrary({Key? key}) : super(key: key); const UserLibrary({Key? key}) : super(key: key);
@override
_UserLibraryState createState() => _UserLibraryState();
}
class _UserLibraryState extends State<UserLibrary> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Expanded( return Expanded(

View File

@ -1,18 +1,18 @@
import 'package:flutter/material.dart' hide Image; 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:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class UserPlaylists extends StatelessWidget { class UserPlaylists extends ConsumerWidget {
const UserPlaylists({Key? key}) : super(key: key); const UserPlaylists({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
SpotifyDI data = context.watch<SpotifyDI>(); SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<Iterable<PlaylistSimple>>( return FutureBuilder<Iterable<PlaylistSimple>>(
future: data.spotifyApi.playlists.me.all(), future: spotifyApi.playlists.me.all(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData) { if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator.adaptive()); return const Center(child: CircularProgressIndicator.adaptive());

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
@ -8,127 +9,109 @@ import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
class Login extends StatefulWidget { class Login extends HookConsumerWidget {
const Login({Key? key}) : super(key: key); const Login({Key? key}) : super(key: key);
@override @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 State<Login> { Future handleLogin(Auth authState) async {
String clientId = ""; try {
String clientSecret = ""; if (clientIdController.value.text == "" ||
String accessToken = ""; clientSecretController.value.text == "") {
bool _fieldError = false; fieldError.value = true;
}
Future handleLogin(Auth authState) async { await oauthLogin(
try { ref.read(authProvider),
if (clientId == "" || clientSecret == "") { clientId: clientIdController.value.text,
return setState(() { clientSecret: clientSecretController.value.text,
_fieldError = true;
});
}
await oauthLogin(context, clientId: clientId, clientSecret: clientSecret);
} catch (e) {
print("[Login.handleLogin] $e");
}
}
@override
Widget build(BuildContext context) {
return Consumer<Auth>(
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<UserPreferences>();
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(accessToken);
await localStorage.setString(
LocalStorageKeys.geniusAccessToken,
accessToken);
setState(() {
accessToken = "";
});
},
child: const Text("Submit"),
)
],
),
),
],
),
),
),
); );
}, } catch (e) {
print("[Login.handleLogin] $e");
}
}
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(
controller: clientIdController,
decoration: const InputDecoration(
hintText: "Spotify Client ID",
label: Text("ClientID"),
),
),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
hintText: "Spotify Client Secret",
label: Text("Client Secret"),
),
controller: clientSecretController,
),
const SizedBox(height: 10),
const Divider(color: Colors.grey),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
label: Text("Genius Access Token (optional)"),
),
controller: accessTokenController,
),
const SizedBox(
height: 10,
),
ElevatedButton(
onPressed: () async {
await handleLogin(authState);
UserPreferences preferences =
ref.read(userPreferencesProvider);
SharedPreferences localStorage =
await SharedPreferences.getInstance();
preferences.setGeniusAccessToken(
accessTokenController.value.text);
await localStorage.setString(
LocalStorageKeys.geniusAccessToken,
accessTokenController.value.text);
accessTokenController.text = "";
},
child: const Text("Submit"),
)
],
),
),
],
),
),
),
); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Settings.dart'; import 'package:spotube/components/Settings.dart';
import 'package:spotube/helpers/artist-to-string.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/Playback.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
class Lyrics extends StatefulWidget { class Lyrics extends HookConsumerWidget {
const Lyrics({Key? key}) : super(key: key); const Lyrics({Key? key}) : super(key: key);
@override @override
State<Lyrics> createState() => _LyricsState(); Widget build(BuildContext context, ref) {
} Playback playback = ref.watch(playbackProvider);
UserPreferences userPreferences = ref.watch(userPreferencesProvider);
class _LyricsState extends State<Lyrics> { var lyrics = useState({});
Map<String, String> _lyrics = {};
@override
Widget build(BuildContext context) {
Playback playback = context.watch<Playback>();
UserPreferences userPreferences = context.watch<UserPreferences>();
bool hasToken = (userPreferences.geniusAccessToken != null || bool hasToken = (userPreferences.geniusAccessToken != null ||
(userPreferences.geniusAccessToken?.isNotEmpty ?? false)); (userPreferences.geniusAccessToken?.isNotEmpty ?? false));
var lyricsFuture = useMemoized(() {
if (playback.currentTrack != null && if (playback.currentTrack == null ||
hasToken && !hasToken ||
playback.currentTrack!.id != _lyrics["id"]) { (playback.currentTrack?.id != null &&
getLyrics( playback.currentTrack?.id == lyrics.value["id"])) {
return null;
}
return getLyrics(
playback.currentTrack!.name!, playback.currentTrack!.name!,
artistsToString<Artist>(playback.currentTrack!.artists ?? []), artistsToString<Artist>(playback.currentTrack!.artists ?? []),
apiKey: userPreferences.geniusAccessToken!, apiKey: userPreferences.geniusAccessToken!,
optimizeQuery: true, optimizeQuery: true,
).then((lyrics) { );
if (lyrics != null) { }, [playback.currentTrack]);
setState(() {
_lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!};
});
}
});
}
if (_lyrics["lyrics"] != null && playback.currentTrack == null) { var lyricsSnapshot = useFuture(lyricsFuture);
setState(() {
_lyrics = {};
});
}
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) { if (!hasToken) {
return Expanded( return Expanded(
child: Column( child: Column(
@ -99,9 +105,10 @@ class _LyricsState extends State<Lyrics> {
child: SingleChildScrollView( child: SingleChildScrollView(
child: Center( child: Center(
child: Text( child: Text(
_lyrics["lyrics"] == null && playback.currentTrack == null lyrics.value["lyrics"] == null &&
playback.currentTrack == null
? "No Track being played currently" ? "No Track being played currently"
: _lyrics["lyrics"]!, : lyrics.value["lyrics"]!,
style: Theme.of(context).textTheme.headline6, style: Theme.of(context).textTheme.headline6,
), ),
), ),

View File

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/DownloadTrackButton.dart'; import 'package:spotube/components/Shared/DownloadTrackButton.dart';
@ -10,76 +12,75 @@ import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/search-youtube.dart'; import 'package:spotube/helpers/search-youtube.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
class Player extends StatefulWidget { class Player extends HookConsumerWidget {
const Player({Key? key}) : super(key: key); const Player({Key? key}) : super(key: key);
@override @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<Duration?>(null);
var _currentTrackId = useState<String?>(null);
class _PlayerState extends State<Player> with WidgetsBindingObserver { AudioPlayer player = useMemoized(() => AudioPlayer(), []);
late AudioPlayer player; YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []);
bool _isPlaying = false;
bool _shuffled = false;
Duration? _duration;
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 var playingStreamListener = player.playingStream.listen((playing) async {
void initState() { _isPlaying.value = playing;
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;
});
}); });
player.durationStream.listen((duration) async { var durationStreamListener =
player.durationStream.listen((duration) async {
if (duration != null) { if (duration != null) {
// Actually things doesn't work all the time as they were // Actually things doesn't work all the time as they were
// described. So instead of listening to a `playback.ready` // described. So instead of listening to a `playback.ready`
// stream, it has to listen to duration stream since duration // stream, it has to listen to duration stream since duration
// is always added to the Stream sink after all icyMetadata has // is always added to the Stream sink after all icyMetadata has
// been loaded thus indicating buffering started // 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 // this line is for prev/next or already playing playlist
if (player.playing) await player.pause(); if (player.playing) await player.pause();
await player.play(); await player.play();
} }
setState(() { _duration.value = duration;
_duration = duration;
});
} }
}); });
player.processingStateStream.listen((event) async { var processingStateStreamListener =
player.processingStateStream.listen((event) async {
try { try {
if (event == ProcessingState.completed && _currentTrackId != null) { if (event == ProcessingState.completed &&
_currentTrackId.value != null) {
_movePlaylistPositionBy(1); _movePlaylistPositionBy(1);
} }
} catch (e, stack) { } catch (e, stack) {
@ -87,127 +88,85 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
print(stack); print(stack);
} }
}); });
} catch (e) { return () {
print("[Player._init()]: $e"); playingStreamListener.cancel();
} durationStreamListener.cancel();
} processingStateStreamListener.cancel();
player.dispose();
youtube.close();
};
}, []);
@override var _playTrack = useCallback((Track currentTrack, Playback playback) async {
void dispose() { try {
WidgetsBinding.instance?.removeObserver(this); if (currentTrack.id != _currentTrackId.value) {
player.dispose(); Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? "");
youtube.close(); if (parsedUri != null && parsedUri.hasAbsolutePath) {
super.dispose(); await player
} .setAudioSource(
AudioSource.uri(parsedUri),
@override preload: true,
void didChangeAppLifecycleState(AppLifecycleState state) { )
if (state == AppLifecycleState.paused) { .then((value) async {
// Release the player's resources when not in use. We use "stop" so that _currentTrackId.value = currentTrack.id;
// if the app resumes later, it will still remember what position to if (_duration.value != null) {
// resume from. _duration.value = value;
player.stop();
}
}
void _movePlaylistPositionBy(int pos) {
Playback playback = context.read<Playback>();
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 ytTrack = await toYoutubeTrack(youtube, currentTrack);
var ytTrack = await toYoutubeTrack(youtube, currentTrack); if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) { await player
await player .setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!))) .then((value) {
.then((value) { _currentTrackId.value = currentTrack.id;
setState(() {
_currentTrackId = currentTrack.id;
}); });
}); }
} }
} catch (e, stack) {
print("[Player._playTrack()] $e");
print(stack);
} }
} catch (e, stack) { }, [player, _currentTrackId, _duration]);
print("[Player._playTrack()] $e");
print(stack);
}
}
_onNext() async { var _onNext = useCallback(() async {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
_movePlaylistPositionBy(1); _movePlaylistPositionBy(1);
print("ON NEXT"); } catch (e, stack) {
} catch (e, stack) { print("[PlayerControls.onNext()] $e");
print("[PlayerControls.onNext()] $e"); print(stack);
print(stack); }
} }, [player]);
}
_onPrevious() async { var _onPrevious = useCallback(() async {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
_movePlaylistPositionBy(-1); _movePlaylistPositionBy(-1);
} catch (e, stack) { } catch (e, stack) {
print("[PlayerControls.onPrevious()] $e"); print("[PlayerControls.onPrevious()] $e");
print(stack); print(stack);
} }
} }, [player]);
@override
Widget build(BuildContext context) {
return Container( return Container(
color: Theme.of(context).backgroundColor, color: Theme.of(context).backgroundColor,
child: Consumer<Playback>( child: HookConsumer(
builder: (context, playback, widget) { builder: (context, ref, widget) {
Playback playback = ref.watch(playbackProvider);
if (playback.currentPlaylist != null && if (playback.currentPlaylist != null &&
playback.currentTrack != null) { playback.currentTrack != null) {
_playTrack(playback.currentTrack!, playback); _playTrack(playback.currentTrack!, playback);
} }
String? albumArt = imageToUrlString( String? albumArt = useMemoized(
playback.currentTrack?.album?.images, () => imageToUrlString(
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1, playback.currentTrack?.album?.images,
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
),
[playback.currentTrack?.album?.images],
); );
return Material( return Material(
@ -249,9 +208,9 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
flex: 3, flex: 3,
child: PlayerControls( child: PlayerControls(
positionStream: player.positionStream, positionStream: player.positionStream,
isPlaying: _isPlaying, isPlaying: _isPlaying.value,
duration: _duration ?? Duration.zero, duration: _duration.value ?? Duration.zero,
shuffled: _shuffled, shuffled: _shuffled.value,
onNext: _onNext, onNext: _onNext,
onPrevious: _onPrevious, onPrevious: _onPrevious,
onPause: () async { onPause: () async {
@ -282,16 +241,12 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
if (playback.currentTrack == null || if (playback.currentTrack == null ||
playback.currentPlaylist == null) return; playback.currentPlaylist == null) return;
try { try {
if (!_shuffled) { if (!_shuffled.value) {
playback.currentPlaylist!.shuffle(); playback.currentPlaylist!.shuffle();
setState(() { _shuffled.value = true;
_shuffled = true;
});
} else { } else {
playback.currentPlaylist!.unshuffle(); playback.currentPlaylist!.unshuffle();
setState(() { _shuffled.value = false;
_shuffled = false;
});
} }
} catch (e, stack) { } catch (e, stack) {
print("[PlayerControls.onShuffle()] $e"); print("[PlayerControls.onShuffle()] $e");
@ -302,12 +257,10 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
try { try {
await player.pause(); await player.pause();
await player.seek(Duration.zero); await player.seek(Duration.zero);
setState(() { _isPlaying.value = false;
_isPlaying = false; _currentTrackId.value = null;
_currentTrackId = null; _duration.value = null;
_duration = null; _shuffled.value = false;
_shuffled = false;
});
playback.reset(); playback.reset();
} catch (e, stack) { } catch (e, stack) {
print("[PlayerControls.onStop()] $e"); print("[PlayerControls.onStop()] $e");
@ -327,13 +280,11 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
height: 20, height: 20,
constraints: const BoxConstraints(maxWidth: 200), constraints: const BoxConstraints(maxWidth: 200),
child: Slider.adaptive( child: Slider.adaptive(
value: _volume, value: _volume.value,
onChanged: (value) async { onChanged: (value) async {
try { try {
await player.setVolume(value).then((_) { await player.setVolume(value).then((_) {
setState(() { _volume.value = value;
_volume = value;
});
}); });
} catch (e, stack) { } catch (e, stack) {
print("[VolumeSlider.onChange()] $e"); print("[VolumeSlider.onChange()] $e");
@ -348,10 +299,11 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
DownloadTrackButton( DownloadTrackButton(
track: playback.currentTrack, track: playback.currentTrack,
), ),
Consumer<SpotifyDI>(builder: (context, data, widget) { Consumer(builder: (context, ref, widget) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
return FutureBuilder<bool>( return FutureBuilder<bool>(
future: playback.currentTrack?.id != null future: playback.currentTrack?.id != null
? data.spotifyApi.tracks.me ? spotifyApi.tracks.me
.containsOne(playback.currentTrack!.id!) .containsOne(playback.currentTrack!.id!)
: Future.value(false), : Future.value(false),
initialData: false, initialData: false,
@ -367,10 +319,8 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
onPressed: () { onPressed: () {
if (!isLiked && if (!isLiked &&
playback.currentTrack?.id != null) { playback.currentTrack?.id != null) {
data.spotifyApi.tracks.me spotifyApi.tracks.me.saveOne(
.saveOne( playback.currentTrack!.id!);
playback.currentTrack!.id!)
.then((value) => setState(() {}));
} }
}); });
}); });

View File

@ -1,13 +1,14 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/models/GlobalKeyActions.dart'; import 'package:spotube/models/GlobalKeyActions.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
import 'package:provider/provider.dart';
class PlayerControls extends StatefulWidget { class PlayerControls extends HookConsumerWidget {
final Stream<Duration> positionStream; final Stream<Duration> positionStream;
final bool isPlaying; final bool isPlaying;
final Duration duration; final Duration duration;
@ -34,33 +35,21 @@ class PlayerControls extends StatefulWidget {
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override
_PlayerControlsState createState() => _PlayerControlsState();
}
class _PlayerControlsState extends State<PlayerControls> {
StreamSubscription? _timePositionListener;
late List<GlobalKeyActions> _hotKeys = [];
@override
void dispose() async {
await _timePositionListener?.cancel();
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
super.dispose();
}
_playOrPause(key) async { _playOrPause(key) async {
try { try {
widget.isPlaying ? widget.onPause?.call() : await widget.onPlay?.call(); isPlaying ? await onPause?.call() : await onPlay?.call();
} catch (e, stack) { } catch (e, stack) {
print("[PlayPauseShortcut] $e"); print("[PlayPauseShortcut] $e");
print(stack); print(stack);
} }
} }
_configureHotKeys(UserPreferences preferences) async { @override
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey))) Widget build(BuildContext context, ref) {
.then((val) async { UserPreferences preferences = ref.watch(userPreferencesProvider);
var _hotKeys = [];
useEffect(() {
_hotKeys = [ _hotKeys = [
GlobalKeyActions( GlobalKeyActions(
HotKey(KeyCode.space, scope: HotKeyScope.inapp), HotKey(KeyCode.space, scope: HotKeyScope.inapp),
@ -68,14 +57,14 @@ class _PlayerControlsState extends State<PlayerControls> {
), ),
if (preferences.nextTrackHotKey != null) if (preferences.nextTrackHotKey != null)
GlobalKeyActions( GlobalKeyActions(
preferences.nextTrackHotKey!, (key) => widget.onNext?.call()), preferences.nextTrackHotKey!, (key) => onNext?.call()),
if (preferences.prevTrackHotKey != null) if (preferences.prevTrackHotKey != null)
GlobalKeyActions( GlobalKeyActions(
preferences.prevTrackHotKey!, (key) => widget.onPrevious?.call()), preferences.prevTrackHotKey!, (key) => onPrevious?.call()),
if (preferences.playPauseHotKey != null) if (preferences.playPauseHotKey != null)
GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause) GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause)
]; ];
await Future.wait( Future.wait(
_hotKeys.map((e) { _hotKeys.map((e) {
return hotKeyManager.register( return hotKeyManager.register(
e.hotKey, e.hotKey,
@ -83,25 +72,22 @@ class _PlayerControlsState extends State<PlayerControls> {
); );
}), }),
); );
return () {
Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)));
};
}); });
}
@override
Widget build(BuildContext context) {
UserPreferences preferences = context.watch<UserPreferences>();
_configureHotKeys(preferences);
return Container( return Container(
constraints: const BoxConstraints(maxWidth: 700), constraints: const BoxConstraints(maxWidth: 700),
child: Column( child: Column(
children: [ children: [
StreamBuilder<Duration>( StreamBuilder<Duration>(
stream: widget.positionStream, stream: positionStream,
builder: (context, snapshot) { builder: (context, snapshot) {
var totalMinutes = var totalMinutes =
zeroPadNumStr(widget.duration.inMinutes.remainder(60)); zeroPadNumStr(duration.inMinutes.remainder(60));
var totalSeconds = var totalSeconds =
zeroPadNumStr(widget.duration.inSeconds.remainder(60)); zeroPadNumStr(duration.inSeconds.remainder(60));
var currentMinutes = snapshot.hasData var currentMinutes = snapshot.hasData
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60)) ? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
: "00"; : "00";
@ -109,7 +95,7 @@ class _PlayerControlsState extends State<PlayerControls> {
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60)) ? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
: "00"; : "00";
var sliderMax = widget.duration.inSeconds; var sliderMax = duration.inSeconds;
var sliderValue = snapshot.data?.inSeconds ?? 0; var sliderValue = snapshot.data?.inSeconds ?? 0;
return Row( return Row(
children: [ children: [
@ -123,7 +109,7 @@ class _PlayerControlsState extends State<PlayerControls> {
: sliderValue / sliderMax, : sliderValue / sliderMax,
onChanged: (value) {}, onChanged: (value) {},
onChangeEnd: (value) { onChangeEnd: (value) {
widget.onSeek?.call(value * sliderMax); onSeek?.call(value * sliderMax);
}, },
), ),
), ),
@ -138,30 +124,27 @@ class _PlayerControlsState extends State<PlayerControls> {
children: [ children: [
IconButton( IconButton(
icon: const Icon(Icons.shuffle_rounded), icon: const Icon(Icons.shuffle_rounded),
color: color: shuffled ? Theme.of(context).primaryColor : null,
widget.shuffled ? Theme.of(context).primaryColor : null,
onPressed: () { onPressed: () {
widget.onShuffle?.call(); onShuffle?.call();
}), }),
IconButton( IconButton(
icon: const Icon(Icons.skip_previous_rounded), icon: const Icon(Icons.skip_previous_rounded),
onPressed: () { onPressed: () {
widget.onPrevious?.call(); onPrevious?.call();
}), }),
IconButton( IconButton(
icon: Icon( icon: Icon(
widget.isPlaying isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
? Icons.pause_rounded
: Icons.play_arrow_rounded,
), ),
onPressed: () => _playOrPause(null), onPressed: () => _playOrPause(null),
), ),
IconButton( IconButton(
icon: const Icon(Icons.skip_next_rounded), icon: const Icon(Icons.skip_next_rounded),
onPressed: () => widget.onNext?.call()), onPressed: () => onNext?.call()),
IconButton( IconButton(
icon: const Icon(Icons.stop_rounded), icon: const Icon(Icons.stop_rounded),
onPressed: () => widget.onStop?.call(), onPressed: () => onStop?.call(),
) )
], ],
) )

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistView.dart'; import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.dart'; import 'package:spotube/components/Shared/PlaybuttonCard.dart';
@ -7,39 +7,34 @@ import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistCard extends StatefulWidget { class PlaylistCard extends ConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
const PlaylistCard(this.playlist, {Key? key}) : super(key: key); const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
@override @override
_PlaylistCardState createState() => _PlaylistCardState(); Widget build(BuildContext context, ref) {
} Playback playback = ref.watch(playbackProvider);
class _PlaylistCardState extends State<PlaylistCard> {
@override
Widget build(BuildContext context) {
Playback playback = context.watch<Playback>();
bool isPlaylistPlaying = playback.currentPlaylist != null && bool isPlaylistPlaying = playback.currentPlaylist != null &&
playback.currentPlaylist!.id == widget.playlist.id; playback.currentPlaylist!.id == playlist.id;
return PlaybuttonCard( return PlaybuttonCard(
title: widget.playlist.name!, title: playlist.name!,
imageUrl: widget.playlist.images![0].url!, imageUrl: playlist.images![0].url!,
isPlaying: isPlaylistPlaying, isPlaying: isPlaylistPlaying,
onTap: () { onTap: () {
Navigator.of(context).push(MaterialPageRoute( Navigator.of(context).push(MaterialPageRoute(
builder: (context) { builder: (context) {
return PlaylistView(widget.playlist); return PlaylistView(playlist);
}, },
)); ));
}, },
onPlaybuttonPressed: () async { onPlaybuttonPressed: () async {
if (isPlaylistPlaying) return; if (isPlaylistPlaying) return;
SpotifyDI data = context.read<SpotifyDI>(); SpotifyApi spotifyApi = ref.read(spotifyProvider);
List<Track> tracks = (widget.playlist.id != "user-liked-tracks" List<Track> tracks = (playlist.id != "user-liked-tracks"
? await data.spotifyApi.playlists ? await spotifyApi.playlists
.getTracksByPlaylistId(widget.playlist.id!) .getTracksByPlaylistId(playlist.id!)
.all() .all()
: await data.spotifyApi.tracks.me.saved : await spotifyApi.tracks.me.saved
.all() .all()
.then((tracks) => tracks.map((e) => e.track!))) .then((tracks) => tracks.map((e) => e.track!)))
.toList(); .toList();
@ -48,9 +43,9 @@ class _PlaylistCardState extends State<PlaylistCard> {
playback.setCurrentPlaylist = CurrentPlaylist( playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks, tracks: tracks,
id: widget.playlist.id!, id: playlist.id!,
name: widget.playlist.name!, name: playlist.name!,
thumbnail: imageToUrlString(widget.playlist.images), thumbnail: imageToUrlString(playlist.images),
); );
playback.setCurrentTrack = tracks.first; playback.setCurrentTrack = tracks.first;
}, },

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart'; import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistGenreView extends StatefulWidget { class PlaylistGenreView extends ConsumerWidget {
final String genreId; final String genreId;
final String genreName; final String genreName;
final Iterable<PlaylistSimple>? playlists; final Iterable<PlaylistSimple>? playlists;
@ -15,13 +15,9 @@ class PlaylistGenreView extends StatefulWidget {
this.playlists, this.playlists,
Key? key, Key? key,
}) : super(key: key); }) : super(key: key);
@override
_PlaylistGenreViewState createState() => _PlaylistGenreViewState();
}
class _PlaylistGenreViewState extends State<PlaylistGenreView> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
return Scaffold( return Scaffold(
appBar: const PageWindowTitleBar( appBar: const PageWindowTitleBar(
leading: BackButton(), leading: BackButton(),
@ -29,43 +25,46 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
body: Column( body: Column(
children: [ children: [
Text( Text(
widget.genreName, genreName,
style: Theme.of(context).textTheme.headline4, style: Theme.of(context).textTheme.headline4,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
Consumer<SpotifyDI>( Consumer(
builder: (context, data, child) => Expanded( builder: (context, ref, child) {
child: SingleChildScrollView( SpotifyApi spotifyApi = ref.watch(spotifyProvider);
child: FutureBuilder<Iterable<PlaylistSimple>>( return Expanded(
future: widget.playlists == null child: SingleChildScrollView(
? (widget.genreId != "user-featured-playlists" child: FutureBuilder<Iterable<PlaylistSimple>>(
? data.spotifyApi.playlists future: playlists == null
.getByCategoryId(widget.genreId) ? (genreId != "user-featured-playlists"
.all() ? spotifyApi.playlists
: data.spotifyApi.playlists.featured.all()) .getByCategoryId(genreId)
: Future.value(widget.playlists), .all()
builder: (context, snapshot) { : spotifyApi.playlists.featured.all())
if (snapshot.hasError) { : Future.value(playlists),
return const Center(child: Text("Error occurred")); builder: (context, snapshot) {
} if (snapshot.hasError) {
if (!snapshot.hasData) { return const Center(child: Text("Error occurred"));
return const CircularProgressIndicator.adaptive(); }
} if (!snapshot.hasData) {
return Center( return const CircularProgressIndicator.adaptive();
child: Wrap( }
children: snapshot.data! return Center(
.map( child: Wrap(
(playlist) => Padding( children: snapshot.data!
padding: const EdgeInsets.all(8.0), .map(
child: PlaylistCard(playlist), (playlist) => Padding(
), padding: const EdgeInsets.all(8.0),
) child: PlaylistCard(playlist),
.toList(), ),
), )
); .toList(),
}), ),
), );
), }),
),
);
},
) )
], ],
), ),

View File

@ -1,30 +1,26 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/components/Shared/TracksTableView.dart'; import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistView extends StatefulWidget { class PlaylistView extends ConsumerWidget {
final PlaylistSimple playlist; final PlaylistSimple playlist;
const PlaylistView(this.playlist, {Key? key}) : super(key: key); const PlaylistView(this.playlist, {Key? key}) : super(key: key);
@override
_PlaylistViewState createState() => _PlaylistViewState();
}
class _PlaylistViewState extends State<PlaylistView> {
playPlaylist(Playback playback, List<Track> tracks, {Track? currentTrack}) { playPlaylist(Playback playback, List<Track> tracks, {Track? currentTrack}) {
currentTrack ??= tracks.first; currentTrack ??= tracks.first;
var isPlaylistPlaying = playback.currentPlaylist?.id != null && var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == widget.playlist.id; playback.currentPlaylist?.id == playlist.id;
if (!isPlaylistPlaying) { if (!isPlaylistPlaying) {
playback.setCurrentPlaylist = CurrentPlaylist( playback.setCurrentPlaylist = CurrentPlaylist(
tracks: tracks, tracks: tracks,
id: widget.playlist.id!, id: playlist.id!,
name: widget.playlist.name!, name: playlist.name!,
thumbnail: imageToUrlString(widget.playlist.images), thumbnail: imageToUrlString(playlist.images),
); );
playback.setCurrentTrack = currentTrack; playback.setCurrentTrack = currentTrack;
} else if (isPlaylistPlaying && } else if (isPlaylistPlaying &&
@ -35,72 +31,69 @@ class _PlaylistViewState extends State<PlaylistView> {
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, ref) {
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
var isPlaylistPlaying = playback.currentPlaylist?.id != null && var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == widget.playlist.id; playback.currentPlaylist?.id == playlist.id;
return Consumer<SpotifyDI>(builder: (_, data, __) { return Scaffold(
return Scaffold( body: FutureBuilder<Iterable<Track>>(
body: FutureBuilder<Iterable<Track>>( future: playlist.id != "user-liked-tracks"
future: widget.playlist.id != "user-liked-tracks" ? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
? data.spotifyApi.playlists : spotifyApi.tracks.me.saved
.getTracksByPlaylistId(widget.playlist.id) .all()
.all() .then((tracks) => tracks.map((e) => e.track!)),
: data.spotifyApi.tracks.me.saved builder: (context, snapshot) {
.all() List<Track> tracks = snapshot.data?.toList() ?? [];
.then((tracks) => tracks.map((e) => e.track!)), return Column(
builder: (context, snapshot) { children: [
List<Track> tracks = snapshot.data?.toList() ?? []; PageWindowTitleBar(
return Column( leading: Row(
children: [ children: [
PageWindowTitleBar( // nav back
leading: Row( const BackButton(),
children: [ // heart playlist
// nav back IconButton(
const BackButton(), icon: const Icon(Icons.favorite_outline_rounded),
// heart playlist onPressed: () {},
IconButton( ),
icon: const Icon(Icons.favorite_outline_rounded), // play playlist
onPressed: () {}, IconButton(
icon: Icon(
isPlaylistPlaying
? Icons.stop_rounded
: Icons.play_arrow_rounded,
), ),
// play playlist onPressed: snapshot.hasData
IconButton( ? () => playPlaylist(playback, tracks)
icon: Icon( : null,
isPlaylistPlaying )
? Icons.stop_rounded ],
: Icons.play_arrow_rounded,
),
onPressed: snapshot.hasData
? () => playPlaylist(playback, tracks)
: null,
)
],
),
), ),
Center( ),
child: Text(widget.playlist.name!, Center(
style: Theme.of(context).textTheme.headline4), child: Text(playlist.name!,
), style: Theme.of(context).textTheme.headline4),
snapshot.hasError ),
? const Center(child: Text("Error occurred")) snapshot.hasError
: !snapshot.hasData ? const Center(child: Text("Error occurred"))
? const Expanded( : !snapshot.hasData
child: Center( ? const Expanded(
child: CircularProgressIndicator.adaptive()), child: Center(
) child: CircularProgressIndicator.adaptive()),
: TracksTableView( )
: TracksTableView(
tracks,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
playback,
tracks, tracks,
onTrackPlayButtonPressed: (currentTrack) => currentTrack: currentTrack,
playPlaylist(
playback,
tracks,
currentTrack: currentTrack,
),
), ),
], ),
); ],
}), );
); }),
}); );
} }
} }

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:provider/provider.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart'; import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistCard.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/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart'; import 'package:spotube/provider/SpotifyDI.dart';
class Search extends StatefulWidget { class Search extends HookConsumerWidget {
const Search({Key? key}) : super(key: key); const Search({Key? key}) : super(key: key);
@override @override
State<Search> createState() => _SearchState(); Widget build(BuildContext context, ref) {
} SpotifyApi spotify = ref.watch(spotifyProvider);
var controller = useTextEditingController();
class _SearchState extends State<Search> { var searchTerm = useState("");
late TextEditingController _controller;
String searchTerm = "";
@override
void initState() {
super.initState();
_controller = TextEditingController();
}
@override
Widget build(BuildContext context) {
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
return Expanded( return Expanded(
child: Column( child: Column(
@ -43,11 +31,9 @@ class _SearchState extends State<Search> {
Expanded( Expanded(
child: TextField( child: TextField(
decoration: const InputDecoration(hintText: "Search..."), decoration: const InputDecoration(hintText: "Search..."),
controller: _controller, controller: controller,
onSubmitted: (value) { onSubmitted: (value) {
setState(() { searchTerm.value = controller.value.text;
searchTerm = _controller.value.text;
});
}, },
), ),
), ),
@ -60,27 +46,25 @@ class _SearchState extends State<Search> {
textColor: Colors.white, textColor: Colors.white,
child: const Icon(Icons.search_rounded), child: const Icon(Icons.search_rounded),
onPressed: () { onPressed: () {
setState(() { searchTerm.value = controller.value.text;
searchTerm = _controller.value.text;
});
}, },
), ),
], ],
), ),
), ),
FutureBuilder<List<Page>>( FutureBuilder<List<Page>>(
future: searchTerm.isNotEmpty future: searchTerm.value.isNotEmpty
? spotify.search.get(searchTerm).first(10) ? spotify.search.get(searchTerm.value).first(10)
: null, : null,
builder: (context, snapshot) { builder: (context, snapshot) {
if (!snapshot.hasData && searchTerm.isNotEmpty) { if (!snapshot.hasData && searchTerm.value.isNotEmpty) {
return const Center( return const Center(
child: CircularProgressIndicator.adaptive(), child: CircularProgressIndicator.adaptive(),
); );
} else if (!snapshot.hasData && searchTerm.isEmpty) { } else if (!snapshot.hasData && searchTerm.value.isEmpty) {
return Container(); return Container();
} }
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
List<AlbumSimple> albums = []; List<AlbumSimple> albums = [];
List<Artist> artists = []; List<Artist> artists = [];
List<Track> tracks = []; List<Track> tracks = [];

View File

@ -1,46 +1,28 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:provider/provider.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/components/Settings/SettingsHotkeyTile.dart'; import 'package:spotube/components/Settings/SettingsHotkeyTile.dart';
import 'package:spotube/components/Shared/Hyperlink.dart'; import 'package:spotube/components/Shared/Hyperlink.dart';
import 'package:spotube/components/Shared/PageWindowTitleBar.dart'; import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
import 'package:spotube/main.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/Auth.dart';
import 'package:spotube/provider/ThemeProvider.dart';
import 'package:spotube/provider/UserPreferences.dart'; import 'package:spotube/provider/UserPreferences.dart';
class Settings extends StatefulWidget { class Settings extends HookConsumerWidget {
const Settings({Key? key}) : super(key: key); const Settings({Key? key}) : super(key: key);
@override @override
_SettingsState createState() => _SettingsState(); Widget build(BuildContext context, ref) {
} UserPreferences preferences = ref.watch(userPreferencesProvider);
ThemeMode theme = ref.watch(themeProvider);
var geniusAccessToken = useState<String?>(null);
TextEditingController textEditingController = useTextEditingController();
class _SettingsState extends State<Settings> { textEditingController.addListener(() {
TextEditingController? _textEditingController; geniusAccessToken.value = textEditingController.value.text;
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) {
UserPreferences preferences = context.watch<UserPreferences>();
return Scaffold( return Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
@ -66,7 +48,7 @@ class _SettingsState extends State<Settings> {
Expanded( Expanded(
flex: 1, flex: 1,
child: TextField( child: TextField(
controller: _textEditingController, controller: textEditingController,
decoration: InputDecoration( decoration: InputDecoration(
hintText: preferences.geniusAccessToken, hintText: preferences.geniusAccessToken,
), ),
@ -75,19 +57,19 @@ class _SettingsState extends State<Settings> {
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: ElevatedButton( child: ElevatedButton(
onPressed: _geniusAccessToken != null onPressed: geniusAccessToken != null
? () async { ? () async {
SharedPreferences localStorage = SharedPreferences localStorage =
await SharedPreferences.getInstance(); await SharedPreferences.getInstance();
preferences preferences
.setGeniusAccessToken(_geniusAccessToken); .setGeniusAccessToken(geniusAccessToken.value);
localStorage.setString( localStorage.setString(
LocalStorageKeys.geniusAccessToken, LocalStorageKeys.geniusAccessToken,
_geniusAccessToken!); geniusAccessToken.value ?? "");
setState(() {
_geniusAccessToken = null; geniusAccessToken.value = null;
});
_textEditingController?.text = ""; textEditingController.text = "";
} }
: null, : null,
child: const Text("Save"), child: const Text("Save"),
@ -122,7 +104,7 @@ class _SettingsState extends State<Settings> {
children: [ children: [
const Text("Theme"), const Text("Theme"),
DropdownButton<ThemeMode>( DropdownButton<ThemeMode>(
value: MyApp.of(context)?.getThemeMode(), value: theme,
items: const [ items: const [
DropdownMenuItem( DropdownMenuItem(
child: Text( child: Text(
@ -143,7 +125,7 @@ class _SettingsState extends State<Settings> {
], ],
onChanged: (value) { onChanged: (value) {
if (value != null) { if (value != null) {
MyApp.of(context)?.setThemeMode(value); ref.read(themeProvider.notifier).state = value;
} }
}, },
) )
@ -151,7 +133,7 @@ class _SettingsState extends State<Settings> {
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
Builder(builder: (context) { Builder(builder: (context) {
var auth = context.read<Auth>(); Auth auth = ref.watch(authProvider);
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class AnchorButton<T> extends StatefulWidget { class AnchorButton<T> extends HookWidget {
final String text; final String text;
final TextStyle style; final TextStyle style;
final TextAlign? textAlign; final TextAlign? textAlign;
@ -16,33 +17,29 @@ class AnchorButton<T> extends StatefulWidget {
this.style = const TextStyle(), this.style = const TextStyle(),
}) : super(key: key); }) : super(key: key);
@override
State<AnchorButton<T>> createState() => _AnchorButtonState<T>();
}
class _AnchorButtonState<T> extends State<AnchorButton<T>> {
bool _hover = false;
bool _tap = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var hover = useState(false);
var tap = useState(false);
return GestureDetector( return GestureDetector(
child: MouseRegion( child: MouseRegion(
cursor: MaterialStateMouseCursor.clickable, cursor: MaterialStateMouseCursor.clickable,
child: Text( child: Text(
widget.text, text,
style: widget.style.copyWith( style: style.copyWith(
decoration: _hover || _tap ? TextDecoration.underline : null, decoration:
hover.value || tap.value ? TextDecoration.underline : null,
), ),
textAlign: widget.textAlign, textAlign: textAlign,
overflow: widget.overflow, overflow: overflow,
), ),
onEnter: (event) => setState(() => _hover = true), onEnter: (event) => hover.value = true,
onExit: (event) => setState(() => _hover = false), onExit: (event) => hover.value = false,
), ),
onTapDown: (event) => setState(() => _tap = true), onTapDown: (event) => tap.value = true,
onTapUp: (event) => setState(() => _tap = false), onTapUp: (event) => tap.value = false,
onTap: widget.onTap, onTap: onTap,
); );
} }
} }

View File

@ -1,106 +1,86 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/helpers/artist-to-string.dart'; import 'package:spotube/helpers/artist-to-string.dart';
import 'package:youtube_explode_dart/youtube_explode_dart.dart'; import 'package:youtube_explode_dart/youtube_explode_dart.dart';
import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
class DownloadTrackButton extends StatefulWidget { enum TrackStatus { downloading, idle, done }
class DownloadTrackButton extends HookWidget {
final Track? track; final Track? track;
const DownloadTrackButton({Key? key, this.track}) : super(key: key); const DownloadTrackButton({Key? key, this.track}) : super(key: key);
@override @override
_DownloadTrackButtonState createState() => _DownloadTrackButtonState(); Widget build(BuildContext context) {
} var status = useState<TrackStatus>(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<DownloadTrackButton> { var audioStream = yt.videos.streamsClient.get(
late YoutubeExplode yt; manifest.audioOnly
TrackStatus status = TrackStatus.idle; .where((audio) => audio.codec.mimeType == "audio/mp4")
.withHighestBitrate(),
);
@override var statusCb = audioStream.listen(
void initState() { (event) {
yt = YoutubeExplode(); if (status.value != TrackStatus.downloading) {
super.initState(); status.value = TrackStatus.downloading;
} }
},
@override onDone: () async {
void dispose() { status.value = TrackStatus.done;
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<Artist>(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;
});
await Future.delayed( await Future.delayed(
const Duration(seconds: 3), const Duration(seconds: 3),
() { () {
if (status == TrackStatus.done) { if (status.value == TrackStatus.done) {
setState(() { status.value = TrackStatus.idle;
status = TrackStatus.idle;
});
} }
}, },
); );
} },
return statusCb.cancel(); );
});
}
}
@override String downloadFolder = path.join(
Widget build(BuildContext context) { (await path_provider.getDownloadsDirectory())!.path, "Spotube");
if (status == TrackStatus.downloading) { String fileName =
"${track?.name} - ${artistsToString<Artist>(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( return const SizedBox(
child: CircularProgressIndicator.adaptive( child: CircularProgressIndicator.adaptive(
strokeWidth: 2, strokeWidth: 2,
@ -108,13 +88,13 @@ class _DownloadTrackButtonState extends State<DownloadTrackButton> {
height: 20, height: 20,
width: 20, width: 20,
); );
} else if (status == TrackStatus.done) { } else if (status.value == TrackStatus.done) {
return const Icon(Icons.download_done_rounded); return const Icon(Icons.download_done_rounded);
} }
return IconButton( return IconButton(
icon: const Icon(Icons.download_rounded), icon: const Icon(Icons.download_rounded),
onPressed: widget.track != null && onPressed: track != null &&
!(widget.track!.href ?? "").startsWith("https://api.spotify.com") !(track!.href ?? "").startsWith("https://api.spotify.com")
? _downloadTrack ? _downloadTrack
: null, : null,
); );

View File

@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
class RecordHotKeyDialog extends StatefulWidget { class RecordHotKeyDialog extends HookWidget {
final ValueChanged<HotKey> onHotKeyRecorded; final ValueChanged<HotKey> onHotKeyRecorded;
const RecordHotKeyDialog({ const RecordHotKeyDialog({
@ -9,15 +10,9 @@ class RecordHotKeyDialog extends StatefulWidget {
required this.onHotKeyRecorded, required this.onHotKeyRecorded,
}) : super(key: key); }) : super(key: key);
@override
_RecordHotKeyDialogState createState() => _RecordHotKeyDialogState();
}
class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
HotKey _hotKey = HotKey(null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
var _hotKey = useState(HotKey(null));
return AlertDialog( return AlertDialog(
content: SingleChildScrollView( content: SingleChildScrollView(
child: ListBody( child: ListBody(
@ -58,9 +53,7 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
children: [ children: [
HotKeyRecorder( HotKeyRecorder(
onHotKeyRecorded: (hotKey) { onHotKeyRecorded: (hotKey) {
setState(() { _hotKey.value = hotKey;
_hotKey = hotKey;
});
}, },
), ),
], ],
@ -78,10 +71,10 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
), ),
TextButton( TextButton(
child: const Text('OK'), child: const Text('OK'),
onPressed: !_hotKey.isSetted onPressed: !_hotKey.value.isSetted
? null ? null
: () { : () {
widget.onHotKeyRecorded(_hotKey); onHotKeyRecorded(_hotKey.value);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
), ),

View File

@ -1,6 +1,6 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.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:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Shared/LinkText.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/helpers/zero-pad-num-str.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
class TracksTableView extends StatelessWidget { class TracksTableView extends ConsumerWidget {
final void Function(Track currentTrack)? onTrackPlayButtonPressed; final void Function(Track currentTrack)? onTrackPlayButtonPressed;
final List<Track> tracks; final List<Track> tracks;
const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed}) const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed})
@ -97,8 +97,8 @@ class TracksTableView extends StatelessWidget {
} }
@override @override
Widget build(BuildContext context) { Widget build(context, ref) {
Playback playback = context.watch<Playback>(); Playback playback = ref.watch(playbackProvider);
TextStyle tableHeadStyle = TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
return Expanded( return Expanded(

View File

@ -1,5 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home.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"; const redirectUri = "http://localhost:4304/auth/spotify/callback";
Future<void> oauthLogin(BuildContext context, Future<void> oauthLogin(Auth auth,
{required String clientId, required String clientSecret}) async { {required String clientId, required String clientSecret}) async {
try { try {
String? accessToken; String? accessToken;
@ -50,7 +48,7 @@ Future<void> oauthLogin(BuildContext context,
clientSecret, clientSecret,
); );
Provider.of<Auth>(context, listen: false).setAuthState( auth.setAuthState(
clientId: clientId, clientId: clientId,
clientSecret: clientSecret, clientSecret: clientSecret,
accessToken: accessToken, accessToken: accessToken,

View File

@ -1,20 +1,17 @@
import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Home.dart'; import 'package:spotube/components/Home.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
import 'package:spotube/provider/Auth.dart'; import 'package:spotube/provider/ThemeProvider.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
import 'package:spotube/provider/UserPreferences.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
await hotKeyManager.unregisterAll(); await hotKeyManager.unregisterAll();
runApp(MyApp()); runApp(ProviderScope(child: MyApp()));
doWhenWindowReady(() { doWhenWindowReady(() {
appWindow.minSize = const Size(900, 700); appWindow.minSize = const Size(900, 700);
appWindow.size = const Size(900, 700); appWindow.size = const Size(900, 700);
@ -24,187 +21,117 @@ void main() async {
}); });
} }
class MyApp extends StatefulWidget { class MyApp extends HookConsumerWidget {
static _MyAppState? of(BuildContext context) =>
context.findAncestorStateOfType<_MyAppState>();
@override @override
State<MyApp> 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<MyApp> {
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) { switch (themeMode) {
case "light": case "light":
_themeMode = ThemeMode.light; themeNotifier.state = ThemeMode.light;
break; break;
case "dark": case "dark":
_themeMode = ThemeMode.dark; themeNotifier.state = ThemeMode.dark;
break; break;
default: default:
_themeMode = ThemeMode.system; themeNotifier.state = ThemeMode.system;
} }
}); });
}); }, []);
super.initState();
}
void setThemeMode(ThemeMode themeMode) { return MaterialApp(
SharedPreferences.getInstance().then((localStorage) { debugShowCheckedModeBanner: false,
localStorage.setString( title: 'Spotube',
LocalStorageKeys.themeMode, themeMode.toString().split(".").last); theme: ThemeData(
setState(() { primaryColor: Colors.green,
_themeMode = themeMode; primarySwatch: Colors.green,
}); buttonTheme: const ButtonThemeData(
}); buttonColor: Colors.green,
}
ThemeMode getThemeMode() {
return _themeMode;
}
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider<Auth>(create: (context) => Auth()),
ChangeNotifierProvider<SpotifyDI>(create: (context) {
Auth authState = Provider.of<Auth>(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<Playback>(create: (context) => Playback()),
ChangeNotifierProvider<UserPreferences>(
create: (context) {
return UserPreferences();
},
), ),
], shadowColor: Colors.grey[300],
child: MaterialApp( backgroundColor: Colors.white,
debugShowCheckedModeBanner: false, textTheme: TextTheme(
title: 'Spotube', bodyText1: TextStyle(color: Colors.grey[850]),
theme: ThemeData( headline1: TextStyle(color: Colors.grey[850]),
primaryColor: Colors.green, headline2: TextStyle(color: Colors.grey[850]),
primarySwatch: Colors.green, headline3: TextStyle(color: Colors.grey[850]),
buttonTheme: const ButtonThemeData( headline4: TextStyle(color: Colors.grey[850]),
buttonColor: Colors.green, headline5: TextStyle(color: Colors.grey[850]),
), headline6: TextStyle(color: Colors.grey[850]),
shadowColor: Colors.grey[300], ),
backgroundColor: Colors.white, listTileTheme: ListTileThemeData(
textTheme: TextTheme( iconColor: Colors.grey[850],
bodyText1: TextStyle(color: Colors.grey[850]), horizontalTitleGap: 0,
headline1: TextStyle(color: Colors.grey[850]), ),
headline2: TextStyle(color: Colors.grey[850]), inputDecorationTheme: InputDecorationTheme(
headline3: TextStyle(color: Colors.grey[850]), focusedBorder: OutlineInputBorder(
headline4: TextStyle(color: Colors.grey[850]), borderSide: BorderSide(
headline5: TextStyle(color: Colors.grey[850]), color: Colors.green[400]!,
headline6: TextStyle(color: Colors.grey[850]), width: 2.0,
),
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]!,
),
), ),
), ),
navigationRailTheme: NavigationRailThemeData( enabledBorder: OutlineInputBorder(
backgroundColor: Colors.blueGrey[50], borderSide: BorderSide(
unselectedIconTheme: color: Colors.grey[800]!,
IconThemeData(color: Colors.grey[850], opacity: 1),
unselectedLabelTextStyle: TextStyle(
color: Colors.grey[850],
), ),
), ),
cardTheme: CardTheme(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
color: Colors.white,
),
), ),
darkTheme: ThemeData( navigationRailTheme: NavigationRailThemeData(
brightness: Brightness.dark, backgroundColor: Colors.blueGrey[50],
primaryColor: Colors.green, unselectedIconTheme:
primarySwatch: Colors.green, IconThemeData(color: Colors.grey[850], opacity: 1),
backgroundColor: Colors.blueGrey[900], unselectedLabelTextStyle: TextStyle(
scaffoldBackgroundColor: Colors.blueGrey[900], color: Colors.grey[850],
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, cardTheme: CardTheme(
home: const Home(), 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(),
); );
} }
} }

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class Auth with ChangeNotifier { class Auth with ChangeNotifier {
String? _clientId; String? _clientId;
@ -52,3 +53,5 @@ class Auth with ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
var authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());

View File

@ -1,4 +1,5 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
class CurrentPlaylist { class CurrentPlaylist {
@ -77,4 +78,4 @@ class Playback extends ChangeNotifier {
} }
} }
var x = Playback(); var playbackProvider = ChangeNotifierProvider<Playback>((_) => Playback());

View File

@ -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: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 { var spotifyProvider = Provider<SpotifyApi>((ref) {
SpotifyApi _spotifyApi; Auth authState = ref.watch(authProvider);
return SpotifyApi(
SpotifyDI(this._spotifyApi); SpotifyApiCredentials(
authState.clientId,
SpotifyApi get spotifyApi => _spotifyApi; 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!,
);
},
);
});

View File

@ -0,0 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
var themeProvider = StateProvider<ThemeMode>((ref) {
return ThemeMode.system;
});

View File

@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hotkey_manager/hotkey_manager.dart'; import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/models/LocalStorageKeys.dart'; import 'package:spotube/models/LocalStorageKeys.dart';
@ -110,3 +111,5 @@ class UserPreferences extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
} }
var userPreferencesProvider = ChangeNotifierProvider((_) => UserPreferences());

View File

@ -188,6 +188,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.3.0" 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: flutter_lints:
dependency: "direct dev" dependency: "direct dev"
description: description:
@ -195,6 +202,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.0.4" 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: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@ -212,6 +226,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.14.3" 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: hotkey_manager:
dependency: "direct main" dependency: "direct main"
description: description:
@ -324,6 +345,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.12.11" 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: meta:
dependency: transitive dependency: transitive
description: description:
@ -338,13 +366,6 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.8.1" version: "2.8.1"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.0"
oauth2: oauth2:
dependency: transitive dependency: transitive
description: description:
@ -457,13 +478,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "4.2.4" version: "4.2.4"
provider: riverpod:
dependency: "direct main" dependency: transitive
description: description:
name: provider name: riverpod
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "6.0.1" version: "1.0.3"
rxdart: rxdart:
dependency: transitive dependency: transitive
description: description:
@ -574,6 +595,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.10.0" 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: stream_channel:
dependency: transitive dependency: transitive
description: description:
@ -608,7 +636,7 @@ packages:
name: test_api name: test_api
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "0.4.3" version: "0.4.8"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -37,7 +37,6 @@ dependencies:
cached_network_image: ^3.2.0 cached_network_image: ^3.2.0
html: ^0.15.0 html: ^0.15.0
http: ^0.13.4 http: ^0.13.4
provider: ^6.0.1
shared_preferences: ^2.0.11 shared_preferences: ^2.0.11
spotify: ^0.6.0 spotify: ^0.6.0
url_launcher: ^6.0.17 url_launcher: ^6.0.17
@ -50,6 +49,9 @@ dependencies:
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.8 path_provider: ^2.0.8
collection: ^1.15.0 collection: ^1.15.0
flutter_riverpod: ^1.0.3
flutter_hooks: ^0.18.2+1
hooks_riverpod: ^1.0.3
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: