Playlist TrackTile is now responsive

PlaylistView/SearchAlbumView are responsive now
ArtistProfile album view & tracks view are paginated now
This commit is contained in:
Kingkor Roy Tirtho 2022-03-01 10:26:20 +06:00
parent 584f431b04
commit b3511e4919
11 changed files with 354 additions and 256 deletions

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
@ -7,10 +8,11 @@ import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/helpers/artist-to-string.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/simple-track-to-track.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class AlbumCard extends ConsumerWidget {
class AlbumCard extends HookConsumerWidget {
final Album album;
const AlbumCard(this.album, {Key? key}) : super(key: key);
@ -19,9 +21,11 @@ class AlbumCard extends ConsumerWidget {
Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null &&
playback.currentPlaylist!.id == album.id;
final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
return PlaybuttonCard(
imageUrl: imageToUrlString(album.images),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
isPlaying: playback.currentPlaylist?.id != null &&
playback.currentPlaylist?.id == album.id,
title: album.name!,

View File

@ -1,7 +1,8 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumCard.dart';
import 'package:spotube/components/Artist/ArtistAlbumView.dart';
@ -12,16 +13,39 @@ import 'package:spotube/components/Shared/TracksTableView.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/readable-number.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class ArtistProfile extends ConsumerWidget {
class ArtistProfile extends HookConsumerWidget {
final String artistId;
const ArtistProfile(this.artistId, {Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
SpotifyApi spotify = ref.watch(spotifyProvider);
final scrollController = useScrollController();
final parentScrollController = useScrollController();
final textTheme = Theme.of(context).textTheme;
final chipTextVariant = useBreakpointValue(
sm: textTheme.bodySmall,
md: textTheme.bodyMedium,
lg: textTheme.headline6,
xl: textTheme.headline6,
xxl: textTheme.headline6,
);
final avatarWidth = useBreakpointValue(
sm: MediaQuery.of(context).size.width * 0.50,
md: MediaQuery.of(context).size.width * 0.40,
lg: MediaQuery.of(context).size.width * 0.18,
xl: MediaQuery.of(context).size.width * 0.18,
xxl: MediaQuery.of(context).size.width * 0.18,
);
final breakpoint = useBreakpoints();
return Scaffold(
appBar: const PageWindowTitleBar(
leading: BackButton(),
@ -34,23 +58,26 @@ class ArtistProfile extends ConsumerWidget {
}
return SingleChildScrollView(
controller: parentScrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
CircleAvatar(
radius: MediaQuery.of(context).size.width * 0.18,
radius: avatarWidth,
backgroundImage: CachedNetworkImageProvider(
imageToUrlString(snapshot.data!.images),
),
),
Flexible(
child: Padding(
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
@ -60,21 +87,24 @@ class ArtistProfile extends ConsumerWidget {
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Text(snapshot.data!.type!.toUpperCase(),
style: Theme.of(context)
.textTheme
.headline6
?.copyWith(color: Colors.white)),
style: chipTextVariant?.copyWith(
color: Colors.white)),
),
Text(
snapshot.data!.name!,
style: Theme.of(context).textTheme.headline2,
style: breakpoint.isSm
? textTheme.headline4
: textTheme.headline2,
),
Text(
"${toReadableNumber(snapshot.data!.followers!.total!.toDouble())} followers",
style: Theme.of(context).textTheme.headline5,
style: breakpoint.isSm
? textTheme.bodyText1
: textTheme.headline5,
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
// TODO: Implement check if user follows this artist
// LIMITATION: spotify-dart lib
@ -99,8 +129,7 @@ class ArtistProfile extends ConsumerWidget {
text: snapshot
.data?.externalUrls?.spotify),
).then((val) {
ScaffoldMessenger.of(context)
.showSnackBar(
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
@ -118,7 +147,6 @@ class ArtistProfile extends ConsumerWidget {
],
),
),
),
],
),
const SizedBox(height: 50),
@ -188,8 +216,7 @@ class ArtistProfile extends ConsumerWidget {
index:
(track.value.album?.images?.length ?? 1) -
1);
return TracksTableView.buildTrackTile(
context,
return TrackTile(
playback,
duration: duration,
track: track,
@ -237,15 +264,19 @@ class ArtistProfile extends ConsumerWidget {
return const Center(
child: CircularProgressIndicator.adaptive());
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: snapshot.data
?.map((album) => AlbumCard(album))
.toList() ??
[],
),
),
);
},
),

View File

@ -1,11 +1,10 @@
import 'package:flutter/material.dart' hide Page;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:infinite_scroll_pagination/infinite_scroll_pagination.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Playlist/PlaylistCard.dart';
import 'package:spotube/components/Playlist/PlaylistGenreView.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/hooks/usePagingController.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class CategoryCard extends HookWidget {
@ -24,26 +23,11 @@ class CategoryCard extends HookWidget {
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
category.name ?? "Unknown",
style: Theme.of(context).textTheme.headline5,
),
TextButton(
onPressed: () {
Navigator.of(context).push(
SpotubePageRoute(
child: PlaylistGenreView(
category.id!,
category.name!,
playlists: playlists,
),
),
);
},
child: const Text("See all"),
)
],
),
),
@ -51,37 +35,63 @@ class CategoryCard extends HookWidget {
builder: (context, ref, child) {
SpotifyApi spotifyApi = ref.watch(spotifyProvider);
final scrollController = useScrollController();
return FutureBuilder<Iterable<PlaylistSimple>>(
future: playlists == null
? (category.id != "user-featured-playlists"
final pagingController =
usePagingController<int, PlaylistSimple>(firstPageKey: 0);
final _error = useState(false);
useEffect(() {
listener(pageKey) async {
try {
if (playlists != null && playlists?.isNotEmpty == true) {
return pagingController.appendLastPage(playlists!.toList());
}
final Page<PlaylistSimple> page = await (category.id !=
"user-featured-playlists"
? spotifyApi.playlists.getByCategoryId(category.id!)
: spotifyApi.playlists.featured)
.getPage(4, 0)
.then((value) => value.items ?? [])
: Future.value(playlists),
builder: (context, snapshot) {
if (snapshot.hasError) {
return const Center(child: Text("Error occurred"));
.getPage(3, pageKey);
if (page.isLast && page.items != null) {
pagingController.appendLastPage(page.items!.toList());
} else if (page.items != null) {
pagingController.appendPage(
page.items!.toList(), page.nextOffset);
}
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator.adaptive(),
);
if (_error.value) _error.value = false;
} catch (e, stack) {
if (!_error.value) _error.value = true;
pagingController.error = e;
print(
"[CategoryCard.pagingController.addPageRequestListener] $e");
print(stack);
}
return Scrollbar(
controller: scrollController,
child: SingleChildScrollView(
}
pagingController.addPageRequestListener(listener);
return () {
pagingController.removePageRequestListener(listener);
};
}, [_error]);
if (_error.value) return const Text("Something Went Wrong");
return SizedBox(
height: 245,
child: Scrollbar(
controller: scrollController,
child: PagedListView<int, PlaylistSimple>(
shrinkWrap: true,
pagingController: pagingController,
scrollController: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: snapshot.data!
.map((playlist) => PlaylistCard(playlist))
.toList(),
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
itemBuilder: (context, playlist, index) {
return PlaylistCard(playlist);
},
),
),
),
);
});
},
)
],

View File

@ -123,16 +123,18 @@ class Home extends HookConsumerWidget {
return null;
}).then((_) {
pagingController.addPageRequestListener(listener);
});
}
} on AuthorizationException catch (_) {
if (clientId != null && clientSecret != null) {
}).catchError((e, stack) {
if (e is AuthorizationException) {
oauthLogin(
auth,
clientId: clientId,
clientSecret: clientSecret,
);
}
print("[Home.useEffect.spotify.getCredentials]: $e");
print(stack);
});
}
} catch (e, stack) {
print("[Home.initState]: $e");
print(stack);

View File

@ -30,7 +30,13 @@ class SpotubeNavigationBar extends HookWidget {
)
],
selectedIndex: selectedIndex,
onDestinationSelected: (i) => Sidebar.goToSettings(context),
onDestinationSelected: (i) {
if (i == 4) {
Sidebar.goToSettings(context);
} else {
onSelectedIndexChanged(i);
}
},
);
}
}

