chore: include interscrollbar in left out pages

This commit is contained in:
Kingkor Roy Tirtho 2023-10-01 13:24:36 +06:00
parent a3250882df
commit 8ca1aa38a2
5 changed files with 641 additions and 619 deletions

View File

@ -7,6 +7,7 @@ import 'package:spotify/spotify.dart' hide Offset;
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.dart';
@ -17,7 +18,6 @@ import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/provider/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_provider.dart';
import 'package:spotube/services/youtube/youtube.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
@ -202,32 +202,36 @@ class SiblingTracksSheet extends HookConsumerWidget {
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child),
child: switch (isSearching.value) {
false => ListView.builder(
itemCount: siblings.length,
itemBuilder: (context, index) =>
itemBuilder(siblings[index]),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
child: InterScrollbar(
child: switch (isSearching.value) {
false => ListView.builder(
itemCount: siblings.length,
itemBuilder: (context, index) =>
itemBuilder(siblings[index]),
),
true => FutureBuilder(
future: searchRequest,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(snapshot.error.toString()),
);
} else if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator());
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]),
);
},
),
},
return InterScrollbar(
child: ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) =>
itemBuilder(snapshot.data![index]),
),
);
},
),
},
),
),
),
),

View File

@ -7,6 +7,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -90,365 +91,372 @@ class ArtistPage extends HookConsumerWidget {
BlacklistedElement.artist(artistId, data.name!),
);
return SingleChildScrollView(
return InterScrollbar(
controller: parentScrollController,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
Padding(
padding: const EdgeInsets.all(16),
child: CircleAvatar(
radius: avatarWidth,
backgroundImage: UniversalImage.imageProvider(
TypeConversionUtils.image_X_UrlString(
data.images,
placeholder: ImagePlaceholder.artist,
child: SingleChildScrollView(
controller: parentScrollController,
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center,
children: [
const SizedBox(width: 50),
Padding(
padding: const EdgeInsets.all(16),
child: 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(
context.l10n.blacklisted,
data.type!.toUpperCase(),
style: chipTextVariant.copyWith(
color: Colors.white,
),
),
),
]
],
),
Text(
data.name!,
style: mediaQuery.smAndDown
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
Text(
context.l10n.followers(
PrimitiveUtils.toReadableNumber(
data.followers!.total!.toDouble(),
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(
context.l10n.blacklisted,
style: chipTextVariant.copyWith(
color: Colors.white,
),
),
),
]
],
),
Text(
data.name!,
style: mediaQuery.smAndDown
? textTheme.headlineSmall
: textTheme.headlineMedium,
),
Text(
context.l10n.followers(
PrimitiveUtils.toReadableNumber(
data.followers!.total!.toDouble(),
),
),
style: textTheme.bodyMedium?.copyWith(
fontWeight: mediaQuery.mdAndUp
? FontWeight.bold
: null,
),
),
style: textTheme.bodyMedium?.copyWith(
fontWeight: mediaQuery.mdAndUp
? FontWeight.bold
: null,
),
),
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
HookBuilder(
builder: (context) {
final isFollowingQuery = useQueries
.artist
.doIFollow(ref, artistId);
const SizedBox(height: 20),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
HookBuilder(
builder: (context) {
final isFollowingQuery = useQueries
.artist
.doIFollow(ref, artistId);
final followUnfollow =
useCallback(() async {
try {
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
await isFollowingQuery.refresh();
final followUnfollow =
useCallback(() async {
try {
isFollowingQuery.data!
? await spotify.me.unfollow(
FollowingType.artist,
[artistId],
)
: await spotify.me.follow(
FollowingType.artist,
[artistId],
);
await isFollowingQuery.refresh();
queryClient
.refreshInfiniteQueryAllPages(
"user-following-artists");
} finally {
queryClient.refreshQuery(
"user-follows-artists-query/$artistId",
queryClient
.refreshInfiniteQueryAllPages(
"user-following-artists");
} finally {
queryClient.refreshQuery(
"user-follows-artists-query/$artistId",
);
}
}, [isFollowingQuery]);
if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) {
return const SizedBox(
height: 20,
width: 20,
child:
CircularProgressIndicator(),
);
}
}, [isFollowingQuery]);
if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) {
return const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(),
);
}
if (isFollowingQuery.data!) {
return OutlinedButton(
onPressed: followUnfollow,
child:
Text(context.l10n.following),
);
}
if (isFollowingQuery.data!) {
return OutlinedButton(
return FilledButton(
onPressed: followUnfollow,
child: Text(context.l10n.following),
child: Text(context.l10n.follow),
);
},
),
const SizedBox(width: 5),
IconButton(
tooltip:
context.l10n.add_artist_to_blacklist,
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 {
if (data.externalUrls?.spotify !=
null) {
await Clipboard.setData(
ClipboardData(
text: data.externalUrls!.spotify!,
),
);
}
return FilledButton(
onPressed: followUnfollow,
child: Text(context.l10n.follow),
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
context.l10n.artist_url_copied,
textAlign: TextAlign.center,
),
),
);
},
)
],
)
],
),
),
],
),
const SizedBox(height: 50),
HookBuilder(
builder: (context) {
final topTracksQuery = useQueries.artist.topTracksOf(
ref,
artistId,
);
final isPlaylistPlaying = playlist.containsTracks(
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.load(
tracks,
initialIndex: tracks.indexWhere(
(s) => s.id == currentTrack?.id),
autoPlay: true,
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist.activeTrack?.id) {
await playlistNotifier.jumpToTrack(currentTrack);
}
}
return Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.top_tracks,
style: theme.textTheme.headlineSmall,
),
),
if (!isPlaylistPlaying)
IconButton(
icon: const Icon(
SpotubeIcons.queueAdd,
),
onPressed: () {
playlistNotifier
.addTracks(topTracks.toList());
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
context.l10n.added_to_queue(
topTracks.length,
),
textAlign: TextAlign.center,
),
),
);
},
),
const SizedBox(width: 5),
IconButton(
tooltip:
context.l10n.add_artist_to_blacklist,
icon: Icon(
SpotubeIcons.userRemove,
color: !isBlackListed
? Colors.red[400]
: Colors.white,
isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
color: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor: isBlackListed
? Colors.red[400]
: null,
backgroundColor:
theme.colorScheme.primary,
),
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 {
if (data.externalUrls?.spotify != null) {
await Clipboard.setData(
ClipboardData(
text: data.externalUrls!.spotify!,
),
);
}
if (!context.mounted) return;
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
context.l10n.artist_url_copied,
textAlign: TextAlign.center,
),
),
);
},
onPressed: () =>
playPlaylist(topTracks.toList()),
)
],
)
),
...topTracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
playPlaylist(
topTracks.toList(),
currentTrack: track,
);
},
);
}),
],
),
);
},
),
const SizedBox(height: 50),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
),
],
),
const SizedBox(height: 50),
HookBuilder(
builder: (context) {
final topTracksQuery = useQueries.artist.topTracksOf(
ref,
artistId,
);
final isPlaylistPlaying = playlist.containsTracks(
topTracksQuery.data ?? <Track>[],
);
if (topTracksQuery.isLoading ||
!topTracksQuery.hasData) {
return const CircularProgressIndicator();
} else if (topTracksQuery.hasError) {
return Center(
child: Text(topTracksQuery.error.toString()),
),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.fans_also_like,
style: theme.textTheme.headlineSmall,
),
),
const SizedBox(height: 10),
HookBuilder(
builder: (context) {
final relatedArtists =
useQueries.artist.relatedArtistsOf(
ref,
artistId,
);
}
final topTracks = topTracksQuery.data!;
void playPlaylist(List<Track> tracks,
{Track? currentTrack}) async {
currentTrack ??= tracks.first;
if (!isPlaylistPlaying) {
playlistNotifier.load(
tracks,
initialIndex: tracks
.indexWhere((s) => s.id == currentTrack?.id),
autoPlay: true,
if (relatedArtists.isLoading ||
!relatedArtists.hasData) {
return const CircularProgressIndicator();
} else if (relatedArtists.hasError) {
return Center(
child: Text(relatedArtists.error.toString()),
);
} else if (isPlaylistPlaying &&
currentTrack.id != null &&
currentTrack.id != playlist.activeTrack?.id) {
await playlistNotifier.jumpToTrack(currentTrack);
}
}
return Column(
children: [
Row(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.top_tracks,
style: theme.textTheme.headlineSmall,
),
),
if (!isPlaylistPlaying)
IconButton(
icon: const Icon(
SpotubeIcons.queueAdd,
),
onPressed: () {
playlistNotifier
.addTracks(topTracks.toList());
scaffoldMessenger.showSnackBar(
SnackBar(
width: 300,
behavior: SnackBarBehavior.floating,
content: Text(
context.l10n.added_to_queue(
topTracks.length,
),
textAlign: TextAlign.center,
),
),
);
},
),
const SizedBox(width: 5),
IconButton(
icon: Icon(
isPlaylistPlaying
? SpotubeIcons.stop
: SpotubeIcons.play,
color: Colors.white,
),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
),
onPressed: () =>
playPlaylist(topTracks.toList()),
)
],
),
...topTracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
playPlaylist(
topTracks.toList(),
currentTrack: track,
);
},
);
}),
],
);
},
),
const SizedBox(height: 50),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.albums,
style: theme.textTheme.headlineSmall,
),
),
ArtistAlbumList(artistId),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
context.l10n.fans_also_like,
style: theme.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()),
child: Wrap(
spacing: 20,
runSpacing: 20,
children: relatedArtists.data!
.map((artist) => ArtistCard(artist))
.toList(),
),
);
}
return Center(
child: Wrap(
spacing: 20,
runSpacing: 20,
children: relatedArtists.data!
.map((artist) => ArtistCard(artist))
.toList(),
),
);
},
),
],
},
),
],
),
),
),
);

