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,89 +58,93 @@ 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(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
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(
), snapshot.data!.name!,
Text( style: breakpoint.isSm
snapshot.data!.name!, ? textTheme.headline4
style: Theme.of(context).textTheme.headline2, : 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
const SizedBox(height: 20), : textTheme.headline5,
Row( ),
children: [ const SizedBox(height: 20),
// TODO: Implement check if user follows this artist Row(
// LIMITATION: spotify-dart lib mainAxisSize: MainAxisSize.min,
FutureBuilder( children: [
future: Future.value(true), // TODO: Implement check if user follows this artist
builder: (context, snapshot) { // LIMITATION: spotify-dart lib
return OutlinedButton( FutureBuilder(
onPressed: () async { future: Future.value(true),
// TODO: make `follow/unfollow` artists button work builder: (context, snapshot) {
// LIMITATION: spotify-dart lib return OutlinedButton(
}, onPressed: () async {
child: Text(snapshot.data == true // TODO: make `follow/unfollow` artists button work
? "Following" // LIMITATION: spotify-dart lib
: "Follow"), },
); child: Text(snapshot.data == true
}), ? "Following"
IconButton( : "Follow"),
icon: const Icon(Icons.share_rounded), );
onPressed: () { }),
Clipboard.setData( IconButton(
ClipboardData( icon: const Icon(Icons.share_rounded),
text: snapshot onPressed: () {
.data?.externalUrls?.spotify), Clipboard.setData(
).then((val) { ClipboardData(
ScaffoldMessenger.of(context) text: snapshot
.showSnackBar( .data?.externalUrls?.spotify),
const SnackBar( ).then((val) {
width: 300, ScaffoldMessenger.of(context).showSnackBar(
behavior: SnackBarBehavior.floating, const SnackBar(
content: Text( width: 300,
"Artist URL copied to clipboard", behavior: SnackBarBehavior.floating,
textAlign: TextAlign.center, content: Text(
), "Artist URL copied to clipboard",
textAlign: TextAlign.center,
), ),
); ),
}); );
}, });
) },
], )
) ],
], )
), ],
), ),
), ),
], ],
@ -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,14 +264,18 @@ 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,
children: snapshot.data scrollDirection: Axis.horizontal,
?.map((album) => AlbumCard(album)) child: Row(
.toList() ?? 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/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"
? spotifyApi.playlists.getByCategoryId(category.id!) final _error = useState(false);
: spotifyApi.playlists.featured)
.getPage(4, 0) useEffect(() {
.then((value) => value.items ?? []) listener(pageKey) async {
: Future.value(playlists), try {
builder: (context, snapshot) { if (playlists != null && playlists?.isNotEmpty == true) {
if (snapshot.hasError) { return pagingController.appendLastPage(playlists!.toList());
return const Center(child: Text("Error occurred"));
} }
if (!snapshot.hasData) { final Page<PlaylistSimple> page = await (category.id !=
return const Center( "user-featured-playlists"
child: CircularProgressIndicator.adaptive(), ? spotifyApi.playlists.getByCategoryId(category.id!)
); : spotifyApi.playlists.featured)
.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);
} }
return Scrollbar( if (_error.value) _error.value = false;
controller: scrollController, } catch (e, stack) {
child: SingleChildScrollView( if (!_error.value) _error.value = true;
controller: scrollController, pagingController.error = e;
scrollDirection: Axis.horizontal, print(
child: Row( "[CategoryCard.pagingController.addPageRequestListener] $e");
mainAxisAlignment: MainAxisAlignment.spaceAround, print(stack);
children: snapshot.data! }
.map((playlist) => PlaylistCard(playlist)) }
.toList(),
), 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,
builderDelegate: PagedChildBuilderDelegate<PlaylistSimple>(
itemBuilder: (context, playlist, index) {
return PlaylistCard(playlist);
},
),
),
),
);
}, },
) )
], ],

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) {
oauthLogin(
auth,
clientId: clientId,
clientSecret: clientSecret,
);
}
print("[Home.useEffect.spotify.getCredentials]: $e");
print(stack);
}); });
} }
} on AuthorizationException catch (_) {
if (clientId != null && clientSecret != null) {
oauthLogin(
auth,
clientId: clientId,
clientSecret: clientSecret,
);
}
} 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(
title, message: title,
style: const TextStyle(fontWeight: FontWeight.bold), child: Text(
title,
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,21 +52,25 @@ class TracksTableView extends ConsumerWidget {
), ),
), ),
// used alignment of this table-head // used alignment of this table-head
const SizedBox(width: 100), if (breakpoint.isMoreThan(Breakpoints.md)) ...[
Expanded( const SizedBox(width: 100),
child: Row( Expanded(
children: [ child: Row(
Text( children: [
"Album", Text(
overflow: TextOverflow.ellipsis, "Album",
style: tableHeadStyle, overflow: TextOverflow.ellipsis,
), style: tableHeadStyle,
], ),
), ],
), ),
const SizedBox(width: 10), )
Text("Time", style: tableHeadStyle), ],
const SizedBox(width: 10), if (!breakpoint.isSm) ...[
const SizedBox(width: 10),
Text("Time", style: tableHeadStyle),
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(
track: track, playback,
duration: duration, track: track,
thumbnailUrl: thumbnailUrl, duration: duration,
onTrackPlayButtonPressed: onTrackPlayButtonPressed); thumbnailUrl: thumbnailUrl,
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 }