feat: show placeholder images where there is no item or on empty page

This commit is contained in:
Kingkor Roy Tirtho 2025-01-12 14:16:18 +06:00
parent a8694a7a8b
commit b8ffb9b75f
24 changed files with 765 additions and 403 deletions

View File

@ -20,6 +20,17 @@ class $AssetsBackgroundsGen {
List<AssetGenImage> get values => [xmasEffect]; List<AssetGenImage> get values => [xmasEffect];
} }
class $AssetsIllustrationsGen {
const $AssetsIllustrationsGen();
/// File path: assets/illustrations/fixing_bugs.png
AssetGenImage get fixingBugs =>
const AssetGenImage('assets/illustrations/fixing_bugs.png');
/// List of all assets
List<AssetGenImage> get values => [fixingBugs];
}
class $AssetsLogosGen { class $AssetsLogosGen {
const $AssetsLogosGen(); const $AssetsLogosGen();
@ -140,6 +151,8 @@ class Assets {
AssetGenImage('assets/bengali-patterns-bg.jpg'); AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.png'); static const AssetGenImage branding = AssetGenImage('assets/branding.png');
static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png'); static const AssetGenImage emptyBox = AssetGenImage('assets/empty_box.png');
static const $AssetsIllustrationsGen illustrations =
$AssetsIllustrationsGen();
static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg'); static const AssetGenImage invidious = AssetGenImage('assets/invidious.jpg');
static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png'); static const AssetGenImage jiosaavn = AssetGenImage('assets/jiosaavn.png');
static const AssetGenImage likedTracks = static const AssetGenImage likedTracks =

View File

@ -1,9 +1,12 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/pages/settings/settings.dart'; import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/authentication/authentication.dart'; import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart'; import 'package:spotube/utils/service_utils.dart';
class AnonymousFallback extends ConsumerWidget { class AnonymousFallback extends ConsumerWidget {
@ -25,9 +28,16 @@ class AnonymousFallback extends ConsumerWidget {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
spacing: 10,
children: [ children: [
Undraw(
illustration: kIsMobile
? UndrawIllustration.accessDenied
: UndrawIllustration.secureLogin,
height: 200 * context.theme.scaling,
color: context.theme.colorScheme.primary,
),
Text(context.l10n.not_logged_in), Text(context.l10n.not_logged_in),
const SizedBox(height: 10),
Button.primary( Button.primary(
child: Text(context.l10n.login_with_spotify), child: Text(context.l10n.login_with_spotify),
onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name), onPressed: () => ServiceUtils.pushNamed(context, SettingsPage.name),

View File

@ -1,4 +1,5 @@
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart'; import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
@ -7,6 +8,7 @@ import 'package:spotube/components/playbutton_view/playbutton_card.dart';
import 'package:spotube/components/playbutton_view/playbutton_tile.dart'; import 'package:spotube/components/playbutton_view/playbutton_tile.dart';
import 'package:spotube/components/waypoint.dart'; import 'package:spotube/components/waypoint.dart';
import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
const _dummyPlaybuttonCard = PlaybuttonCard( const _dummyPlaybuttonCard = PlaybuttonCard(
@ -99,7 +101,28 @@ class PlaybuttonView extends StatelessWidget {
const SliverGap(10), const SliverGap(10),
// Toggle between grid and list view // Toggle between grid and list view
switch ((isGrid.value, isLoading)) { switch ((isGrid.value, isLoading)) {
(true, _) => SliverGrid.builder( (true, _) => !isLoading && itemCount == 0
? SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.taken,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.nothing_found,
textAlign: TextAlign.center,
).muted().small()
],
),
),
)
: SliverGrid.builder(
itemCount: isLoading ? 6 : itemCount + 1, itemCount: isLoading ? 6 : itemCount + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150 * scale, maxCrossAxisExtent: 150 * scale,
@ -150,6 +173,23 @@ class PlaybuttonView extends StatelessWidget {
onFetchData: onRequestMore, onFetchData: onRequestMore,
hasReachedMax: !hasMore, hasReachedMax: !hasMore,
isLoading: isLoading, isLoading: isLoading,
emptyBuilder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.taken,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.nothing_found,
textAlign: TextAlign.center,
).muted().small()
],
);
},
), ),
} }
], ],

View File

@ -1,6 +1,8 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_undraw/flutter_undraw.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';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
import 'package:spotube/components/track_presentation/presentation_props.dart'; import 'package:spotube/components/track_presentation/presentation_props.dart';
@ -8,6 +10,7 @@ import 'package:spotube/components/track_presentation/presentation_state.dart';
import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart'; import 'package:spotube/components/track_presentation/use_track_tile_play_callback.dart';
import 'package:spotube/components/track_tile/track_tile.dart'; import 'package:spotube/components/track_tile/track_tile.dart';
import 'package:spotube/components/track_presentation/use_is_user_playlist.dart'; import 'package:spotube/components/track_presentation/use_is_user_playlist.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/audio_player/audio_player.dart'; import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart'; import 'package:very_good_infinite_list/very_good_infinite_list.dart';
@ -25,6 +28,30 @@ class PresentationListSection extends HookConsumerWidget {
final onTileTap = useTrackTilePlayCallback(ref); final onTileTap = useTrackTilePlayCallback(ref);
if (state.presentationTracks.isEmpty && !options.pagination.isLoading) {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Undraw(
illustration: UndrawIllustration.dreamer,
color: context.theme.colorScheme.primary,
height: 200 * context.theme.scaling,
),
Text(
isUserPlaylist
? context.l10n.no_tracks_added_yet
: context.l10n.no_tracks,
textAlign: TextAlign.center,
).muted().small(),
],
),
),
);
}
return SliverInfiniteList( return SliverInfiniteList(
isLoading: options.pagination.isLoading, isLoading: options.pagination.isLoading,
onFetchData: options.pagination.onFetchMore, onFetchData: options.pagination.onFetchMore,

View File

@ -409,5 +409,10 @@
"add_all_to_queue": "Add all to queue", "add_all_to_queue": "Add all to queue",
"play_all_next": "Play all next", "play_all_next": "Play all next",
"pause": "Pause", "pause": "Pause",
"view_all": "View all" "view_all": "View all",
"no_tracks_added_yet": "Looks like you haven't added any tracks yet",
"no_tracks": "Looks like there are no tracks here",
"no_tracks_listened_yet": "Looks like you haven't listened to anything yet",
"not_following_artists": "You're not following any artists",
"no_favorite_albums_yet": "Looks like you haven't added any albums to your favorites yet"
} }

