feat: add getting started page

This commit is contained in:
Kingkor Roy Tirtho 2024-02-25 22:01:38 +06:00
parent ca71406505
commit 96a2a1f5a6
15 changed files with 896 additions and 225 deletions

View File

@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/collections/routes.dart'; import 'package:spotube/collections/routes.dart';
import 'package:spotube/components/player/player_controls.dart';
import 'package:spotube/models/logger.dart'; import 'package:spotube/models/logger.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
@ -64,6 +64,7 @@ class HomeTabIntent extends Intent {
class HomeTabAction extends Action<HomeTabIntent> { class HomeTabAction extends Action<HomeTabIntent> {
@override @override
invoke(intent) { invoke(intent) {
final router = intent.ref.read(routerProvider);
switch (intent.tab) { switch (intent.tab) {
case HomeTabs.browse: case HomeTabs.browse:
router.go("/"); router.go("/");

View File

@ -6,6 +6,11 @@ class ISOLanguageName {
required this.name, required this.name,
required this.nativeName, required this.nativeName,
}); });
@override
String toString() {
return "$name ($nativeName)";
}
} }
// Uncomment the languages as we add support for them // Uncomment the languages as we add support for them

View File

@ -2,8 +2,10 @@ import 'package:catcher_2/catcher_2.dart';
import 'package:flutter/foundation.dart' hide Category; import 'package:flutter/foundation.dart' hide Category;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart' hide Search; import 'package:spotify/spotify.dart' hide Search;
import 'package:spotube/pages/album/album.dart'; import 'package:spotube/pages/album/album.dart';
import 'package:spotube/pages/getting_started/getting_started.dart';
import 'package:spotube/pages/home/genres/genre_playlists.dart'; import 'package:spotube/pages/home/genres/genre_playlists.dart';
import 'package:spotube/pages/home/genres/genres.dart'; import 'package:spotube/pages/home/genres/genres.dart';
import 'package:spotube/pages/home/home.dart'; import 'package:spotube/pages/home/home.dart';
@ -18,6 +20,8 @@ import 'package:spotube/pages/settings/blacklist.dart';
import 'package:spotube/pages/settings/about.dart'; import 'package:spotube/pages/settings/about.dart';
import 'package:spotube/pages/settings/logs.dart'; import 'package:spotube/pages/settings/logs.dart';
import 'package:spotube/pages/track/track.dart'; import 'package:spotube/pages/track/track.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/utils/platform.dart'; import 'package:spotube/utils/platform.dart';
import 'package:spotube/components/shared/spotube_page_route.dart'; import 'package:spotube/components/shared/spotube_page_route.dart';
import 'package:spotube/pages/artist/artist.dart'; import 'package:spotube/pages/artist/artist.dart';
@ -31,7 +35,8 @@ import 'package:spotube/pages/mobile_login/mobile_login.dart';
final rootNavigatorKey = Catcher2.navigatorKey; final rootNavigatorKey = Catcher2.navigatorKey;
final shellRouteNavigatorKey = GlobalKey<NavigatorState>(); final shellRouteNavigatorKey = GlobalKey<NavigatorState>();
final router = GoRouter( final routerProvider = Provider((ref) {
return GoRouter(
navigatorKey: rootNavigatorKey, navigatorKey: rootNavigatorKey,
routes: [ routes: [
ShellRoute( ShellRoute(
@ -40,7 +45,20 @@ final router = GoRouter(
routes: [ routes: [
GoRoute( GoRoute(
path: "/", path: "/",
pageBuilder: (context, state) => const SpotubePage(child: HomePage()), redirect: (context, state) async {
final authNotifier =
ref.read(AuthenticationNotifier.provider.notifier);
final json = await authNotifier.box.get(authNotifier.cacheKey);
if (json["cookie"] == null &&
!KVStoreService.doneGettingStarted) {
return "/getting-started";
}
return null;
},
pageBuilder: (context, state) =>
const SpotubePage(child: HomePage()),
routes: [ routes: [
GoRoute( GoRoute(
path: "genres", path: "genres",
@ -131,7 +149,8 @@ final router = GoRouter(
path: "/artist/:id", path: "/artist/:id",
pageBuilder: (context, state) { pageBuilder: (context, state) {
assert(state.pathParameters["id"] != null); assert(state.pathParameters["id"] != null);
return SpotubePage(child: ArtistPage(state.pathParameters["id"]!)); return SpotubePage(
child: ArtistPage(state.pathParameters["id"]!));
}, },
), ),
GoRoute( GoRoute(
@ -163,6 +182,13 @@ final router = GoRouter(
child: MiniLyricsPage(prevSize: state.extra as Size), child: MiniLyricsPage(prevSize: state.extra as Size),
), ),
), ),
GoRoute(
path: "/getting-started",
parentNavigatorKey: rootNavigatorKey,
pageBuilder: (context, state) => const SpotubePage(
child: GettingStarting(),
),
),
GoRoute( GoRoute(
path: "/login", path: "/login",
parentNavigatorKey: rootNavigatorKey, parentNavigatorKey: rootNavigatorKey,
@ -185,3 +211,4 @@ final router = GoRouter(
), ),
], ],
); );
});

View File

@ -112,4 +112,7 @@ abstract class SpotubeIcons {
static const discord = SimpleIcons.discord; static const discord = SimpleIcons.discord;
static const youtube = SimpleIcons.youtube; static const youtube = SimpleIcons.youtube;
static const radio = FeatherIcons.radio; static const radio = FeatherIcons.radio;
static const github = SimpleIcons.github;
static const openCollective = SimpleIcons.opencollective;
static const anonymous = FeatherIcons.user;
} }

View File

@ -0,0 +1,31 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class BlurCard extends HookConsumerWidget {
final Widget child;
const BlurCard({super.key, required this.child});
@override
Widget build(BuildContext context, ref) {
return Container(
margin: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
constraints: const BoxConstraints(maxWidth: 400),
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: double.infinity,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: child,
),
),
),
);
}
}

View File

@ -19,6 +19,8 @@ void useDeepLinking(WidgetRef ref) {
final spotify = ref.watch(spotifyProvider); final spotify = ref.watch(spotifyProvider);
final queryClient = useQueryClient(); final queryClient = useQueryClient();
final router = ref.watch(routerProvider);
useEffect(() { useEffect(() {
void uriListener(List<SharedFile> files) async { void uriListener(List<SharedFile> files) async {
for (final file in files) { for (final file in files) {

View File

@ -29,6 +29,7 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/cli/cli.dart'; import 'package:spotube/services/cli/cli.dart';
import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/services/connectivity_adapter.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/themes/theme.dart'; import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/persisted_state_notifier.dart';
import 'package:system_theme/system_theme.dart'; import 'package:system_theme/system_theme.dart';
@ -68,6 +69,9 @@ Future<void> main(List<String> rawArgs) async {
DiscordRPC.initialize(); DiscordRPC.initialize();
} }
await KVStoreService.initialize();
KVStoreService.doneGettingStarted = false;
final hiveCacheDir = final hiveCacheDir =
kIsWeb ? null : (await getApplicationSupportDirectory()).path; kIsWeb ? null : (await getApplicationSupportDirectory()).path;
@ -184,6 +188,7 @@ class SpotubeState extends ConsumerState<Spotube> {
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale)); final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
final paletteColor = final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider);
useDisableBatteryOptimizations(); useDisableBatteryOptimizations();
useInitSysTray(ref); useInitSysTray(ref);

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/components/shared/page_window_title_bar.dart';
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';
class GettingStarting extends HookConsumerWidget {
const GettingStarting({super.key});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:colorScheme) = Theme.of(context);
final pageController = usePageController();
final onNext = useCallback(() {
pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}, [pageController]);
final onPrevious = useCallback(() {
pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}, [pageController]);
return Scaffold(
appBar: PageWindowTitleBar(
backgroundColor: Colors.transparent,
actions: [
ListenableBuilder(
listenable: pageController,
builder: (context, _) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
child: pageController.hasClients &&
(pageController.page == 0 || pageController.page == 3)
? const SizedBox()
: TextButton(
onPressed: () {
pageController.animateToPage(
3,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
child: Text(
"Skip this nonsense",
style: TextStyle(
decoration: TextDecoration.underline,
decorationColor: colorScheme.primary,
),
),
),
);
},
),
],
),
extendBodyBehindAppBar: true,
body: DecoratedBox(
decoration: BoxDecoration(
image: DecorationImage(
image: Assets.bengaliPatternsBg.provider(),
fit: BoxFit.cover,
colorFilter: ColorFilter.mode(
colorScheme.background.withOpacity(0.2),
BlendMode.srcOver,
),
),
),
child: PageView(
controller: pageController,
children: [
GettingStartedPageGreetingSection(onNext: onNext),
GettingStartedPageLanguageRegionSection(onNext: onNext),
GettingStartedPagePlaybackSection(
onNext: onNext,
onPrevious: onPrevious,
),
const GettingStartedScreenSupportSection(),
],
),
),
);
}
}

View File

@ -0,0 +1,54 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/getting_started/blur_card.dart';
import 'package:spotube/utils/platform.dart';
class GettingStartedPageGreetingSection extends HookConsumerWidget {
final VoidCallback onNext;
const GettingStartedPageGreetingSection({super.key, required this.onNext});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme) = Theme.of(context);
return Center(
child: BlurCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Assets.spotubeLogoPng.image(height: 200),
const Gap(24),
Text(
"Spotube",
style:
textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
const Gap(4),
Text(
"“Freedom of music${kIsMobile ? "in the palm of your hands" : ""}",
textAlign: TextAlign.center,
style: textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w300,
fontStyle: FontStyle.italic,
),
),
const Gap(84),
Directionality(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
onPressed: onNext,
icon: const Icon(SpotubeIcons.angleRight),
label: const Text("Let's get started"),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,154 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
final audioSourceToIconMap = {
AudioSource.youtube: const Icon(
SpotubeIcons.youtube,
color: Colors.red,
size: 30,
),
AudioSource.piped: const Icon(SpotubeIcons.piped, size: 30),
AudioSource.jiosaavn: Assets.jiosaavn.image(width: 48, height: 48),
};
final audioSourceToDescription = {
AudioSource.youtube:
"Recommended and works best.\nHighest quality: 148kbps mp4, 128kbps opus",
AudioSource.piped: "Feeling free? Same as YouTube but a lot free",
AudioSource.jiosaavn:
"Best for South Asian region.\nHighest quality: 320kbps mp4",
};
class GettingStartedPagePlaybackSection extends HookConsumerWidget {
final VoidCallback onNext;
final VoidCallback onPrevious;
const GettingStartedPagePlaybackSection({
super.key,
required this.onNext,
required this.onPrevious,
});
@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);
return Center(
child: BlurCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Icon(SpotubeIcons.album, size: 16),
const Gap(8),
Text(context.l10n.playback, style: textTheme.titleMedium),
],
),
const Gap(16),
ListTile(
title: Text("Select Audio Source", style: textTheme.titleMedium),
),
const Gap(16),
ToggleButtons(
isSelected: [
for (final source in AudioSource.values)
preferences.audioSource == source,
],
onPressed: (index) {
preferencesNotifier.setAudioSource(AudioSource.values[index]);
},
borderRadius: BorderRadius.circular(8),
children: [
for (final source in AudioSource.values)
SizedBox.square(
dimension: 84,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
audioSourceToIconMap[source]!,
const Gap(8),
Text(
source.name,
style: textTheme.bodySmall!.copyWith(
color: preferences.audioSource == source
? colorScheme.primary
: null,
),
),
],
),
),
],
),
ListTile(
title: Align(
alignment: switch (preferences.audioSource) {
AudioSource.youtube => Alignment.centerLeft,
AudioSource.piped => Alignment.center,
AudioSource.jiosaavn => Alignment.centerRight,
},
child: Text(
audioSourceToDescription[preferences.audioSource]!,
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
),
),
const Gap(16),
ListTile(
title: Text(context.l10n.endless_playback),
subtitle: Text(
"Automatically append new songs\nto the end of the queue",
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
onTap: () {
preferencesNotifier
.setEndlessPlayback(!preferences.endlessPlayback);
},
trailing: Switch(
value: preferences.endlessPlayback,
onChanged: (value) {
preferencesNotifier.setEndlessPlayback(value);
},
),
),
const Gap(34),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
FilledButton.icon(
icon: const Icon(SpotubeIcons.angleLeft),
label: Text(context.l10n.previous),
onPressed: onPrevious,
),
Directionality(
textDirection: TextDirection.rtl,
child: FilledButton.icon(
icon: const Icon(SpotubeIcons.angleRight),
label: Text(context.l10n.next),
onPressed: onNext,
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/language_codes.dart';
import 'package:spotube/collections/spotify_markets.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/getting_started/blur_card.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/l10n/l10n.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
final void Function() onNext;
const GettingStartedPageLanguageRegionSection(
{super.key, required this.onNext});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :dividerColor) = Theme.of(context);
final preferences = ref.watch(userPreferencesProvider);
return SafeArea(
child: Center(
child: BlurCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
const Icon(
SpotubeIcons.language,
size: 16,
),
const SizedBox(width: 8),
Text(
"Language and Region",
style: textTheme.titleMedium,
),
],
),
const Gap(48),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Choose your region",
style: textTheme.titleSmall,
),
Text(
"This will help us show you the right content\nfor your location.",
style: textTheme.bodySmall?.copyWith(
color: dividerColor,
),
),
const Gap(16),
DropdownMenu(
initialSelection: preferences.recommendationMarket,
onSelected: (value) {
if (value == null) return;
ref
.read(userPreferencesProvider.notifier)
.setRecommendationMarket(value);
},
hintText: preferences.recommendationMarket.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,
),
],
),
const Gap(36),
Text(
"Choose your language",
style: textTheme.titleSmall,
),
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(),
),
],
),
],
),
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,
),
),
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,131 @@
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:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/getting_started/blur_card.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:url_launcher/url_launcher_string.dart';
class GettingStartedScreenSupportSection extends HookConsumerWidget {
const GettingStartedScreenSupportSection({super.key});
@override
Widget build(BuildContext context, ref) {
final ThemeData(:textTheme, :colorScheme) = Theme.of(context);
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BlurCard(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(SpotubeIcons.heartFilled, color: Colors.pink),
const SizedBox(width: 8),
Text(
"Help this project grow",
style:
textTheme.titleMedium?.copyWith(color: Colors.pink),
),
],
),
const Gap(16),
const Text(
"Spotube is an open-source project. You can help this project grow by contributing to the project, reporting bugs, or suggesting new features.",
),
const Gap(16),
Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
FilledButton.icon(
icon: const Icon(SpotubeIcons.github),
label: const Text("Contribute on GitHub"),
style: FilledButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
onPressed: () async {
await launchUrlString(
"https://github.com/KRTirtho/spotube",
mode: LaunchMode.externalApplication,
);
},
),
const Gap(16),
FilledButton.icon(
icon: const Icon(SpotubeIcons.openCollective),
label: const Text("Donate on Open Collective"),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xff4cb7f6),
foregroundColor: Colors.white,
),
onPressed: () async {
await launchUrlString(
"https://opencollective.com/spotube",
mode: LaunchMode.externalApplication,
);
},
),
],
),
],
),
),
const Gap(48),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 250),
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: const Text("Browse anonymously"),
style: TextButton.styleFrom(
foregroundColor: Colors.white,
),
onPressed: () {
KVStoreService.doneGettingStarted = true;
context.go("/");
},
),
),
const Gap(16),
FilledButton.icon(
icon: const Icon(SpotubeIcons.spotify),
label: const Text("Connect Spotify Account"),
style: FilledButton.styleFrom(
backgroundColor: const Color(0xff1db954),
foregroundColor: Colors.white,
),
onPressed: () {
KVStoreService.doneGettingStarted = true;
context.push("/login");
},
),
],
),
),
],
),
);
}
}

