mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
chore: include interscrollbar in left out pages
This commit is contained in:
parent
a3250882df
commit
8ca1aa38a2
@ -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]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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() ?? "",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -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));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user