chore: add back pull down to refresh

This commit is contained in:
Kingkor Roy Tirtho 2025-03-02 22:04:22 +06:00
parent c82b68a513
commit 30bf0bed62
15 changed files with 239 additions and 182 deletions

View File

@ -40,6 +40,8 @@ class PlaybuttonView extends StatelessWidget {
final VoidCallback onRequestMore; final VoidCallback onRequestMore;
final ScrollController controller; final ScrollController controller;
final Widget? leading;
const PlaybuttonView({ const PlaybuttonView({
super.key, super.key,
required this.itemCount, required this.itemCount,
@ -49,6 +51,7 @@ class PlaybuttonView extends StatelessWidget {
required this.isLoading, required this.isLoading,
required this.onRequestMore, required this.onRequestMore,
required this.controller, required this.controller,
this.leading,
}); });
@override @override
@ -74,6 +77,7 @@ class PlaybuttonView extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (leading != null) leading!,
Toggle( Toggle(
value: isGrid.value, value: isGrid.value,
style: style:

View File

@ -224,6 +224,11 @@ class Spotube extends HookConsumerWidget {
surfaceBlur: 10, surfaceBlur: 10,
), ),
materialTheme: material.ThemeData( materialTheme: material.ThemeData(
brightness: switch (themeMode) {
ThemeMode.system => MediaQuery.platformBrightnessOf(context),
ThemeMode.light => Brightness.light,
ThemeMode.dark => Brightness.dark,
},
splashFactory: material.NoSplash.splashFactory, splashFactory: material.NoSplash.splashFactory,
appBarTheme: const material.AppBarTheme( appBarTheme: const material.AppBarTheme(
surfaceTintColor: Colors.transparent, surfaceTintColor: Colors.transparent,

View File

@ -52,6 +52,7 @@ class ConnectPageLocalDevices extends HookWidget {
); );
}, },
), ),
const SliverGap(200)
], ],
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -27,44 +28,51 @@ class AlbumPage extends HookConsumerWidget {
final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier); final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier);
final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!)); final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!));
return TrackPresentation( return material.RefreshIndicator.adaptive(
options: TrackPresentationOptions( onRefresh: () async {
collection: album, ref.invalidate(albumTracksProvider(album));
image: album.images.asUrlString( ref.invalidate(favoriteAlbumsProvider);
placeholder: ImagePlaceholder.albumArt, ref.invalidate(albumsIsSavedProvider(album.id!));
},
child: TrackPresentation(
options: TrackPresentationOptions(
collection: album,
image: album.images.asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
title: album.name!,
description:
"${context.l10n.released}${album.releaseDate}${album.artists!.first.name}",
tracks: tracks.asData?.value.items ?? [],
pagination: PaginationProps(
hasNextPage: tracks.asData?.value.hasMore ?? false,
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: () async {
await tracksNotifier.fetchMore();
},
onFetchAll: () async {
return tracksNotifier.fetchAll();
},
onRefresh: () async {
ref.invalidate(albumTracksProvider(album));
},
),
routePath: "/album/${album.id}",
shareUrl: album.externalUrls?.spotify ??
"https://open.spotify.com/album/${album.id}",
isLiked: isSavedAlbum.asData?.value ?? false,
owner: album.artists!.first.name,
onHeart: isSavedAlbum.asData?.value == null
? null
: () async {
if (isSavedAlbum.asData!.value) {
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
} else {
await favoriteAlbumsNotifier.addFavorites([album.id!]);
}
return null;
},
), ),
title: album.name!,
description:
"${context.l10n.released}${album.releaseDate}${album.artists!.first.name}",
tracks: tracks.asData?.value.items ?? [],
pagination: PaginationProps(
hasNextPage: tracks.asData?.value.hasMore ?? false,
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: () async {
await tracksNotifier.fetchMore();
},
onFetchAll: () async {
return tracksNotifier.fetchAll();
},
onRefresh: () async {
ref.invalidate(albumTracksProvider(album));
},
),
routePath: "/album/${album.id}",
shareUrl: album.externalUrls?.spotify ??
"https://open.spotify.com/album/${album.id}",
isLiked: isSavedAlbum.asData?.value ?? false,
owner: album.artists!.first.name,
onHeart: isSavedAlbum.asData?.value == null
? null
: () async {
if (isSavedAlbum.asData!.value) {
await favoriteAlbumsNotifier.removeFavorites([album.id!]);
} else {
await favoriteAlbumsNotifier.addFavorites([album.id!]);
}
return null;
},
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -42,45 +43,59 @@ class ArtistPage extends HookConsumerWidget {
) )
], ],
floatingHeader: true, floatingHeader: true,
child: Builder(builder: (context) { child: material.RefreshIndicator.adaptive(
if (artistQuery.hasError && artistQuery.asData?.value == null) { onRefresh: () async {
return Center(child: Text(artistQuery.error.toString())); ref.invalidate(artistProvider(artistId));
} ref.invalidate(relatedArtistsProvider(artistId));
return Skeletonizer( ref.invalidate(artistAlbumsProvider(artistId));
enabled: artistQuery.isLoading, ref.invalidate(artistIsFollowingProvider(artistId));
child: CustomScrollView( ref.invalidate(artistTopTracksProvider(artistId));
controller: scrollController, if (artistQuery.hasValue) {
slivers: [ ref.invalidate(
SliverToBoxAdapter( artistWikipediaSummaryProvider(artistQuery.asData!.value));
child: SafeArea( }
bottom: false, },
child: ArtistPageHeader(artistId: artistId), child: Builder(builder: (context) {
), if (artistQuery.hasError && artistQuery.asData?.value == null) {
), return Center(child: Text(artistQuery.error.toString()));
const SliverGap(20), }
ArtistPageTopTracks(artistId: artistId), return Skeletonizer(
const SliverGap(20), enabled: artistQuery.isLoading,
SliverToBoxAdapter(child: ArtistAlbumList(artistId)), child: CustomScrollView(
SliverPadding( controller: scrollController,
padding: const EdgeInsets.all(8.0), slivers: [
sliver: SliverToBoxAdapter( SliverToBoxAdapter(
child: Text( child: SafeArea(
context.l10n.fans_also_like, bottom: false,
style: theme.typography.h4, child: ArtistPageHeader(artistId: artistId),
), ),
), ),
), const SliverGap(20),
ArtistPageRelatedArtists(artistId: artistId), ArtistPageTopTracks(artistId: artistId),
const SliverGap(20), const SliverGap(20),
if (artistQuery.asData?.value != null) SliverToBoxAdapter(child: ArtistAlbumList(artistId)),
SliverToBoxAdapter( SliverPadding(
child: ArtistPageFooter(artist: artistQuery.asData!.value), padding: const EdgeInsets.all(8.0),
sliver: SliverToBoxAdapter(
child: Text(
context.l10n.fans_also_like,
style: theme.typography.h4,
),
),
), ),
const SliverSafeArea(sliver: SliverGap(10)), ArtistPageRelatedArtists(artistId: artistId),
], const SliverGap(20),
), if (artistQuery.asData?.value != null)
); SliverToBoxAdapter(
}), child:
ArtistPageFooter(artist: artistQuery.asData!.value),
),
const SliverSafeArea(sliver: SliverGap(10)),
],
),
);
}),
),
), ),
); );
} }