View File

@ -9,6 +9,7 @@ import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
@ -103,240 +104,243 @@ class SearchPage extends HookConsumerWidget {
}
}
return SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.songs,
style: theme.textTheme.titleLarge!,
return InterScrollbar(
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (tracks.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.songs,
style: theme.textTheme.titleLarge!,
),
),
),
if (searchTrack.isLoadingPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(
searchTrack.errors.lastOrNull?.toString() ?? "",
)
else
...tracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
final isTrackPlaying =
playlist.activeTrack?.id == track.id;
if (!isTrackPlaying && context.mounted) {
final shouldPlay = (playlist.tracks.length) > 20
? await showPromptDialog(
context: context,
title: context.l10n.playing_track(
track.name!,
),
message: context.l10n.queue_clear_alert(
playlist.tracks.length,
),
)
: true;
if (searchTrack.isLoadingPage)
const CircularProgressIndicator()
else if (searchTrack.hasPageError)
Text(
searchTrack.errors.lastOrNull?.toString() ?? "",
)
else
...tracks.mapIndexed((i, track) {
return TrackTile(
index: i,
track: track,
onTap: () async {
final isTrackPlaying =
playlist.activeTrack?.id == track.id;
if (!isTrackPlaying && context.mounted) {
final shouldPlay = (playlist.tracks.length) > 20
? await showPromptDialog(
context: context,
title: context.l10n.playing_track(
track.name!,
),
message: context.l10n.queue_clear_alert(
playlist.tracks.length,
),
)
: true;
if (shouldPlay) {
await playlistNotifier.load(
[track],
autoPlay: true,
);
}
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isRefreshingPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isRefreshingPage
? const CircularProgressIndicator()
: Text(context.l10n.load_more),
),
),
if (playlists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.playlists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation: mediaQuery.lgAndUp
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNext();
},
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
padding: const EdgeInsets.symmetric(vertical: 4),
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)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchPlaylist.errors.lastOrNull?.toString() ?? "",
),
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.artists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: Waypoint(
controller: artistController,
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)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchArtist.errors.lastOrNull?.toString() ?? "",
),
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.albums,
style: theme.textTheme.titleLarge!,
),
),
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 (shouldPlay) {
await playlistNotifier.load(
[track],
autoPlay: true,
);
}),
],
}
}
},
);
}),
if (searchTrack.hasNextPage && tracks.isNotEmpty)
Center(
child: TextButton(
onPressed: searchTrack.isRefreshingPage
? null
: () => searchTrack.fetchNext(),
child: searchTrack.isRefreshingPage
? const CircularProgressIndicator()
: Text(context.l10n.load_more),
),
),
if (playlists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.playlists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
scrollbarOrientation: mediaQuery.lgAndUp
? ScrollbarOrientation.bottom
: ScrollbarOrientation.top,
controller: playlistController,
child: Waypoint(
onTouchEdge: () {
searchPlaylist.fetchNext();
},
controller: playlistController,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
controller: playlistController,
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
...playlists.mapIndexed(
(i, playlist) {
if (i == playlists.length - 1 &&
searchPlaylist.hasNextPage) {
return const ShimmerPlaybuttonCard(
count: 1);
}
return PlaylistCard(playlist);
},
),
],
),
),
),
),
),
),
if (searchAlbum.isLoadingPage)
const CircularProgressIndicator(),
if (searchAlbum.hasPageError)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchAlbum.errors.lastOrNull?.toString() ?? "",
if (searchPlaylist.isLoadingPage)
const CircularProgressIndicator(),
if (searchPlaylist.hasPageError)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchPlaylist.errors.lastOrNull?.toString() ?? "",
),
),
const SizedBox(height: 20),
if (artists.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.artists,
style: theme.textTheme.titleLarge!,
),
),
const SizedBox(height: 10),
ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
dragDevices: {
PointerDeviceKind.touch,
PointerDeviceKind.mouse,
},
),
child: Scrollbar(
controller: artistController,
child: Waypoint(
controller: artistController,
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)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchArtist.errors.lastOrNull?.toString() ?? "",
),
),
const SizedBox(height: 20),
if (albums.isNotEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
context.l10n.albums,
style: theme.textTheme.titleLarge!,
),
),
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)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: Text(
searchAlbum.errors.lastOrNull?.toString() ?? "",
),
),
],
),
),
),
),