View File

@ -1,7 +1,5 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
class BlurCard extends HookConsumerWidget { class BlurCard extends HookConsumerWidget {
final Widget child; final Widget child;
@ -18,8 +16,7 @@ class BlurCard extends HookConsumerWidget {
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: SizedBox( child: SizedBox(
width: double.infinity, width: double.infinity,
child: BackdropFilter( child: SurfaceCard(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: child, child: child,

View File

@ -1,5 +1,7 @@
import 'package:flutter_undraw/flutter_undraw.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';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart'; import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
@ -15,6 +17,21 @@ class HomeFeaturedSection extends HookConsumerWidget {
final featuredPlaylistsNotifier = final featuredPlaylistsNotifier =
ref.watch(featuredPlaylistsProvider.notifier); ref.watch(featuredPlaylistsProvider.notifier);
if (featuredPlaylists.hasError) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Undraw(
illustration: UndrawIllustration.fixingBugs,
height: 200 * context.theme.scaling,
color: context.theme.colorScheme.primary,
),
Text(context.l10n.something_went_wrong).small().muted(),
const Gap(8),
],
);
}
return Skeletonizer( return Skeletonizer(
enabled: featuredPlaylists.isLoading, enabled: featuredPlaylists.isLoading,
child: HorizontalPlaybuttonCardView<PlaylistSimple>( child: HorizontalPlaybuttonCardView<PlaylistSimple>(

View File

@ -89,6 +89,7 @@ class GenreSectionCard extends HookConsumerWidget {
), ),
], ],
), ),
if (playlists?.hasError != true)
Expanded( Expanded(
child: Skeleton.ignore( child: Skeleton.ignore(
child: Skeletonizer( child: Skeletonizer(

View File

@ -1,9 +1,11 @@
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';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/playbutton_view/playbutton_view.dart'; import 'package:spotube/components/playbutton_view/playbutton_view.dart';
@ -75,6 +77,30 @@ class UserAlbums extends HookConsumerWidget {
), ),
), ),
const SliverGap(10), const SliverGap(10),
if (albums.isEmpty &&
!albumsQuery.isLoading &&
searchText.value.isEmpty)
SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.followMeDrone,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.not_following_artists,
textAlign: TextAlign.center,
).muted().small()
],
),
),
)
else
SliverPadding( SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 8), padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: PlaybuttonView( sliver: PlaybuttonView(

View File

@ -1,9 +1,11 @@
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:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:fuzzywuzzy/fuzzywuzzy.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';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/fake.dart'; import 'package:spotube/collections/fake.dart';
@ -79,11 +81,10 @@ class UserArtists extends HookConsumerWidget {
), ),
), ),
const SliverGap(10), const SliverGap(10),
if (filteredArtists.isNotEmpty)
SliverLayoutBuilder(builder: (context, constrains) { SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder( return SliverGrid.builder(
itemCount: filteredArtists.isEmpty itemCount: filteredArtists.length + 1,
? 6
: filteredArtists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200, maxCrossAxisExtent: 200,
mainAxisExtent: constrains.smAndDown ? 225 : 250, mainAxisExtent: constrains.smAndDown ? 225 : 250,
@ -117,7 +118,45 @@ class UserArtists extends HookConsumerWidget {
); );
}, },
); );
}), })
else if (filteredArtists.isEmpty &&
searchText.value.isEmpty &&
!artistQuery.isLoading)
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.followMeDrone,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.not_following_artists,
textAlign: TextAlign.center,
).muted().small()
],
),
)
else
SliverToBoxAdapter(
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 10,
children: [
Undraw(
height: 200 * context.theme.scaling,
illustration: UndrawIllustration.taken,
color: Theme.of(context).colorScheme.primary,
),
Text(
context.l10n.nothing_found,
textAlign: TextAlign.center,
).muted().small()
],
),
),
const SliverSafeArea(sliver: SliverGap(10)), const SliverSafeArea(sliver: SliverGap(10)),
], ],
), ),

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter_undraw/flutter_undraw.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_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/album_item.dart'; import 'package:spotube/modules/stats/common/album_item.dart';
@ -31,6 +33,24 @@ class TopAlbums extends HookConsumerWidget {
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage, isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
hasReachedMax: topAlbums.asData?.value.hasMore ?? true, hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
itemCount: albumsData.length, itemCount: albumsData.length,
emptyBuilder: (context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(50),
Undraw(
illustration: UndrawIllustration.happyMusic,
color: context.theme.colorScheme.primary,
height: 200 * context.theme.scaling,
),
Text(
context.l10n.no_tracks_listened_yet,
textAlign: TextAlign.center,
).muted().small(),
],
),
),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final album = albumsData[index]; final album = albumsData[index];
return StatsAlbumItem( return StatsAlbumItem(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_undraw/flutter_undraw.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_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/artist_item.dart'; import 'package:spotube/modules/stats/common/artist_item.dart';
@ -35,6 +37,24 @@ class TopArtists extends HookConsumerWidget {
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: artistsData.length, itemCount: artistsData.length,
emptyBuilder: (context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(50),
Undraw(
illustration: UndrawIllustration.happyMusic,
color: context.theme.colorScheme.primary,
height: 200 * context.theme.scaling,
),
Text(
context.l10n.no_tracks_listened_yet,
textAlign: TextAlign.center,
).muted().small(),
],
),
),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final artist = artistsData[index]; final artist = artistsData[index];
return StatsArtistItem( return StatsArtistItem(

View File

@ -1,5 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter_undraw/flutter_undraw.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_extension.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart'; import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/track_item.dart'; import 'package:spotube/modules/stats/common/track_item.dart';
@ -33,6 +35,24 @@ class TopTracks extends HookConsumerWidget {
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage, isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true, hasReachedMax: topTracks.asData?.value.hasMore ?? true,
itemCount: tracksData.length, itemCount: tracksData.length,
emptyBuilder: (context) => Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Gap(50),
Undraw(
illustration: UndrawIllustration.happyMusic,
color: context.theme.colorScheme.primary,
height: 200 * context.theme.scaling,
),
Text(
context.l10n.no_tracks_listened_yet,
textAlign: TextAlign.center,
).muted().small(),
],
),
),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final track = tracksData[index]; final track = tracksData[index];
return StatsTrackItem( return StatsTrackItem(

View File

@ -35,7 +35,7 @@ class AlbumPage extends HookConsumerWidget {
tracks: tracks.asData?.value.items ?? [], tracks: tracks.asData?.value.items ?? [],
pagination: PaginationProps( pagination: PaginationProps(
hasNextPage: tracks.asData?.value.hasMore ?? false, hasNextPage: tracks.asData?.value.hasMore ?? false,
isLoading: tracks.isLoadingNextPage, isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: () async { onFetchMore: () async {
await tracksNotifier.fetchMore(); await tracksNotifier.fetchMore();
}, },

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
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:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/titlebar/titlebar.dart'; import 'package:spotube/components/titlebar/titlebar.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -8,8 +8,6 @@ import 'package:spotube/pages/getting_started/sections/greeting.dart';
import 'package:spotube/pages/getting_started/sections/playback.dart'; import 'package:spotube/pages/getting_started/sections/playback.dart';
import 'package:spotube/pages/getting_started/sections/region.dart'; import 'package:spotube/pages/getting_started/sections/region.dart';
import 'package:spotube/pages/getting_started/sections/support.dart'; import 'package:spotube/pages/getting_started/sections/support.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/themes/theme.dart';
class GettingStarting extends HookConsumerWidget { class GettingStarting extends HookConsumerWidget {
static const name = "getting_started"; static const name = "getting_started";
@ -18,12 +16,6 @@ class GettingStarting extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider);
final themeData = theme(
preferences.accentColorScheme,
Brightness.dark,
preferences.amoledDarkTheme,
);
final pageController = usePageController(); final pageController = usePageController();
final onNext = useCallback(() { final onNext = useCallback(() {
@ -40,11 +32,11 @@ class GettingStarting extends HookConsumerWidget {
); );
}, [pageController]); }, [pageController]);
return Theme( return Scaffold(
data: themeData, headers: [
child: Scaffold( TitleBar(
appBar: TitleBar(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
surfaceBlur: 0,
trailing: [ trailing: [
ListenableBuilder( ListenableBuilder(
listenable: pageController, listenable: pageController,
@ -54,7 +46,7 @@ class GettingStarting extends HookConsumerWidget {
child: pageController.hasClients && child: pageController.hasClients &&
(pageController.page == 0 || pageController.page == 3) (pageController.page == 0 || pageController.page == 3)
? const SizedBox() ? const SizedBox()
: TextButton( : Button.secondary(
onPressed: () { onPressed: () {
pageController.animateToPage( pageController.animateToPage(
3, 3,
@ -62,29 +54,20 @@ class GettingStarting extends HookConsumerWidget {
curve: Curves.easeInOut, curve: Curves.easeInOut,
); );
}, },
child: Text( child: Text(context.l10n.skip_this_nonsense),
context.l10n.skip_this_nonsense,
style: TextStyle(
decoration: TextDecoration.underline,
decorationColor: themeData.colorScheme.primary,
),
),
), ),
); );
}, },
), ),
], ],
), ),
extendBodyBehindAppBar: true, ],
body: DecoratedBox( floatingHeader: true,
child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: Assets.bengaliPatternsBg.provider(), image: Assets.bengaliPatternsBg.provider(),
fit: BoxFit.cover, fit: BoxFit.cover,
colorFilter: const ColorFilter.mode(
Colors.black38,
BlendMode.srcOver,
),
), ),
), ),
child: PageView( child: PageView(
@ -100,7 +83,6 @@ class GettingStarting extends HookConsumerWidget {
], ],
), ),
), ),
),
); );
} }
} }

