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];
}
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 {
const $AssetsLogosGen();
@ -140,6 +151,8 @@ class Assets {
AssetGenImage('assets/bengali-patterns-bg.jpg');
static const AssetGenImage branding = AssetGenImage('assets/branding.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 jiosaavn = AssetGenImage('assets/jiosaavn.png');
static const AssetGenImage likedTracks =

View File

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

View File

@ -1,4 +1,5 @@
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_extension.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/waypoint.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:very_good_infinite_list/very_good_infinite_list.dart';
const _dummyPlaybuttonCard = PlaybuttonCard(
@ -99,7 +101,28 @@ class PlaybuttonView extends StatelessWidget {
const SliverGap(10),
// Toggle between grid and list view
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,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 150 * scale,
@ -150,6 +173,23 @@ class PlaybuttonView extends StatelessWidget {
onFetchData: onRequestMore,
hasReachedMax: !hasMore,
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_undraw/flutter_undraw.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:spotube/collections/fake.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_tile/track_tile.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:very_good_infinite_list/very_good_infinite_list.dart';
@ -25,6 +28,30 @@ class PresentationListSection extends HookConsumerWidget {
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(
isLoading: options.pagination.isLoading,
onFetchData: options.pagination.onFetchMore,

View File

@ -409,5 +409,10 @@
"add_all_to_queue": "Add all to queue",
"play_all_next": "Play all next",
"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:shadcn_flutter/shadcn_flutter.dart';
class BlurCard extends HookConsumerWidget {
final Widget child;
@ -18,8 +16,7 @@ class BlurCard extends HookConsumerWidget {
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: double.infinity,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: SurfaceCard(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: child,

View File

@ -1,5 +1,7 @@
import 'package:flutter_undraw/flutter_undraw.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:spotify/spotify.dart';
import 'package:spotube/components/horizontal_playbutton_card_view/horizontal_playbutton_card_view.dart';
@ -15,6 +17,21 @@ class HomeFeaturedSection extends HookConsumerWidget {
final featuredPlaylistsNotifier =
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(
enabled: featuredPlaylists.isLoading,
child: HorizontalPlaybuttonCardView<PlaylistSimple>(

View File

@ -89,6 +89,7 @@ class GenreSectionCard extends HookConsumerWidget {
),
],
),
if (playlists?.hasError != true)
Expanded(
child: Skeleton.ignore(
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:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.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/components/playbutton_view/playbutton_view.dart';
@ -75,6 +77,30 @@ class UserAlbums extends HookConsumerWidget {
),
),
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(
padding: const EdgeInsets.symmetric(horizontal: 8),
sliver: PlaybuttonView(

View File

@ -1,9 +1,11 @@
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:collection/collection.dart';
import 'package:flutter_undraw/flutter_undraw.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.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:spotube/collections/fake.dart';
@ -79,11 +81,10 @@ class UserArtists extends HookConsumerWidget {
),
),
const SliverGap(10),
if (filteredArtists.isNotEmpty)
SliverLayoutBuilder(builder: (context, constrains) {
return SliverGrid.builder(
itemCount: filteredArtists.isEmpty
? 6
: filteredArtists.length + 1,
itemCount: filteredArtists.length + 1,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200,
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)),
],
),

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:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/album_item.dart';
@ -31,6 +33,24 @@ class TopAlbums extends HookConsumerWidget {
isLoading: topAlbums.isLoading && !topAlbums.isLoadingNextPage,
hasReachedMax: topAlbums.asData?.value.hasMore ?? true,
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) {
final album = albumsData[index];
return StatsAlbumItem(

View File

@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_undraw/flutter_undraw.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:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/artist_item.dart';
@ -35,6 +37,24 @@ class TopArtists extends HookConsumerWidget {
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
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) {
final artist = artistsData[index];
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:spotube/collections/formatters.dart';
import 'package:spotube/modules/stats/common/track_item.dart';
@ -33,6 +35,24 @@ class TopTracks extends HookConsumerWidget {
isLoading: topTracks.isLoading && !topTracks.isLoadingNextPage,
hasReachedMax: topTracks.asData?.value.hasMore ?? true,
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) {
final track = tracksData[index];
return StatsTrackItem(

View File

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

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.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/components/titlebar/titlebar.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/region.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 {
static const name = "getting_started";
@ -18,12 +16,6 @@ class GettingStarting extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final preferences = ref.watch(userPreferencesProvider);
final themeData = theme(
preferences.accentColorScheme,
Brightness.dark,
preferences.amoledDarkTheme,
);
final pageController = usePageController();
final onNext = useCallback(() {
@ -40,11 +32,11 @@ class GettingStarting extends HookConsumerWidget {
);
}, [pageController]);
return Theme(
data: themeData,
child: Scaffold(
appBar: TitleBar(
return Scaffold(
headers: [
TitleBar(
backgroundColor: Colors.transparent,
surfaceBlur: 0,
trailing: [
ListenableBuilder(
listenable: pageController,
@ -54,7 +46,7 @@ class GettingStarting extends HookConsumerWidget {
child: pageController.hasClients &&
(pageController.page == 0 || pageController.page == 3)
? const SizedBox()
: TextButton(
: Button.secondary(
onPressed: () {
pageController.animateToPage(
3,
@ -62,29 +54,20 @@ class GettingStarting extends HookConsumerWidget {
curve: Curves.easeInOut,
);
},
child: Text(
context.l10n.skip_this_nonsense,
style: TextStyle(
decoration: TextDecoration.underline,
decorationColor: themeData.colorScheme.primary,
),
),
child: Text(context.l10n.skip_this_nonsense),
),
);
},
),
],
),
extendBodyBehindAppBar: true,
body: DecoratedBox(
],
floatingHeader: true,
child: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: Assets.bengaliPatternsBg.provider(),
fit: BoxFit.cover,
colorFilter: const ColorFilter.mode(
Colors.black38,
BlendMode.srcOver,
),
),
),
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/assets.gen.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/modules/getting_started/blur_card.dart';
import 'package:spotube/utils/platform.dart';
class GettingStartedPageGreetingSection extends HookConsumerWidget {
@ -13,8 +12,6 @@ class GettingStartedPageGreetingSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
return Center(
child: BlurCard(
child: Column(
@ -22,30 +19,19 @@ class GettingStartedPageGreetingSection extends HookConsumerWidget {
children: [
Assets.spotubeLogoPng.image(height: 200),
const Gap(24),
Text(
"Spotube",
style:
textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Text("Spotube").semiBold().h4(),
const Gap(4),
Text(
kIsMobile
? context.l10n.freedom_of_music_palm
: context.l10n.freedom_of_music,
textAlign: TextAlign.center,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w300,
fontStyle: FontStyle.italic,
),
),
).light().large().italic(),
const Gap(84),
Directionality(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
Button.primary(
onPressed: onNext,
icon: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.get_started),
),
trailing: const Icon(SpotubeIcons.angleRight),
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:gap/gap.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/spotube_icons.dart';
import 'package:spotube/components/ui/button_tile.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart';
@ -14,14 +14,14 @@ final audioSourceToIconMap = {
AudioSource.youtube: const Icon(
SpotubeIcons.youtube,
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(
borderRadius: BorderRadius.circular(48),
child: Assets.invidious.image(width: 48, height: 48),
borderRadius: BorderRadius.circular(26),
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 {
@ -36,8 +36,6 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme, :dividerColor) =
Theme.of(context);
final preferences = ref.watch(userPreferencesProvider);
final preferencesNotifier = ref.read(userPreferencesProvider.notifier);
@ -62,76 +60,56 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
children: [
const Icon(SpotubeIcons.album, size: 16),
const Gap(8),
Text(context.l10n.playback, style: textTheme.titleMedium),
Text(context.l10n.playback).semiBold().large(),
],
),
const Gap(16),
ListTile(
title: Text(
context.l10n.select_audio_source,
style: textTheme.titleMedium,
),
Align(
alignment: Alignment.centerLeft,
child: Text(context.l10n.select_audio_source).semiBold().large(),
),
const Gap(16),
ToggleButtons(
isSelected: [
for (final source in AudioSource.values)
preferences.audioSource == source,
],
onPressed: (index) {
preferencesNotifier.setAudioSource(AudioSource.values[index]);
Select<AudioSource>(
value: preferences.audioSource,
onChanged: (value) {
if (value == null) return;
preferencesNotifier.setAudioSource(value);
},
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: [
for (final source in AudioSource.values)
SizedBox.square(
dimension: 84,
child: Column(
SelectItemButton(
value: source,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
spacing: 6,
children: [
audioSourceToIconMap[source]!,
const Gap(8),
Text(
source.name.capitalize(),
style: textTheme.bodySmall!.copyWith(
color: preferences.audioSource == source
? colorScheme.primary
: null,
),
),
Text(source.name.capitalize()),
],
),
),
],
),
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),
ListTile(
Text(
audioSourceToDescription[preferences.audioSource]!,
).small().muted(),
const Gap(16),
ButtonTile(
title: Text(context.l10n.endless_playback),
subtitle: Text(
context.l10n.endless_playback_description,
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
onTap: () {
).small().muted(),
onPressed: () {
preferencesNotifier
.setEndlessPlayback(!preferences.endlessPlayback);
},
@ -146,17 +124,17 @@ class GettingStartedPagePlaybackSection extends HookConsumerWidget {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FilledButton.icon(
icon: const Icon(SpotubeIcons.angleLeft),
label: Text(context.l10n.previous),
Button.secondary(
leading: const Icon(SpotubeIcons.angleLeft),
onPressed: onPrevious,
child: Text(context.l10n.previous),
),
Directionality(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
icon: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.next),
child: Button.primary(
leading: const Icon(SpotubeIcons.angleRight),
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:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/collections/language_codes.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart';
@ -16,7 +16,6 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :dividerColor) = Theme.of(context);
final preferences = ref.watch(userPreferencesProvider);
return SafeArea(
@ -32,92 +31,119 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
size: 16,
),
const SizedBox(width: 8),
Text(
context.l10n.language_region,
style: textTheme.titleMedium,
),
Text(context.l10n.language_region).semiBold(),
],
),
const Gap(48),
const Gap(30),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
context.l10n.choose_your_region,
style: textTheme.titleSmall,
),
Text(context.l10n.choose_your_region).semiBold(),
Text(
context.l10n.choose_your_region_description,
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
).small().muted(),
const Gap(16),
DropdownMenu(
initialSelection: preferences.market,
onSelected: (value) {
Text(context.l10n.market_place_region).small(),
const Gap(8),
SizedBox(
width: double.infinity,
child: Select<Market>(
value: preferences.market,
onChanged: (value) {
if (value == null) return;
ref
.read(userPreferencesProvider.notifier)
.setRecommendationMarket(value);
},
hintText: preferences.market.name,
label: Text(context.l10n.market_place_region),
inputDecorationTheme:
const InputDecorationTheme(isDense: true),
dropdownMenuEntries: [
placeholder: Text(preferences.market.name),
itemBuilder: (context, value) => Text(
spotifyMarkets
.firstWhere((element) => element.$1 == value)
.$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)
DropdownMenuEntry(
SelectItemButton(
value: market.$1,
label: market.$2,
child: Text(market.$2),
),
],
),
),
const Gap(36),
Text(
context.l10n.choose_your_language,
style: textTheme.titleSmall,
),
).semiBold(),
const Gap(16),
DropdownMenu(
initialSelection: preferences.locale,
onSelected: (locale) {
Text(context.l10n.language).small(),
const Gap(8),
SizedBox(
width: double.infinity,
child: Select<Locale>(
value: preferences.locale,
onChanged: (locale) {
if (locale == null) return;
ref
.read(userPreferencesProvider.notifier)
.setLocale(locale);
},
hintText: context.l10n.system_default,
label: Text(context.l10n.language),
inputDecorationTheme:
const InputDecorationTheme(isDense: true),
dropdownMenuEntries: [
DropdownMenuEntry(
placeholder: Text(context.l10n.system_default),
itemBuilder: (context, value) =>
value.languageCode == "system"
? Text(context.l10n.system_default)
: Text(
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"),
label: context.l10n.system_default,
child: Text(context.l10n.system_default),
),
for (final locale in L10n.all)
DropdownMenuEntry(
SelectItemButton(
value: locale,
label: LanguageLocals.getDisplayLanguage(
child: Text(
LanguageLocals.getDisplayLanguage(
locale.languageCode)
.toString(),
),
),
],
),
),
],
),
const Gap(48),
Align(
alignment: Alignment.centerRight,
child: Directionality(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
icon: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.next),
child: Button.primary(
trailing: const Icon(SpotubeIcons.angleRight),
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/modules/getting_started/blur_card.dart';
@ -16,7 +15,6 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
final onLogin = useLoginCallback(ref);
return Center(
@ -34,9 +32,8 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
const SizedBox(width: 8),
Text(
context.l10n.help_project_grow,
style:
textTheme.titleMedium?.copyWith(color: Colors.pink),
),
style: const TextStyle(color: Colors.pink),
).semiBold(),
],
),
const Gap(16),
@ -46,38 +43,57 @@ class GettingStartedScreenSupportSection extends HookConsumerWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
icon: const Icon(SpotubeIcons.github),
label: Text(context.l10n.contribute_on_github),
style: FilledButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
Button(
leading: const Icon(SpotubeIcons.github),
style: ButtonVariance.primary.copyWith(
decoration: (context, states, value) {
if (states.isNotEmpty) {
return ButtonVariance.primary
.decoration(context, states);
}
return BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(8),
),
),
);
}),
onPressed: () async {
await launchUrlString(
"https://github.com/KRTirtho/spotube",
mode: LaunchMode.externalApplication,
);
},
child: Text(
context.l10n.contribute_on_github,
style: const TextStyle(color: Colors.white),
),
),
if (!Env.hideDonations) ...[
const Gap(16),
FilledButton.icon(
icon: const Icon(SpotubeIcons.openCollective),
label: Text(context.l10n.donate_on_open_collective),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xff4cb7f6),
foregroundColor: Colors.white,
),
Button(
leading: const Icon(SpotubeIcons.openCollective),
style: ButtonVariance.primary.copyWith(
decoration: (context, states, value) {
if (states.isNotEmpty) {
return ButtonVariance.primary
.decoration(context, states);
}
return BoxDecoration(
color: const Color(0xff4cb7f6),
borderRadius: BorderRadius.circular(8),
);
}),
onPressed: () async {
await launchUrlString(
"https://opencollective.com/spotube",
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(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
DecoratedBox(
decoration: BoxDecoration(
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,
),
Button.secondary(
leading: const Icon(SpotubeIcons.anonymous),
onPressed: () async {
await KVStoreService.setDoneGettingStarted(true);
if (context.mounted) {
context.goNamed(HomePage.name);
}
},
),
child: Text(context.l10n.browse_anonymously),
),
const Gap(16),
FilledButton.icon(
icon: const Icon(SpotubeIcons.spotify),
label: Text(context.l10n.connect_with_spotify),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xff1db954),
foregroundColor: Colors.white,
Button.primary(
leading: const Icon(SpotubeIcons.spotify),
style: ButtonVariance.primary.copyWith(
decoration: (context, states, value) {
if (states.isNotEmpty) {
return ButtonVariance.primary
.decoration(context, states);
}
return BoxDecoration(
color: const Color(0xff1db954),
borderRadius: BorderRadius.circular(8),
);
},
),
onPressed: () async {
await KVStoreService.setDoneGettingStarted(true);
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(
hasNextPage: tracks.asData?.value.hasMore ?? false,
isLoading: tracks.isLoadingNextPage,
isLoading: tracks.isLoading || tracks.isLoadingNextPage,
onFetchMore: tracksNotifier.fetchMore,
onRefresh: () async {
ref.invalidate(playlistTracksProvider(playlist.id!));

View File

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

View File

@ -963,18 +963,26 @@ packages:
source: hosted
version: "1.1.1"
flutter_svg:
dependency: transitive
dependency: "direct overridden"
description:
name: flutter_svg
sha256: "6ff9fa12892ae074092de2fa6a9938fb21dbabfdaa2ff57dc697ff912fc8d4b2"
sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b
url: "https://pub.dev"
source: hosted
version: "1.1.6"
version: "2.0.17"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
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:
dependency: transitive
description: flutter
@ -1662,14 +1670,6 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -2525,6 +2525,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View File

@ -63,6 +63,7 @@ dependencies:
flutter_riverpod: ^2.5.1
flutter_secure_storage: ^9.0.0
flutter_sharing_intent: ^1.1.0
flutter_undraw: ^0.2.0
form_builder_validators: ^11.1.1
form_validator: ^2.1.1
freezed_annotation: ^2.4.1
@ -163,6 +164,7 @@ dependency_overrides:
path: packages/bonsoir_android
web: ^1.1.0
meta: 1.16.0
flutter_svg: ^2.0.17
flutter:
generate: true
@ -174,6 +176,14 @@ flutter:
- assets/backgrounds/
- assets/patterns/
- 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:
- family: GeistSans
fonts:

View File

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