From b92583d0df7b8dee0d121cd2bb666b14c77d8c86 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Mon, 4 Dec 2023 22:20:47 +0600 Subject: [PATCH] feat: improve loading animations --- lib/collections/fake.dart | 161 ++++++++++++++ lib/components/artist/artist_card.dart | 15 +- lib/components/library/user_albums.dart | 53 ++--- lib/components/library/user_artists.dart | 21 +- lib/components/library/user_local_tracks.dart | 61 ++++-- lib/components/library/user_playlists.dart | 18 +- .../horizontal_playbutton_card_view.dart | 60 +++-- lib/components/shared/playbutton_card.dart | 37 ++-- .../shimmers/shimmer_artist_profile.dart | 57 ----- .../shared/shimmers/shimmer_categories.dart | 53 ----- .../shared/shimmers/shimmer_lyrics.dart | 81 +++---- .../shimmers/shimmer_playbutton_card.dart | 119 ---------- .../shared/shimmers/shimmer_track_tile.dart | 123 ----------- .../sections/body/track_view_body.dart | 20 +- .../shared/tracks_view/track_view.dart | 11 +- lib/pages/artist/artist.dart | 67 +++--- lib/pages/artist/section/header.dart | 206 +++++++++--------- lib/pages/artist/section/top_tracks.dart | 19 +- lib/pages/home/genres.dart | 32 ++- lib/pages/home/personalized.dart | 52 ++--- lib/pages/lyrics/synced_lyrics.dart | 6 +- pubspec.lock | 8 + pubspec.yaml | 1 + 23 files changed, 582 insertions(+), 699 deletions(-) create mode 100644 lib/collections/fake.dart delete mode 100644 lib/components/shared/shimmers/shimmer_artist_profile.dart delete mode 100644 lib/components/shared/shimmers/shimmer_categories.dart delete mode 100644 lib/components/shared/shimmers/shimmer_playbutton_card.dart delete mode 100644 lib/components/shared/shimmers/shimmer_track_tile.dart diff --git a/lib/collections/fake.dart b/lib/collections/fake.dart new file mode 100644 index 000000000..a02e85874 --- /dev/null +++ b/lib/collections/fake.dart @@ -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 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"; +} diff --git a/lib/components/artist/artist_card.dart b/lib/components/artist/artist_card.dart index 434b90ad3..3526e88f9 100644 --- a/lib/components/artist/artist_card.dart +++ b/lib/components/artist/artist_card.dart @@ -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,12 +92,14 @@ class ArtistCard extends HookConsumerWidget { decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(50)), - child: Text( - context.l10n.artist, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, + child: Skeleton.ignore( + child: Text( + context.l10n.artist, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), ), ), ), diff --git a/lib/components/library/user_albums.dart b/lib/components/library/user_albums.dart index ccde43f94..492438703 100644 --- a/lib/components/library/user_albums.dart +++ b/lib/components/library/user_albums.dart @@ -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,30 +83,32 @@ class UserAlbums extends HookConsumerWidget { child: SingleChildScrollView( padding: const EdgeInsets.all(8.0), controller: controller, - child: Wrap( - runSpacing: 20, - alignment: WrapAlignment.center, - runAlignment: WrapAlignment.center, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - if (albums.isEmpty) - Container( - alignment: Alignment.topLeft, - padding: const EdgeInsets.all(16.0), - child: const ShimmerPlaybuttonCard(count: 4), - ), - for (final album in albums) - AlbumCard( - TypeConversionUtils.simpleAlbum_X_Album(album), - ), - if (albumsQuery.hasNextPage) - Waypoint( - controller: controller, - isGrid: true, - onTouchEdge: albumsQuery.fetchNext, - child: const ShimmerPlaybuttonCard(count: 1), - ) - ], + child: Skeletonizer( + enabled: albums.isEmpty, + child: Wrap( + runSpacing: 20, + alignment: WrapAlignment.center, + runAlignment: WrapAlignment.center, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + if (albums.isEmpty) + ...List.generate( + 10, + (index) => AlbumCard(FakeData.album), + ), + for (final album in albums) + AlbumCard( + TypeConversionUtils.simpleAlbum_X_Album(album), + ), + if (albums.isNotEmpty && albumsQuery.hasNextPage) + Waypoint( + controller: controller, + isGrid: true, + onTouchEdge: albumsQuery.fetchNext, + child: AlbumCard(FakeData.album), + ) + ], + ), ), ), ), diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 881451b0d..7269d7eb2 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -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,12 +89,19 @@ class UserArtists extends HookConsumerWidget { width: double.infinity, child: SafeArea( child: Center( - child: Wrap( - spacing: 15, - runSpacing: 5, - children: filteredArtists - .mapIndexed((index, artist) => ArtistCard(artist)) - .toList(), + child: Skeletonizer( + enabled: artistQuery.isLoading, + child: Wrap( + spacing: 15, + runSpacing: 5, + children: artistQuery.isLoading + ? List.generate( + 10, (index) => ArtistCard(FakeData.artist)) + : filteredArtists + .mapIndexed( + (index, artist) => ArtistCard(artist)) + .toList(), + ), ), ), ), diff --git a/lib/components/library/user_local_tracks.dart b/lib/components/library/user_local_tracks.dart index cc8b10cf3..fcaada9e7 100644 --- a/lib/components/library/user_local_tracks.dart +++ b/lib/components/library/user_local_tracks.dart @@ -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,32 +262,48 @@ class UserLocalTracks extends HookConsumerWidget { }, child: InterScrollbar( controller: controller, - child: ListView.builder( - controller: controller, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: filteredTracks.length, - itemBuilder: (context, index) { - final track = filteredTracks[index]; - return TrackTile( - index: index, - track: track, - userPlaylist: false, - onTap: () async { - await playLocalTracks( - ref, - sortedTracks, - currentTrack: track, - ); - }, - ); - }, + child: Skeletonizer( + enabled: trackSnapshot.isLoading, + child: ListView.builder( + controller: controller, + physics: const AlwaysScrollableScrollPhysics(), + 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, + track: track, + userPlaylist: false, + onTap: () async { + await playLocalTracks( + ref, + sortedTracks, + currentTrack: track, + ); + }, + ); + }, + ), ), ), ), ); }, - 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()), ) diff --git a/lib/components/library/user_playlists.dart b/lib/components/library/user_playlists.dart index f7736ca7e..a65c6d0ef 100644 --- a/lib/components/library/user_playlists.dart +++ b/lib/components/library/user_playlists.dart @@ -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, + ); }, ); }) diff --git a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart index dca772330..d00e5c4b8 100644 --- a/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart +++ b/lib/components/shared/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart @@ -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,30 +62,41 @@ class HorizontalPlaybuttonCardView extends HookWidget { PointerDeviceKind.mouse, }, ), - child: 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), - isLoading: isLoadingNextPage, - hasReachedMax: !hasNextPage, - itemBuilder: (context, index) { - final item = items[index]; + 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) => 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), - Album => AlbumCard(item as Album), - Artist => Padding( - padding: const EdgeInsets.symmetric(horizontal: 12.0), - child: ArtistCard(item as Artist), - ), - _ => const SizedBox.shrink(), - }; - }), + return switch (item.runtimeType) { + PlaylistSimple => + PlaylistCard(item as PlaylistSimple), + Album => AlbumCard(item as Album), + Artist => Padding( + padding: + const EdgeInsets.symmetric(horizontal: 12.0), + child: ArtistCard(item as Artist), + ), + _ => const SizedBox.shrink(), + }; + }), ), ), ], diff --git a/lib/components/shared/playbutton_card.dart b/lib/components/shared/playbutton_card.dart index d9c486401..60db648b6 100644 --- a/lib/components/shared/playbutton_card.dart +++ b/lib/components/shared/playbutton_card.dart @@ -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,14 +147,16 @@ class PlaybuttonCard extends HookWidget { mainAxisSize: MainAxisSize.min, children: [ if (!isPlaying) - IconButton( - style: IconButton.styleFrom( - backgroundColor: theme.colorScheme.background, - foregroundColor: theme.colorScheme.primary, - minimumSize: const Size.square(10), + Skeleton.keep( + child: IconButton( + style: IconButton.styleFrom( + backgroundColor: theme.colorScheme.background, + foregroundColor: theme.colorScheme.primary, + minimumSize: const Size.square(10), + ), + icon: const Icon(SpotubeIcons.queueAdd), + onPressed: isLoading ? null : onAddToQueuePressed, ), - icon: const Icon(SpotubeIcons.queueAdd), - onPressed: isLoading ? null : onAddToQueuePressed, ), const SizedBox(height: 5), IconButton( @@ -162,15 +165,17 @@ class PlaybuttonCard extends HookWidget { foregroundColor: theme.colorScheme.primary, minimumSize: const Size.square(10), ), - icon: isLoading - ? SizedBox.fromSize( - size: const Size.square(15), - child: const CircularProgressIndicator( - strokeWidth: 2), - ) - : isPlaying - ? const Icon(SpotubeIcons.pause) - : const Icon(SpotubeIcons.play), + icon: Skeleton.keep( + child: isLoading + ? SizedBox.fromSize( + size: const Size.square(15), + child: const CircularProgressIndicator( + strokeWidth: 2), + ) + : isPlaying + ? const Icon(SpotubeIcons.pause) + : const Icon(SpotubeIcons.play), + ), onPressed: isLoading ? null : onPlaybuttonPressed, ), ], diff --git a/lib/components/shared/shimmers/shimmer_artist_profile.dart b/lib/components/shared/shimmers/shimmer_artist_profile.dart deleted file mode 100644 index 75e50cd01..000000000 --- a/lib/components/shared/shimmers/shimmer_artist_profile.dart +++ /dev/null @@ -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)), - ], - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_categories.dart b/lib/components/shared/shimmers/shimmer_categories.dart deleted file mode 100644 index 9bc773da3..000000000 --- a/lib/components/shared/shimmers/shimmer_categories.dart +++ /dev/null @@ -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), - ), - ], - ), - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_lyrics.dart b/lib/components/shared/shimmers/shimmer_lyrics.dart index b0fba3401..b225c008c 100644 --- a/lib/components/shared/shimmers/shimmer_lyrics.dart +++ b/lib/components/shared/shimmers/shimmer_lyrics.dart @@ -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, - physics: const NeverScrollableScrollPhysics(), - 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( + return Skeletonizer( + enabled: true, + child: ListView.builder( + itemCount: 30, + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemBuilder: (context, index) { + 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), - ), - ), - ); - }, - ).toList(), - ), - ); - }, + children: [ + for (final text in texts) ...[ + Text(text), + if (text != texts.last) const Gap(10), + ], + ], + ); + }, + ), ); } } diff --git a/lib/components/shared/shimmers/shimmer_playbutton_card.dart b/lib/components/shared/shimmers/shimmer_playbutton_card.dart deleted file mode 100644 index 2259c9b0c..000000000 --- a/lib/components/shared/shimmers/shimmer_playbutton_card.dart +++ /dev/null @@ -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( - 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!, - ), - ), - ] - ], - ); - } -} diff --git a/lib/components/shared/shimmers/shimmer_track_tile.dart b/lib/components/shared/shimmers/shimmer_track_tile.dart deleted file mode 100644 index dcb634edc..000000000 --- a/lib/components/shared/shimmers/shimmer_track_tile.dart +++ /dev/null @@ -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, - ), - ); - } -} diff --git a/lib/components/shared/tracks_view/sections/body/track_view_body.dart b/lib/components/shared/tracks_view/sections/body/track_view_body.dart index d77a3e6ff..b7149cc2d 100644 --- a/lib/components/shared/tracks_view/sections/body/track_view_body.dart +++ b/lib/components/shared/tracks_view/sections/body/track_view_body.dart @@ -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( diff --git a/lib/components/shared/tracks_view/track_view.dart b/lib/components/shared/tracks_view/track_view.dart index 217aaed41..a1a2d48b8 100644 --- a/lib/components/shared/tracks_view/track_view.dart +++ b/lib/components/shared/tracks_view/track_view.dart @@ -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(), ), ], ), diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 693e825b4..92470397e 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -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,45 +35,46 @@ 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( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: SafeArea( - bottom: false, - child: ArtistPageHeader(artistId: artistId), - ), - ), - const SliverGap(50), - ArtistPageTopTracks(artistId: artistId), - const SliverGap(50), - SliverToBoxAdapter(child: ArtistAlbumList(artistId)), - const SliverGap(20), - SliverPadding( - padding: const EdgeInsets.all(8.0), - sliver: SliverToBoxAdapter( - child: Text( - context.l10n.fans_also_like, - style: theme.textTheme.headlineSmall, + return Skeletonizer( + enabled: artistQuery.isLoading, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: SafeArea( + bottom: false, + child: ArtistPageHeader(artistId: artistId), ), ), - ), - SliverSafeArea( - sliver: ArtistPageRelatedArtists(artistId: artistId), - ), - if (artistQuery.data != null) - SliverSafeArea( - top: false, + const SliverGap(50), + ArtistPageTopTracks(artistId: artistId), + const SliverGap(50), + SliverToBoxAdapter(child: ArtistAlbumList(artistId)), + const SliverGap(20), + SliverPadding( + padding: const EdgeInsets.all(8.0), sliver: SliverToBoxAdapter( - child: ArtistPageFooter(artist: artistQuery.data!), + child: Text( + context.l10n.fans_also_like, + style: theme.textTheme.headlineSmall, + ), ), ), - ], + SliverSafeArea( + sliver: ArtistPageRelatedArtists(artistId: artistId), + ), + if (artistQuery.data != null) + SliverSafeArea( + top: false, + sliver: SliverToBoxAdapter( + child: ArtistPageFooter(artist: artistQuery.data!), + ), + ), + ], + ), ); }), ), diff --git a/lib/pages/artist/section/header.dart b/lib/pages/artist/section/header.dart index 9fc9d78ed..7cee7a015 100644 --- a/lib/pages/artist/section/header.dart +++ b/lib/pages/artist/section/header.dart @@ -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,10 +94,12 @@ class ArtistPageHeader extends HookConsumerWidget { decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(50)), - child: Text( - artist.type!.toUpperCase(), - style: chipTextVariant.copyWith( - color: Colors.white, + child: Skeleton.keep( + child: Text( + artist.type!.toUpperCase(), + style: chipTextVariant.copyWith( + color: Colors.white, + ), ), ), ), @@ -138,113 +138,115 @@ class ArtistPageHeader extends HookConsumerWidget { ), ), const Gap(20), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (auth != null) - HookBuilder( - builder: (context) { - final isFollowingQuery = - useQueries.artist.doIFollow(ref, artistId); + Skeleton.keep( + child: 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", + ); + } + }, [isFollowingQuery]); - queryClient.refreshInfiniteQueryAllPages( - "user-following-artists"); - } finally { - queryClient.refreshQuery( - "user-follows-artists-query/$artistId", + 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( + artist.id!, artist.name!), + ); + } else { + ref.read(BlackListNotifier.provider.notifier).add( + BlacklistedElement.artist( + artist.id!, artist.name!), + ); } - - return FilledButton( - onPressed: followUnfollow, - 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( - artist.id!, artist.name!), - ); - } else { - ref.read(BlackListNotifier.provider.notifier).add( - BlacklistedElement.artist( - artist.id!, artist.name!), - ); - } - }, - ), - IconButton( - icon: const Icon(SpotubeIcons.share), - onPressed: () async { - if (artist.externalUrls?.spotify != null) { - await Clipboard.setData( - ClipboardData( - text: artist.externalUrls!.spotify!, - ), - ); - } + IconButton( + icon: const Icon(SpotubeIcons.share), + onPressed: () async { + if (artist.externalUrls?.spotify != null) { + await Clipboard.setData( + ClipboardData( + text: artist.externalUrls!.spotify!, + ), + ); + } - if (!context.mounted) return; + if (!context.mounted) return; - scaffoldMessenger.showSnackBar( - SnackBar( - width: 300, - behavior: SnackBarBehavior.floating, - content: Text( - context.l10n.artist_url_copied, - textAlign: TextAlign.center, + scaffoldMessenger.showSnackBar( + SnackBar( + width: 300, + behavior: SnackBarBehavior.floating, + content: Text( + context.l10n.artist_url_copied, + textAlign: TextAlign.center, + ), ), - ), - ); - }, - ) - ], + ); + }, + ) + ], + ), ) ], ), diff --git a/lib/pages/artist/section/top_tracks.dart b/lib/pages/artist/section/top_tracks.dart index 9e3e40543..771757b9c 100644 --- a/lib/pages/artist/section/top_tracks.dart +++ b/lib/pages/artist/section/top_tracks.dart @@ -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 ?? [], ); - 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 tracks, {Track? currentTrack}) async { currentTrack ??= tracks.first; @@ -92,9 +91,11 @@ class ArtistPageTopTracks extends HookConsumerWidget { ), const SizedBox(width: 5), IconButton( - icon: Icon( - isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, - color: Colors.white, + icon: Skeleton.keep( + child: Icon( + isPlaylistPlaying ? SpotubeIcons.stop : SpotubeIcons.play, + color: Colors.white, + ), ), style: IconButton.styleFrom( backgroundColor: theme.colorScheme.primary, diff --git a/lib/pages/home/genres.dart b/lib/pages/home/genres.dart index 54fb6786e..88eaef700 100644 --- a/lib/pages/home/genres.dart +++ b/lib/pages/home/genres.dart @@ -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( + 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( + title: const Text("Loading"), + items: const [], + hasNextPage: true, + isLoadingNextPage: false, + onFetchMore: () {}, + ), + ), itemBuilder: (context, index) { return CategoryCard(categories[index]); }, diff --git a/lib/pages/home/personalized.dart b/lib/pages/home/personalized.dart index 7fbd27aee..22224c396 100644 --- a/lib/pages/home/personalized.dart +++ b/lib/pages/home/personalized.dart @@ -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( - items: playlists.toList(), - title: Text(context.l10n.featured), - isLoadingNextPage: - featuredPlaylistsQuery.isLoadingNextPage, - hasNextPage: featuredPlaylistsQuery.hasNextPage, - onFetchMore: featuredPlaylistsQuery.fetchNext, - ), + Skeletonizer( + enabled: isLoadingFeaturedPlaylists, + child: HorizontalPlaybuttonCardView( + items: playlists.toList(), + title: Text(context.l10n.featured), + 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( - items: albums, - title: Text(context.l10n.new_releases), - isLoadingNextPage: newReleases.isLoadingNextPage, - hasNextPage: newReleases.hasNextPage, - onFetchMore: newReleases.fetchNext, - ) - : const ShimmerCategories(), + if (auth != null || hasNewReleases) + HorizontalPlaybuttonCardView( + items: albums, + title: Text(context.l10n.new_releases), + isLoadingNextPage: newReleases.isLoadingNextPage, + hasNextPage: newReleases.hasNextPage, + onFetchMore: newReleases.fetchNext, ), ], ), diff --git a/lib/pages/lyrics/synced_lyrics.dart b/lib/pages/lyrics/synced_lyrics.dart index 36a9f3163..9af71d94c 100644 --- a/lib/pages/lyrics/synced_lyrics.dart +++ b/lib/pages/lyrics/synced_lyrics.dart @@ -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( diff --git a/pubspec.lock b/pubspec.lock index 06ca82028..8921f8a7d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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 diff --git a/pubspec.yaml b/pubspec.yaml index ba758cbf8..77a26911c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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