View File

@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
class GettingStartedPageGreetingSection extends HookConsumerWidget { class GettingStartedPageGreetingSection extends HookConsumerWidget {
@ -13,8 +12,6 @@ class GettingStartedPageGreetingSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
return Center( return Center(
child: BlurCard( child: BlurCard(
child: Column( child: Column(
@ -22,30 +19,19 @@ class GettingStartedPageGreetingSection extends HookConsumerWidget {
children: [ children: [
Assets.spotubeLogoPng.image(height: 200), Assets.spotubeLogoPng.image(height: 200),
const Gap(24), const Gap(24),
Text( const Text("Spotube").semiBold().h4(),
"Spotube",
style:
textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(4), const Gap(4),
Text( Text(
kIsMobile kIsMobile
? context.l10n.freedom_of_music_palm ? context.l10n.freedom_of_music_palm
: context.l10n.freedom_of_music, : context.l10n.freedom_of_music,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: textTheme.titleMedium?.copyWith( ).light().large().italic(),
fontWeight: FontWeight.w300,
fontStyle: FontStyle.italic,
),
),
const Gap(84), const Gap(84),
Directionality( Button.primary(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
onPressed: onNext, onPressed: onNext,
icon: const Icon(SpotubeIcons.angleRight), trailing: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.get_started), child: Text(context.l10n.get_started),
),
), ),
], ],
), ),

View File

@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.dart'; import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/database/database.dart'; import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart'; import 'package:spotube/extensions/context.dart';
@ -14,14 +14,14 @@ final audioSourceToIconMap = {
AudioSource.youtube: const Icon( AudioSource.youtube: const Icon(
SpotubeIcons.youtube, SpotubeIcons.youtube,
color: Colors.red, color: Colors.red,
size: 30, size: 20,
), ),
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30), AudioSource.piped: const Icon(SpotubeIcons.piped, size: 20),
AudioSource.invidious: ClipRRect( AudioSource.invidious: ClipRRect(
borderRadius: BorderRadius.circular(48), borderRadius: BorderRadius.circular(26),
child: Assets.invidious.image(width: 48, height: 48), child: Assets.invidious.image(width: 26, height: 26),
), ),
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48), AudioSource.jiosaavn: Assets.jiosaavn.image(width: 20, height: 20),
}; };
class GettingStartedPagePlaybackSection extends HookConsumerWidget { class GettingStartedPagePlaybackSection extends HookConsumerWidget {
@ -36,8 +36,6 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme, :dividerColor) =
Theme.of(context);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.read(userPreferencesProvider.notifier); final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
@ -62,76 +60,56 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
children: [ children: [
const Icon(SpotubeIcons.album, size: 16), const Icon(SpotubeIcons.album, size: 16),
const Gap(8), const Gap(8),
Text(context.l10n.playback, style: textTheme.titleMedium), Text(context.l10n.playback).semiBold().large(),
], ],
), ),
const Gap(16), const Gap(16),
ListTile( Align(
title: Text( alignment: Alignment.centerLeft,
context.l10n.select_audio_source, child: Text(context.l10n.select_audio_source).semiBold().large(),
style: textTheme.titleMedium,
),
), ),
const Gap(16), const Gap(16),
ToggleButtons( Select<AudioSource>(
isSelected: [ value: preferences.audioSource,
for (final source in AudioSource.values) onChanged: (value) {
preferences.audioSource == source, if (value == null) return;
], preferencesNotifier.setAudioSource(value);
onPressed: (index) {
preferencesNotifier.setAudioSource(AudioSource.values[index]);
}, },
borderRadius: BorderRadius.circular(8), placeholder: Text(preferences.audioSource.name.capitalize()),
itemBuilder: (context, value) => Row(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
audioSourceToIconMap[value]!,
Text(value.name.capitalize()),
],
),
children: [ children: [
for (final source in AudioSource.values) for (final source in AudioSource.values)
SizedBox.square( SelectItemButton(
dimension: 84, value: source,
child: Column( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center, spacing: 6,
children: [ children: [
audioSourceToIconMap[source]!, audioSourceToIconMap[source]!,
const Gap(8), Text(source.name.capitalize()),
Text(
source.name.capitalize(),
style: textTheme.bodySmall!.copyWith(
color: preferences.audioSource == source
? colorScheme.primary
: null,
),
),
], ],
), ),
), ),
], ],
), ),
ListTile(
title: Align(
alignment: switch (preferences.audioSource) {
AudioSource.youtube => Alignment.centerLeft,
AudioSource.piped ||
AudioSource.invidious =>
Alignment.center,
AudioSource.jiosaavn => Alignment.centerRight,
},
child: Text(
audioSourceToDescription[preferences.audioSource]!,
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
),
),
const Gap(16), const Gap(16),
ListTile( Text(
audioSourceToDescription[preferences.audioSource]!,
).small().muted(),
const Gap(16),
ButtonTile(
title: Text(context.l10n.endless_playback), title: Text(context.l10n.endless_playback),
subtitle: Text( subtitle: Text(
context.l10n.endless_playback_description, context.l10n.endless_playback_description,
style: textTheme.bodySmall?.copyWith( ).small().muted(),
color: dividerColor, onPressed: () {
),
),
onTap: () {
preferencesNotifier preferencesNotifier
.setEndlessPlayback(!preferences.endlessPlayback); .setEndlessPlayback(!preferences.endlessPlayback);
}, },
@ -146,17 +124,17 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
FilledButton.icon( Button.secondary(
icon: const Icon(SpotubeIcons.angleLeft), leading: const Icon(SpotubeIcons.angleLeft),
label: Text(context.l10n.previous),
onPressed: onPrevious, onPressed: onPrevious,
child: Text(context.l10n.previous),
), ),
Directionality( Directionality(
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
child: FilledButton.icon( child: Button.primary(
icon: const Icon(SpotubeIcons.angleRight), leading: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.next),
onPressed: onNext, onPressed: onNext,
child: Text(context.l10n.next),
), ),
), ),
], ],

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/language_codes.dart'; import 'package:spotube/collections/language_codes.dart';
import 'package:spotube/collections/spotify_markets.dart'; import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
@ -16,7 +16,6 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :dividerColor) = Theme.of(context);
final preferences = ref.watch(userPreferencesProvider); final preferences = ref.watch(userPreferencesProvider);
return SafeArea( return SafeArea(
@ -32,92 +31,119 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
size: 16, size: 16,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(context.l10n.language_region).semiBold(),
context.l10n.language_region,
style: textTheme.titleMedium,
),
], ],
), ),
const Gap(48), const Gap(30),
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(context.l10n.choose_your_region).semiBold(),
context.l10n.choose_your_region,
style: textTheme.titleSmall,
),
Text( Text(
context.l10n.choose_your_region_description, context.l10n.choose_your_region_description,
style: textTheme.bodySmall?.copyWith( ).small().muted(),
color: dividerColor,
),
),
const Gap(16), const Gap(16),
DropdownMenu( Text(context.l10n.market_place_region).small(),
initialSelection: preferences.market, const Gap(8),
onSelected: (value) { SizedBox(
width: double.infinity,
child: Select<Market>(
value: preferences.market,
onChanged: (value) {
if (value == null) return; if (value == null) return;
ref ref
.read(userPreferencesProvider.notifier) .read(userPreferencesProvider.notifier)
.setRecommendationMarket(value); .setRecommendationMarket(value);
}, },
hintText: preferences.market.name, placeholder: Text(preferences.market.name),
label: Text(context.l10n.market_place_region), itemBuilder: (context, value) => Text(
inputDecorationTheme: spotifyMarkets
const InputDecorationTheme(isDense: true), .firstWhere((element) => element.$1 == value)
dropdownMenuEntries: [ .$2,
),
searchPlaceholder: Text(context.l10n.search),
searchFilter: (item, query) {
final market = spotifyMarkets
.firstWhere((element) => element.$1 == item)
.$2
.toLowerCase();
return market.contains(query.toLowerCase()) ? 1 : 0;
},
children: [
for (final market in spotifyMarkets) for (final market in spotifyMarkets)
DropdownMenuEntry( SelectItemButton(
value: market.$1, value: market.$1,
label: market.$2, child: Text(market.$2),
), ),
], ],
), ),
),
const Gap(36), const Gap(36),
Text( Text(
context.l10n.choose_your_language, context.l10n.choose_your_language,
style: textTheme.titleSmall, ).semiBold(),
),
const Gap(16), const Gap(16),
DropdownMenu( Text(context.l10n.language).small(),
initialSelection: preferences.locale, const Gap(8),
onSelected: (locale) { SizedBox(
width: double.infinity,
child: Select<Locale>(
value: preferences.locale,
onChanged: (locale) {
if (locale == null) return; if (locale == null) return;
ref ref
.read(userPreferencesProvider.notifier) .read(userPreferencesProvider.notifier)
.setLocale(locale); .setLocale(locale);
}, },
hintText: context.l10n.system_default, placeholder: Text(context.l10n.system_default),
label: Text(context.l10n.language), itemBuilder: (context, value) =>
inputDecorationTheme: value.languageCode == "system"
const InputDecorationTheme(isDense: true), ? Text(context.l10n.system_default)
dropdownMenuEntries: [ : Text(
DropdownMenuEntry( LanguageLocals.getDisplayLanguage(
value.languageCode)
.toString(),
),
searchPlaceholder: Text(context.l10n.search),
searchFilter: (locale, query) {
final language = LanguageLocals.getDisplayLanguage(
locale.languageCode)
.toString();
return language
.toLowerCase()
.contains(query.toLowerCase())
? 1
: 0;
},
children: [
SelectItemButton(
value: const Locale("system", "system"), value: const Locale("system", "system"),
label: context.l10n.system_default, child: Text(context.l10n.system_default),
), ),
for (final locale in L10n.all) for (final locale in L10n.all)
DropdownMenuEntry( SelectItemButton(
value: locale, value: locale,
label: LanguageLocals.getDisplayLanguage( child: Text(
LanguageLocals.getDisplayLanguage(
locale.languageCode) locale.languageCode)
.toString(), .toString(),
), ),
),
], ],
), ),
),
], ],
), ),
const Gap(48), const Gap(48),
Align( Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Directionality( child: Button.primary(
textDirection: TextDirection.rtl, trailing: const Icon(SpotubeIcons.angleRight),
child: FilledButton.icon(
icon: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.next),
onPressed: onNext, onPressed: onNext,
), child: Text(context.l10n.next),
), ),
), ),
], ],

