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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_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:spotify/spotify.dart';
import 'package:spotube/components/Album/AlbumView.dart'; import 'package:spotube/components/Album/AlbumView.dart';
import 'package:spotube/components/Shared/LinkText.dart'; import 'package:spotube/components/Shared/LinkText.dart';
@ -8,100 +9,23 @@ import 'package:spotube/components/Shared/SpotubePageRoute.dart';
import 'package:spotube/helpers/artists-to-clickable-artists.dart'; import 'package:spotube/helpers/artists-to-clickable-artists.dart';
import 'package:spotube/helpers/image-to-url-string.dart'; import 'package:spotube/helpers/image-to-url-string.dart';
import 'package:spotube/helpers/zero-pad-num-str.dart'; import 'package:spotube/helpers/zero-pad-num-str.dart';
import 'package:spotube/hooks/useBreakpoints.dart';
import 'package:spotube/provider/Playback.dart'; import 'package:spotube/provider/Playback.dart';
class TracksTableView extends ConsumerWidget { class TracksTableView extends HookConsumerWidget {
final void Function(Track currentTrack)? onTrackPlayButtonPressed; final void Function(Track currentTrack)? onTrackPlayButtonPressed;
final List<Track> tracks; final List<Track> tracks;
const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed}) const TracksTableView(this.tracks, {Key? key, this.onTrackPlayButtonPressed})
: super(key: key); : 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 @override
Widget build(context, ref) { Widget build(context, ref) {
Playback playback = ref.watch(playbackProvider); Playback playback = ref.watch(playbackProvider);
TextStyle tableHeadStyle = TextStyle tableHeadStyle =
const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
final breakpoint = useBreakpoints();
return Expanded( return Expanded(
child: Scrollbar( child: Scrollbar(
child: ListView( child: ListView(
@ -128,6 +52,7 @@ class TracksTableView extends ConsumerWidget {
), ),
), ),
// used alignment of this table-head // used alignment of this table-head
if (breakpoint.isMoreThan(Breakpoints.md)) ...[
const SizedBox(width: 100), const SizedBox(width: 100),
Expanded( Expanded(
child: Row( child: Row(
@ -139,10 +64,13 @@ class TracksTableView extends ConsumerWidget {
), ),
], ],
), ),
), )
],
if (!breakpoint.isSm) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Text("Time", style: tableHeadStyle), Text("Time", style: tableHeadStyle),
const SizedBox(width: 10), const SizedBox(width: 10),
]
], ],
), ),
...tracks.asMap().entries.map((track) { ...tracks.asMap().entries.map((track) {
@ -152,11 +80,13 @@ class TracksTableView extends ConsumerWidget {
); );
String duration = String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}"; "${track.value.duration?.inMinutes.remainder(60)}:${zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return buildTrackTile(context, playback, return TrackTile(
playback,
track: track, track: track,
duration: duration, duration: duration,
thumbnailUrl: thumbnailUrl, thumbnailUrl: thumbnailUrl,
onTrackPlayButtonPressed: onTrackPlayButtonPressed); onTrackPlayButtonPressed: onTrackPlayButtonPressed,
);
}).toList() }).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, { List<ArtistSimple> artists, {
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
TextStyle textStyle = const TextStyle(),
}) { }) {
return Row( return Row(
crossAxisAlignment: crossAxisAlignment, crossAxisAlignment: crossAxisAlignment,
@ -24,6 +25,7 @@ Widget artistsToClickableArtists(
child: ArtistProfile(artist.value.id!), child: ArtistProfile(artist.value.id!),
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: textStyle,
), ),
) )
.toList(), .toList(),

View File

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