feat: improve loading animations

This commit is contained in:
Kingkor Roy Tirtho 2023-12-04 22:20:47 +06:00
parent 2ceb6a8e53
commit b92583d0df
23 changed files with 583 additions and 700 deletions

161
lib/collections/fake.dart Normal file
View File

@ -0,0 +1,161 @@
import 'package:spotify/spotify.dart';
import 'package:spotube/extensions/track.dart';
abstract class FakeData {
static final Image image = Image()
..height = 1
..width = 1
..url = "url";
static final Followers followers = Followers()
..href = "text"
..total = 1;
static final Artist artist = Artist()
..id = "1"
..name = "Wow artist Good!"
..images = [image]
..popularity = 1
..type = "type"
..uri = "uri"
..externalUrls = externalUrls
..genres = ["genre"]
..href = "text"
..followers = followers;
static final externalIds = ExternalIds()
..isrc = "text"
..ean = "text"
..upc = "text";
static final externalUrls = ExternalUrls()..spotify = "text";
static final Album album = Album()
..id = "1"
..genres = ["genre"]
..label = "label"
..popularity = 1
..albumType = AlbumType.album
..artists = [artist]
..availableMarkets = [Market.BD]
..externalUrls = externalUrls
..href = "text"
..images = [image]
..name = "Another good album"
..releaseDate = "2021-01-01"
..releaseDatePrecision = DatePrecision.day
..tracks = [track]
..type = "type"
..uri = "uri"
..externalIds = externalIds
..copyrights = [
Copyright()
..type = CopyrightType.C
..text = "text",
];
static final ArtistSimple artistSimple = ArtistSimple()
..id = "1"
..name = "What an artist"
..type = "type"
..uri = "uri"
..externalUrls = externalUrls;
static final AlbumSimple albumSimple = AlbumSimple()
..id = "1"
..albumType = AlbumType.album
..artists = [artistSimple]
..availableMarkets = [Market.BD]
..externalUrls = externalUrls
..href = "text"
..images = [image]
..name = "A good album"
..releaseDate = "2021-01-01"
..releaseDatePrecision = DatePrecision.day
..type = "type"
..uri = "uri";
static final Track track = Track()
..id = "1"
..artists = [artist, artist, artist]
..album = albumSimple
..availableMarkets = [Market.BD]
..discNumber = 1
..durationMs = 50000
..explicit = false
..externalUrls = externalUrls
..href = "text"
..name = "A Track Name"
..popularity = 1
..previewUrl = "url"
..trackNumber = 1
..type = "type"
..uri = "uri"
..isPlayable = true
..explicit = false
..linkedFrom = trackLink;
static final TrackLink trackLink = TrackLink()
..id = "1"
..type = "type"
..uri = "uri"
..externalUrls = {"spotify": "text"}
..href = "text";
static final Paging<Track> paging = Paging()
..href = "text"
..itemsNative = [track.toJson()]
..limit = 1
..next = "text"
..offset = 1
..previous = "text"
..total = 1;
static final User user = User()
..id = "1"
..displayName = "Your Name"
..birthdate = "2021-01-01"
..country = Market.BD
..email = "test@email.com"
..followers = followers
..href = "text"
..images = [image]
..type = "type"
..uri = "uri";
static final TracksLink tracksLink = TracksLink()
..href = "text"
..total = 1;
static final Playlist playlist = Playlist()
..id = "1"
..collaborative = false
..description = "A very good playlist description"
..externalUrls = externalUrls
..followers = followers
..href = "text"
..images = [image]
..name = "A good playlist"
..owner = user
..public = true
..snapshotId = "text"
..tracks = paging
..tracksLink = tracksLink
..type = "type"
..uri = "uri";
static final PlaylistSimple playlistSimple = PlaylistSimple()
..id = "1"
..collaborative = false
..externalUrls = externalUrls
..href = "text"
..images = [image]
..name = "A good playlist"
..owner = user
..public = true
..snapshotId = "text"
..tracksLink = tracksLink
..type = "type"
..description = "A very good playlist description"
..uri = "uri";
}

View File

@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
@ -91,6 +92,7 @@ class ArtistCard extends HookConsumerWidget {
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Skeleton.ignore(
child: Text(
context.l10n.artist,
style: const TextStyle(
@ -101,6 +103,7 @@ class ArtistCard extends HookConsumerWidget {
),
),
),
),
],
),
const SizedBox(height: 10),