View File

@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/env.dart'; import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/getting_started/blur_card.dart'; import 'package:spotube/modules/getting_started/blur_card.dart';
@ -16,7 +15,6 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final onLogin = useLoginCallback(ref); final onLogin = useLoginCallback(ref);
return Center( return Center(
@ -34,9 +32,8 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
context.l10n.help_project_grow, context.l10n.help_project_grow,
style: style: const TextStyle(color: Colors.pink),
textTheme.titleMedium?.copyWith(color: Colors.pink), ).semiBold(),
),
], ],
), ),
const Gap(16), const Gap(16),
@ -46,38 +43,57 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
FilledButton.icon( Button(
icon: const Icon(SpotubeIcons.github), leading: const Icon(SpotubeIcons.github),
label: Text(context.l10n.contribute_on_github), style: ButtonVariance.primary.copyWith(
style: FilledButton.styleFrom( decoration: (context, states, value) {
backgroundColor: Colors.black, if (states.isNotEmpty) {
foregroundColor: Colors.white, return ButtonVariance.primary
shape: RoundedRectangleBorder( .decoration(context, states);
}
return BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), );
), }),
onPressed: () async { onPressed: () async {
await launchUrlString( await launchUrlString(
"https://github.com/KRTirtho/spotube", "https://github.com/KRTirtho/spotube",
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
}, },
child: Text(
context.l10n.contribute_on_github,
style: const TextStyle(color: Colors.white),
),
), ),
if (!Env.hideDonations) ...[ if (!Env.hideDonations) ...[
const Gap(16), const Gap(16),
FilledButton.icon( Button(
icon: const Icon(SpotubeIcons.openCollective), leading: const Icon(SpotubeIcons.openCollective),
label: Text(context.l10n.donate_on_open_collective), style: ButtonVariance.primary.copyWith(
style: FilledButton.styleFrom( decoration: (context, states, value) {
backgroundColor: const Color(0xff4cb7f6), if (states.isNotEmpty) {
foregroundColor: Colors.white, return ButtonVariance.primary
), .decoration(context, states);
}
return BoxDecoration(
color: const Color(0xff4cb7f6),
borderRadius: BorderRadius.circular(8),
);
}),
onPressed: () async { onPressed: () async {
await launchUrlString( await launchUrlString(
"https://opencollective.com/spotube", "https://opencollective.com/spotube",
mode: LaunchMode.externalApplication, mode: LaunchMode.externalApplication,
); );
}, },
child: Text(
context.l10n.donate_on_open_collective,
style: const TextStyle(color: Colors.white),
),
), ),
] ]
], ],
@ -91,42 +107,40 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
DecoratedBox( Button.secondary(
decoration: BoxDecoration( leading: const Icon(SpotubeIcons.anonymous),
borderRadius: BorderRadius.circular(8),
gradient: LinearGradient(
colors: [
colorScheme.primary,
colorScheme.secondary,
],
),
),
child: TextButton.icon(
icon: const Icon(SpotubeIcons.anonymous),
label: Text(context.l10n.browse_anonymously),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
),
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) { if (context.mounted) {
context.goNamed(HomePage.name); context.goNamed(HomePage.name);
} }
}, },
), child: Text(context.l10n.browse_anonymously),
), ),
const Gap(16), const Gap(16),
FilledButton.icon( Button.primary(
icon: const Icon(SpotubeIcons.spotify), leading: const Icon(SpotubeIcons.spotify),
label: Text(context.l10n.connect_with_spotify), style: ButtonVariance.primary.copyWith(
style: FilledButton.styleFrom( decoration: (context, states, value) {
backgroundColor: const Color(0xff1db954), if (states.isNotEmpty) {
foregroundColor: Colors.white, return ButtonVariance.primary
.decoration(context, states);
}
return BoxDecoration(
color: const Color(0xff1db954),
borderRadius: BorderRadius.circular(8),
);
},
), ),
onPressed: () async { onPressed: () async {
await KVStoreService.setDoneGettingStarted(true); await KVStoreService.setDoneGettingStarted(true);
await onLogin(); await onLogin();
}, },
child: Text(
context.l10n.connect_with_spotify,
style: const TextStyle(color: Colors.white),
),
), ),
], ],
), ),

