mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
feat: show placeholder images where there is no item or on empty page
This commit is contained in:
parent
a8694a7a8b
commit
b8ffb9b75f
@ -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 =
|
||||
|
@ -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),
|
||||
|
@ -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,38 +101,59 @@ class PlaybuttonView extends StatelessWidget {
|
||||
const SliverGap(10),
|
||||
// Toggle between grid and list view
|
||||
switch ((isGrid.value, isLoading)) {
|
||||
(true, _) => SliverGrid.builder(
|
||||
itemCount: isLoading ? 6 : itemCount + 1,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 150 * scale,
|
||||
mainAxisExtent: 225 * scale,
|
||||
crossAxisSpacing: 12 * scale,
|
||||
mainAxisSpacing: 12 * scale,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (isLoading) {
|
||||
return const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
);
|
||||
}
|
||||
|
||||
if (index == itemCount) {
|
||||
if (!hasMore) return const SizedBox.shrink();
|
||||
return Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: onRequestMore,
|
||||
child: const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
(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,
|
||||
mainAxisExtent: 225 * scale,
|
||||
crossAxisSpacing: 12 * scale,
|
||||
mainAxisSpacing: 12 * scale,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (isLoading) {
|
||||
return const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
);
|
||||
}
|
||||
|
||||
return gridItemBuilder(context, index);
|
||||
},
|
||||
),
|
||||
if (index == itemCount) {
|
||||
if (!hasMore) return const SizedBox.shrink();
|
||||
return Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: onRequestMore,
|
||||
child: const Skeletonizer(
|
||||
enabled: true,
|
||||
child: _dummyPlaybuttonCard,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return gridItemBuilder(context, index);
|
||||
},
|
||||
),
|
||||
(false, true) => Skeletonizer.sliver(
|
||||
enabled: true,
|
||||
child: SliverList(
|
||||
@ -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()
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
}
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -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"
|
||||
}
|
@ -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,
|
||||
|
@ -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>(
|
||||
|
@ -89,23 +89,24 @@ class GenreSectionCard extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Skeleton.ignore(
|
||||
child: Skeletonizer(
|
||||
enabled: playlists?.isLoading ?? false,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: playlistsData.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlistsData.elementAt(index);
|
||||
if (playlists?.hasError != true)
|
||||
Expanded(
|
||||
child: Skeleton.ignore(
|
||||
child: Skeletonizer(
|
||||
enabled: playlists?.isLoading ?? false,
|
||||
child: ListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: playlistsData.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
itemBuilder: (context, index) {
|
||||
final playlist = playlistsData.elementAt(index);
|
||||
|
||||
return GenreSectionCardPlaylistCard(playlist: playlist);
|
||||
},
|
||||
return GenreSectionCardPlaylistCard(playlist: playlist);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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,21 +77,45 @@ class UserAlbums extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SliverGap(10),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
sliver: PlaybuttonView(
|
||||
controller: controller,
|
||||
itemCount: albums.length,
|
||||
hasMore: albumsQuery.asData?.value.hasMore == true,
|
||||
isLoading: albumsQuery.isLoading,
|
||||
onRequestMore: albumsQueryNotifier.fetchMore,
|
||||
gridItemBuilder: (context, index) => AlbumCard(
|
||||
albums[index],
|
||||
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(
|
||||
controller: controller,
|
||||
itemCount: albums.length,
|
||||
hasMore: albumsQuery.asData?.value.hasMore == true,
|
||||
isLoading: albumsQuery.isLoading,
|
||||
onRequestMore: albumsQueryNotifier.fetchMore,
|
||||
gridItemBuilder: (context, index) => AlbumCard(
|
||||
albums[index],
|
||||
),
|
||||
listItemBuilder: (context, index) =>
|
||||
AlbumCard.tile(albums[index]),
|
||||
),
|
||||
listItemBuilder: (context, index) =>
|
||||
AlbumCard.tile(albums[index]),
|
||||
),
|
||||
),
|
||||
const SliverSafeArea(sliver: SliverGap(10)),
|
||||
],
|
||||
),
|
||||
|
@ -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,45 +81,82 @@ class UserArtists extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SliverGap(10),
|
||||
SliverLayoutBuilder(builder: (context, constrains) {
|
||||
return SliverGrid.builder(
|
||||
itemCount: filteredArtists.isEmpty
|
||||
? 6
|
||||
: filteredArtists.length + 1,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (filteredArtists.isNotEmpty &&
|
||||
index == filteredArtists.length) {
|
||||
if (artistQuery.asData?.value.hasMore != true) {
|
||||
return const SizedBox.shrink();
|
||||
if (filteredArtists.isNotEmpty)
|
||||
SliverLayoutBuilder(builder: (context, constrains) {
|
||||
return SliverGrid.builder(
|
||||
itemCount: filteredArtists.length + 1,
|
||||
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 200,
|
||||
mainAxisExtent: constrains.smAndDown ? 225 : 250,
|
||||
crossAxisSpacing: 8,
|
||||
mainAxisSpacing: 8,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
if (filteredArtists.isNotEmpty &&
|
||||
index == filteredArtists.length) {
|
||||
if (artistQuery.asData?.value.hasMore != true) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: artistQueryNotifier.fetchMore,
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: ArtistCard(FakeData.artist),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Waypoint(
|
||||
controller: controller,
|
||||
isGrid: true,
|
||||
onTouchEdge: artistQueryNotifier.fetchMore,
|
||||
child: Skeletonizer(
|
||||
enabled: true,
|
||||
child: ArtistCard(FakeData.artist),
|
||||
return Skeletonizer(
|
||||
enabled: artistQuery.isLoading,
|
||||
child: ArtistCard(
|
||||
filteredArtists.elementAtOrNull(index) ??
|
||||
FakeData.artist,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: artistQuery.isLoading,
|
||||
child: ArtistCard(
|
||||
filteredArtists.elementAtOrNull(index) ??
|
||||
FakeData.artist,
|
||||
},
|
||||
);
|
||||
})
|
||||
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)),
|
||||
],
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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(
|
||||
|
@ -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();
|
||||
},
|
||||
|
@ -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,43 +54,33 @@ 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(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: Assets.bengaliPatternsBg.provider(),
|
||||
fit: BoxFit.cover,
|
||||
colorFilter: const ColorFilter.mode(
|
||||
Colors.black38,
|
||||
BlendMode.srcOver,
|
||||
),
|
||||
],
|
||||
floatingHeader: true,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
image: Assets.bengaliPatternsBg.provider(),
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
children: [
|
||||
GettingStartedPageGreetingSection(onNext: onNext),
|
||||
GettingStartedPageLanguageRegionSection(onNext: onNext),
|
||||
GettingStartedPagePlaybackSection(
|
||||
onNext: onNext,
|
||||
onPrevious: onPrevious,
|
||||
),
|
||||
),
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
children: [
|
||||
GettingStartedPageGreetingSection(onNext: onNext),
|
||||
GettingStartedPageLanguageRegionSection(onNext: onNext),
|
||||
GettingStartedPagePlaybackSection(
|
||||
onNext: onNext,
|
||||
onPrevious: onPrevious,
|
||||
),
|
||||
const GettingStartedScreenSupportSection(),
|
||||
],
|
||||
),
|
||||
const GettingStartedScreenSupportSection(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
@ -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(
|
||||
onPressed: onNext,
|
||||
icon: const Icon(SpotubeIcons.angleRight),
|
||||
label: Text(context.l10n.get_started),
|
||||
),
|
||||
Button.primary(
|
||||
onPressed: onNext,
|
||||
trailing: const Icon(SpotubeIcons.angleRight),
|
||||
child: Text(context.l10n.get_started),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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) {
|
||||
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: [
|
||||
for (final market in spotifyMarkets)
|
||||
DropdownMenuEntry(
|
||||
value: market.$1,
|
||||
label: market.$2,
|
||||
),
|
||||
],
|
||||
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);
|
||||
},
|
||||
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)
|
||||
SelectItemButton(
|
||||
value: market.$1,
|
||||
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) {
|
||||
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(
|
||||
value: const Locale("system", "system"),
|
||||
label: context.l10n.system_default,
|
||||
),
|
||||
for (final locale in L10n.all)
|
||||
DropdownMenuEntry(
|
||||
value: locale,
|
||||
label: LanguageLocals.getDisplayLanguage(
|
||||
locale.languageCode)
|
||||
.toString(),
|
||||
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);
|
||||
},
|
||||
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"),
|
||||
child: Text(context.l10n.system_default),
|
||||
),
|
||||
],
|
||||
for (final locale in L10n.all)
|
||||
SelectItemButton(
|
||||
value: locale,
|
||||
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),
|
||||
onPressed: onNext,
|
||||
),
|
||||
child: Button.primary(
|
||||
trailing: const Icon(SpotubeIcons.angleRight),
|
||||
onPressed: onNext,
|
||||
child: Text(context.l10n.next),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
),
|
||||
onPressed: () async {
|
||||
await KVStoreService.setDoneGettingStarted(true);
|
||||
if (context.mounted) {
|
||||
context.goNamed(HomePage.name);
|
||||
}
|
||||
},
|
||||
),
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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!));
|
||||
|
@ -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(
|
||||
|
30
pubspec.lock
30
pubspec.lock
@ -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:
|
||||
|
10
pubspec.yaml
10
pubspec.yaml
@ -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:
|
||||
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user