View File

@ -3,12 +3,13 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/album/album_card.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/waypoint.dart';
import 'package:spotube/extensions/context.dart';
@ -82,6 +83,8 @@ class UserAlbums extends HookConsumerWidget {
child: SingleChildScrollView(
padding: const EdgeInsets.all(8.0),
controller: controller,
child: Skeletonizer(
enabled: albums.isEmpty,
child: Wrap(
runSpacing: 20,
alignment: WrapAlignment.center,
@ -89,21 +92,20 @@ class UserAlbums extends HookConsumerWidget {
crossAxisAlignment: WrapCrossAlignment.center,
children: [
if (albums.isEmpty)
Container(
alignment: Alignment.topLeft,
padding: const EdgeInsets.all(16.0),
child: const ShimmerPlaybuttonCard(count: 4),
...List.generate(
10,
(index) => AlbumCard(FakeData.album),
),
for (final album in albums)
AlbumCard(
TypeConversionUtils.simpleAlbum_X_Album(album),
),
if (albumsQuery.hasNextPage)
if (albums.isNotEmpty && albumsQuery.hasNextPage)
Waypoint(
controller: controller,
isGrid: true,
onTouchEdge: albumsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
child: AlbumCard(FakeData.album),
)
],
),
@ -112,6 +114,7 @@ class UserAlbums extends HookConsumerWidget {
),
),
),
),
);
}
}

View File

@ -3,6 +3,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/anonymous_fallback.dart';
@ -87,11 +89,17 @@ class UserArtists extends HookConsumerWidget {
width: double.infinity,
child: SafeArea(
child: Center(
child: Skeletonizer(
enabled: artistQuery.isLoading,
child: Wrap(
spacing: 15,
runSpacing: 5,
children: filteredArtists
.mapIndexed((index, artist) => ArtistCard(artist))
children: artistQuery.isLoading
? List.generate(
10, (index) => ArtistCard(FakeData.artist))
: filteredArtists
.mapIndexed(
(index, artist) => ArtistCard(artist))
.toList(),
),
),
@ -100,6 +108,7 @@ class UserArtists extends HookConsumerWidget {
),
),
),
),
);
}
}

View File

@ -11,12 +11,13 @@ import 'package:metadata_god/metadata_god.dart';
import 'package:mime/mime.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/inter_scrollbar/inter_scrollbar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/sort_tracks_dropdown.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
@ -261,11 +262,18 @@ class UserLocalTracks extends HookConsumerWidget {
},
child: InterScrollbar(
controller: controller,
child: Skeletonizer(
enabled: trackSnapshot.isLoading,
child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredTracks.length,
itemCount:
trackSnapshot.isLoading ? 5 : filteredTracks.length,
itemBuilder: (context, index) {
if (trackSnapshot.isLoading) {
return TrackTile(track: FakeData.track, index: index);
}
final track = filteredTracks[index];
return TrackTile(
index: index,
@ -283,10 +291,19 @@ class UserLocalTracks extends HookConsumerWidget {
),
),
),
),
);
},
loading: () =>
const Expanded(child: ShimmerTrackTileGroup(noSliver: true)),
loading: () => Expanded(
child: Skeletonizer(
enabled: true,
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) =>
TrackTile(track: FakeData.track, index: index),
),
),
),
error: (error, stackTrace) =>
Text(error.toString() + stackTrace.toString()),
)

View File

@ -1,16 +1,16 @@
import 'package:flutter/material.dart' hide Image;
import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playlist/playlist_create_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/playlist/playlist_card.dart';
import 'package:spotube/components/shared/waypoint.dart';
@ -123,7 +123,7 @@ class UserPlaylists extends HookConsumerWidget {
),
SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder(
itemCount: playlists.length + 1,
itemCount: playlists.isEmpty ? 6 : playlists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
mainAxisExtent: constrains.smAndDown ? 225 : 250,
@ -131,7 +131,7 @@ class UserPlaylists extends HookConsumerWidget {
mainAxisSpacing: 8,
),
itemBuilder: (context, index) {
if (index == playlists.length) {
if (playlists.isNotEmpty && index == playlists.length) {
if (!playlistsQuery.hasNextPage) {
return const SizedBox.shrink();
}
@ -140,11 +140,17 @@ class UserPlaylists extends HookConsumerWidget {
controller: controller,
isGrid: true,
onTouchEdge: playlistsQuery.fetchNext,
child: const ShimmerPlaybuttonCard(count: 1),
child: Skeletonizer(
enabled: true,
child: PlaylistCard(FakeData.playlistSimple),
),
);
}
return PlaylistCard(playlists[index]);
return PlaylistCard(
playlists.elementAtOrNull(index) ??
FakeData.playlistSimple,
);
},
);
})