View File

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart'; import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
@ -10,7 +11,11 @@ import 'package:spotube/provider/user_preferences/user_preferences_provider.dart
import 'package:spotube/provider/user_preferences/user_preferences_state.dart'; import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
class SettingsAppearanceSection extends HookConsumerWidget { class SettingsAppearanceSection extends HookConsumerWidget {
const SettingsAppearanceSection({Key? key}) : super(key: key); final bool isGettingStarted;
const SettingsAppearanceSection({
Key? key,
this.isGettingStarted = false,
}) : super(key: key);
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
@ -24,9 +29,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
}); });
}, []); }, []);
return SectionCardWithHeading( final children = [
heading: context.l10n.appearance,
children: [
AdaptiveSelectTile<LayoutMode>( AdaptiveSelectTile<LayoutMode>(
secondary: const Icon(SpotubeIcons.dashboard), secondary: const Icon(SpotubeIcons.dashboard),
title: Text(context.l10n.layout_mode), title: Text(context.l10n.layout_mode),
@ -104,7 +107,23 @@ class SettingsAppearanceSection extends HookConsumerWidget {
value: preferences.albumColorSync, value: preferences.albumColorSync,
onChanged: preferencesNotifier.setAlbumColorSync, onChanged: preferencesNotifier.setAlbumColorSync,
), ),
];
if (isGettingStarted) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (final child in children) ...[
child,
const Gap(16),
],
], ],
); );
} }
return SectionCardWithHeading(
heading: context.l10n.appearance,
children: children,
);
}
} }

View File

@ -0,0 +1,15 @@
import 'package:shared_preferences/shared_preferences.dart';
abstract class KVStoreService {
static SharedPreferences? _sharedPreferences;
static SharedPreferences get sharedPreferences => _sharedPreferences!;
static Future<void> initialize() async {
_sharedPreferences = await SharedPreferences.getInstance();
}
static bool get doneGettingStarted =>
sharedPreferences.getBool('doneGettingStarted') ?? false;
static set doneGettingStarted(bool value) =>
sharedPreferences.setBool('doneGettingStarted', value);
}

View File

@ -119,7 +119,9 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
Future<void> _load() async { Future<void> _load() async {
final json = await box.get(cacheKey); final json = await box.get(cacheKey);
if (json != null) { if (json != null ||
(json is Map && json.entries.isNotEmpty) ||
(json is List && json.isNotEmpty)) {
state = await fromJson(castNestedJson(json)); state = await fromJson(castNestedJson(json));
} }
} }