From b9d5c70301dd33ec26332e5e9a456ce5bfe73da0 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 18:19:47 +0600 Subject: [PATCH] feat: search loading animation --- lib/collections/spotube_icons.dart | 1 + .../track_collection_heading.dart | 22 +- lib/l10n/app_en.arb | 6 +- lib/pages/search/search.dart | 619 ++++++++++-------- untranslated_messages.json | 44 +- 5 files changed, 395 insertions(+), 297 deletions(-) diff --git a/lib/collections/spotube_icons.dart b/lib/collections/spotube_icons.dart index 7b5221b51..4781050dd 100644 --- a/lib/collections/spotube_icons.dart +++ b/lib/collections/spotube_icons.dart @@ -96,4 +96,5 @@ abstract class SpotubeIcons { static const window = Icons.window_rounded; static const user = FeatherIcons.user; static const edit = FeatherIcons.edit; + static const web = FeatherIcons.globe; } diff --git a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart index c82b8177a..a8a60109d 100644 --- a/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart +++ b/lib/components/shared/track_table/track_collection_view/track_collection_heading.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:fl_query/fl_query.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -103,11 +104,19 @@ class TrackCollectionHeading extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.max, children: [ - Text( - title, - style: theme.textTheme.titleLarge!.copyWith( - color: Colors.white, - fontWeight: FontWeight.w600, + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: constrains.mdAndDown ? 400 : 300, + ), + child: AutoSizeText( + title, + style: theme.textTheme.titleLarge!.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + maxLines: 2, + minFontSize: 16, + overflow: TextOverflow.ellipsis, ), ), if (album != null) @@ -125,11 +134,12 @@ class TrackCollectionHeading extends HookConsumerWidget { constraints: BoxConstraints( maxWidth: constrains.mdAndDown ? 400 : 300, ), - child: Text( + child: AutoSizeText( cleanDescription, style: const TextStyle(color: Colors.white), maxLines: 2, overflow: TextOverflow.fade, + minFontSize: 14, ), ), const SizedBox(height: 10), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index bdfb7983b..fa81450b1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -27,7 +27,7 @@ "update_playlist": "Update playlist", "create": "Create", "cancel": "Cancel", - "update": "update", + "update": "Update", "playlist_name": "Playlist Name", "name_of_playlist": "Name of the playlist", "description": "Description", @@ -261,5 +261,7 @@ "piped_down_error_instructions": "The Piped instance {pipedInstance} is currently down\n\nEither change the instance or change the 'API type' to official YouTube API\n\nMake sure to restart the app after change", "you_are_offline": "You are currently offline", "connection_restored": "Your internet connection was restored", - "use_system_title_bar": "Use system title bar" + "use_system_title_bar": "Use system title bar", + "crunching_results": "Crunching results...", + "search_to_get_results": "Search to get results" } \ No newline at end of file diff --git a/lib/pages/search/search.dart b/lib/pages/search/search.dart index 9d5e7eed6..7ceecd58f 100644 --- a/lib/pages/search/search.dart +++ b/lib/pages/search/search.dart @@ -55,13 +55,295 @@ class SearchPage extends HookConsumerWidget { Future onSearch() async { await Future.wait([ - searchTrack.refreshAll(), - searchAlbum.refreshAll(), - searchPlaylist.refreshAll(), - searchArtist.refreshAll(), - ]); + searchTrack.reset(), + searchAlbum.reset(), + searchPlaylist.reset(), + searchArtist.reset(), + ]).then((_) { + return Future.wait([ + searchTrack.refreshAll(), + searchAlbum.refreshAll(), + searchPlaylist.refreshAll(), + searchArtist.refreshAll(), + ]); + }); } + final queries = [searchTrack, searchAlbum, searchPlaylist, searchArtist]; + final isFetching = queries.every( + (s) => s.isLoadingPage || s.isRefreshingPage || !s.hasPageData, + ) && + searchTerm.isNotEmpty; + + final resultWidget = HookBuilder( + builder: (context) { + final playlist = ref.watch(ProxyPlaylistNotifier.provider); + final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); + List albums = []; + List artists = []; + List tracks = []; + List playlists = []; + final pages = [ + ...searchTrack.pages, + ...searchAlbum.pages, + ...searchPlaylist.pages, + ...searchArtist.pages, + ].expand((page) => page).toList(); + for (MapEntry page in pages.asMap().entries) { + for (var item in page.value.items ?? []) { + if (item is AlbumSimple) { + albums.add(item); + } else if (item is PlaylistSimple) { + playlists.add(item); + } else if (item is Artist) { + artists.add(item); + } else if (item is Track) { + tracks.add(item); + } + } + } + + 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!, + ), + ), + 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 (searchAlbum.isLoadingPage) + const CircularProgressIndicator(), + if (searchAlbum.hasPageError) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text( + searchAlbum.errors.lastOrNull?.toString() ?? "", + ), + ), + ], + ), + ), + ), + ); + }, + ); + return SafeArea( bottom: false, child: Scaffold( @@ -77,7 +359,7 @@ class SearchPage extends HookConsumerWidget { ), color: theme.scaffoldBackgroundColor, child: TextField( - autofocus: true, + autofocus: queries.none((s) => s.hasPageData), decoration: InputDecoration( prefixIcon: const Icon(SpotubeIcons.search), hintText: "${context.l10n.search}...", @@ -93,283 +375,64 @@ class SearchPage extends HookConsumerWidget { }, ), ), - HookBuilder( - builder: (context) { - final playlist = - ref.watch(ProxyPlaylistNotifier.provider); - final playlistNotifier = - ref.watch(ProxyPlaylistNotifier.notifier); - List albums = []; - List artists = []; - List tracks = []; - List playlists = []; - final pages = [ - ...searchTrack.pages, - ...searchAlbum.pages, - ...searchPlaylist.pages, - ...searchArtist.pages, - ].expand((page) => page).toList(); - for (MapEntry page in pages.asMap().entries) { - for (var item in page.value.items ?? []) { - if (item is AlbumSimple) { - albums.add(item); - } else if (item is PlaylistSimple) { - playlists.add(item); - } else if (item is Artist) { - artists.add(item); - } else if (item is Track) { - tracks.add(item); - } - } - } - return Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 20, - ), - child: SafeArea( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (tracks.isNotEmpty) - 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 (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) - 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, - child: Row( - children: [ - ...playlists.mapIndexed( - (i, playlist) { - if (i == - playlists.length - - 1 && - searchPlaylist - .hasNextPage) { - return const ShimmerPlaybuttonCard( - count: 1); - } - return PlaylistCard(playlist); - }, - ), - ], - ), - ), - ), - ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: searchTerm.isEmpty + ? Column( + children: [ + SizedBox( + height: mediaQuery.size.height * 0.2, + ), + Icon( + SpotubeIcons.web, + size: 120, + color: theme.colorScheme.onBackground + .withOpacity(0.7), + ), + const SizedBox(height: 20), + Text( + context.l10n.search_to_get_results, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + color: theme.colorScheme.onBackground + .withOpacity(0.5), ), - if (searchPlaylist.isLoadingPage) - const CircularProgressIndicator(), - if (searchPlaylist.hasPageError) - Text( - searchPlaylist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (artists.isNotEmpty) - 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), - ); - }, - ), - ], - ), - ), - ), - ), + ), + ], + ) + : isFetching + ? Container( + constraints: BoxConstraints( + maxWidth: mediaQuery.lgAndUp + ? mediaQuery.size.width * 0.5 + : mediaQuery.size.width, ), - if (searchArtist.isLoadingPage) - const CircularProgressIndicator(), - if (searchArtist.hasPageError) - Text( - searchArtist.errors.lastOrNull - ?.toString() ?? - "", - ), - const SizedBox(height: 20), - if (albums.isNotEmpty) - Text( - context.l10n.albums, - style: theme.textTheme.titleMedium!, - ), - 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, - ), - ); - }), - ], - ), + padding: const EdgeInsets.symmetric( + horizontal: 20, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + Text( + context.l10n.crunching_results, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w900, + color: theme.colorScheme.onBackground + .withOpacity(0.7), ), ), - ), + const SizedBox(height: 20), + const LinearProgressIndicator(), + ], ), - if (searchAlbum.isLoadingPage) - const CircularProgressIndicator(), - if (searchAlbum.hasPageError) - Text( - searchAlbum.errors.lastOrNull - ?.toString() ?? - "", - ), - ], - ), - ), - ), - ), - ); - }, - ) + ) + : resultWidget, + ), + ), ], ), ), diff --git a/untranslated_messages.json b/untranslated_messages.json index 74a878963..ec30b430d 100644 --- a/untranslated_messages.json +++ b/untranslated_messages.json @@ -1,56 +1,78 @@ { "bn": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ca": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "de": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "es": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "fr": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "hi": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ja": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "pl": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "pt": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "ru": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ], "zh": [ "update_playlist", - "update" + "update", + "crunching_results", + "search_to_get_results" ] }