View File

@ -5,10 +5,11 @@ import 'package:spotube/components/Playlist/PlaylistView.dart';
import 'package:spotube/components/Shared/PlaybuttonCard.dart';
import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/hooks/useBreakpointValue.dart';
import 'package:spotube/provider/Playback.dart';
import 'package:spotube/provider/SpotifyDI.dart';
class PlaylistCard extends ConsumerWidget {
class PlaylistCard extends HookConsumerWidget {
final PlaylistSimple playlist;
const PlaylistCard(this.playlist, {Key? key}) : super(key: key);
@override
@ -16,8 +17,11 @@ class PlaylistCard extends ConsumerWidget {
Playback playback = ref.watch(playbackProvider);
bool isPlaylistPlaying = playback.currentPlaylist != null &&
playback.currentPlaylist!.id == playlist.id;
final int marginH =
useBreakpointValue(sm: 10, md: 15, lg: 20, xl: 20, xxl: 20);
return PlaybuttonCard(
margin: const EdgeInsets.symmetric(horizontal: 20),
margin: EdgeInsets.symmetric(horizontal: marginH.toDouble()),
title: playlist.name!,
imageUrl: playlist.images![0].url!,
isPlaying: isPlaylistPlaying,

View File

@ -99,8 +99,7 @@ class Search extends HookConsumerWidget {
...tracks.asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TracksTableView.buildTrackTile(
context,
return TrackTile(
playback,
track: track,
duration: duration,

View File

@ -85,9 +85,13 @@ class PlaybuttonCard extends StatelessWidget {
const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
child: Column(
children: [
Text(
Tooltip(
message: title,
child: Text(
title,
style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis,
),
),
if (description != null) ...[
const SizedBox(height: 10),

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Shared/LinkText.dart';
@ -8,100 +9,23 @@ import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/helpers/artists-to-clickable-artists.dart';
import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart';
class TracksTableView extends ConsumerWidget {
class TracksTableView extends HookConsumerWidget {
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
final List<Track> tracks;
const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed})
: super(key: key);
static Widget buildTrackTile(
BuildContext context,
Playback playback, {
required MapEntry<int, Track> track,
required String duration,
String? thumbnailUrl,
final void Function(Track currentTrack)? onTrackPlayButtonPressed,
}) {
return Row(
children: [
SizedBox(
height: 20,
width: 25,
child: Text(
(track.key + 1).toString(),
textAlign: TextAlign.center,
),
),
if (thumbnailUrl != null)
Padding(
padding: const EdgeInsets.all(8.0),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5)),
child: CachedNetworkImage(
placeholder: (context, url) {
return Container(
height: 40,
width: 40,
color: Colors.green[300],
);
},
imageUrl: thumbnailUrl,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
),
),
),
IconButton(
icon: Icon(
playback.currentTrack?.id != null &&
playback.currentTrack?.id == track.value.id
? Icons.pause_circle_rounded
: Icons.play_circle_rounded,
color: Theme.of(context).primaryColor,
),
onPressed: () => onTrackPlayButtonPressed?.call(
track.value,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.value.name ?? "",
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 17,
),
overflow: TextOverflow.ellipsis,
),
artistsToClickableArtists(track.value.artists ?? []),
],
),
),
Expanded(
child: LinkText(
track.value.album!.name!,
SpotubePageRoute(
child: AlbumView(track.value.album!),
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 10),
Text(duration),
const SizedBox(width: 10),
],
);
}
@override
Widget build(context, ref) {
Playback playback = ref.watch(playbackProvider);
TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
final breakpoint = useBreakpoints();
return Expanded(
child: Scrollbar(
child: ListView(
@ -128,6 +52,7 @@ class TracksTableView extends ConsumerWidget {
),
),
// used alignment of this table-head
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
const SizedBox(width: 100),
Expanded(
child: Row(
@ -139,10 +64,13 @@ class TracksTableView extends ConsumerWidget {
),
],
),
),
)
],
if (!breakpoint.isSm) ...[
const SizedBox(width: 10),
Text("Time", style: tableHeadStyle),
const SizedBox(width: 10),
]
],
),
...tracks.asMap().entries.map((track) {
@ -152,11 +80,13 @@ class TracksTableView extends ConsumerWidget {
);
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return buildTrackTile(context, playback,
return TrackTile(
playback,
track: track,
duration: duration,
thumbnailUrl: thumbnailUrl,
onTrackPlayButtonPressed: onTrackPlayButtonPressed);
onTrackPlayButtonPressed: onTrackPlayButtonPressed,
);
}).toList()
],
),
@ -164,3 +94,104 @@ class TracksTableView extends ConsumerWidget {
);
}
}
class TrackTile extends HookWidget {
final Playback playback;
final MapEntry<int, Track> track;
final String duration;
final String? thumbnailUrl;
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
const TrackTile(
this.playback, {
required this.track,
required this.duration,
this.thumbnailUrl,
this.onTrackPlayButtonPressed,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final breakpoint = useBreakpoints();
return Row(
children: [
SizedBox(
height: 20,
width: 25,
child: Text(
(track.key + 1).toString(),
textAlign: TextAlign.center,
),
),
if (thumbnailUrl != null)
Padding(
padding: EdgeInsets.symmetric(
horizontal: breakpoint.isMoreThan(Breakpoints.md) ? 8.0 : 0,
vertical: 8.0,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(5)),
child: CachedNetworkImage(
placeholder: (context, url) {
return Container(
height: 40,
width: 40,
color: Colors.green[300],
);
},
imageUrl: thumbnailUrl!,
maxHeightDiskCache: 40,
maxWidthDiskCache: 40,
),
),
),
IconButton(
icon: Icon(
playback.currentTrack?.id != null &&
playback.currentTrack?.id == track.value.id
? Icons.pause_circle_rounded
: Icons.play_circle_rounded,
color: Theme.of(context).primaryColor,
),
onPressed: () => onTrackPlayButtonPressed?.call(
track.value,
),
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
track.value.name ?? "",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: breakpoint.isSm ? 14 : 17,
),
overflow: TextOverflow.ellipsis,
),
artistsToClickableArtists(track.value.artists ?? [],
textStyle: TextStyle(
fontSize:
breakpoint.isLessThan(Breakpoints.lg) ? 12 : 14)),
],
),
),
if (breakpoint.isMoreThan(Breakpoints.md))
Expanded(
child: LinkText(
track.value.album!.name!,
SpotubePageRoute(
child: AlbumView(track.value.album!),
),
overflow: TextOverflow.ellipsis,
),
),
if (!breakpoint.isSm) ...[
const SizedBox(width: 10),
Text(duration),
const SizedBox(width: 10)
],
],
);
}
}

