fix: content going below bottom player or nav bar

This commit is contained in:
Kingkor Roy Tirtho 2023-03-11 11:58:48 +06:00
parent a0b377104f
commit 1bdce9fe96
11 changed files with 611 additions and 582 deletions

View File

@ -63,11 +63,9 @@ class UserAlbums extends HookConsumerWidget {
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Material(
type: MaterialType.transparency,
color: Theme.of(context).scaffoldBackgroundColor,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SafeArea(
child: Column(
children: [
TextField(

View File

@ -84,31 +84,34 @@ class UserArtists extends HookConsumerWidget {
onRefresh: () async {
await artistQuery.refreshAll();
},
child: GridView.builder(
itemCount: filteredArtists.length,
physics: const AlwaysScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: 250,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
child: SafeArea(
child: GridView.builder(
itemCount: filteredArtists.length,
physics: const AlwaysScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: 250,
crossAxisSpacing: 20,
mainAxisSpacing: 20,
),
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
return HookBuilder(builder: (context) {
if (index == artistQuery.pages.length - 1 &&
hasNextPage) {
return Waypoint(
controller: useScrollController(),
isGrid: true,
onTouchEdge: () {
artistQuery.fetchNext();
},
child: ArtistCard(filteredArtists[index]),
);
}
return ArtistCard(filteredArtists[index]);
});
},
),
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) {
return HookBuilder(builder: (context) {
if (index == artistQuery.pages.length - 1 && hasNextPage) {
return Waypoint(
controller: useScrollController(),
isGrid: true,
onTouchEdge: () {
artistQuery.fetchNext();
},
child: ArtistCard(filteredArtists[index]),
);
}
return ArtistCard(filteredArtists[index]);
});
},
),
),
);

View File

@ -44,38 +44,40 @@ class UserDownloads extends HookConsumerWidget {
),
),
Expanded(
child: ListView.builder(
itemCount: downloader.inQueue.length,
itemBuilder: (context, index) {
final track = downloader.inQueue.elementAt(index);
return ListTile(
title: Text(track.name ?? ''),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
height: 40,
width: 40,
path: TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.albumArt,
child: SafeArea(
child: ListView.builder(
itemCount: downloader.inQueue.length,
itemBuilder: (context, index) {
final track = downloader.inQueue.elementAt(index);
return ListTile(
title: Text(track.name ?? ''),
leading: Padding(
padding: const EdgeInsets.symmetric(horizontal: 5),
child: ClipRRect(
borderRadius: BorderRadius.circular(10),
child: UniversalImage(
height: 40,
width: 40,
path: TypeConversionUtils.image_X_UrlString(
track.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
),
),
),
),
trailing: const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(),
),
subtitle: Text(
TypeConversionUtils.artists_X_String(
track.artists ?? <Artist>[],
trailing: const SizedBox(
width: 30,
height: 30,
child: CircularProgressIndicator(),
),
),
);
},
subtitle: Text(
TypeConversionUtils.artists_X_String(
track.artists ?? <Artist>[],
),
),
);
},
),
),
),
],

View File

@ -92,10 +92,9 @@ class UserPlaylists extends HookConsumerWidget {
onRefresh: playlistsQuery.refresh,
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SafeArea(
child: Column(
children: [
TextField(

View File

@ -260,8 +260,12 @@ class TracksTableView extends HookConsumerWidget {
];
if (isSliver) {
return SliverList(delegate: SliverChildListDelegate(children));
return SliverSafeArea(
sliver: SliverList(delegate: SliverChildListDelegate(children)),
);
}
return ListView(children: children);
return SafeArea(
child: ListView(children: children),
);
}
}

View File

@ -85,325 +85,338 @@ class ArtistPage extends HookConsumerWidget {
return SingleChildScrollView(
controller: parentScrollController,
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
data.images,
placeholder: ImagePlaceholder.artist,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
CircleAvatar(
radius: avatarWidth,
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
data.images,
placeholder: ImagePlaceholder.artist,
),
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Text(
data.type!.toUpperCase(),
style: chipTextVariant?.copyWith(
color: Colors.white,
),
),
),
if (isBlackListed) ...[
const SizedBox(width: 5),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.red[400],
color: Colors.blue,
borderRadius:
BorderRadius.circular(50)),
child: Text(
"Blacklisted",
data.type!.toUpperCase(),
style: chipTextVariant?.copyWith(
color: Colors.white,
),
),
),
]
],
),
Text(
data.name!,
style: breakpoint.isSm
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
Text(
"${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers",
style: textTheme.bodyMedium?.copyWith(
fontWeight:
breakpoint.isSm ? null : FontWeight.bold,
if (isBlackListed) ...[
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: Colors.red[400],
borderRadius:
BorderRadius.circular(50)),
child: Text(
"Blacklisted",
style: chipTextVariant?.copyWith(
color: Colors.white,
),
),
),
]
],
),
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
HookBuilder(
builder: (context) {
final isFollowingQuery = useQueries.artist
.doIFollow(ref, artistId);
Text(
data.name!,
style: breakpoint.isSm
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
Text(
"${PrimitiveUtils.toReadableNumber(data.followers!.total!.toDouble())} followers",
style: textTheme.bodyMedium?.copyWith(
fontWeight:
breakpoint.isSm ? null : FontWeight.bold,
),
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
HookBuilder(
builder: (context) {
final isFollowingQuery = useQueries
.artist
.doIFollow(ref, artistId);
if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
);
}
if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
);
}
final queryBowl = QueryClient.of(context);
final queryBowl =
QueryClient.of(context);
return FilledButton(
onPressed: () async {
try {
return FilledButton(
onPressed: () async {
try {
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
await isFollowingQuery.refresh();
queryBowl
.refreshInfiniteQueryAllPages(
"user-following-artists");
} finally {
QueryClient.of(context).refreshQuery(
"user-follows-artists-query/$artistId");
}
},
child: Text(
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
await isFollowingQuery.refresh();
? "Following"
: "Follow",
),
);
},
),
const SizedBox(width: 5),
IconButton(
tooltip: "Add to blacklisted artists",
icon: Icon(
SpotubeIcons.userRemove,
color: !isBlackListed
? Colors.red[400]
: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor: isBlackListed
? Colors.red[400]
: null,
),
onPressed: () async {
if (isBlackListed) {
ref
.read(BlackListNotifier
.provider.notifier)
.remove(
BlacklistedElement.artist(
data.id!, data.name!),
);
} else {
ref
.read(BlackListNotifier
.provider.notifier)
.add(
BlacklistedElement.artist(
data.id!, data.name!),
);
}
},
),
IconButton(
icon: const Icon(SpotubeIcons.share),
onPressed: () async {
await Clipboard.setData(
ClipboardData(
text: data.externalUrls?.spotify),
);
queryBowl
.refreshInfiniteQueryAllPages(
"user-following-artists");
} finally {
QueryClient.of(context).refreshQuery(
"user-follows-artists-query/$artistId");
}
},
child: Text(
isFollowingQuery.data!
? "Following"
: "Follow",
ScaffoldMessenger.of(context)
.showSnackBar(
const SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
textAlign: TextAlign.center,
),
),
);
},
),
const SizedBox(width: 5),
IconButton(
tooltip: "Add to blacklisted artists",
icon: Icon(
SpotubeIcons.userRemove,
color: !isBlackListed
? Colors.red[400]
: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor:
isBlackListed ? Colors.red[400] : null,
),
onPressed: () async {
if (isBlackListed) {
ref
.read(BlackListNotifier
.provider.notifier)
.remove(
BlacklistedElement.artist(
data.id!, data.name!),
);
} else {
ref
.read(BlackListNotifier
.provider.notifier)
.add(
BlacklistedElement.artist(
data.id!, data.name!),
);
}
},
),
IconButton(
icon: const Icon(SpotubeIcons.share),
onPressed: () async {
await Clipboard.setData(
ClipboardData(
text: data.externalUrls?.spotify),
);
)
],
)
],
),
),
],
),
const SizedBox(height: 50),
HookBuilder(
builder: (context) {
final topTracksQuery = useQueries.artist.topTracksOf(
ref,
artistId,
);
final isPlaylistPlaying =
playlistNotifier.isPlayingPlaylist(
topTracksQuery.data ?? <Track>[],
);
if (topTracksQuery.isLoading ||
!topTracksQuery.hasData) {
return const CircularProgressIndicator();
} else if (topTracksQuery.hasError) {
return Center(
child: Text(topTracksQuery.error.toString()),
);
}
final topTracks = topTracksQuery.data!;
void playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playlistNotifier.loadAndPlay(tracks,
active: tracks.indexWhere(
(s) => s.id == currentTrack?.id));
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist?.activeTrack.id) {
await playlistNotifier.playTrack(currentTrack);
}
}
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style:
Theme.of(context).textTheme.headlineSmall,
),
if (!isPlaylistPlaying)
IconButton(
icon: const Icon(
SpotubeIcons.queueAdd,
),
onPressed: () {
playlistNotifier.add(topTracks.toList());
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Artist URL copied to clipboard",
"Added ${topTracks.length} tracks to queue",
textAlign: TextAlign.center,
),
),
);
},
)
],
)
],
),
),
],
),
const SizedBox(height: 50),
HookBuilder(
builder: (context) {
final topTracksQuery = useQueries.artist.topTracksOf(
ref,
artistId,
);
final isPlaylistPlaying =
playlistNotifier.isPlayingPlaylist(
topTracksQuery.data ?? <Track>[],
);
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
return const CircularProgressIndicator();
} else if (topTracksQuery.hasError) {
return Center(
child: Text(topTracksQuery.error.toString()),
);
}
final topTracks = topTracksQuery.data!;
void playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playlistNotifier.loadAndPlay(tracks,
active: tracks
.indexWhere((s) => s.id == currentTrack?.id));
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist?.activeTrack.id) {
await playlistNotifier.playTrack(currentTrack);
}
}
return Column(children: [
Row(
children: [
Text(
"Top Tracks",
style: Theme.of(context).textTheme.headlineSmall,
),
if (!isPlaylistPlaying)
IconButton(
icon: const Icon(
SpotubeIcons.queueAdd,
),
onPressed: () {
playlistNotifier.add(topTracks.toList());
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
"Added ${topTracks.length} tracks to queue",
textAlign: TextAlign.center,
),
),
);
},
const SizedBox(width: 5),
IconButton(
icon: Icon(
isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
color: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor:
Theme.of(context).primaryColor,
),
onPressed: () =>
playPlaylist(topTracks.toList()),
)
],
),
...topTracks.toList().asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile(
playlist,
duration: duration,
track: track,
isActive:
playlist?.activeTrack.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
topTracks.toList(),
currentTrack: track.value,
),
const SizedBox(width: 5),
IconButton(
icon: Icon(
isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
color: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor: Theme.of(context).primaryColor,
),
onPressed: () => playPlaylist(topTracks.toList()),
)
],
),
...topTracks.toList().asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile(
playlist,
duration: duration,
track: track,
isActive:
playlist?.activeTrack.id == track.value.id,
onTrackPlayButtonPressed: (currentTrack) =>
playPlaylist(
topTracks.toList(),
currentTrack: track.value,
),
);
}),
]);
},
),
const SizedBox(height: 50),
Text(
"Albums",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Text(
"Fans also likes",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
HookBuilder(
builder: (context) {
final relatedArtists = useQueries.artist.relatedArtistsOf(
ref,
artistId,
);
if (relatedArtists.isLoading || !relatedArtists.hasData) {
return const CircularProgressIndicator();
} else if (relatedArtists.hasError) {
return Center(
child: Text(relatedArtists.error.toString()),
);
}),
]);
},
),
const SizedBox(height: 50),
Text(
"Albums",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Text(
"Fans also likes",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 10),
HookBuilder(
builder: (context) {
final relatedArtists =
useQueries.artist.relatedArtistsOf(
ref,
artistId,
);
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: relatedArtists.data!
.map((artist) => ArtistCard(artist))
.toList(),
),
);
},
),
],
if (relatedArtists.isLoading ||
!relatedArtists.hasData) {
return const CircularProgressIndicator();
} else if (relatedArtists.hasError) {
return Center(
child: Text(relatedArtists.error.toString()),
);
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: relatedArtists.data!
.map((artist) => ArtistCard(artist))
.toList(),
),
);
},
),
],
),
),
);
},