View File

@ -53,7 +53,7 @@ class PlaylistPage extends HookConsumerWidget {
), ),
pagination: PaginationProps( pagination: PaginationProps(
hasNextPage: tracks.asData?.value.hasMore ?? false, hasNextPage: tracks.asData?.value.hasMore ?? false,
isLoading: tracks.isLoadingNextPage, isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: tracksNotifier.fetchMore, onFetchMore: tracksNotifier.fetchMore,
onRefresh: () async { onRefresh: () async {
ref.invalidate(playlistTracksProvider(playlist.id!)); ref.invalidate(playlistTracksProvider(playlist.id!));

View File

@ -1,4 +1,5 @@
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
@ -159,21 +160,13 @@ class SearchPage extends HookConsumerWidget {
SizedBox( SizedBox(
height: mediaQuery.height * 0.2, height: mediaQuery.height * 0.2,
), ),
Icon( Undraw(
SpotubeIcons.web, illustration: UndrawIllustration.explore,
size: 120, color: theme.colorScheme.primary,
color: theme.colorScheme.foreground height: 200 * theme.scaling,
.withOpacity(0.7),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
Text( Text(context.l10n.search_to_get_results).large(),
context.l10n.search_to_get_results,
style: theme.typography.h3.copyWith(
fontWeight: FontWeight.w900,
color: theme.colorScheme.foreground
.withOpacity(0.5),
),
),
], ],
), ),
(false, true) => Container( (false, true) => Container(

View File

@ -963,18 +963,26 @@ packages:
source: hosted source: hosted
version: "1.1.1" version: "1.1.1"
flutter_svg: flutter_svg:
dependency: transitive dependency: "direct overridden"
description: description:
name: flutter_svg name: flutter_svg
sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2" sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.6" version: "2.0.17"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_undraw:
dependency: "direct main"
description:
name: flutter_undraw
sha256: "17fe2738231c502171f984c003f6e40979de1a2550ef2debdd29fec27ae006ea"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
flutter_web_plugins: flutter_web_plugins:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -1662,14 +1670,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" version: "1.9.1"
path_drawing:
dependency: transitive
description:
name: path_drawing
sha256: bbb1934c0cbb03091af082a6389ca2080345291ef07a5fa6d6e078ba8682f977
url: "https://pub.dev"
source: hosted
version: "1.0.1"
path_parsing: path_parsing:
dependency: transitive dependency: transitive
description: description:
@ -2525,6 +2525,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.5.1" version: "4.5.1"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7"
url: "https://pub.dev"
source: hosted
version: "1.1.15"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:

View File

@ -63,6 +63,7 @@ dependencies:
flutter_riverpod: ^2.5.1 flutter_riverpod: ^2.5.1
flutter_secure_storage: ^9.0.0 flutter_secure_storage: ^9.0.0
flutter_sharing_intent: ^1.1.0 flutter_sharing_intent: ^1.1.0
flutter_undraw: ^0.2.0
form_builder_validators: ^11.1.1 form_builder_validators: ^11.1.1
form_validator: ^2.1.1 form_validator: ^2.1.1
freezed_annotation: ^2.4.1 freezed_annotation: ^2.4.1
@ -163,6 +164,7 @@ dependency_overrides:
path: packages/bonsoir_android path: packages/bonsoir_android
web: ^1.1.0 web: ^1.1.0
meta: 1.16.0 meta: 1.16.0
flutter_svg: ^2.0.17
flutter: flutter:
generate: true generate: true
@ -174,6 +176,14 @@ flutter:
- assets/backgrounds/ - assets/backgrounds/
- assets/patterns/ - assets/patterns/
- LICENSE - LICENSE
- packages/flutter_undraw/assets/undraw/access_denied.svg
- packages/flutter_undraw/assets/undraw/fixing_bugs.svg
- packages/flutter_undraw/assets/undraw/secure_login.svg
- packages/flutter_undraw/assets/undraw/explore.svg
- packages/flutter_undraw/assets/undraw/dreamer.svg
- packages/flutter_undraw/assets/undraw/happy_music.svg
- packages/flutter_undraw/assets/undraw/follow_me_drone.svg
- packages/flutter_undraw/assets/undraw/taken.svg
fonts: fonts:
- family: GeistSans - family: GeistSans
fonts: fonts:

View File

@ -9,7 +9,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"bn": [ "bn": [
@ -22,7 +27,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ca": [ "ca": [
@ -35,7 +45,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"cs": [ "cs": [
@ -48,7 +63,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"de": [ "de": [
@ -61,7 +81,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"es": [ "es": [
@ -74,7 +99,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"eu": [ "eu": [
@ -87,7 +117,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"fa": [ "fa": [
@ -100,7 +135,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"fi": [ "fi": [
@ -113,7 +153,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"fr": [ "fr": [
@ -126,7 +171,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"hi": [ "hi": [
@ -139,7 +189,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"id": [ "id": [
@ -152,7 +207,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"it": [ "it": [
@ -165,7 +225,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ja": [ "ja": [
@ -178,7 +243,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ka": [ "ka": [
@ -191,7 +261,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ko": [ "ko": [
@ -204,7 +279,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ne": [ "ne": [
@ -217,7 +297,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"nl": [ "nl": [
@ -230,7 +315,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"pl": [ "pl": [
@ -243,7 +333,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"pt": [ "pt": [
@ -256,7 +351,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"ru": [ "ru": [
@ -269,7 +369,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"th": [ "th": [
@ -282,7 +387,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"tr": [ "tr": [
@ -295,7 +405,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"uk": [ "uk": [
@ -308,7 +423,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"vi": [ "vi": [
@ -321,7 +441,12 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
], ],
"zh": [ "zh": [
@ -334,6 +459,11 @@
"add_all_to_queue", "add_all_to_queue",
"play_all_next", "play_all_next",
"pause", "pause",
"view_all" "view_all",
"no_tracks_added_yet",
"no_tracks",
"no_tracks_listened_yet",
"not_following_artists",
"no_favorite_albums_yet"
] ]
} }