View File

@ -8,6 +8,7 @@ Widget artistsToClickableArtists(
List<ArtistSimple> artists, {
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
TextStyle textStyle = const TextStyle(),
}) {
return Row(
crossAxisAlignment: crossAxisAlignment,
@ -24,6 +25,7 @@ Widget artistsToClickableArtists(
child: ArtistProfile(artist.value.id!),
),
overflow: TextOverflow.ellipsis,
style: textStyle,
),
)
.toList(),

View File

@ -12,11 +12,11 @@ class BreakpointUtils {
];
BreakpointUtils(this.breakpoint);
get isSm => breakpoint == Breakpoints.sm;
get isMd => breakpoint == Breakpoints.md;
get isLg => breakpoint == Breakpoints.lg;
get isXl => breakpoint == Breakpoints.xl;
get isXxl => breakpoint == Breakpoints.xxl;
bool get isSm => breakpoint == Breakpoints.sm;
bool get isMd => breakpoint == Breakpoints.md;
bool get isLg => breakpoint == Breakpoints.lg;
bool get isXl => breakpoint == Breakpoints.xl;
bool get isXxl => breakpoint == Breakpoints.xxl;
bool isMoreThanOrEqualTo(Breakpoints b) {
return breakpointList
@ -57,6 +57,11 @@ class BreakpointUtils {
bool operator <=(other) {
return isLessThanOrEqualTo(other);
}
@override
String toString() {
return "BreakpointUtils($breakpoint)";
}
}
enum Breakpoints { sm, md, lg, xl, xxl }