mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
Merge branch 'riverpod-&-hooks'
This commit is contained in:
commit
5a9a988d26
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumView.dart';
|
||||
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
|
||||
@ -9,13 +9,13 @@ import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class AlbumCard extends StatelessWidget {
|
||||
class AlbumCard extends ConsumerWidget {
|
||||
final Album album;
|
||||
const AlbumCard(this.album, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
bool isPlaylistPlaying = playback.currentPlaylist != null &&
|
||||
playback.currentPlaylist!.id == album.id;
|
||||
|
||||
@ -34,7 +34,7 @@ class AlbumCard extends StatelessWidget {
|
||||
));
|
||||
},
|
||||
onPlaybuttonPressed: () async {
|
||||
SpotifyApi spotify = context.read<SpotifyDI>().spotifyApi;
|
||||
SpotifyApi spotify = ref.read(spotifyProvider);
|
||||
if (isPlaylistPlaying) return;
|
||||
List<Track> tracks = (await spotify.albums.getTracks(album.id!).all())
|
||||
.map((track) => simpleTrackToTrack(track, album))
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
@ -8,7 +8,7 @@ import 'package:spotube/helpers/simple-track-to-track.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class AlbumView extends StatelessWidget {
|
||||
class AlbumView extends ConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
const AlbumView(this.album, {Key? key}) : super(key: key);
|
||||
|
||||
@ -31,11 +31,11 @@ class AlbumView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
|
||||
var isPlaylistPlaying = playback.currentPlaylist?.id == album.id;
|
||||
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
return Scaffold(
|
||||
body: FutureBuilder<Iterable<TrackSimple>>(
|
||||
future: spotify.albums.getTracks(album.id!).all(),
|
||||
|
@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class ArtistAlbumView extends StatefulWidget {
|
||||
class ArtistAlbumView extends ConsumerStatefulWidget {
|
||||
final String artistId;
|
||||
final String artistName;
|
||||
const ArtistAlbumView(
|
||||
@ -16,10 +16,10 @@ class ArtistAlbumView extends StatefulWidget {
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<ArtistAlbumView> createState() => _ArtistAlbumViewState();
|
||||
ConsumerState<ArtistAlbumView> createState() => _ArtistAlbumViewState();
|
||||
}
|
||||
|
||||
class _ArtistAlbumViewState extends State<ArtistAlbumView> {
|
||||
class _ArtistAlbumViewState extends ConsumerState<ArtistAlbumView> {
|
||||
final PagingController<int, Album> _pagingController =
|
||||
PagingController<int, Album>(firstPageKey: 0);
|
||||
|
||||
@ -39,10 +39,9 @@ class _ArtistAlbumViewState extends State<ArtistAlbumView> {
|
||||
|
||||
_fetchPage(int pageKey) async {
|
||||
try {
|
||||
SpotifyDI data = context.read<SpotifyDI>();
|
||||
Page<Album> albums = await data.spotifyApi.artists
|
||||
.albums(widget.artistId)
|
||||
.getPage(8, pageKey);
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
Page<Album> albums =
|
||||
await spotifyApi.artists.albums(widget.artistId).getPage(8, pageKey);
|
||||
|
||||
var items = albums.items!.toList();
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Artist/ArtistAlbumView.dart';
|
||||
@ -14,24 +14,19 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class ArtistProfile extends StatefulWidget {
|
||||
class ArtistProfile extends ConsumerWidget {
|
||||
final String artistId;
|
||||
const ArtistProfile(this.artistId, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_ArtistProfileState createState() => _ArtistProfileState();
|
||||
}
|
||||
|
||||
class _ArtistProfileState extends State<ArtistProfile> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
),
|
||||
body: FutureBuilder<Artist>(
|
||||
future: spotify.artists.get(widget.artistId),
|
||||
future: spotify.artists.get(artistId),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
@ -134,7 +129,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive());
|
||||
}
|
||||
Playback playback = context.watch<Playback>();
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
var isPlaylistPlaying =
|
||||
playback.currentPlaylist?.id == snapshot.data?.id;
|
||||
playPlaylist(List<Track> tracks, {Track? currentTrack}) {
|
||||
@ -222,7 +217,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) => ArtistAlbumView(
|
||||
widget.artistId,
|
||||
artistId,
|
||||
snapshot.data?.name ?? "KRTX",
|
||||
),
|
||||
));
|
||||
@ -260,7 +255,7 @@ class _ArtistProfileState extends State<ArtistProfile> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
FutureBuilder<Iterable<Artist>>(
|
||||
future: spotify.artists.getRelatedArtists(widget.artistId),
|
||||
future: spotify.artists.getRelatedArtists(artistId),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistGenreView.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class CategoryCard extends StatefulWidget {
|
||||
class CategoryCard extends StatelessWidget {
|
||||
final Category category;
|
||||
final Iterable<PlaylistSimple>? playlists;
|
||||
const CategoryCard(
|
||||
@ -14,11 +14,6 @@ class CategoryCard extends StatefulWidget {
|
||||
this.playlists,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_CategoryCardState createState() => _CategoryCardState();
|
||||
}
|
||||
|
||||
class _CategoryCardState extends State<CategoryCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
@ -29,7 +24,7 @@ class _CategoryCardState extends State<CategoryCard> {
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.category.name ?? "Unknown",
|
||||
category.name ?? "Unknown",
|
||||
style: Theme.of(context).textTheme.headline5,
|
||||
),
|
||||
TextButton(
|
||||
@ -38,9 +33,9 @@ class _CategoryCardState extends State<CategoryCard> {
|
||||
MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return PlaylistGenreView(
|
||||
widget.category.id!,
|
||||
widget.category.name!,
|
||||
playlists: widget.playlists,
|
||||
category.id!,
|
||||
category.name!,
|
||||
playlists: playlists,
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -51,17 +46,17 @@ class _CategoryCardState extends State<CategoryCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
Consumer<SpotifyDI>(
|
||||
builder: (context, data, child) {
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
return FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: widget.playlists == null
|
||||
? (widget.category.id != "user-featured-playlists"
|
||||
? data.spotifyApi.playlists
|
||||
.getByCategoryId(widget.category.id!)
|
||||
: data.spotifyApi.playlists.featured)
|
||||
future: playlists == null
|
||||
? (category.id != "user-featured-playlists"
|
||||
? spotifyApi.playlists.getByCategoryId(category.id!)
|
||||
: spotifyApi.playlists.featured)
|
||||
.getPage(4, 0)
|
||||
.then((value) => value.items ?? [])
|
||||
: Future.value(widget.playlists),
|
||||
: Future.value(playlists),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(child: Text("Error occurred"));
|
||||
|
@ -3,8 +3,8 @@ import 'dart:io';
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart' hide Page;
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:oauth2/oauth2.dart' show AuthorizationException;
|
||||
import 'package:spotify/spotify.dart' hide Image, Player, Search;
|
||||
@ -33,14 +33,14 @@ List<String> spotifyScopes = [
|
||||
"playlist-read-collaborative"
|
||||
];
|
||||
|
||||
class Home extends StatefulWidget {
|
||||
class Home extends ConsumerStatefulWidget {
|
||||
const Home({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_HomeState createState() => _HomeState();
|
||||
}
|
||||
|
||||
class _HomeState extends State<Home> {
|
||||
class _HomeState extends ConsumerState<Home> {
|
||||
final PagingController<int, Category> _pagingController =
|
||||
PagingController(firstPageKey: 0);
|
||||
|
||||
@ -63,7 +63,7 @@ class _HomeState extends State<Home> {
|
||||
DateTime? expiration =
|
||||
expirationStr != null ? DateTime.parse(expirationStr) : null;
|
||||
try {
|
||||
Auth authProvider = context.read<Auth>();
|
||||
Auth auth = ref.read(authProvider);
|
||||
|
||||
if (clientId != null && clientSecret != null) {
|
||||
SpotifyApi spotifyApi = SpotifyApi(
|
||||
@ -78,7 +78,7 @@ class _HomeState extends State<Home> {
|
||||
);
|
||||
SpotifyApiCredentials credentials = await spotifyApi.getCredentials();
|
||||
if (credentials.accessToken?.isNotEmpty ?? false) {
|
||||
authProvider.setAuthState(
|
||||
auth.setAuthState(
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
accessToken:
|
||||
@ -91,8 +91,8 @@ class _HomeState extends State<Home> {
|
||||
}
|
||||
_pagingController.addPageRequestListener((pageKey) async {
|
||||
try {
|
||||
SpotifyDI data = context.read<SpotifyDI>();
|
||||
Page<Category> categories = await data.spotifyApi.categories
|
||||
SpotifyApi spotifyApi = ref.read(spotifyProvider);
|
||||
Page<Category> categories = await spotifyApi.categories
|
||||
.list(country: "US")
|
||||
.getPage(15, pageKey);
|
||||
|
||||
@ -113,10 +113,10 @@ class _HomeState extends State<Home> {
|
||||
_pagingController.error = e;
|
||||
}
|
||||
});
|
||||
} on AuthorizationException catch (e) {
|
||||
} on AuthorizationException catch (_) {
|
||||
if (clientId != null && clientSecret != null) {
|
||||
oauthLogin(
|
||||
context,
|
||||
ref.read(authProvider),
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
);
|
||||
@ -136,8 +136,9 @@ class _HomeState extends State<Home> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Auth authProvider = Provider.of<Auth>(context);
|
||||
if (!authProvider.isLoggedIn) {
|
||||
Auth auth = ref.watch(authProvider);
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
if (!auth.isLoggedIn) {
|
||||
return const Login();
|
||||
}
|
||||
|
||||
@ -199,49 +200,45 @@ class _HomeState extends State<Home> {
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
]),
|
||||
),
|
||||
trailing:
|
||||
Consumer<SpotifyDI>(builder: (context, data, widget) {
|
||||
return FutureBuilder<User>(
|
||||
future: data.spotifyApi.me.get(),
|
||||
builder: (context, snapshot) {
|
||||
var avatarImg = imageToUrlString(snapshot.data?.images,
|
||||
index: (snapshot.data?.images?.length ?? 1) - 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage:
|
||||
CachedNetworkImageProvider(avatarImg),
|
||||
trailing: FutureBuilder<User>(
|
||||
future: spotify.me.get(),
|
||||
builder: (context, snapshot) {
|
||||
var avatarImg = imageToUrlString(snapshot.data?.images,
|
||||
index: (snapshot.data?.images?.length ?? 1) - 1);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
backgroundImage:
|
||||
CachedNetworkImageProvider(avatarImg),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
snapshot.data?.displayName ?? "User's name",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
snapshot.data?.displayName ?? "User's name",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push(MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const Settings();
|
||||
},
|
||||
));
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.settings_outlined),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return const Settings();
|
||||
},
|
||||
));
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
// contents of the spotify
|
||||
if (_selectedIndex == 0)
|
||||
|
@ -1,18 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class UserArtists extends StatefulWidget {
|
||||
class UserArtists extends ConsumerStatefulWidget {
|
||||
const UserArtists({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UserArtists> createState() => _UserArtistsState();
|
||||
ConsumerState<UserArtists> createState() => _UserArtistsState();
|
||||
}
|
||||
|
||||
class _UserArtistsState extends State<UserArtists> {
|
||||
class _UserArtistsState extends ConsumerState<UserArtists> {
|
||||
final PagingController<String, Artist> _pagingController =
|
||||
PagingController(firstPageKey: "");
|
||||
|
||||
@ -22,8 +22,8 @@ class _UserArtistsState extends State<UserArtists> {
|
||||
WidgetsBinding.instance?.addPostFrameCallback((timestamp) {
|
||||
_pagingController.addPageRequestListener((pageKey) async {
|
||||
try {
|
||||
SpotifyDI data = context.read<SpotifyDI>();
|
||||
CursorPage<Artist> artists = await data.spotifyApi.me
|
||||
SpotifyApi spotifyApi = ref.read(spotifyProvider);
|
||||
CursorPage<Artist> artists = await spotifyApi.me
|
||||
.following(FollowingType.artist)
|
||||
.getPage(15, pageKey);
|
||||
|
||||
@ -51,10 +51,10 @@ class _UserArtistsState extends State<UserArtists> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SpotifyDI data = context.watch<SpotifyDI>();
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
|
||||
return FutureBuilder<CursorPage<Artist>>(
|
||||
future: data.spotifyApi.me.following(FollowingType.artist).first(),
|
||||
future: spotifyApi.me.following(FollowingType.artist).first(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
|
@ -2,14 +2,8 @@ import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:spotube/components/Library/UserArtists.dart';
|
||||
import 'package:spotube/components/Library/UserPlaylists.dart';
|
||||
|
||||
class UserLibrary extends StatefulWidget {
|
||||
class UserLibrary extends StatelessWidget {
|
||||
const UserLibrary({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_UserLibraryState createState() => _UserLibraryState();
|
||||
}
|
||||
|
||||
class _UserLibraryState extends State<UserLibrary> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
|
@ -1,18 +1,18 @@
|
||||
import 'package:flutter/material.dart' hide Image;
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class UserPlaylists extends StatelessWidget {
|
||||
class UserPlaylists extends ConsumerWidget {
|
||||
const UserPlaylists({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SpotifyDI data = context.watch<SpotifyDI>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
|
||||
return FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: data.spotifyApi.playlists.me.all(),
|
||||
future: spotifyApi.playlists.me.all(),
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
return const Center(child: CircularProgressIndicator.adaptive());
|
||||
|
@ -1,5 +1,6 @@
|
||||
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:spotube/components/Shared/Hyperlink.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/UserPreferences.dart';
|
||||
|
||||
class Login extends StatefulWidget {
|
||||
class Login extends HookConsumerWidget {
|
||||
const Login({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_LoginState createState() => _LoginState();
|
||||
}
|
||||
Widget build(BuildContext context, ref) {
|
||||
var clientIdController = useTextEditingController();
|
||||
var clientSecretController = useTextEditingController();
|
||||
var accessTokenController = useTextEditingController();
|
||||
var fieldError = useState(false);
|
||||
|
||||
class _LoginState extends State<Login> {
|
||||
String clientId = "";
|
||||
String clientSecret = "";
|
||||
String accessToken = "";
|
||||
bool _fieldError = false;
|
||||
|
||||
Future handleLogin(Auth authState) async {
|
||||
try {
|
||||
if (clientId == "" || clientSecret == "") {
|
||||
return setState(() {
|
||||
_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"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Future handleLogin(Auth authState) async {
|
||||
try {
|
||||
if (clientIdController.value.text == "" ||
|
||||
clientSecretController.value.text == "") {
|
||||
fieldError.value = true;
|
||||
}
|
||||
await oauthLogin(
|
||||
ref.read(authProvider),
|
||||
clientId: clientIdController.value.text,
|
||||
clientSecret: clientSecretController.value.text,
|
||||
);
|
||||
},
|
||||
} catch (e) {
|
||||
print("[Login.handleLogin] $e");
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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:spotube/components/Settings.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
@ -7,48 +8,53 @@ import 'package:spotube/helpers/getLyrics.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
class Lyrics extends StatefulWidget {
|
||||
class Lyrics extends HookConsumerWidget {
|
||||
const Lyrics({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Lyrics> createState() => _LyricsState();
|
||||
}
|
||||
|
||||
class _LyricsState extends State<Lyrics> {
|
||||
Map<String, String> _lyrics = {};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
UserPreferences userPreferences = context.watch<UserPreferences>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
UserPreferences userPreferences = ref.watch(userPreferencesProvider);
|
||||
var lyrics = useState({});
|
||||
|
||||
bool hasToken = (userPreferences.geniusAccessToken != null ||
|
||||
(userPreferences.geniusAccessToken?.isNotEmpty ?? false));
|
||||
|
||||
if (playback.currentTrack != null &&
|
||||
hasToken &&
|
||||
playback.currentTrack!.id != _lyrics["id"]) {
|
||||
getLyrics(
|
||||
var lyricsFuture = useMemoized(() {
|
||||
if (playback.currentTrack == null ||
|
||||
!hasToken ||
|
||||
(playback.currentTrack?.id != null &&
|
||||
playback.currentTrack?.id == lyrics.value["id"])) {
|
||||
return null;
|
||||
}
|
||||
return getLyrics(
|
||||
playback.currentTrack!.name!,
|
||||
artistsToString<Artist>(playback.currentTrack!.artists ?? []),
|
||||
apiKey: userPreferences.geniusAccessToken!,
|
||||
optimizeQuery: true,
|
||||
).then((lyrics) {
|
||||
if (lyrics != null) {
|
||||
setState(() {
|
||||
_lyrics = {"lyrics": lyrics, "id": playback.currentTrack!.id!};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}, [playback.currentTrack]);
|
||||
|
||||
if (_lyrics["lyrics"] != null && playback.currentTrack == null) {
|
||||
setState(() {
|
||||
_lyrics = {};
|
||||
});
|
||||
}
|
||||
var lyricsSnapshot = useFuture(lyricsFuture);
|
||||
|
||||
if (_lyrics["lyrics"] == null && playback.currentTrack != null) {
|
||||
useEffect(() {
|
||||
if (lyricsSnapshot.hasData && lyricsSnapshot.data != null) {
|
||||
lyrics.value = {
|
||||
"lyrics": lyricsSnapshot.data,
|
||||
"id": playback.currentTrack!.id!
|
||||
};
|
||||
}
|
||||
|
||||
if (lyrics.value["lyrics"] != null && playback.currentTrack == null) {
|
||||
lyrics.value = {};
|
||||
}
|
||||
}, [
|
||||
lyricsSnapshot.data,
|
||||
lyricsSnapshot.hasData,
|
||||
lyrics.value,
|
||||
playback.currentTrack,
|
||||
]);
|
||||
|
||||
if (lyrics.value["lyrics"] == null && playback.currentTrack != null) {
|
||||
if (!hasToken) {
|
||||
return Expanded(
|
||||
child: Column(
|
||||
@ -99,9 +105,10 @@ class _LyricsState extends State<Lyrics> {
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Text(
|
||||
_lyrics["lyrics"] == null && playback.currentTrack == null
|
||||
lyrics.value["lyrics"] == null &&
|
||||
playback.currentTrack == null
|
||||
? "No Track being played currently"
|
||||
: _lyrics["lyrics"]!,
|
||||
: lyrics.value["lyrics"]!,
|
||||
style: Theme.of(context).textTheme.headline6,
|
||||
),
|
||||
),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:just_audio/just_audio.dart';
|
||||
import 'package:spotify/spotify.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/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
|
||||
class Player extends StatefulWidget {
|
||||
class Player extends HookConsumerWidget {
|
||||
const Player({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_PlayerState createState() => _PlayerState();
|
||||
}
|
||||
Widget build(BuildContext context, ref) {
|
||||
var _isPlaying = useState(false);
|
||||
var _shuffled = useState(false);
|
||||
var _volume = useState(0.0);
|
||||
var _duration = useState<Duration?>(null);
|
||||
var _currentTrackId = useState<String?>(null);
|
||||
|
||||
class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
late AudioPlayer player;
|
||||
bool _isPlaying = false;
|
||||
bool _shuffled = false;
|
||||
Duration? _duration;
|
||||
AudioPlayer player = useMemoized(() => AudioPlayer(), []);
|
||||
YoutubeExplode youtube = useMemoized(() => YoutubeExplode(), []);
|
||||
|
||||
String? _currentTrackId;
|
||||
var _movePlaylistPositionBy = useCallback((int pos) {
|
||||
Playback playback = ref.read(playbackProvider);
|
||||
if (playback.currentTrack != null && playback.currentPlaylist != null) {
|
||||
int index = playback.currentPlaylist!.trackIds
|
||||
.indexOf(playback.currentTrack!.id!) +
|
||||
pos;
|
||||
|
||||
double _volume = 0;
|
||||
var safeIndex = index > playback.currentPlaylist!.trackIds.length - 1
|
||||
? 0
|
||||
: index < 0
|
||||
? playback.currentPlaylist!.trackIds.length
|
||||
: index;
|
||||
Track? track =
|
||||
playback.currentPlaylist!.tracks.asMap().containsKey(safeIndex)
|
||||
? playback.currentPlaylist!.tracks.elementAt(safeIndex)
|
||||
: null;
|
||||
if (track != null) {
|
||||
playback.setCurrentTrack = track;
|
||||
_duration.value = null;
|
||||
}
|
||||
}
|
||||
}, [_duration]);
|
||||
|
||||
late YoutubeExplode youtube;
|
||||
useEffect(() {
|
||||
_volume.value = player.volume;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
try {
|
||||
super.initState();
|
||||
player = AudioPlayer();
|
||||
youtube = YoutubeExplode();
|
||||
|
||||
WidgetsBinding.instance?.addObserver(this);
|
||||
WidgetsBinding.instance?.addPostFrameCallback(_init);
|
||||
} catch (e, stack) {
|
||||
print("[Player.initState()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
_init(Duration timeStamp) async {
|
||||
try {
|
||||
setState(() {
|
||||
_volume = player.volume;
|
||||
});
|
||||
player.playingStream.listen((playing) async {
|
||||
setState(() {
|
||||
_isPlaying = playing;
|
||||
});
|
||||
var playingStreamListener = player.playingStream.listen((playing) async {
|
||||
_isPlaying.value = playing;
|
||||
});
|
||||
|
||||
player.durationStream.listen((duration) async {
|
||||
var durationStreamListener =
|
||||
player.durationStream.listen((duration) async {
|
||||
if (duration != null) {
|
||||
// Actually things doesn't work all the time as they were
|
||||
// described. So instead of listening to a `playback.ready`
|
||||
// stream, it has to listen to duration stream since duration
|
||||
// is always added to the Stream sink after all icyMetadata has
|
||||
// been loaded thus indicating buffering started
|
||||
if (duration != Duration.zero && duration != _duration) {
|
||||
if (duration != Duration.zero && duration != _duration.value) {
|
||||
// this line is for prev/next or already playing playlist
|
||||
if (player.playing) await player.pause();
|
||||
await player.play();
|
||||
}
|
||||
setState(() {
|
||||
_duration = duration;
|
||||
});
|
||||
_duration.value = duration;
|
||||
}
|
||||
});
|
||||
|
||||
player.processingStateStream.listen((event) async {
|
||||
var processingStateStreamListener =
|
||||
player.processingStateStream.listen((event) async {
|
||||
try {
|
||||
if (event == ProcessingState.completed && _currentTrackId != null) {
|
||||
if (event == ProcessingState.completed &&
|
||||
_currentTrackId.value != null) {
|
||||
_movePlaylistPositionBy(1);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
@ -87,127 +88,85 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
print(stack);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
print("[Player._init()]: $e");
|
||||
}
|
||||
}
|
||||
return () {
|
||||
playingStreamListener.cancel();
|
||||
durationStreamListener.cancel();
|
||||
processingStateStreamListener.cancel();
|
||||
player.dispose();
|
||||
youtube.close();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance?.removeObserver(this);
|
||||
player.dispose();
|
||||
youtube.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.paused) {
|
||||
// Release the player's resources when not in use. We use "stop" so that
|
||||
// if the app resumes later, it will still remember what position to
|
||||
// resume from.
|
||||
player.stop();
|
||||
}
|
||||
}
|
||||
|
||||
void _movePlaylistPositionBy(int pos) {
|
||||
Playback playback = 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 _playTrack = useCallback((Track currentTrack, Playback playback) async {
|
||||
try {
|
||||
if (currentTrack.id != _currentTrackId.value) {
|
||||
Uri? parsedUri = Uri.tryParse(currentTrack.uri ?? "");
|
||||
if (parsedUri != null && parsedUri.hasAbsolutePath) {
|
||||
await player
|
||||
.setAudioSource(
|
||||
AudioSource.uri(parsedUri),
|
||||
preload: true,
|
||||
)
|
||||
.then((value) async {
|
||||
_currentTrackId.value = currentTrack.id;
|
||||
if (_duration.value != null) {
|
||||
_duration.value = value;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
var ytTrack = await toYoutubeTrack(youtube, currentTrack);
|
||||
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
|
||||
await player
|
||||
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
|
||||
.then((value) {
|
||||
setState(() {
|
||||
_currentTrackId = currentTrack.id;
|
||||
}
|
||||
var ytTrack = await toYoutubeTrack(youtube, currentTrack);
|
||||
if (playback.setTrackUriById(currentTrack.id!, ytTrack.uri!)) {
|
||||
await player
|
||||
.setAudioSource(AudioSource.uri(Uri.parse(ytTrack.uri!)))
|
||||
.then((value) {
|
||||
_currentTrackId.value = currentTrack.id;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[Player._playTrack()] $e");
|
||||
print(stack);
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[Player._playTrack()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
}, [player, _currentTrackId, _duration]);
|
||||
|
||||
_onNext() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(1);
|
||||
print("ON NEXT");
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
var _onNext = useCallback(() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onNext()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
_onPrevious() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
var _onPrevious = useCallback(() async {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
_movePlaylistPositionBy(-1);
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onPrevious()] $e");
|
||||
print(stack);
|
||||
}
|
||||
}, [player]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).backgroundColor,
|
||||
child: Consumer<Playback>(
|
||||
builder: (context, playback, widget) {
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, widget) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
if (playback.currentPlaylist != null &&
|
||||
playback.currentTrack != null) {
|
||||
_playTrack(playback.currentTrack!, playback);
|
||||
}
|
||||
|
||||
String? albumArt = imageToUrlString(
|
||||
playback.currentTrack?.album?.images,
|
||||
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
|
||||
String? albumArt = useMemoized(
|
||||
() => imageToUrlString(
|
||||
playback.currentTrack?.album?.images,
|
||||
index: (playback.currentTrack?.album?.images?.length ?? 1) - 1,
|
||||
),
|
||||
[playback.currentTrack?.album?.images],
|
||||
);
|
||||
|
||||
return Material(
|
||||
@ -249,9 +208,9 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
flex: 3,
|
||||
child: PlayerControls(
|
||||
positionStream: player.positionStream,
|
||||
isPlaying: _isPlaying,
|
||||
duration: _duration ?? Duration.zero,
|
||||
shuffled: _shuffled,
|
||||
isPlaying: _isPlaying.value,
|
||||
duration: _duration.value ?? Duration.zero,
|
||||
shuffled: _shuffled.value,
|
||||
onNext: _onNext,
|
||||
onPrevious: _onPrevious,
|
||||
onPause: () async {
|
||||
@ -282,16 +241,12 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
if (playback.currentTrack == null ||
|
||||
playback.currentPlaylist == null) return;
|
||||
try {
|
||||
if (!_shuffled) {
|
||||
if (!_shuffled.value) {
|
||||
playback.currentPlaylist!.shuffle();
|
||||
setState(() {
|
||||
_shuffled = true;
|
||||
});
|
||||
_shuffled.value = true;
|
||||
} else {
|
||||
playback.currentPlaylist!.unshuffle();
|
||||
setState(() {
|
||||
_shuffled = false;
|
||||
});
|
||||
_shuffled.value = false;
|
||||
}
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onShuffle()] $e");
|
||||
@ -302,12 +257,10 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
try {
|
||||
await player.pause();
|
||||
await player.seek(Duration.zero);
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_currentTrackId = null;
|
||||
_duration = null;
|
||||
_shuffled = false;
|
||||
});
|
||||
_isPlaying.value = false;
|
||||
_currentTrackId.value = null;
|
||||
_duration.value = null;
|
||||
_shuffled.value = false;
|
||||
playback.reset();
|
||||
} catch (e, stack) {
|
||||
print("[PlayerControls.onStop()] $e");
|
||||
@ -327,13 +280,11 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
height: 20,
|
||||
constraints: const BoxConstraints(maxWidth: 200),
|
||||
child: Slider.adaptive(
|
||||
value: _volume,
|
||||
value: _volume.value,
|
||||
onChanged: (value) async {
|
||||
try {
|
||||
await player.setVolume(value).then((_) {
|
||||
setState(() {
|
||||
_volume = value;
|
||||
});
|
||||
_volume.value = value;
|
||||
});
|
||||
} catch (e, stack) {
|
||||
print("[VolumeSlider.onChange()] $e");
|
||||
@ -348,10 +299,11 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
DownloadTrackButton(
|
||||
track: playback.currentTrack,
|
||||
),
|
||||
Consumer<SpotifyDI>(builder: (context, data, widget) {
|
||||
Consumer(builder: (context, ref, widget) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
return FutureBuilder<bool>(
|
||||
future: playback.currentTrack?.id != null
|
||||
? data.spotifyApi.tracks.me
|
||||
? spotifyApi.tracks.me
|
||||
.containsOne(playback.currentTrack!.id!)
|
||||
: Future.value(false),
|
||||
initialData: false,
|
||||
@ -367,10 +319,8 @@ class _PlayerState extends State<Player> with WidgetsBindingObserver {
|
||||
onPressed: () {
|
||||
if (!isLiked &&
|
||||
playback.currentTrack?.id != null) {
|
||||
data.spotifyApi.tracks.me
|
||||
.saveOne(
|
||||
playback.currentTrack!.id!)
|
||||
.then((value) => setState(() {}));
|
||||
spotifyApi.tracks.me.saveOne(
|
||||
playback.currentTrack!.id!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -1,13 +1,14 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/models/GlobalKeyActions.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class PlayerControls extends StatefulWidget {
|
||||
class PlayerControls extends HookConsumerWidget {
|
||||
final Stream<Duration> positionStream;
|
||||
final bool isPlaying;
|
||||
final Duration duration;
|
||||
@ -34,33 +35,21 @@ class PlayerControls extends StatefulWidget {
|
||||
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 {
|
||||
try {
|
||||
widget.isPlaying ? widget.onPause?.call() : await widget.onPlay?.call();
|
||||
isPlaying ? await onPause?.call() : await onPlay?.call();
|
||||
} catch (e, stack) {
|
||||
print("[PlayPauseShortcut] $e");
|
||||
print(stack);
|
||||
}
|
||||
}
|
||||
|
||||
_configureHotKeys(UserPreferences preferences) async {
|
||||
await Future.wait(_hotKeys.map((e) => hotKeyManager.unregister(e.hotKey)))
|
||||
.then((val) async {
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
UserPreferences preferences = ref.watch(userPreferencesProvider);
|
||||
|
||||
var _hotKeys = [];
|
||||
useEffect(() {
|
||||
_hotKeys = [
|
||||
GlobalKeyActions(
|
||||
HotKey(KeyCode.space, scope: HotKeyScope.inapp),
|
||||
@ -68,14 +57,14 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
),
|
||||
if (preferences.nextTrackHotKey != null)
|
||||
GlobalKeyActions(
|
||||
preferences.nextTrackHotKey!, (key) => widget.onNext?.call()),
|
||||
preferences.nextTrackHotKey!, (key) => onNext?.call()),
|
||||
if (preferences.prevTrackHotKey != null)
|
||||
GlobalKeyActions(
|
||||
preferences.prevTrackHotKey!, (key) => widget.onPrevious?.call()),
|
||||
preferences.prevTrackHotKey!, (key) => onPrevious?.call()),
|
||||
if (preferences.playPauseHotKey != null)
|
||||
GlobalKeyActions(preferences.playPauseHotKey!, _playOrPause)
|
||||
];
|
||||
await Future.wait(
|
||||
Future.wait(
|
||||
_hotKeys.map((e) {
|
||||
return hotKeyManager.register(
|
||||
e.hotKey,
|
||||
@ -83,25 +72,22 @@ class _PlayerControlsState extends 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(
|
||||
constraints: const BoxConstraints(maxWidth: 700),
|
||||
child: Column(
|
||||
children: [
|
||||
StreamBuilder<Duration>(
|
||||
stream: widget.positionStream,
|
||||
stream: positionStream,
|
||||
builder: (context, snapshot) {
|
||||
var totalMinutes =
|
||||
zeroPadNumStr(widget.duration.inMinutes.remainder(60));
|
||||
zeroPadNumStr(duration.inMinutes.remainder(60));
|
||||
var totalSeconds =
|
||||
zeroPadNumStr(widget.duration.inSeconds.remainder(60));
|
||||
zeroPadNumStr(duration.inSeconds.remainder(60));
|
||||
var currentMinutes = snapshot.hasData
|
||||
? zeroPadNumStr(snapshot.data!.inMinutes.remainder(60))
|
||||
: "00";
|
||||
@ -109,7 +95,7 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
? zeroPadNumStr(snapshot.data!.inSeconds.remainder(60))
|
||||
: "00";
|
||||
|
||||
var sliderMax = widget.duration.inSeconds;
|
||||
var sliderMax = duration.inSeconds;
|
||||
var sliderValue = snapshot.data?.inSeconds ?? 0;
|
||||
return Row(
|
||||
children: [
|
||||
@ -123,7 +109,7 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
: sliderValue / sliderMax,
|
||||
onChanged: (value) {},
|
||||
onChangeEnd: (value) {
|
||||
widget.onSeek?.call(value * sliderMax);
|
||||
onSeek?.call(value * sliderMax);
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -138,30 +124,27 @@ class _PlayerControlsState extends State<PlayerControls> {
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.shuffle_rounded),
|
||||
color:
|
||||
widget.shuffled ? Theme.of(context).primaryColor : null,
|
||||
color: shuffled ? Theme.of(context).primaryColor : null,
|
||||
onPressed: () {
|
||||
widget.onShuffle?.call();
|
||||
onShuffle?.call();
|
||||
}),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_previous_rounded),
|
||||
onPressed: () {
|
||||
widget.onPrevious?.call();
|
||||
onPrevious?.call();
|
||||
}),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
widget.isPlaying
|
||||
? Icons.pause_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
isPlaying ? Icons.pause_rounded : Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: () => _playOrPause(null),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.skip_next_rounded),
|
||||
onPressed: () => widget.onNext?.call()),
|
||||
onPressed: () => onNext?.call()),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.stop_rounded),
|
||||
onPressed: () => widget.onStop?.call(),
|
||||
onPressed: () => onStop?.call(),
|
||||
)
|
||||
],
|
||||
)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistView.dart';
|
||||
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
|
||||
@ -7,39 +7,34 @@ import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class PlaylistCard extends StatefulWidget {
|
||||
class PlaylistCard extends ConsumerWidget {
|
||||
final PlaylistSimple playlist;
|
||||
const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
|
||||
@override
|
||||
_PlaylistCardState createState() => _PlaylistCardState();
|
||||
}
|
||||
|
||||
class _PlaylistCardState extends State<PlaylistCard> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
bool isPlaylistPlaying = playback.currentPlaylist != null &&
|
||||
playback.currentPlaylist!.id == widget.playlist.id;
|
||||
playback.currentPlaylist!.id == playlist.id;
|
||||
return PlaybuttonCard(
|
||||
title: widget.playlist.name!,
|
||||
imageUrl: widget.playlist.images![0].url!,
|
||||
title: playlist.name!,
|
||||
imageUrl: playlist.images![0].url!,
|
||||
isPlaying: isPlaylistPlaying,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(MaterialPageRoute(
|
||||
builder: (context) {
|
||||
return PlaylistView(widget.playlist);
|
||||
return PlaylistView(playlist);
|
||||
},
|
||||
));
|
||||
},
|
||||
onPlaybuttonPressed: () async {
|
||||
if (isPlaylistPlaying) return;
|
||||
SpotifyDI data = context.read<SpotifyDI>();
|
||||
SpotifyApi spotifyApi = ref.read(spotifyProvider);
|
||||
|
||||
List<Track> tracks = (widget.playlist.id != "user-liked-tracks"
|
||||
? await data.spotifyApi.playlists
|
||||
.getTracksByPlaylistId(widget.playlist.id!)
|
||||
List<Track> tracks = (playlist.id != "user-liked-tracks"
|
||||
? await spotifyApi.playlists
|
||||
.getTracksByPlaylistId(playlist.id!)
|
||||
.all()
|
||||
: await data.spotifyApi.tracks.me.saved
|
||||
: await spotifyApi.tracks.me.saved
|
||||
.all()
|
||||
.then((tracks) => tracks.map((e) => e.track!)))
|
||||
.toList();
|
||||
@ -48,9 +43,9 @@ class _PlaylistCardState extends State<PlaylistCard> {
|
||||
|
||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
||||
tracks: tracks,
|
||||
id: widget.playlist.id!,
|
||||
name: widget.playlist.name!,
|
||||
thumbnail: imageToUrlString(widget.playlist.images),
|
||||
id: playlist.id!,
|
||||
name: playlist.name!,
|
||||
thumbnail: imageToUrlString(playlist.images),
|
||||
);
|
||||
playback.setCurrentTrack = tracks.first;
|
||||
},
|
||||
|
@ -1,11 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Playlist/PlaylistCard.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class PlaylistGenreView extends StatefulWidget {
|
||||
class PlaylistGenreView extends ConsumerWidget {
|
||||
final String genreId;
|
||||
final String genreName;
|
||||
final Iterable<PlaylistSimple>? playlists;
|
||||
@ -15,13 +15,9 @@ class PlaylistGenreView extends StatefulWidget {
|
||||
this.playlists,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
@override
|
||||
_PlaylistGenreViewState createState() => _PlaylistGenreViewState();
|
||||
}
|
||||
|
||||
class _PlaylistGenreViewState extends State<PlaylistGenreView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, ref) {
|
||||
return Scaffold(
|
||||
appBar: const PageWindowTitleBar(
|
||||
leading: BackButton(),
|
||||
@ -29,43 +25,46 @@ class _PlaylistGenreViewState extends State<PlaylistGenreView> {
|
||||
body: Column(
|
||||
children: [
|
||||
Text(
|
||||
widget.genreName,
|
||||
genreName,
|
||||
style: Theme.of(context).textTheme.headline4,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Consumer<SpotifyDI>(
|
||||
builder: (context, data, child) => Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: widget.playlists == null
|
||||
? (widget.genreId != "user-featured-playlists"
|
||||
? data.spotifyApi.playlists
|
||||
.getByCategoryId(widget.genreId)
|
||||
.all()
|
||||
: data.spotifyApi.playlists.featured.all())
|
||||
: Future.value(widget.playlists),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(child: Text("Error occurred"));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return Center(
|
||||
child: Wrap(
|
||||
children: snapshot.data!
|
||||
.map(
|
||||
(playlist) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PlaylistCard(playlist),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
return Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: FutureBuilder<Iterable<PlaylistSimple>>(
|
||||
future: playlists == null
|
||||
? (genreId != "user-featured-playlists"
|
||||
? spotifyApi.playlists
|
||||
.getByCategoryId(genreId)
|
||||
.all()
|
||||
: spotifyApi.playlists.featured.all())
|
||||
: Future.value(playlists),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return const Center(child: Text("Error occurred"));
|
||||
}
|
||||
if (!snapshot.hasData) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return Center(
|
||||
child: Wrap(
|
||||
children: snapshot.data!
|
||||
.map(
|
||||
(playlist) => Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: PlaylistCard(playlist),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
|
@ -1,30 +1,26 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class PlaylistView extends StatefulWidget {
|
||||
class PlaylistView extends ConsumerWidget {
|
||||
final PlaylistSimple playlist;
|
||||
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}) {
|
||||
currentTrack ??= tracks.first;
|
||||
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||
playback.currentPlaylist?.id == widget.playlist.id;
|
||||
playback.currentPlaylist?.id == playlist.id;
|
||||
if (!isPlaylistPlaying) {
|
||||
playback.setCurrentPlaylist = CurrentPlaylist(
|
||||
tracks: tracks,
|
||||
id: widget.playlist.id!,
|
||||
name: widget.playlist.name!,
|
||||
thumbnail: imageToUrlString(widget.playlist.images),
|
||||
id: playlist.id!,
|
||||
name: playlist.name!,
|
||||
thumbnail: imageToUrlString(playlist.images),
|
||||
);
|
||||
playback.setCurrentTrack = currentTrack;
|
||||
} else if (isPlaylistPlaying &&
|
||||
@ -35,72 +31,69 @@ class _PlaylistViewState extends State<PlaylistView> {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
Widget build(BuildContext context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
|
||||
var isPlaylistPlaying = playback.currentPlaylist?.id != null &&
|
||||
playback.currentPlaylist?.id == widget.playlist.id;
|
||||
return Consumer<SpotifyDI>(builder: (_, data, __) {
|
||||
return Scaffold(
|
||||
body: FutureBuilder<Iterable<Track>>(
|
||||
future: widget.playlist.id != "user-liked-tracks"
|
||||
? data.spotifyApi.playlists
|
||||
.getTracksByPlaylistId(widget.playlist.id)
|
||||
.all()
|
||||
: data.spotifyApi.tracks.me.saved
|
||||
.all()
|
||||
.then((tracks) => tracks.map((e) => e.track!)),
|
||||
builder: (context, snapshot) {
|
||||
List<Track> tracks = snapshot.data?.toList() ?? [];
|
||||
return Column(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
IconButton(
|
||||
icon: const Icon(Icons.favorite_outline_rounded),
|
||||
onPressed: () {},
|
||||
playback.currentPlaylist?.id == playlist.id;
|
||||
return Scaffold(
|
||||
body: FutureBuilder<Iterable<Track>>(
|
||||
future: playlist.id != "user-liked-tracks"
|
||||
? spotifyApi.playlists.getTracksByPlaylistId(playlist.id).all()
|
||||
: spotifyApi.tracks.me.saved
|
||||
.all()
|
||||
.then((tracks) => tracks.map((e) => e.track!)),
|
||||
builder: (context, snapshot) {
|
||||
List<Track> tracks = snapshot.data?.toList() ?? [];
|
||||
return Column(
|
||||
children: [
|
||||
PageWindowTitleBar(
|
||||
leading: Row(
|
||||
children: [
|
||||
// nav back
|
||||
const BackButton(),
|
||||
// heart playlist
|
||||
IconButton(
|
||||
icon: const Icon(Icons.favorite_outline_rounded),
|
||||
onPressed: () {},
|
||||
),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
// play playlist
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
isPlaylistPlaying
|
||||
? Icons.stop_rounded
|
||||
: Icons.play_arrow_rounded,
|
||||
),
|
||||
onPressed: snapshot.hasData
|
||||
? () => playPlaylist(playback, tracks)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
onPressed: snapshot.hasData
|
||||
? () => playPlaylist(playback, tracks)
|
||||
: null,
|
||||
)
|
||||
],
|
||||
),
|
||||
Center(
|
||||
child: Text(widget.playlist.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
snapshot.hasError
|
||||
? const Center(child: Text("Error occurred"))
|
||||
: !snapshot.hasData
|
||||
? const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive()),
|
||||
)
|
||||
: TracksTableView(
|
||||
),
|
||||
Center(
|
||||
child: Text(playlist.name!,
|
||||
style: Theme.of(context).textTheme.headline4),
|
||||
),
|
||||
snapshot.hasError
|
||||
? const Center(child: Text("Error occurred"))
|
||||
: !snapshot.hasData
|
||||
? const Expanded(
|
||||
child: Center(
|
||||
child: CircularProgressIndicator.adaptive()),
|
||||
)
|
||||
: TracksTableView(
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) =>
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
onTrackPlayButtonPressed: (currentTrack) =>
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracks,
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
currentTrack: currentTrack,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
});
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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:spotube/components/Album/AlbumCard.dart';
|
||||
import 'package:spotube/components/Artist/ArtistCard.dart';
|
||||
@ -11,27 +12,14 @@ import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
|
||||
class Search extends StatefulWidget {
|
||||
class Search extends HookConsumerWidget {
|
||||
const Search({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<Search> createState() => _SearchState();
|
||||
}
|
||||
|
||||
class _SearchState extends State<Search> {
|
||||
late TextEditingController _controller;
|
||||
|
||||
String searchTerm = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
SpotifyApi spotify = context.watch<SpotifyDI>().spotifyApi;
|
||||
Widget build(BuildContext context, ref) {
|
||||
SpotifyApi spotify = ref.watch(spotifyProvider);
|
||||
var controller = useTextEditingController();
|
||||
var searchTerm = useState("");
|
||||
|
||||
return Expanded(
|
||||
child: Column(
|
||||
@ -43,11 +31,9 @@ class _SearchState extends State<Search> {
|
||||
Expanded(
|
||||
child: TextField(
|
||||
decoration: const InputDecoration(hintText: "Search..."),
|
||||
controller: _controller,
|
||||
controller: controller,
|
||||
onSubmitted: (value) {
|
||||
setState(() {
|
||||
searchTerm = _controller.value.text;
|
||||
});
|
||||
searchTerm.value = controller.value.text;
|
||||
},
|
||||
),
|
||||
),
|
||||
@ -60,27 +46,25 @@ class _SearchState extends State<Search> {
|
||||
textColor: Colors.white,
|
||||
child: const Icon(Icons.search_rounded),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
searchTerm = _controller.value.text;
|
||||
});
|
||||
searchTerm.value = controller.value.text;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
FutureBuilder<List<Page>>(
|
||||
future: searchTerm.isNotEmpty
|
||||
? spotify.search.get(searchTerm).first(10)
|
||||
future: searchTerm.value.isNotEmpty
|
||||
? spotify.search.get(searchTerm.value).first(10)
|
||||
: null,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData && searchTerm.isNotEmpty) {
|
||||
if (!snapshot.hasData && searchTerm.value.isNotEmpty) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
} else if (!snapshot.hasData && searchTerm.isEmpty) {
|
||||
} else if (!snapshot.hasData && searchTerm.value.isEmpty) {
|
||||
return Container();
|
||||
}
|
||||
Playback playback = context.watch<Playback>();
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
List<AlbumSimple> albums = [];
|
||||
List<Artist> artists = [];
|
||||
List<Track> tracks = [];
|
||||
|
@ -1,46 +1,28 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.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:spotube/components/Settings/SettingsHotkeyTile.dart';
|
||||
import 'package:spotube/components/Shared/Hyperlink.dart';
|
||||
import 'package:spotube/components/Shared/PageWindowTitleBar.dart';
|
||||
import 'package:spotube/main.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/ThemeProvider.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
|
||||
class Settings extends StatefulWidget {
|
||||
class Settings extends HookConsumerWidget {
|
||||
const Settings({Key? key}) : super(key: key);
|
||||
|
||||
@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? _textEditingController;
|
||||
String? _geniusAccessToken;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textEditingController = TextEditingController();
|
||||
_textEditingController?.addListener(() {
|
||||
setState(() {
|
||||
_geniusAccessToken = _textEditingController?.value.text;
|
||||
});
|
||||
textEditingController.addListener(() {
|
||||
geniusAccessToken.value = textEditingController.value.text;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textEditingController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
UserPreferences preferences = context.watch<UserPreferences>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: PageWindowTitleBar(
|
||||
@ -66,7 +48,7 @@ class _SettingsState extends State<Settings> {
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: TextField(
|
||||
controller: _textEditingController,
|
||||
controller: textEditingController,
|
||||
decoration: InputDecoration(
|
||||
hintText: preferences.geniusAccessToken,
|
||||
),
|
||||
@ -75,19 +57,19 @@ class _SettingsState extends State<Settings> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: _geniusAccessToken != null
|
||||
onPressed: geniusAccessToken != null
|
||||
? () async {
|
||||
SharedPreferences localStorage =
|
||||
await SharedPreferences.getInstance();
|
||||
preferences
|
||||
.setGeniusAccessToken(_geniusAccessToken);
|
||||
.setGeniusAccessToken(geniusAccessToken.value);
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.geniusAccessToken,
|
||||
_geniusAccessToken!);
|
||||
setState(() {
|
||||
_geniusAccessToken = null;
|
||||
});
|
||||
_textEditingController?.text = "";
|
||||
geniusAccessToken.value ?? "");
|
||||
|
||||
geniusAccessToken.value = null;
|
||||
|
||||
textEditingController.text = "";
|
||||
}
|
||||
: null,
|
||||
child: const Text("Save"),
|
||||
@ -122,7 +104,7 @@ class _SettingsState extends State<Settings> {
|
||||
children: [
|
||||
const Text("Theme"),
|
||||
DropdownButton<ThemeMode>(
|
||||
value: MyApp.of(context)?.getThemeMode(),
|
||||
value: theme,
|
||||
items: const [
|
||||
DropdownMenuItem(
|
||||
child: Text(
|
||||
@ -143,7 +125,7 @@ class _SettingsState extends State<Settings> {
|
||||
],
|
||||
onChanged: (value) {
|
||||
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),
|
||||
Builder(builder: (context) {
|
||||
var auth = context.read<Auth>();
|
||||
Auth auth = ref.watch(authProvider);
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
|
@ -1,6 +1,7 @@
|
||||
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 TextStyle style;
|
||||
final TextAlign? textAlign;
|
||||
@ -16,33 +17,29 @@ class AnchorButton<T> extends StatefulWidget {
|
||||
this.style = const TextStyle(),
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<AnchorButton<T>> createState() => _AnchorButtonState<T>();
|
||||
}
|
||||
|
||||
class _AnchorButtonState<T> extends State<AnchorButton<T>> {
|
||||
bool _hover = false;
|
||||
bool _tap = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var hover = useState(false);
|
||||
var tap = useState(false);
|
||||
|
||||
return GestureDetector(
|
||||
child: MouseRegion(
|
||||
cursor: MaterialStateMouseCursor.clickable,
|
||||
child: Text(
|
||||
widget.text,
|
||||
style: widget.style.copyWith(
|
||||
decoration: _hover || _tap ? TextDecoration.underline : null,
|
||||
text,
|
||||
style: style.copyWith(
|
||||
decoration:
|
||||
hover.value || tap.value ? TextDecoration.underline : null,
|
||||
),
|
||||
textAlign: widget.textAlign,
|
||||
overflow: widget.overflow,
|
||||
textAlign: textAlign,
|
||||
overflow: overflow,
|
||||
),
|
||||
onEnter: (event) => setState(() => _hover = true),
|
||||
onExit: (event) => setState(() => _hover = false),
|
||||
onEnter: (event) => hover.value = true,
|
||||
onExit: (event) => hover.value = false,
|
||||
),
|
||||
onTapDown: (event) => setState(() => _tap = true),
|
||||
onTapUp: (event) => setState(() => _tap = false),
|
||||
onTap: widget.onTap,
|
||||
onTapDown: (event) => tap.value = true,
|
||||
onTapUp: (event) => tap.value = false,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,106 +1,86 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/helpers/artist-to-string.dart';
|
||||
import 'package:youtube_explode_dart/youtube_explode_dart.dart';
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class DownloadTrackButton extends StatefulWidget {
|
||||
enum TrackStatus { downloading, idle, done }
|
||||
|
||||
class DownloadTrackButton extends HookWidget {
|
||||
final Track? track;
|
||||
const DownloadTrackButton({Key? key, this.track}) : super(key: key);
|
||||
|
||||
@override
|
||||
_DownloadTrackButtonState createState() => _DownloadTrackButtonState();
|
||||
}
|
||||
Widget build(BuildContext context) {
|
||||
var status = useState<TrackStatus>(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> {
|
||||
late YoutubeExplode yt;
|
||||
TrackStatus status = TrackStatus.idle;
|
||||
var audioStream = yt.videos.streamsClient.get(
|
||||
manifest.audioOnly
|
||||
.where((audio) => audio.codec.mimeType == "audio/mp4")
|
||||
.withHighestBitrate(),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
yt = YoutubeExplode();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
yt.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
_downloadTrack() async {
|
||||
if (widget.track == null) return;
|
||||
StreamManifest manifest =
|
||||
await yt.videos.streamsClient.getManifest(widget.track?.href);
|
||||
|
||||
var audioStream = yt.videos.streamsClient
|
||||
.get(manifest.audioOnly.withHighestBitrate())
|
||||
.asBroadcastStream();
|
||||
|
||||
var statusCb = audioStream.listen(
|
||||
(event) {
|
||||
if (status != TrackStatus.downloading) {
|
||||
setState(() {
|
||||
status = TrackStatus.downloading;
|
||||
});
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
setState(() {
|
||||
status = TrackStatus.done;
|
||||
});
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
if (status == TrackStatus.done) {
|
||||
setState(() {
|
||||
status = TrackStatus.idle;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
String downloadFolder = path.join(
|
||||
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
|
||||
String fileName =
|
||||
"${widget.track?.name} - ${artistsToString<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;
|
||||
});
|
||||
var statusCb = audioStream.listen(
|
||||
(event) {
|
||||
if (status.value != TrackStatus.downloading) {
|
||||
status.value = TrackStatus.downloading;
|
||||
}
|
||||
},
|
||||
onDone: () async {
|
||||
status.value = TrackStatus.done;
|
||||
await Future.delayed(
|
||||
const Duration(seconds: 3),
|
||||
() {
|
||||
if (status == TrackStatus.done) {
|
||||
setState(() {
|
||||
status = TrackStatus.idle;
|
||||
});
|
||||
if (status.value == TrackStatus.done) {
|
||||
status.value = TrackStatus.idle;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
return statusCb.cancel();
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (status == TrackStatus.downloading) {
|
||||
String downloadFolder = path.join(
|
||||
(await path_provider.getDownloadsDirectory())!.path, "Spotube");
|
||||
String fileName =
|
||||
"${track?.name} - ${artistsToString<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(
|
||||
child: CircularProgressIndicator.adaptive(
|
||||
strokeWidth: 2,
|
||||
@ -108,13 +88,13 @@ class _DownloadTrackButtonState extends State<DownloadTrackButton> {
|
||||
height: 20,
|
||||
width: 20,
|
||||
);
|
||||
} else if (status == TrackStatus.done) {
|
||||
} else if (status.value == TrackStatus.done) {
|
||||
return const Icon(Icons.download_done_rounded);
|
||||
}
|
||||
return IconButton(
|
||||
icon: const Icon(Icons.download_rounded),
|
||||
onPressed: widget.track != null &&
|
||||
!(widget.track!.href ?? "").startsWith("https://api.spotify.com")
|
||||
onPressed: track != null &&
|
||||
!(track!.href ?? "").startsWith("https://api.spotify.com")
|
||||
? _downloadTrack
|
||||
: null,
|
||||
);
|
||||
|
@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
|
||||
class RecordHotKeyDialog extends StatefulWidget {
|
||||
class RecordHotKeyDialog extends HookWidget {
|
||||
final ValueChanged<HotKey> onHotKeyRecorded;
|
||||
|
||||
const RecordHotKeyDialog({
|
||||
@ -9,15 +10,9 @@ class RecordHotKeyDialog extends StatefulWidget {
|
||||
required this.onHotKeyRecorded,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
_RecordHotKeyDialogState createState() => _RecordHotKeyDialogState();
|
||||
}
|
||||
|
||||
class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
|
||||
HotKey _hotKey = HotKey(null);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var _hotKey = useState(HotKey(null));
|
||||
return AlertDialog(
|
||||
content: SingleChildScrollView(
|
||||
child: ListBody(
|
||||
@ -58,9 +53,7 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
|
||||
children: [
|
||||
HotKeyRecorder(
|
||||
onHotKeyRecorded: (hotKey) {
|
||||
setState(() {
|
||||
_hotKey = hotKey;
|
||||
});
|
||||
_hotKey.value = hotKey;
|
||||
},
|
||||
),
|
||||
],
|
||||
@ -78,10 +71,10 @@ class _RecordHotKeyDialogState extends State<RecordHotKeyDialog> {
|
||||
),
|
||||
TextButton(
|
||||
child: const Text('OK'),
|
||||
onPressed: !_hotKey.isSetted
|
||||
onPressed: !_hotKey.value.isSetted
|
||||
? null
|
||||
: () {
|
||||
widget.onHotKeyRecorded(_hotKey);
|
||||
onHotKeyRecorded(_hotKey.value);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
),
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Album/AlbumView.dart';
|
||||
import 'package:spotube/components/Shared/LinkText.dart';
|
||||
@ -9,7 +9,7 @@ import 'package:spotube/helpers/image-to-url-string.dart';
|
||||
import 'package:spotube/helpers/zero-pad-num-str.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
|
||||
class TracksTableView extends StatelessWidget {
|
||||
class TracksTableView extends ConsumerWidget {
|
||||
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||
final List<Track> tracks;
|
||||
const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed})
|
||||
@ -97,8 +97,8 @@ class TracksTableView extends StatelessWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Playback playback = context.watch<Playback>();
|
||||
Widget build(context, ref) {
|
||||
Playback playback = ref.watch(playbackProvider);
|
||||
TextStyle tableHeadStyle =
|
||||
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
|
||||
return Expanded(
|
||||
|
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Home.dart';
|
||||
@ -9,7 +7,7 @@ import 'package:spotube/provider/Auth.dart';
|
||||
|
||||
const redirectUri = "http://localhost:4304/auth/spotify/callback";
|
||||
|
||||
Future<void> oauthLogin(BuildContext context,
|
||||
Future<void> oauthLogin(Auth auth,
|
||||
{required String clientId, required String clientSecret}) async {
|
||||
try {
|
||||
String? accessToken;
|
||||
@ -50,7 +48,7 @@ Future<void> oauthLogin(BuildContext context,
|
||||
clientSecret,
|
||||
);
|
||||
|
||||
Provider.of<Auth>(context, listen: false).setAuthState(
|
||||
auth.setAuthState(
|
||||
clientId: clientId,
|
||||
clientSecret: clientSecret,
|
||||
accessToken: accessToken,
|
||||
|
263
lib/main.dart
263
lib/main.dart
@ -1,20 +1,17 @@
|
||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Home.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/provider/ThemeProvider.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await hotKeyManager.unregisterAll();
|
||||
runApp(MyApp());
|
||||
runApp(ProviderScope(child: MyApp()));
|
||||
doWhenWindowReady(() {
|
||||
appWindow.minSize = const Size(900, 700);
|
||||
appWindow.size = const Size(900, 700);
|
||||
@ -24,187 +21,117 @@ void main() async {
|
||||
});
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
static _MyAppState? of(BuildContext context) =>
|
||||
context.findAncestorStateOfType<_MyAppState>();
|
||||
class MyApp extends HookConsumerWidget {
|
||||
@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) {
|
||||
case "light":
|
||||
_themeMode = ThemeMode.light;
|
||||
themeNotifier.state = ThemeMode.light;
|
||||
break;
|
||||
case "dark":
|
||||
_themeMode = ThemeMode.dark;
|
||||
themeNotifier.state = ThemeMode.dark;
|
||||
break;
|
||||
default:
|
||||
_themeMode = ThemeMode.system;
|
||||
themeNotifier.state = ThemeMode.system;
|
||||
}
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
}, []);
|
||||
|
||||
void setThemeMode(ThemeMode themeMode) {
|
||||
SharedPreferences.getInstance().then((localStorage) {
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.themeMode, themeMode.toString().split(".").last);
|
||||
setState(() {
|
||||
_themeMode = themeMode;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
ThemeMode getThemeMode() {
|
||||
return _themeMode;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return 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();
|
||||
},
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Spotube',
|
||||
theme: ThemeData(
|
||||
primaryColor: Colors.green,
|
||||
primarySwatch: Colors.green,
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Colors.green,
|
||||
),
|
||||
],
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: 'Spotube',
|
||||
theme: ThemeData(
|
||||
primaryColor: Colors.green,
|
||||
primarySwatch: Colors.green,
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Colors.green,
|
||||
),
|
||||
shadowColor: Colors.grey[300],
|
||||
backgroundColor: Colors.white,
|
||||
textTheme: TextTheme(
|
||||
bodyText1: TextStyle(color: Colors.grey[850]),
|
||||
headline1: TextStyle(color: Colors.grey[850]),
|
||||
headline2: TextStyle(color: Colors.grey[850]),
|
||||
headline3: TextStyle(color: Colors.grey[850]),
|
||||
headline4: TextStyle(color: Colors.grey[850]),
|
||||
headline5: TextStyle(color: Colors.grey[850]),
|
||||
headline6: TextStyle(color: Colors.grey[850]),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: Colors.grey[850],
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.green[400]!,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey[800]!,
|
||||
),
|
||||
shadowColor: Colors.grey[300],
|
||||
backgroundColor: Colors.white,
|
||||
textTheme: TextTheme(
|
||||
bodyText1: TextStyle(color: Colors.grey[850]),
|
||||
headline1: TextStyle(color: Colors.grey[850]),
|
||||
headline2: TextStyle(color: Colors.grey[850]),
|
||||
headline3: TextStyle(color: Colors.grey[850]),
|
||||
headline4: TextStyle(color: Colors.grey[850]),
|
||||
headline5: TextStyle(color: Colors.grey[850]),
|
||||
headline6: TextStyle(color: Colors.grey[850]),
|
||||
),
|
||||
listTileTheme: ListTileThemeData(
|
||||
iconColor: Colors.grey[850],
|
||||
horizontalTitleGap: 0,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.green[400]!,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: Colors.blueGrey[50],
|
||||
unselectedIconTheme:
|
||||
IconThemeData(color: Colors.grey[850], opacity: 1),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
color: Colors.grey[850],
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey[800]!,
|
||||
),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: Colors.green,
|
||||
primarySwatch: Colors.green,
|
||||
backgroundColor: Colors.blueGrey[900],
|
||||
scaffoldBackgroundColor: Colors.blueGrey[900],
|
||||
dialogBackgroundColor: Colors.blueGrey[800],
|
||||
shadowColor: Colors.black26,
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Colors.green,
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: Colors.blueGrey[50],
|
||||
unselectedIconTheme:
|
||||
IconThemeData(color: Colors.grey[850], opacity: 1),
|
||||
unselectedLabelTextStyle: TextStyle(
|
||||
color: Colors.grey[850],
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.green[400]!,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey[800]!,
|
||||
),
|
||||
),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: Colors.blueGrey[800],
|
||||
unselectedIconTheme: const IconThemeData(opacity: 1),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
color: Colors.blueGrey[900],
|
||||
elevation: 20,
|
||||
),
|
||||
canvasColor: Colors.blueGrey[900],
|
||||
),
|
||||
themeMode: _themeMode,
|
||||
home: const Home(),
|
||||
cardTheme: CardTheme(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
primaryColor: Colors.green,
|
||||
primarySwatch: Colors.green,
|
||||
backgroundColor: Colors.blueGrey[900],
|
||||
scaffoldBackgroundColor: Colors.blueGrey[900],
|
||||
dialogBackgroundColor: Colors.blueGrey[800],
|
||||
shadowColor: Colors.black26,
|
||||
buttonTheme: const ButtonThemeData(
|
||||
buttonColor: Colors.green,
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.green[400]!,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey[800]!,
|
||||
),
|
||||
),
|
||||
),
|
||||
navigationRailTheme: NavigationRailThemeData(
|
||||
backgroundColor: Colors.blueGrey[800],
|
||||
unselectedIconTheme: const IconThemeData(opacity: 1),
|
||||
),
|
||||
cardTheme: CardTheme(
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
color: Colors.blueGrey[900],
|
||||
elevation: 20,
|
||||
),
|
||||
canvasColor: Colors.blueGrey[900],
|
||||
),
|
||||
themeMode: themeMode,
|
||||
home: const Home(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class Auth with ChangeNotifier {
|
||||
String? _clientId;
|
||||
@ -52,3 +53,5 @@ class Auth with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
var authProvider = ChangeNotifierProvider<Auth>((ref) => Auth());
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
|
||||
class CurrentPlaylist {
|
||||
@ -77,4 +78,4 @@ class Playback extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
var x = Playback();
|
||||
var playbackProvider = ChangeNotifierProvider<Playback>((_) => Playback());
|
||||
|
@ -1,10 +1,36 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Home.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
|
||||
class SpotifyDI extends ChangeNotifier {
|
||||
SpotifyApi _spotifyApi;
|
||||
|
||||
SpotifyDI(this._spotifyApi);
|
||||
|
||||
SpotifyApi get spotifyApi => _spotifyApi;
|
||||
}
|
||||
var spotifyProvider = Provider<SpotifyApi>((ref) {
|
||||
Auth authState = ref.watch(authProvider);
|
||||
return SpotifyApi(
|
||||
SpotifyApiCredentials(
|
||||
authState.clientId,
|
||||
authState.clientSecret,
|
||||
accessToken: authState.accessToken,
|
||||
refreshToken: authState.refreshToken,
|
||||
expiration: authState.expiration,
|
||||
scopes: spotifyScopes,
|
||||
),
|
||||
onCredentialsRefreshed: (credentials) async {
|
||||
SharedPreferences localStorage = await SharedPreferences.getInstance();
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.refreshToken,
|
||||
credentials.refreshToken!,
|
||||
);
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.accessToken,
|
||||
credentials.accessToken!,
|
||||
);
|
||||
localStorage.setString(LocalStorageKeys.clientId, credentials.clientId!);
|
||||
localStorage.setString(
|
||||
LocalStorageKeys.clientSecret,
|
||||
credentials.clientSecret!,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
6
lib/provider/ThemeProvider.dart
Normal file
6
lib/provider/ThemeProvider.dart
Normal 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;
|
||||
});
|
@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
@ -110,3 +111,5 @@ class UserPreferences extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
var userPreferencesProvider = ChangeNotifierProvider((_) => UserPreferences());
|
||||
|
52
pubspec.lock
52
pubspec.lock
@ -188,6 +188,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
flutter_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_hooks
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.18.2+1"
|
||||
flutter_lints:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@ -195,6 +202,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
flutter_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_riverpod
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -212,6 +226,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.14.3"
|
||||
hooks_riverpod:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hooks_riverpod
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
hotkey_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -324,6 +345,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.12.11"
|
||||
material_color_utilities:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: material_color_utilities
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.1.3"
|
||||
meta:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -338,13 +366,6 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "2.8.1"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
oauth2:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -457,13 +478,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "4.2.4"
|
||||
provider:
|
||||
dependency: "direct main"
|
||||
riverpod:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
name: riverpod
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "6.0.1"
|
||||
version: "1.0.3"
|
||||
rxdart:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -574,6 +595,13 @@ packages:
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "1.10.0"
|
||||
state_notifier:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: state_notifier
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.7.2+1"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -608,7 +636,7 @@ packages:
|
||||
name: test_api
|
||||
url: "https://pub.dartlang.org"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.4.8"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -37,7 +37,6 @@ dependencies:
|
||||
cached_network_image: ^3.2.0
|
||||
html: ^0.15.0
|
||||
http: ^0.13.4
|
||||
provider: ^6.0.1
|
||||
shared_preferences: ^2.0.11
|
||||
spotify: ^0.6.0
|
||||
url_launcher: ^6.0.17
|
||||
@ -50,6 +49,9 @@ dependencies:
|
||||
path: ^1.8.0
|
||||
path_provider: ^2.0.8
|
||||
collection: ^1.15.0
|
||||
flutter_riverpod: ^1.0.3
|
||||
flutter_hooks: ^0.18.2+1
|
||||
hooks_riverpod: ^1.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
Loading…
Reference in New Issue
Block a user