View File

@ -5,6 +5,7 @@ import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/blacklist_provider.dart';
@ -56,25 +57,27 @@ class BlackListPage extends HookConsumerWidget {
),
),
),
ListView.builder(
shrinkWrap: true,
itemCount: filteredBlacklist.length,
itemBuilder: (context, index) {
final item = filteredBlacklist.elementAt(index);
return ListTile(
leading: Text("${index + 1}."),
title: Text("${item.name} (${item.type.name})"),
subtitle: Text(item.id),
trailing: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () {
ref
.read(BlackListNotifier.provider.notifier)
.remove(filteredBlacklist.elementAt(index));
},
),
);
},
InterScrollbar(
child: ListView.builder(
shrinkWrap: true,
itemCount: filteredBlacklist.length,
itemBuilder: (context, index) {
final item = filteredBlacklist.elementAt(index);
return ListTile(
leading: Text("${index + 1}."),
title: Text("${item.name} (${item.type.name})"),
subtitle: Text(item.id),
trailing: IconButton(
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
onPressed: () {
ref
.read(BlackListNotifier.provider.notifier)
.remove(filteredBlacklist.elementAt(index));
},
),
);
},
),
),
],
),

View File

@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/settings/section_card_with_heading.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
@ -91,47 +92,49 @@ class LogsPage extends HookWidget {
],
),
body: SafeArea(
child: ListView.builder(
itemCount: logs.value.length,
itemBuilder: (context, index) {
final log = logs.value[index];
return Stack(
children: [
SectionCardWithHeading(
heading: log.date.toString(),
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: SelectableText(log.body),
),
],
),
Positioned(
right: 10,
top: 0,
child: IconButton(
icon: const Icon(SpotubeIcons.clipboard),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: log.body),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.copied_to_clipboard(
log.date.toString(),
child: InterScrollbar(
child: ListView.builder(
itemCount: logs.value.length,
itemBuilder: (context, index) {
final log = logs.value[index];
return Stack(
children: [
SectionCardWithHeading(
heading: log.date.toString(),
children: [
Padding(
padding: const EdgeInsets.all(12.0),
child: SelectableText(log.body),
),
],
),
Positioned(
right: 10,
top: 0,
child: IconButton(
icon: const Icon(SpotubeIcons.clipboard),
onPressed: () async {
await Clipboard.setData(
ClipboardData(text: log.body),
);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context.l10n.copied_to_clipboard(
log.date.toString(),
),
),
),
),
);
}
},
);
}
},
),
),
),
],
);
},
],
);
},
),
),
),
);