View File

@ -69,6 +69,13 @@ class LibraryPage extends HookConsumerWidget {
], ],
), ),
), ),
)
else
const TitleBar(
automaticallyImplyLeading: false,
backgroundColor: Colors.transparent,
surfaceBlur: 0,
height: 32,
), ),
const Gap(10), const Gap(10),
], ],

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_undraw/flutter_undraw.dart'; import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -55,10 +56,10 @@ class UserAlbumsPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
child: RefreshTrigger( child: material.RefreshIndicator.adaptive(
// onRefresh: () async { onRefresh: () async {
// ref.invalidate(favoriteAlbumsProvider); ref.invalidate(favoriteAlbumsProvider);
// }, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: CustomScrollView( child: CustomScrollView(

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_undraw/flutter_undraw.dart'; import 'package:flutter_undraw/flutter_undraw.dart';
@ -60,10 +61,10 @@ class UserArtistsPage extends HookConsumerWidget {
return SafeArea( return SafeArea(
bottom: false, bottom: false,
child: Scaffold( child: Scaffold(
child: RefreshTrigger( child: material.RefreshIndicator.adaptive(
// onRefresh: () async { onRefresh: () async {
// ref.invalidate(followedArtistsProvider); ref.invalidate(followedArtistsProvider);
// }, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,
child: Padding( child: Padding(

View File

@ -1,6 +1,7 @@
import 'dart:io'; import 'dart:io';
import 'dart:math'; import 'dart:math';
import 'package:flutter/material.dart' as material;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
@ -342,9 +343,9 @@ class LocalLibraryPage extends HookConsumerWidget {
} }
return Expanded( return Expanded(
child: RefreshTrigger( child: material.RefreshIndicator.adaptive(
onRefresh: () async { onRefresh: () async {
// ref.invalidate(localTracksProvider); ref.invalidate(localTracksProvider);
}, },
child: InterScrollbar( child: InterScrollbar(
controller: controller, controller: controller,

View File

@ -1,4 +1,4 @@
import 'package:flutter/material.dart' show kToolbarHeight; import 'package:flutter/material.dart' as material;
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@ -17,7 +17,6 @@ import 'package:spotube/modules/playlist/playlist_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/spotify/spotify.dart'; import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/utils/platform.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@RoutePage() @RoutePage()
@ -79,10 +78,10 @@ class UserPlaylistsPage extends HookConsumerWidget {
return const AnonymousFallback(); return const AnonymousFallback();
} }
return RefreshTrigger( return material.RefreshIndicator.adaptive(
// onRefresh: () async { onRefresh: () async {
// ref.invalidate(favoritePlaylistsProvider); ref.invalidate(favoritePlaylistsProvider);
// }, },
child: SafeArea( child: SafeArea(
bottom: false, bottom: false,
child: InterScrollbar( child: InterScrollbar(
@ -103,30 +102,27 @@ class UserPlaylistsPage extends HookConsumerWidget {
leading: const Icon(SpotubeIcons.filter), leading: const Icon(SpotubeIcons.filter),
), ),
), ),
bottom: PreferredSize(
preferredSize:
Size.fromHeight(kIsDesktop ? 35 : kToolbarHeight),
child: Row(
children: [
const Gap(10),
const PlaylistCreateDialogButton(),
const Gap(10),
Button.primary(
leading: const Icon(SpotubeIcons.magic),
child: Text(context.l10n.generate),
onPressed: () {
context.navigateTo(const PlaylistGeneratorRoute());
},
),
const Gap(10),
],
),
),
), ),
const SliverGap(10), const SliverGap(10),
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: PlaybuttonView( sliver: PlaybuttonView(
leading: Expanded(
child: Row(
children: [
const PlaylistCreateDialogButton(),
const Gap(10),
Button.primary(
leading: const Icon(SpotubeIcons.magic),
child: Text(context.l10n.generate),
onPressed: () {
context.navigateTo(const PlaylistGeneratorRoute());
},
),
const Gap(10),
],
),
),
controller: controller, controller: controller,
hasMore: playlistsQuery.asData?.value.hasMore == true, hasMore: playlistsQuery.asData?.value.hasMore == true,
isLoading: playlistsQuery.isLoading, isLoading: playlistsQuery.isLoading,

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -22,29 +23,34 @@ class LikedPlaylistPage extends HookConsumerWidget {
final likedTracks = ref.watch(likedTracksProvider); final likedTracks = ref.watch(likedTracksProvider);
final tracks = likedTracks.asData?.value ?? <Track>[]; final tracks = likedTracks.asData?.value ?? <Track>[];
return TrackPresentation( return material.RefreshIndicator.adaptive(
options: TrackPresentationOptions( onRefresh: () async {
collection: playlist, ref.invalidate(likedTracksProvider);
image: "assets/liked-tracks.jpg", },
pagination: PaginationProps( child: TrackPresentation(
hasNextPage: false, options: TrackPresentationOptions(
isLoading: likedTracks.isLoading, collection: playlist,
onFetchMore: () {}, image: "assets/liked-tracks.jpg",
onFetchAll: () async { pagination: PaginationProps(
return tracks.toList(); hasNextPage: false,
}, isLoading: likedTracks.isLoading,
onRefresh: () async { onFetchMore: () {},
ref.invalidate(likedTracksProvider); onFetchAll: () async {
}, return tracks.toList();
},
onRefresh: () async {
ref.invalidate(likedTracksProvider);
},
),
title: playlist.name!,
description: playlist.description,
tracks: tracks,
routePath: '/playlist/${playlist.id}',
isLiked: false,
shareUrl: null,
onHeart: null,
owner: playlist.owner?.displayName,
), ),
title: playlist.name!,
description: playlist.description,
tracks: tracks,
routePath: '/playlist/${playlist.id}',
isLiked: false,
shareUrl: null,
onHeart: null,
owner: playlist.owner?.displayName,
), ),
); );
} }

View File

@ -1,3 +1,4 @@
import 'package:flutter/material.dart' as material;
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart' hide Page; import 'package:flutter/material.dart' hide Page;
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@ -49,51 +50,58 @@ class PlaylistPage extends HookConsumerWidget {
final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!); final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!);
return TrackPresentation( return material.RefreshIndicator.adaptive(
options: TrackPresentationOptions( onRefresh: () async {
collection: playlist, ref.invalidate(playlistTracksProvider(playlist.id!));
image: playlist.images.asUrlString( ref.invalidate(isFavoritePlaylistProvider(playlist.id!));
placeholder: ImagePlaceholder.collection, ref.invalidate(favoritePlaylistsProvider);
), },
pagination: PaginationProps( child: TrackPresentation(
hasNextPage: tracks.asData?.value.hasMore ?? false, options: TrackPresentationOptions(
isLoading: tracks.isLoading || tracks.isLoadingNextPage, collection: playlist,
onFetchMore: tracksNotifier.fetchMore, image: playlist.images.asUrlString(
onRefresh: () async { placeholder: ImagePlaceholder.collection,
ref.invalidate(playlistTracksProvider(playlist.id!)); ),
}, pagination: PaginationProps(
onFetchAll: () async { hasNextPage: tracks.asData?.value.hasMore ?? false,
return await tracksNotifier.fetchAll(); isLoading: tracks.isLoading || tracks.isLoadingNextPage,
}, onFetchMore: tracksNotifier.fetchMore,
), onRefresh: () async {
title: playlist.name!, ref.invalidate(playlistTracksProvider(playlist.id!));
description: playlist.description, },
owner: playlist.owner?.displayName, onFetchAll: () async {
ownerImage: playlist.owner?.images?.lastOrNull?.url, return await tracksNotifier.fetchAll();
tracks: tracks.asData?.value.items ?? [], },
routePath: '/playlist/${playlist.id}', ),
isLiked: isFavoritePlaylist.asData?.value ?? false, title: playlist.name!,
shareUrl: playlist.externalUrls?.spotify ?? description: playlist.description,
"https://open.spotify.com/playlist/${playlist.id}", owner: playlist.owner?.displayName,
onHeart: isFavoritePlaylist.asData?.value == null ownerImage: playlist.owner?.images?.lastOrNull?.url,
? null tracks: tracks.asData?.value.items ?? [],
: () async { routePath: '/playlist/${playlist.id}',
final confirmed = isUserPlaylist isLiked: isFavoritePlaylist.asData?.value ?? false,
? await showPromptDialog( shareUrl: playlist.externalUrls?.spotify ??
context: context, "https://open.spotify.com/playlist/${playlist.id}",
title: context.l10n.delete_playlist, onHeart: isFavoritePlaylist.asData?.value == null
message: context.l10n.delete_playlist_confirmation, ? null
) : () async {
: true; final confirmed = isUserPlaylist
if (!confirmed) return null; ? await showPromptDialog(
context: context,
title: context.l10n.delete_playlist,
message: context.l10n.delete_playlist_confirmation,
)
: true;
if (!confirmed) return null;
if (isFavoritePlaylist.asData!.value) { if (isFavoritePlaylist.asData!.value) {
await favoritePlaylistsNotifier.removeFavorite(playlist); await favoritePlaylistsNotifier.removeFavorite(playlist);
} else { } else {
await favoritePlaylistsNotifier.addFavorite(playlist); await favoritePlaylistsNotifier.addFavorite(playlist);
} }
return isUserPlaylist; return isUserPlaylist;
}, },
),
), ),
); );
} }

View File

@ -111,14 +111,17 @@ class TrackPage extends HookConsumerWidget {
crossAxisAlignment: WrapCrossAlignment.center, crossAxisAlignment: WrapCrossAlignment.center,
runAlignment: WrapAlignment.center, runAlignment: WrapAlignment.center,
children: [ children: [
ClipRRect( Padding(
borderRadius: BorderRadius.circular(10), padding: const EdgeInsets.only(top: 20),
child: UniversalImage( child: ClipRRect(
path: track.album!.images.asUrlString( borderRadius: BorderRadius.circular(10),
placeholder: ImagePlaceholder.albumArt, child: UniversalImage(
path: track.album!.images.asUrlString(
placeholder: ImagePlaceholder.albumArt,
),
height: 200,
width: 200,
), ),
height: 200,
width: 200,
), ),
), ),
Padding( Padding(

View File

@ -376,7 +376,7 @@ packages:
source: hosted source: hosted
version: "4.10.1" version: "4.10.1"
collection: collection:
dependency: "direct overridden" dependency: "direct main"
description: description:
name: collection name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf

View File

@ -144,6 +144,7 @@ dependencies:
git: git:
url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git
http_parser: ^4.1.2 http_parser: ^4.1.2
collection: any
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13