View File

@ -2,11 +2,12 @@ import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/album/album_card.dart';
import 'package:spotube/components/artist/artist_card.dart';
import 'package:spotube/components/playlist/playlist_card.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -61,25 +62,36 @@ class HorizontalPlaybuttonCardView<T> extends HookWidget {
PointerDeviceKind.mouse,
},
),
child: InfiniteList(
child: items.isEmpty
? ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: 5,
itemBuilder: (context, index) {
return AlbumCard(FakeData.albumSimple);
},
)
: InfiniteList(
scrollController: scrollController,
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(vertical: 8.0),
itemCount: items.length,
onFetchData: onFetchMore,
loadingBuilder: (context) => const ShimmerPlaybuttonCard(),
emptyBuilder: (context) =>
const ShimmerPlaybuttonCard(count: 5),
loadingBuilder: (context) => Skeletonizer(
enabled: true,
child: AlbumCard(FakeData.albumSimple),
),
isLoading: isLoadingNextPage,
hasReachedMax: !hasNextPage,
itemBuilder: (context, index) {
final item = items[index];
return switch (item.runtimeType) {
PlaylistSimple => PlaylistCard(item as PlaylistSimple),
PlaylistSimple =>
PlaylistCard(item as PlaylistSimple),
Album => AlbumCard(item as Album),
Artist => Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
padding:
const EdgeInsets.symmetric(horizontal: 12.0),
child: ArtistCard(item as Artist),
),
_ => const SizedBox.shrink(),

View File

@ -1,6 +1,7 @@
import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -146,7 +147,8 @@ class PlaybuttonCard extends HookWidget {
mainAxisSize: MainAxisSize.min,
children: [
if (!isPlaying)
IconButton(
Skeleton.keep(
child: IconButton(
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.background,
foregroundColor: theme.colorScheme.primary,
@ -155,6 +157,7 @@ class PlaybuttonCard extends HookWidget {
icon: const Icon(SpotubeIcons.queueAdd),
onPressed: isLoading ? null : onAddToQueuePressed,
),
),
const SizedBox(height: 5),
IconButton(
style: IconButton.styleFrom(
@ -162,7 +165,8 @@ class PlaybuttonCard extends HookWidget {
foregroundColor: theme.colorScheme.primary,
minimumSize: const Size.square(10),
),
icon: isLoading
icon: Skeleton.keep(
child: isLoading
? SizedBox.fromSize(
size: const Size.square(15),
child: const CircularProgressIndicator(
@ -171,6 +175,7 @@ class PlaybuttonCard extends HookWidget {
: isPlaying
? const Icon(SpotubeIcons.pause)
: const Icon(SpotubeIcons.play),
),
onPressed: isLoading ? null : onPlaybuttonPressed,
),
],

View File

@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:skeleton_text/skeleton_text.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/extensions/theme.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
class ShimmerArtistProfile extends HookWidget {
const ShimmerArtistProfile({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final shimmerTheme = ShimmerColorTheme(
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
);
final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white;
final shimmerBackgroundColor =
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
final avatarWidth = useBreakpointValue(
xs: MediaQuery.of(context).size.width * 0.80,
sm: MediaQuery.of(context).size.width * 0.80,
md: MediaQuery.of(context).size.width * 0.50,
lg: MediaQuery.of(context).size.width * 0.30,
xl: MediaQuery.of(context).size.width * 0.30,
xxl: MediaQuery.of(context).size.width * 0.30,
) ??
0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(20),
child: SkeletonAnimation(
shimmerColor: shimmerColor,
borderRadius: BorderRadius.circular(avatarWidth),
shimmerDuration: 1000,
child: Container(
width: avatarWidth,
height: avatarWidth,
decoration: BoxDecoration(
color: shimmerBackgroundColor,
borderRadius: BorderRadius.circular(avatarWidth),
),
),
),
),
const SizedBox(width: 10),
const Flexible(child: ShimmerTrackTileGroup(noSliver: true)),
],
);
}
}

View File

@ -1,53 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/components/shared/shimmers/shimmer_playbutton_card.dart';
import 'package:spotube/extensions/theme.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
class ShimmerCategories extends HookWidget {
const ShimmerCategories({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final shimmerTheme = ShimmerColorTheme(
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
);
final shimmerBackgroundColor =
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
final shimmerCount = useBreakpointValue(
xs: 2,
sm: 2,
md: 3,
lg: 3,
xl: 6,
xxl: 8,
);
return Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.only(left: 15),
height: 10,
width: 100,
decoration: BoxDecoration(
color: shimmerBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 10),
Align(
alignment: Alignment.topLeft,
child: ShimmerPlaybuttonCard(count: shimmerCount),
),
],
),
);
}
}

View File

@ -1,69 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:skeleton_text/skeleton_text.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/theme.dart';
const widths = [20, 56, 89, 60, 25, 69];
import 'package:skeletonizer/skeletonizer.dart';
class ShimmerLyrics extends HookWidget {
const ShimmerLyrics({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final shimmerTheme = ShimmerColorTheme(
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
);
final shimmerColor = shimmerTheme.shimmerColor ?? Colors.white;
final shimmerBackgroundColor =
shimmerTheme.shimmerBackgroundColor ?? Colors.grey;
final mediaQuery = MediaQuery.of(context);
return ListView.builder(
itemCount: 20,
shrinkWrap: true,
return Skeletonizer(
enabled: true,
child: ListView.builder(
itemCount: 30,
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, index) {
final widthsCp = [...widths];
if (mediaQuery.isMd) {
widthsCp.removeLast();
}
if (mediaQuery.smAndDown) {
widthsCp.removeLast();
widthsCp.removeLast();
}
widthsCp.shuffle();
return Container(
margin: const EdgeInsets.symmetric(vertical: 5),
child: Row(
final texts = [
"Lorem ipsum",
"consectetur.",
"Sed",
"Sed non risus",
]..shuffle();
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widthsCp.map(
(width) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: SkeletonAnimation(
shimmerColor: shimmerColor,
shimmerDuration: 1000,
child: Container(
height: 10,
width: width.toDouble(),
decoration: BoxDecoration(
color: shimmerBackgroundColor,
borderRadius: BorderRadius.circular(10),
),
margin: const EdgeInsets.only(top: 10),
),
),
children: [
for (final text in texts) ...[
Text(text),
if (text != texts.last) const Gap(10),
],
],
);
},
).toList(),
),
);
},
);
}
}

View File

@ -1,119 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:spotube/hooks/utils/use_breakpoint_value.dart';
class ShimmerPlaybuttonCardPainter extends CustomPainter {
final Color background;
final Color foreground;
ShimmerPlaybuttonCardPainter({
required this.background,
required this.foreground,
});
@override
void paint(Canvas canvas, Size size) {
const radius = Radius.circular(15);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
radius,
),
Paint()..color = background,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(8, 8, size.width - 16, size.height - 90),
radius,
),
Paint()..color = foreground,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(12, size.height - 67, size.width / 2, 10),
radius,
),
Paint()..color = foreground,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(12, size.height - 45, size.width - 24, 8),
radius,
),
Paint()..color = foreground,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(12, size.height - 30, size.width * .4, 8),
radius,
),
Paint()..color = foreground,
);
canvas.drawCircle(
Offset(size.width * .85, size.height * .50),
17,
Paint()..color = background,
);
canvas.drawCircle(
Offset(size.width * .85, size.height * .67),
17,
Paint()..color = background,
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return false;
}
}
class ShimmerPlaybuttonCard extends HookWidget {
final int count;
const ShimmerPlaybuttonCard({
Key? key,
this.count = 1,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final Size size = useBreakpointValue<Size>(
xs: const Size(130, 200),
sm: const Size(130, 200),
md: const Size(150, 220),
others: const Size(170, 240),
);
final isDark = theme.brightness == Brightness.dark;
final bgColor = theme.colorScheme.surfaceVariant.withOpacity(.2);
final fgColor = Color.lerp(
theme.colorScheme.surfaceVariant,
isDark ? Colors.black : Colors.white,
.4,
);
return Wrap(
spacing: 20,
runSpacing: 20,
children: [
for (var i = 0; i < count; i++) ...[
CustomPaint(
size: size,
painter: ShimmerPlaybuttonCardPainter(
background: bgColor,
foreground: fgColor!,
),
),
]
],
);
}
}

View File

@ -1,123 +0,0 @@
import 'package:flutter/material.dart';
import 'package:spotube/extensions/theme.dart';
class ShimmerTrackTilePainter extends CustomPainter {
final Color background;
final Color foreground;
ShimmerTrackTilePainter({
required this.background,
required this.foreground,
});
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = background
..style = PaintingStyle.fill;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.width, size.height),
const Radius.circular(5),
),
paint,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(0, 0, size.height, size.height),
const Radius.circular(5),
),
Paint()..color = foreground,
);
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(70, 10, 100, 10),
const Radius.circular(5),
),
Paint()..color = foreground,
);
// draw Icons.play
const icon = Icons.play_arrow_outlined;
TextPainter textPainter = TextPainter(textDirection: TextDirection.rtl);
textPainter.text = TextSpan(
text: String.fromCharCode(icon.codePoint),
style: TextStyle(
fontSize: 40.0,
fontFamily: icon.fontFamily,
color: background,
),
);
textPainter.layout();
textPainter.paint(canvas, const Offset(10, 10));
canvas.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTWH(70, 30, 170, 7),
const Radius.circular(5),
),
Paint()..color = foreground,
);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return false;
}
}
class ShimmerTrackTile extends StatelessWidget {
const ShimmerTrackTile({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final isDark = theme.brightness == Brightness.dark;
final shimmerTheme = ShimmerColorTheme(
shimmerBackgroundColor: isDark ? Colors.grey[700] : Colors.grey[200],
shimmerColor: isDark ? Colors.grey[800] : Colors.grey[300],
);
return Padding(
padding: const EdgeInsets.only(bottom: 8.0, left: 8, right: 8),
child: CustomPaint(
size: const Size(double.infinity, 60),
painter: ShimmerTrackTilePainter(
background: shimmerTheme.shimmerBackgroundColor ??
theme.scaffoldBackgroundColor,
foreground: shimmerTheme.shimmerColor ?? theme.cardColor,
),
),
);
}
}
class ShimmerTrackTileGroup extends StatelessWidget {
final bool noSliver;
final int count;
const ShimmerTrackTileGroup({
super.key,
this.noSliver = false,
this.count = 5,
});
@override
Widget build(BuildContext context) {
if (noSliver) {
return ListView.builder(
itemCount: 5,
itemBuilder: (context, index) => const ShimmerTrackTile(),
);
}
return SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) => const ShimmerTrackTile(),
childCount: count,
),
);
}
}