View File

@ -77,7 +77,7 @@ class GenrePage extends HookConsumerWidget {
if (searchText.value.isEmpty && index == categories.length - 1) {
return const ShimmerCategories();
}
return CategoryCard(category);
return SafeArea(child: CategoryCard(category));
},
),
),

View File

@ -75,21 +75,23 @@ class PersonalizedItemCard extends HookWidget {
child: Waypoint(
controller: scrollController,
onTouchEdge: hasNextPage ? onFetchMore : null,
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: [
...?playlistItems
?.map((playlist) => PlaylistCard(playlist)),
...?albumItems?.map(
(album) => AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
child: SafeArea(
child: ListView(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
controller: scrollController,
physics: const AlwaysScrollableScrollPhysics(),
children: [
...?playlistItems
?.map((playlist) => PlaylistCard(playlist)),
...?albumItems?.map(
(album) => AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
),
),
),
if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
],
if (hasNextPage) const ShimmerPlaybuttonCard(count: 1),
],
),
),
),
),

View File

@ -13,10 +13,10 @@ class LibraryPage extends HookConsumerWidget {
const LibraryPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context, ref) {
return const SafeArea(
bottom: false,
child: DefaultTabController(
length: 5,
return const DefaultTabController(
length: 5,
child: SafeArea(
bottom: false,
child: Scaffold(
appBar: PageWindowTitleBar(
centerTitle: true,

View File

@ -65,7 +65,6 @@ class SearchPage extends HookConsumerWidget {
bottom: false,
child: Scaffold(
appBar: kIsDesktop && !kIsMacOS ? const PageWindowTitleBar() : null,
extendBody: true,
body: !authenticationNotifier.isLoggedIn
? const AnonymousFallback()
: Column(
@ -128,241 +127,250 @@ class SearchPage extends HookConsumerWidget {
vertical: 8,
horizontal: 20,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Text(
"Songs",
style:
Theme.of(context).textTheme.titleLarge!,
),
if (searchTrack.isLoadingPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(searchTrack.errors.lastOrNull
?.toString() ??
"")
else
...tracks.asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile(
playlist,
track: track,
duration: duration,
isActive: playlist?.activeTrack.id ==
track.value.id,
onTrackPlayButtonPressed:
(currentTrack) async {
final isTrackPlaying =
playlist?.activeTrack.id ==
currentTrack.id;
if (!isTrackPlaying &&
context.mounted) {
final shouldPlay =
(playlist?.tracks.length ?? 0) >
20
? await showPromptDialog(
context: context,
title:
"Playing ${currentTrack.name}",
message:
"This will clear the current queue. "
"${playlist?.tracks.length ?? 0} tracks will be removed\n"
"Do you want to continue?",
)
: true;
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Text(
"Songs",
style: Theme.of(context)
.textTheme
.titleLarge!,
),
if (searchTrack.isLoadingPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(searchTrack.errors.lastOrNull
?.toString() ??
"")
else
...tracks.asMap().entries.map((track) {
String duration =
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
return TrackTile(
playlist,
track: track,
duration: duration,
isActive: playlist?.activeTrack.id ==
track.value.id,
onTrackPlayButtonPressed:
(currentTrack) async {
final isTrackPlaying =
playlist?.activeTrack.id ==
currentTrack.id;
if (!isTrackPlaying &&
context.mounted) {
final shouldPlay =
(playlist?.tracks.length ?? 0) >
20
? await showPromptDialog(
context: context,
title:
"Playing ${currentTrack.name}",
message:
"This will clear the current queue. "
"${playlist?.tracks.length ?? 0} tracks will be removed\n"
"Do you want to continue?",
)
: true;
if (shouldPlay) {
await playlistNotifier
.loadAndPlay([currentTrack]);
if (shouldPlay) {
await playlistNotifier
.loadAndPlay([currentTrack]);
}
}
}
},
);
}),
if (searchTrack.hasNextPage &&
tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isRefreshingPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isRefreshingPage
? const CircularProgressIndicator()
: const Text("Load more"),
},
);
}),
if (searchTrack.hasNextPage &&
tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isRefreshingPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isRefreshingPage
? const CircularProgressIndicator()
: const Text("Load more"),
),
),
),
if (playlists.isNotEmpty)
Text(
"Playlists",
style:
Theme.of(context).textTheme.titleLarge!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNext();
if (playlists.isNotEmpty)
Text(
"Playlists",
style: Theme.of(context)
.textTheme
.titleLarge!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation:
breakpoint > Breakpoints.md
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNext();
},
controller: playlistController,
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist
.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return PlaylistCard(playlist);
},
),
],
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i ==
playlists.length -
1 &&
searchPlaylist
.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return PlaylistCard(playlist);
},
),
],
),
),
),
),
),
),
if (searchPlaylist.isLoadingPage)
const CircularProgressIndicator(),
if (searchPlaylist.hasPageError)
Text(
searchPlaylist.errors.lastOrNull
?.toString() ??
"",
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Text(
"Artists",
style:
Theme.of(context).textTheme.titleLarge!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: Waypoint(
if (searchPlaylist.isLoadingPage)
const CircularProgressIndicator(),
if (searchPlaylist.hasPageError)
Text(
searchPlaylist.errors.lastOrNull
?.toString() ??
"",
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Text(
"Artists",
style: Theme.of(context)
.textTheme
.titleLarge!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
onTouchEdge: () {
searchArtist.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Waypoint(
controller: artistController,
child: Row(
children: [
...artists.mapIndexed(
(i, artist) {
if (i == artists.length - 1 &&
searchArtist.hasNextPage) {
onTouchEdge: () {
searchArtist.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: artistController,
child: Row(
children: [
...artists.mapIndexed(
(i, artist) {
if (i == artists.length - 1 &&
searchArtist
.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return Container(
margin: const EdgeInsets
.symmetric(
horizontal: 15),
child: ArtistCard(artist),
);
},
),
],
),
),
),
),
),
if (searchArtist.isLoadingPage)
const CircularProgressIndicator(),
if (searchArtist.hasPageError)
Text(
searchArtist.errors.lastOrNull
?.toString() ??
"",
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Text(
"Albums",
style: Theme.of(context)
.textTheme
.titleMedium!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context)
.copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: Waypoint(
controller: albumController,
onTouchEdge: () {
searchAlbum.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: [
...albums.mapIndexed((i, album) {
if (i == albums.length - 1 &&
searchAlbum.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return Container(
margin: const EdgeInsets
.symmetric(
horizontal: 15),
child: ArtistCard(artist),
return AlbumCard(
TypeConversionUtils
.simpleAlbum_X_Album(
album,
),
);
},
),
],
}),
],
),
),
),
),
),
),
if (searchArtist.isLoadingPage)
const CircularProgressIndicator(),
if (searchArtist.hasPageError)
Text(
searchArtist.errors.lastOrNull
?.toString() ??
"",
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Text(
"Albums",
style: Theme.of(context)
.textTheme
.titleMedium!,
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior:
ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: albumController,
child: Waypoint(
controller: albumController,
onTouchEdge: () {
searchAlbum.fetchNext();
},
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: albumController,
child: Row(
children: [
...albums.mapIndexed((i, album) {
if (i == albums.length - 1 &&
searchAlbum.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return AlbumCard(
TypeConversionUtils
.simpleAlbum_X_Album(
album,
),
);
}),
],
),
),
if (searchAlbum.isLoadingPage)
const CircularProgressIndicator(),
if (searchAlbum.hasPageError)
Text(
searchAlbum.errors.lastOrNull
?.toString() ??
"",
),
),
),
if (searchAlbum.isLoadingPage)
const CircularProgressIndicator(),
if (searchAlbum.hasPageError)
Text(
searchAlbum.errors.lastOrNull?.toString() ??
"",
),
],
],
),
),
),
),

View File

@ -17,9 +17,9 @@ class AboutSpotube extends HookConsumerWidget {
final packageInfo = usePackageInfo();
return Scaffold(
appBar: PageWindowTitleBar(
leading: const BackButton(),
title: const Text("About Spotube"),
appBar: const PageWindowTitleBar(
leading: BackButton(),
title: Text("About Spotube"),
),
body: SingleChildScrollView(
child: Padding(