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/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.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/constrains.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/duration.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/user_preferences_provider.dart';
import 'package:spotube/provider/youtube_provider.dart'; import 'package:spotube/provider/youtube_provider.dart';
import 'package:spotube/services/youtube/youtube.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/service_utils.dart';
import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/utils/type_conversion_utils.dart';
@ -202,32 +202,36 @@ class SiblingTracksSheet extends HookConsumerWidget {
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) => transitionBuilder: (child, animation) =>
FadeTransition(opacity: animation, child: child), FadeTransition(opacity: animation, child: child),
child: switch (isSearching.value) { child: InterScrollbar(
false => ListView.builder( child: switch (isSearching.value) {
itemCount: siblings.length, false => ListView.builder(
itemBuilder: (context, index) => itemCount: siblings.length,
itemBuilder(siblings[index]), itemBuilder: (context, index) =>
), itemBuilder(siblings[index]),
true => FutureBuilder( ),
future: searchRequest, true => FutureBuilder(
builder: (context, snapshot) { future: searchRequest,
if (snapshot.hasError) { builder: (context, snapshot) {
return Center( if (snapshot.hasError) {
child: Text(snapshot.error.toString()), return Center(
); child: Text(snapshot.error.toString()),
} else if (!snapshot.hasData) { );
return const Center( } else if (!snapshot.hasData) {
child: CircularProgressIndicator()); return const Center(
} child: CircularProgressIndicator());
}
return ListView.builder( return InterScrollbar(
itemCount: snapshot.data!.length, child: ListView.builder(
itemBuilder: (context, index) => itemCount: snapshot.data!.length,
itemBuilder(snapshot.data![index]), 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:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.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/shimmers/shimmer_artist_profile.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/track_table/track_tile.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart';
@ -90,365 +91,372 @@ class ArtistPage extends HookConsumerWidget {
BlacklistedElement.artist(artistId, data.name!), BlacklistedElement.artist(artistId, data.name!),
); );
return SingleChildScrollView( return InterScrollbar(
controller: parentScrollController, controller: parentScrollController,
child: SafeArea( child: SingleChildScrollView(
child: Column( controller: parentScrollController,
crossAxisAlignment: CrossAxisAlignment.start, child: SafeArea(
children: [ child: Column(
Wrap( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: WrapCrossAlignment.center, children: [
runAlignment: WrapAlignment.center, Wrap(
children: [ crossAxisAlignment: WrapCrossAlignment.center,
const SizedBox(width: 50), runAlignment: WrapAlignment.center,
Padding( children: [
padding: const EdgeInsets.all(16), const SizedBox(width: 50),
child: CircleAvatar( Padding(
radius: avatarWidth, padding: const EdgeInsets.all(16),
backgroundImage: UniversalImage.imageProvider( child: CircleAvatar(
TypeConversionUtils.image_X_UrlString( radius: avatarWidth,
data.images, backgroundImage: UniversalImage.imageProvider(
placeholder: ImagePlaceholder.artist, TypeConversionUtils.image_X_UrlString(
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(
context.l10n.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: mediaQuery.smAndDown decoration: BoxDecoration(
? textTheme.headlineSmall color: Colors.red[400],
: textTheme.headlineMedium, borderRadius:
), BorderRadius.circular(50)),
Text( child: Text(
context.l10n.followers( context.l10n.blacklisted,
PrimitiveUtils.toReadableNumber( style: chipTextVariant.copyWith(
data.followers!.total!.toDouble(), 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( const SizedBox(height: 20),
fontWeight: mediaQuery.mdAndUp Row(
? FontWeight.bold mainAxisSize: MainAxisSize.min,
: null, children: [
), if (auth != null)
), HookBuilder(
const SizedBox(height: 20), builder: (context) {
Row( final isFollowingQuery = useQueries
mainAxisSize: MainAxisSize.min, .artist
children: [ .doIFollow(ref, artistId);
if (auth != null)
HookBuilder(
builder: (context) {
final isFollowingQuery = useQueries
.artist
.doIFollow(ref, artistId);
final followUnfollow = final followUnfollow =
useCallback(() async { useCallback(() async {
try { try {
isFollowingQuery.data! isFollowingQuery.data!
? await spotify.me.unfollow( ? await spotify.me.unfollow(
FollowingType.artist, FollowingType.artist,
[artistId], [artistId],
) )
: await spotify.me.follow( : await spotify.me.follow(
FollowingType.artist, FollowingType.artist,
[artistId], [artistId],
); );
await isFollowingQuery.refresh(); await isFollowingQuery.refresh();
queryClient queryClient
.refreshInfiniteQueryAllPages( .refreshInfiniteQueryAllPages(
"user-following-artists"); "user-following-artists");
} finally { } finally {
queryClient.refreshQuery( queryClient.refreshQuery(
"user-follows-artists-query/$artistId", "user-follows-artists-query/$artistId",
);
}
}, [isFollowingQuery]);
if (isFollowingQuery.isLoading ||
!isFollowingQuery.hasData) {
return const SizedBox(
height: 20,
width: 20,
child:
CircularProgressIndicator(),
); );
} }
}, [isFollowingQuery]);
if (isFollowingQuery.isLoading || if (isFollowingQuery.data!) {
!isFollowingQuery.hasData) { return OutlinedButton(
return const SizedBox( onPressed: followUnfollow,
height: 20, child:
width: 20, Text(context.l10n.following),
child: CircularProgressIndicator(), );
); }
}
if (isFollowingQuery.data!) { return FilledButton(
return OutlinedButton(
onPressed: followUnfollow, 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( if (!context.mounted) return;
onPressed: followUnfollow,
child: Text(context.l10n.follow), 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), const SizedBox(width: 5),
IconButton( IconButton(
tooltip:
context.l10n.add_artist_to_blacklist,
icon: Icon( icon: Icon(
SpotubeIcons.userRemove, isPlaylistPlaying
color: !isBlackListed ? SpotubeIcons.stop
? Colors.red[400] : SpotubeIcons.play,
: Colors.white, color: Colors.white,
), ),
style: IconButton.styleFrom( style: IconButton.styleFrom(
backgroundColor: isBlackListed backgroundColor:
? Colors.red[400] theme.colorScheme.primary,
: null,
), ),
onPressed: () async { onPressed: () =>
if (isBlackListed) { playPlaylist(topTracks.toList()),
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,
),
),
);
},
) )
], ],
) ),
...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: 50), const SizedBox(height: 20),
HookBuilder( Padding(
builder: (context) { padding: const EdgeInsets.all(8.0),
final topTracksQuery = useQueries.artist.topTracksOf( child: Text(
ref, context.l10n.fans_also_like,
artistId, style: theme.textTheme.headlineSmall,
); ),
),
final isPlaylistPlaying = playlist.containsTracks( const SizedBox(height: 10),
topTracksQuery.data ?? <Track>[], HookBuilder(
); builder: (context) {
final relatedArtists =
if (topTracksQuery.isLoading || useQueries.artist.relatedArtistsOf(
!topTracksQuery.hasData) { ref,
return const CircularProgressIndicator(); artistId,
} else if (topTracksQuery.hasError) {
return Center(
child: Text(topTracksQuery.error.toString()),
); );
}
final topTracks = topTracksQuery.data!; if (relatedArtists.isLoading ||
!relatedArtists.hasData) {
void playPlaylist(List<Track> tracks, return const CircularProgressIndicator();
{Track? currentTrack}) async { } else if (relatedArtists.hasError) {
currentTrack ??= tracks.first; return Center(
if (!isPlaylistPlaying) { child: Text(relatedArtists.error.toString()),
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(
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( 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/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.dart'; import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/shared/dialogs/prompt_dialog.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/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart'; import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/page_window_title_bar.dart';
@ -103,240 +104,243 @@ class SearchPage extends HookConsumerWidget {
} }
} }
return SingleChildScrollView( return InterScrollbar(
child: Padding( child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 8), child: Padding(
child: SafeArea( padding: const EdgeInsets.symmetric(vertical: 8),
child: Column( child: SafeArea(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
if (tracks.isNotEmpty) children: [
Padding( if (tracks.isNotEmpty)
padding: const EdgeInsets.symmetric(horizontal: 8), Padding(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 8),
context.l10n.songs, child: Text(
style: theme.textTheme.titleLarge!, context.l10n.songs,
style: theme.textTheme.titleLarge!,
),
), ),
), if (searchTrack.isLoadingPage)
if (searchTrack.isLoadingPage) const CircularProgressIndicator()
const CircularProgressIndicator() else if (searchTrack.hasPageError)
else if (searchTrack.hasPageError) Text(
Text( searchTrack.errors.lastOrNull?.toString() ?? "",
searchTrack.errors.lastOrNull?.toString() ?? "", )
) else
else ...tracks.mapIndexed((i, track) {
...tracks.mapIndexed((i, track) { return TrackTile(
return TrackTile( index: i,
index: i, track: track,
track: track, onTap: () async {
onTap: () async { final isTrackPlaying =
final isTrackPlaying = playlist.activeTrack?.id == track.id;
playlist.activeTrack?.id == track.id; if (!isTrackPlaying && context.mounted) {
if (!isTrackPlaying && context.mounted) { final shouldPlay = (playlist.tracks.length) > 20
final shouldPlay = (playlist.tracks.length) > 20 ? await showPromptDialog(
? await showPromptDialog( context: context,
context: context, title: context.l10n.playing_track(
title: context.l10n.playing_track( track.name!,
track.name!, ),
), message: context.l10n.queue_clear_alert(
message: context.l10n.queue_clear_alert( playlist.tracks.length,
playlist.tracks.length, ),
), )
) : true;
: true;
if (shouldPlay) { if (shouldPlay) {
await playlistNotifier.load( await playlistNotifier.load(
[track], [track],
autoPlay: true, 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 (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)
if (searchAlbum.isLoadingPage) const CircularProgressIndicator(),
const CircularProgressIndicator(), if (searchPlaylist.hasPageError)
if (searchAlbum.hasPageError) Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 8),
padding: const EdgeInsets.symmetric(horizontal: 8), child: Text(
child: Text( searchPlaylist.errors.lastOrNull?.toString() ?? "",
searchAlbum.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:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.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/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/blacklist_provider.dart'; import 'package:spotube/provider/blacklist_provider.dart';
@ -56,25 +57,27 @@ class BlackListPage extends HookConsumerWidget {
), ),
), ),
), ),
ListView.builder( InterScrollbar(
shrinkWrap: true, child: ListView.builder(
itemCount: filteredBlacklist.length, shrinkWrap: true,
itemBuilder: (context, index) { itemCount: filteredBlacklist.length,
final item = filteredBlacklist.elementAt(index); itemBuilder: (context, index) {
return ListTile( final item = filteredBlacklist.elementAt(index);
leading: Text("${index + 1}."), return ListTile(
title: Text("${item.name} (${item.type.name})"), leading: Text("${index + 1}."),
subtitle: Text(item.id), title: Text("${item.name} (${item.type.name})"),
trailing: IconButton( subtitle: Text(item.id),
icon: Icon(SpotubeIcons.trash, color: Colors.red[400]), trailing: IconButton(
onPressed: () { icon: Icon(SpotubeIcons.trash, color: Colors.red[400]),
ref onPressed: () {
.read(BlackListNotifier.provider.notifier) ref
.remove(filteredBlacklist.elementAt(index)); .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:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/settings/section_card_with_heading.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/components/shared/page_window_title_bar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
@ -91,47 +92,49 @@ class LogsPage extends HookWidget {
], ],
), ),
body: SafeArea( body: SafeArea(
child: ListView.builder( child: InterScrollbar(
itemCount: logs.value.length, child: ListView.builder(
itemBuilder: (context, index) { itemCount: logs.value.length,
final log = logs.value[index]; itemBuilder: (context, index) {
return Stack( final log = logs.value[index];
children: [ return Stack(
SectionCardWithHeading( children: [
heading: log.date.toString(), SectionCardWithHeading(
children: [ heading: log.date.toString(),
Padding( children: [
padding: const EdgeInsets.all(12.0), Padding(
child: SelectableText(log.body), padding: const EdgeInsets.all(12.0),
), child: SelectableText(log.body),
], ),
), ],
Positioned( ),
right: 10, Positioned(
top: 0, right: 10,
child: IconButton( top: 0,
icon: const Icon(SpotubeIcons.clipboard), child: IconButton(
onPressed: () async { icon: const Icon(SpotubeIcons.clipboard),
await Clipboard.setData( onPressed: () async {
ClipboardData(text: log.body), await Clipboard.setData(
); ClipboardData(text: log.body),
if (context.mounted) { );
ScaffoldMessenger.of(context).showSnackBar( if (context.mounted) {
SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text( SnackBar(
context.l10n.copied_to_clipboard( content: Text(
log.date.toString(), context.l10n.copied_to_clipboard(
log.date.toString(),
),
), ),
), ),
), );
); }
} },
}, ),
), ),
), ],
], );
); },
}, ),
), ),
), ),
); );