View File

@ -4,9 +4,10 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body_headers.dart';
import 'package:spotube/components/shared/tracks_view/sections/body/use_is_user_playlist.dart';
@ -84,7 +85,22 @@ class TrackViewBodySection extends HookConsumerWidget {
onFetchData: props.pagination.onFetchMore,
isLoading: props.pagination.isLoading,
hasReachedMax: !props.pagination.hasNextPage,
loadingBuilder: (context) => const ShimmerTrackTile(),
loadingBuilder: (context) => Skeletonizer(
enabled: true,
child: TrackTile(
track: FakeData.track,
index: 0,
),
),
emptyBuilder: (context) => Skeletonizer(
enabled: true,
child: Column(
children: List.generate(
10,
(index) => TrackTile(track: FakeData.track, index: index),
),
),
),
itemBuilder: (context, index) {
final track = tracks[index];
return TrackTile(

View File

@ -3,7 +3,6 @@ import 'package:flutter_desktop_tools/flutter_desktop_tools.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/shared/shimmers/shimmer_track_tile.dart';
import 'package:spotube/components/shared/tracks_view/sections/header/flexible_header.dart';
import 'package:spotube/components/shared/tracks_view/sections/body/track_view_body.dart';
import 'package:spotube/components/shared/tracks_view/track_view_props.dart';
@ -30,14 +29,12 @@ class TrackView extends HookConsumerWidget {
extendBodyBehindAppBar: true,
body: RefreshIndicator(
onRefresh: props.pagination.onRefresh,
child: CustomScrollView(
child: const CustomScrollView(
slivers: [
const TrackViewFlexHeader(),
TrackViewFlexHeader(),
SliverAnimatedSwitcher(
duration: const Duration(milliseconds: 500),
child: props.tracks.isEmpty
? const ShimmerTrackTileGroup()
: const TrackViewBodySection(),
duration: Duration(milliseconds: 500),
child: TrackViewBodySection(),
),
],
),

View File

@ -2,10 +2,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
import 'package:spotube/components/artist/artist_album_list.dart';
import 'package:spotube/components/shared/shimmers/shimmer_artist_profile.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/logger.dart';
import 'package:spotube/pages/artist/section/footer.dart';
@ -35,12 +35,12 @@ class ArtistPage extends HookConsumerWidget {
),
extendBodyBehindAppBar: true,
body: Builder(builder: (context) {
if (artistQuery.isLoading || !artistQuery.hasData) {
const ShimmerArtistProfile();
} else if (artistQuery.hasError) {
if (artistQuery.hasError) {
return Center(child: Text(artistQuery.error.toString()));
}
return CustomScrollView(
return Skeletonizer(
enabled: artistQuery.isLoading,
child: CustomScrollView(
controller: scrollController,
slivers: [
SliverToBoxAdapter(
@ -74,6 +74,7 @@ class ArtistPage extends HookConsumerWidget {
),
),
],
),
);
}),
),

View File

@ -4,7 +4,9 @@ import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/image/universal_image.dart';
import 'package:spotube/extensions/constrains.dart';
@ -25,7 +27,7 @@ class ArtistPageHeader extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final queryClient = useQueryClient();
final artistQuery = useQueries.artist.get(ref, artistId);
final artist = artistQuery.data;
final artist = artistQuery.data ?? FakeData.artist;
final scaffoldMessenger = ScaffoldMessenger.of(context);
final mediaQuery = MediaQuery.of(context);
@ -41,10 +43,6 @@ class ArtistPageHeader extends HookConsumerWidget {
xxl: textTheme.titleMedium,
);
if (artist == null) {
return const SizedBox.shrink();
}
final spotify = ref.read(spotifyProvider);
final auth = ref.watch(AuthenticationNotifier.provider);
final blacklist = ref.watch(BlackListNotifier.provider);
@ -96,6 +94,7 @@ class ArtistPageHeader extends HookConsumerWidget {
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50)),
child: Skeleton.keep(
child: Text(
artist.type!.toUpperCase(),
style: chipTextVariant.copyWith(
@ -103,6 +102,7 @@ class ArtistPageHeader extends HookConsumerWidget {
),
),
),
),
if (isBlackListed) ...[
const SizedBox(width: 5),
Container(
@ -138,7 +138,8 @@ class ArtistPageHeader extends HookConsumerWidget {
),
),
const Gap(20),
Row(
Skeleton.keep(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (auth != null)
@ -245,6 +246,7 @@ class ArtistPageHeader extends HookConsumerWidget {
},
)
],
),
)
],
),

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/fake.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/track_tile/track_tile.dart';
import 'package:spotube/extensions/context.dart';
@ -28,11 +30,7 @@ class ArtistPageTopTracks extends HookConsumerWidget {
topTracksQuery.data ?? <Track>[],
);
if (topTracksQuery.isLoading || !topTracksQuery.hasData) {
return const SliverToBoxAdapter(
child: Center(child: CircularProgressIndicator()),
);
} else if (topTracksQuery.hasError) {
if (topTracksQuery.hasError) {
return SliverToBoxAdapter(
child: Center(
child: Text(topTracksQuery.error.toString()),
@ -40,7 +38,8 @@ class ArtistPageTopTracks extends HookConsumerWidget {
);
}
final topTracks = topTracksQuery.data!;
final topTracks =
topTracksQuery.data ?? List.generate(10, (index) => FakeData.track);
void playPlaylist(List<Track> tracks, {Track? currentTrack}) async {
currentTrack ??= tracks.first;
@ -92,10 +91,12 @@ class ArtistPageTopTracks extends HookConsumerWidget {
),
const SizedBox(width: 5),
IconButton(
icon: Icon(
icon: Skeleton.keep(
child: Icon(
isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play,
color: Colors.white,
),
),
style: IconButton.styleFrom(
backgroundColor: theme.colorScheme.primary,
),

View File

@ -3,11 +3,12 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/genre/category_card.dart';
import 'package:spotube/components/shared/expandable_search/expandable_search.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/components/shared/waypoint.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
@ -77,7 +78,23 @@ class GenrePage extends HookConsumerWidget {
),
if (!categoriesQuery.hasPageData &&
!categoriesQuery.isLoadingNextPage)
const ShimmerCategories()
Expanded(
child: Skeletonizer(
enabled: true,
child: ListView.builder(
itemCount: 5,
itemBuilder: (context, index) {
return HorizontalPlaybuttonCardView<PlaylistSimple>(
title: const Text("Loading"),
items: const [],
hasNextPage: true,
isLoadingNextPage: false,
onFetchMore: () {},
);
},
),
),
)
else
Expanded(
child: InfiniteList(
@ -86,7 +103,16 @@ class GenrePage extends HookConsumerWidget {
onFetchData: categoriesQuery.fetchNext,
isLoading: categoriesQuery.isLoadingNextPage,
hasReachedMax: !categoriesQuery.hasNextPage,
loadingBuilder: (context) => const ShimmerCategories(),
loadingBuilder: (context) => Skeletonizer(
enabled: true,
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
title: const Text("Loading"),
items: const [],
hasNextPage: true,
isLoadingNextPage: false,
onFetchMore: () {},
),
),
itemBuilder: (context, index) {
return CategoryCard(categories[index]);
},

View File

@ -4,11 +4,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
import 'package:spotube/components/shared/shimmers/shimmer_categories.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/queries/queries.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
import 'package:skeletonizer/skeletonizer.dart';
class PersonalizedPage extends HookConsumerWidget {
const PersonalizedPage({Key? key}) : super(key: key);
@ -46,39 +46,35 @@ class PersonalizedPage extends HookConsumerWidget {
[newReleases.pages],
);
final hasNewReleases = newReleases.hasPageData &&
userArtistsQuery.hasData &&
!newReleases.isLoadingNextPage;
final isLoadingFeaturedPlaylists = !featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage;
return CustomScrollView(
controller: controller,
slivers: [
SliverList.list(
children: [
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: !featuredPlaylistsQuery.hasPageData &&
!featuredPlaylistsQuery.isLoadingNextPage
? const ShimmerCategories()
: HorizontalPlaybuttonCardView<PlaylistSimple>(
Skeletonizer(
enabled: isLoadingFeaturedPlaylists,
child: HorizontalPlaybuttonCardView<PlaylistSimple>(
items: playlists.toList(),
title: Text(context.l10n.featured),
isLoadingNextPage:
featuredPlaylistsQuery.isLoadingNextPage,
isLoadingNextPage: featuredPlaylistsQuery.isLoadingNextPage,
hasNextPage: featuredPlaylistsQuery.hasNextPage,
onFetchMore: featuredPlaylistsQuery.fetchNext,
),
),
if (auth != null)
AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: newReleases.hasPageData &&
userArtistsQuery.hasData &&
!newReleases.isLoadingNextPage
? HorizontalPlaybuttonCardView<Album>(
if (auth != null || hasNewReleases)
HorizontalPlaybuttonCardView<Album>(
items: albums,
title: Text(context.l10n.new_releases),
isLoadingNextPage: newReleases.isLoadingNextPage,
hasNextPage: newReleases.hasNextPage,
onFetchMore: newReleases.fetchNext,
)
: const ShimmerCategories(),
),
],
),

View File

@ -77,7 +77,7 @@ class SyncedLyrics extends HookConsumerWidget {
: textTheme.headlineMedium?.copyWith(fontSize: 25))
?.copyWith(color: palette.titleTextColor);
var bodyTextTheme = textTheme.bodyLarge?.copyWith(
final bodyTextTheme = textTheme.bodyLarge?.copyWith(
color: palette.bodyTextColor,
);
return Stack(
@ -184,7 +184,9 @@ class SyncedLyrics extends HookConsumerWidget {
),
if (playlist.activeTrack != null &&
(timedLyricsQuery.isLoading || timedLyricsQuery.isRefreshing))
const Expanded(child: ShimmerLyrics())
const Expanded(
child: ShimmerLyrics(),
)
else if (playlist.activeTrack != null &&
(timedLyricsQuery.hasError))
Text(

View File

@ -1832,6 +1832,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
skeletonizer:
dependency: "direct main"
description:
name: skeletonizer
sha256: ff4c36e826efd5288d7a84e7619a6e9be8185d3064cecf101a9133762f3b401b
url: "https://pub.dev"
source: hosted
version: "0.8.0"
sky_engine:
dependency: transitive
description: flutter

View File

@ -118,6 +118,7 @@ dependencies:
url: https://github.com/Tommypop2/dart_discord_rpc.git
html_unescape: ^2.0.0
wikipedia_api: ^0.1.0
skeletonizer: ^0.8.0
dev_dependencies:
build_runner: ^2.3.2