diff --git a/lib/components/playbutton_view/playbutton_view.dart b/lib/components/playbutton_view/playbutton_view.dart index 46e67e25..7880bb8c 100644 --- a/lib/components/playbutton_view/playbutton_view.dart +++ b/lib/components/playbutton_view/playbutton_view.dart @@ -40,6 +40,8 @@ class PlaybuttonView extends StatelessWidget { final VoidCallback onRequestMore; final ScrollController controller; + final Widget? leading; + const PlaybuttonView({ super.key, required this.itemCount, @@ -49,6 +51,7 @@ class PlaybuttonView extends StatelessWidget { required this.isLoading, required this.onRequestMore, required this.controller, + this.leading, }); @override @@ -74,6 +77,7 @@ class PlaybuttonView extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + if (leading != null) leading!, Toggle( value: isGrid.value, style: diff --git a/lib/main.dart b/lib/main.dart index 6f3cbfbf..5cd916e5 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -224,6 +224,11 @@ class Spotube extends HookConsumerWidget { surfaceBlur: 10, ), materialTheme: material.ThemeData( + brightness: switch (themeMode) { + ThemeMode.system => MediaQuery.platformBrightnessOf(context), + ThemeMode.light => Brightness.light, + ThemeMode.dark => Brightness.dark, + }, splashFactory: material.NoSplash.splashFactory, appBarTheme: const material.AppBarTheme( surfaceTintColor: Colors.transparent, diff --git a/lib/modules/connect/local_devices.dart b/lib/modules/connect/local_devices.dart index 138e9e13..dc192e44 100644 --- a/lib/modules/connect/local_devices.dart +++ b/lib/modules/connect/local_devices.dart @@ -52,6 +52,7 @@ class ConnectPageLocalDevices extends HookWidget { ); }, ), + const SliverGap(200) ], ); } diff --git a/lib/pages/album/album.dart b/lib/pages/album/album.dart index 5d19f5ed..5773f9fa 100644 --- a/lib/pages/album/album.dart +++ b/lib/pages/album/album.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:auto_route/auto_route.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -27,44 +28,51 @@ class AlbumPage extends HookConsumerWidget { final favoriteAlbumsNotifier = ref.watch(favoriteAlbumsProvider.notifier); final isSavedAlbum = ref.watch(albumsIsSavedProvider(album.id!)); - return TrackPresentation( - options: TrackPresentationOptions( - collection: album, - image: album.images.asUrlString( - placeholder: ImagePlaceholder.albumArt, + return material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(albumTracksProvider(album)); + ref.invalidate(favoriteAlbumsProvider); + 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; - }, ), ); } diff --git a/lib/pages/artist/artist.dart b/lib/pages/artist/artist.dart index 67db398d..2037174a 100644 --- a/lib/pages/artist/artist.dart +++ b/lib/pages/artist/artist.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart'; @@ -42,45 +43,59 @@ class ArtistPage extends HookConsumerWidget { ) ], floatingHeader: true, - child: Builder(builder: (context) { - if (artistQuery.hasError && artistQuery.asData?.value == null) { - return Center(child: Text(artistQuery.error.toString())); - } - return Skeletonizer( - enabled: artistQuery.isLoading, - child: CustomScrollView( - controller: scrollController, - slivers: [ - SliverToBoxAdapter( - child: SafeArea( - bottom: false, - child: ArtistPageHeader(artistId: artistId), - ), - ), - const SliverGap(20), - ArtistPageTopTracks(artistId: artistId), - const SliverGap(20), - SliverToBoxAdapter(child: ArtistAlbumList(artistId)), - SliverPadding( - padding: const EdgeInsets.all(8.0), - sliver: SliverToBoxAdapter( - child: Text( - context.l10n.fans_also_like, - style: theme.typography.h4, + child: material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(artistProvider(artistId)); + ref.invalidate(relatedArtistsProvider(artistId)); + ref.invalidate(artistAlbumsProvider(artistId)); + ref.invalidate(artistIsFollowingProvider(artistId)); + ref.invalidate(artistTopTracksProvider(artistId)); + if (artistQuery.hasValue) { + ref.invalidate( + artistWikipediaSummaryProvider(artistQuery.asData!.value)); + } + }, + child: Builder(builder: (context) { + if (artistQuery.hasError && artistQuery.asData?.value == null) { + return Center(child: Text(artistQuery.error.toString())); + } + return Skeletonizer( + enabled: artistQuery.isLoading, + child: CustomScrollView( + controller: scrollController, + slivers: [ + SliverToBoxAdapter( + child: SafeArea( + bottom: false, + child: ArtistPageHeader(artistId: artistId), ), ), - ), - ArtistPageRelatedArtists(artistId: artistId), - const SliverGap(20), - if (artistQuery.asData?.value != null) - SliverToBoxAdapter( - child: ArtistPageFooter(artist: artistQuery.asData!.value), + const SliverGap(20), + ArtistPageTopTracks(artistId: artistId), + const SliverGap(20), + SliverToBoxAdapter(child: ArtistAlbumList(artistId)), + SliverPadding( + 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)), + ], + ), + ); + }), + ), ), ); } diff --git a/lib/pages/library/library.dart b/lib/pages/library/library.dart index a115112c..172d9af3 100644 --- a/lib/pages/library/library.dart +++ b/lib/pages/library/library.dart @@ -69,6 +69,13 @@ class LibraryPage extends HookConsumerWidget { ], ), ), + ) + else + const TitleBar( + automaticallyImplyLeading: false, + backgroundColor: Colors.transparent, + surfaceBlur: 0, + height: 32, ), const Gap(10), ], diff --git a/lib/pages/library/user_albums.dart b/lib/pages/library/user_albums.dart index 99cee135..60ba7319 100644 --- a/lib/pages/library/user_albums.dart +++ b/lib/pages/library/user_albums.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:flutter_undraw/flutter_undraw.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart' hide Image; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -55,10 +56,10 @@ class UserAlbumsPage extends HookConsumerWidget { return SafeArea( bottom: false, child: Scaffold( - child: RefreshTrigger( - // onRefresh: () async { - // ref.invalidate(favoriteAlbumsProvider); - // }, + child: material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(favoriteAlbumsProvider); + }, child: InterScrollbar( controller: controller, child: CustomScrollView( diff --git a/lib/pages/library/user_artists.dart b/lib/pages/library/user_artists.dart index c8c1dda8..5f304f5e 100644 --- a/lib/pages/library/user_artists.dart +++ b/lib/pages/library/user_artists.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:collection/collection.dart'; import 'package:flutter_undraw/flutter_undraw.dart'; @@ -60,10 +61,10 @@ class UserArtistsPage extends HookConsumerWidget { return SafeArea( bottom: false, child: Scaffold( - child: RefreshTrigger( - // onRefresh: () async { - // ref.invalidate(followedArtistsProvider); - // }, + child: material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(followedArtistsProvider); + }, child: InterScrollbar( controller: controller, child: Padding( diff --git a/lib/pages/library/user_local_tracks/local_folder.dart b/lib/pages/library/user_local_tracks/local_folder.dart index 028d4e69..c3833659 100644 --- a/lib/pages/library/user_local_tracks/local_folder.dart +++ b/lib/pages/library/user_local_tracks/local_folder.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:math'; +import 'package:flutter/material.dart' as material; import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; @@ -342,9 +343,9 @@ class LocalLibraryPage extends HookConsumerWidget { } return Expanded( - child: RefreshTrigger( + child: material.RefreshIndicator.adaptive( onRefresh: () async { - // ref.invalidate(localTracksProvider); + ref.invalidate(localTracksProvider); }, child: InterScrollbar( controller: controller, diff --git a/lib/pages/library/user_playlists.dart b/lib/pages/library/user_playlists.dart index 6b92f8ea..8249726d 100644 --- a/lib/pages/library/user_playlists.dart +++ b/lib/pages/library/user_playlists.dart @@ -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:fuzzywuzzy/fuzzywuzzy.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/provider/authentication/authentication.dart'; import 'package:spotube/provider/spotify/spotify.dart'; -import 'package:spotube/utils/platform.dart'; import 'package:auto_route/auto_route.dart'; @RoutePage() @@ -79,10 +78,10 @@ class UserPlaylistsPage extends HookConsumerWidget { return const AnonymousFallback(); } - return RefreshTrigger( - // onRefresh: () async { - // ref.invalidate(favoritePlaylistsProvider); - // }, + return material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(favoritePlaylistsProvider); + }, child: SafeArea( bottom: false, child: InterScrollbar( @@ -103,30 +102,27 @@ class UserPlaylistsPage extends HookConsumerWidget { 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), SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 8), 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, hasMore: playlistsQuery.asData?.value.hasMore == true, isLoading: playlistsQuery.isLoading, diff --git a/lib/pages/playlist/liked_playlist.dart b/lib/pages/playlist/liked_playlist.dart index 54df2c88..5f7591ab 100644 --- a/lib/pages/playlist/liked_playlist.dart +++ b/lib/pages/playlist/liked_playlist.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:spotify/spotify.dart'; @@ -22,29 +23,34 @@ class LikedPlaylistPage extends HookConsumerWidget { final likedTracks = ref.watch(likedTracksProvider); final tracks = likedTracks.asData?.value ?? []; - return TrackPresentation( - options: TrackPresentationOptions( - collection: playlist, - image: "assets/liked-tracks.jpg", - pagination: PaginationProps( - hasNextPage: false, - isLoading: likedTracks.isLoading, - onFetchMore: () {}, - onFetchAll: () async { - return tracks.toList(); - }, - onRefresh: () async { - ref.invalidate(likedTracksProvider); - }, + return material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(likedTracksProvider); + }, + child: TrackPresentation( + options: TrackPresentationOptions( + collection: playlist, + image: "assets/liked-tracks.jpg", + pagination: PaginationProps( + hasNextPage: false, + isLoading: likedTracks.isLoading, + onFetchMore: () {}, + 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, ), ); } diff --git a/lib/pages/playlist/playlist.dart b/lib/pages/playlist/playlist.dart index e3992db8..62ced353 100644 --- a/lib/pages/playlist/playlist.dart +++ b/lib/pages/playlist/playlist.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart' as material; import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide Page; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -49,51 +50,58 @@ class PlaylistPage extends HookConsumerWidget { final isUserPlaylist = useIsUserPlaylist(ref, playlist.id!); - return TrackPresentation( - options: TrackPresentationOptions( - collection: playlist, - image: playlist.images.asUrlString( - placeholder: ImagePlaceholder.collection, - ), - pagination: PaginationProps( - hasNextPage: tracks.asData?.value.hasMore ?? false, - isLoading: tracks.isLoading || tracks.isLoadingNextPage, - onFetchMore: tracksNotifier.fetchMore, - onRefresh: () async { - ref.invalidate(playlistTracksProvider(playlist.id!)); - }, - onFetchAll: () async { - return await tracksNotifier.fetchAll(); - }, - ), - title: playlist.name!, - description: playlist.description, - owner: playlist.owner?.displayName, - ownerImage: playlist.owner?.images?.lastOrNull?.url, - tracks: tracks.asData?.value.items ?? [], - routePath: '/playlist/${playlist.id}', - isLiked: isFavoritePlaylist.asData?.value ?? false, - shareUrl: playlist.externalUrls?.spotify ?? - "https://open.spotify.com/playlist/${playlist.id}", - onHeart: isFavoritePlaylist.asData?.value == null - ? null - : () async { - final confirmed = isUserPlaylist - ? await showPromptDialog( - context: context, - title: context.l10n.delete_playlist, - message: context.l10n.delete_playlist_confirmation, - ) - : true; - if (!confirmed) return null; + return material.RefreshIndicator.adaptive( + onRefresh: () async { + ref.invalidate(playlistTracksProvider(playlist.id!)); + ref.invalidate(isFavoritePlaylistProvider(playlist.id!)); + ref.invalidate(favoritePlaylistsProvider); + }, + child: TrackPresentation( + options: TrackPresentationOptions( + collection: playlist, + image: playlist.images.asUrlString( + placeholder: ImagePlaceholder.collection, + ), + pagination: PaginationProps( + hasNextPage: tracks.asData?.value.hasMore ?? false, + isLoading: tracks.isLoading || tracks.isLoadingNextPage, + onFetchMore: tracksNotifier.fetchMore, + onRefresh: () async { + ref.invalidate(playlistTracksProvider(playlist.id!)); + }, + onFetchAll: () async { + return await tracksNotifier.fetchAll(); + }, + ), + title: playlist.name!, + description: playlist.description, + owner: playlist.owner?.displayName, + ownerImage: playlist.owner?.images?.lastOrNull?.url, + tracks: tracks.asData?.value.items ?? [], + routePath: '/playlist/${playlist.id}', + isLiked: isFavoritePlaylist.asData?.value ?? false, + shareUrl: playlist.externalUrls?.spotify ?? + "https://open.spotify.com/playlist/${playlist.id}", + onHeart: isFavoritePlaylist.asData?.value == null + ? null + : () async { + final confirmed = isUserPlaylist + ? await showPromptDialog( + context: context, + title: context.l10n.delete_playlist, + message: context.l10n.delete_playlist_confirmation, + ) + : true; + if (!confirmed) return null; - if (isFavoritePlaylist.asData!.value) { - await favoritePlaylistsNotifier.removeFavorite(playlist); - } else { - await favoritePlaylistsNotifier.addFavorite(playlist); - } - return isUserPlaylist; - }, + if (isFavoritePlaylist.asData!.value) { + await favoritePlaylistsNotifier.removeFavorite(playlist); + } else { + await favoritePlaylistsNotifier.addFavorite(playlist); + } + return isUserPlaylist; + }, + ), ), ); } diff --git a/lib/pages/track/track.dart b/lib/pages/track/track.dart index 0f8dae5d..2918d1d7 100644 --- a/lib/pages/track/track.dart +++ b/lib/pages/track/track.dart @@ -111,14 +111,17 @@ class TrackPage extends HookConsumerWidget { crossAxisAlignment: WrapCrossAlignment.center, runAlignment: WrapAlignment.center, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(10), - child: UniversalImage( - path: track.album!.images.asUrlString( - placeholder: ImagePlaceholder.albumArt, + Padding( + padding: const EdgeInsets.only(top: 20), + child: ClipRRect( + borderRadius: BorderRadius.circular(10), + child: UniversalImage( + path: track.album!.images.asUrlString( + placeholder: ImagePlaceholder.albumArt, + ), + height: 200, + width: 200, ), - height: 200, - width: 200, ), ), Padding( diff --git a/pubspec.lock b/pubspec.lock index 5b92f19e..508cdda3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -376,7 +376,7 @@ packages: source: hosted version: "4.10.1" collection: - dependency: "direct overridden" + dependency: "direct main" description: name: collection sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf diff --git a/pubspec.yaml b/pubspec.yaml index 89d09e22..848e56aa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -144,6 +144,7 @@ dependencies: git: url: https://github.com/KRTirtho/flutter_new_pipe_extractor.git http_parser: ^4.1.2 + collection: any dev_dependencies: build_runner: ^2.4.13