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( child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
child: Material( child: Padding(
type: MaterialType.transparency, padding: const EdgeInsets.all(8.0),
color: Theme.of(context).scaffoldBackgroundColor, child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column( child: Column(
children: [ children: [
TextField( TextField(

View File

@ -84,31 +84,34 @@ class UserArtists extends HookConsumerWidget {
onRefresh: () async { onRefresh: () async {
await artistQuery.refreshAll(); await artistQuery.refreshAll();
}, },
child: GridView.builder( child: SafeArea(
itemCount: filteredArtists.length, child: GridView.builder(
physics: const AlwaysScrollableScrollPhysics(), itemCount: filteredArtists.length,
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( physics: const AlwaysScrollableScrollPhysics(),
maxCrossAxisExtent: 200, gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
mainAxisExtent: 250, maxCrossAxisExtent: 200,
crossAxisSpacing: 20, mainAxisExtent: 250,
mainAxisSpacing: 20, 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( Expanded(
child: ListView.builder( child: SafeArea(
itemCount: downloader.inQueue.length, child: ListView.builder(
itemBuilder: (context, index) { itemCount: downloader.inQueue.length,
final track = downloader.inQueue.elementAt(index); itemBuilder: (context, index) {
return ListTile( final track = downloader.inQueue.elementAt(index);
title: Text(track.name ?? ''), return ListTile(
leading: Padding( title: Text(track.name ?? ''),
padding: const EdgeInsets.symmetric(horizontal: 5), leading: Padding(
child: ClipRRect( padding: const EdgeInsets.symmetric(horizontal: 5),
borderRadius: BorderRadius.circular(10), child: ClipRRect(
child: UniversalImage( borderRadius: BorderRadius.circular(10),
height: 40, child: UniversalImage(
width: 40, height: 40,
path: TypeConversionUtils.image_X_UrlString( width: 40,
track.album?.images, path: TypeConversionUtils.image_X_UrlString(
placeholder: ImagePlaceholder.albumArt, track.album?.images,
placeholder: ImagePlaceholder.albumArt,
),
), ),
), ),
), ),
), trailing: const SizedBox(
trailing: const SizedBox( width: 30,
width: 30, height: 30,
height: 30, child: CircularProgressIndicator(),
child: CircularProgressIndicator(),
),
subtitle: Text(
TypeConversionUtils.artists_X_String(
track.artists ?? <Artist>[],
), ),
), subtitle: Text(
); TypeConversionUtils.artists_X_String(
}, track.artists ?? <Artist>[],
),
),
);
},
),
), ),
), ),
], ],

View File

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

View File

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

View File

@ -75,21 +75,23 @@ class PersonalizedItemCard extends HookWidget {
child: Waypoint( child: Waypoint(
controller: scrollController, controller: scrollController,
onTouchEdge: hasNextPage ? onFetchMore : null, onTouchEdge: hasNextPage ? onFetchMore : null,
child: ListView( child: SafeArea(
scrollDirection: Axis.horizontal, child: ListView(
shrinkWrap: true, scrollDirection: Axis.horizontal,
controller: scrollController, shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(), controller: scrollController,
children: [ physics: const AlwaysScrollableScrollPhysics(),
...?playlistItems children: [
?.map((playlist) => PlaylistCard(playlist)), ...?playlistItems
...?albumItems?.map( ?.map((playlist) => PlaylistCard(playlist)),
(album) => AlbumCard( ...?albumItems?.map(
TypeConversionUtils.simpleAlbum_X_Album(album), (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); const LibraryPage({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
return const SafeArea( return const DefaultTabController(
bottom: false, length: 5,
child: DefaultTabController( child: SafeArea(
length: 5, bottom: false,
child: Scaffold( child: Scaffold(
appBar: PageWindowTitleBar( appBar: PageWindowTitleBar(
centerTitle: true, centerTitle: true,

View File

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

View File

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