mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: add getting started page
This commit is contained in:
parent
ca71406505
commit
96a2a1f5a6
@ -4,8 +4,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.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/components/player/player_controls.dart';
|
||||
import 'package:spotube/models/logger.dart';
|
||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||
@ -64,6 +64,7 @@ class HomeTabIntent extends Intent {
|
||||
class HomeTabAction extends Action<HomeTabIntent> {
|
||||
@override
|
||||
invoke(intent) {
|
||||
final router = intent.ref.read(routerProvider);
|
||||
switch (intent.tab) {
|
||||
case HomeTabs.browse:
|
||||
router.go("/");
|
||||
|
@ -6,6 +6,11 @@ class ISOLanguageName {
|
||||
required this.name,
|
||||
required this.nativeName,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "$name ($nativeName)";
|
||||
}
|
||||
}
|
||||
|
||||
// Uncomment the languages as we add support for them
|
||||
|
@ -2,8 +2,10 @@ import 'package:catcher_2/catcher_2.dart';
|
||||
import 'package:flutter/foundation.dart' hide Category;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart' hide Search;
|
||||
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/genres.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/logs.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/components/shared/spotube_page_route.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 shellRouteNavigatorKey = GlobalKey<NavigatorState>();
|
||||
final router = GoRouter(
|
||||
final routerProvider = Provider((ref) {
|
||||
return GoRouter(
|
||||
navigatorKey: rootNavigatorKey,
|
||||
routes: [
|
||||
ShellRoute(
|
||||
@ -40,7 +45,20 @@ final router = GoRouter(
|
||||
routes: [
|
||||
GoRoute(
|
||||
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: [
|
||||
GoRoute(
|
||||
path: "genres",
|
||||
@ -131,7 +149,8 @@ final router = GoRouter(
|
||||
path: "/artist/:id",
|
||||
pageBuilder: (context, state) {
|
||||
assert(state.pathParameters["id"] != null);
|
||||
return SpotubePage(child: ArtistPage(state.pathParameters["id"]!));
|
||||
return SpotubePage(
|
||||
child: ArtistPage(state.pathParameters["id"]!));
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
@ -163,6 +182,13 @@ final router = GoRouter(
|
||||
child: MiniLyricsPage(prevSize: state.extra as Size),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/getting-started",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
pageBuilder: (context, state) => const SpotubePage(
|
||||
child: GettingStarting(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: "/login",
|
||||
parentNavigatorKey: rootNavigatorKey,
|
||||
@ -185,3 +211,4 @@ final router = GoRouter(
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
@ -112,4 +112,7 @@ abstract class SpotubeIcons {
|
||||
static const discord = SimpleIcons.discord;
|
||||
static const youtube = SimpleIcons.youtube;
|
||||
static const radio = FeatherIcons.radio;
|
||||
static const github = SimpleIcons.github;
|
||||
static const openCollective = SimpleIcons.opencollective;
|
||||
static const anonymous = FeatherIcons.user;
|
||||
}
|
||||
|
31
lib/components/getting_started/blur_card.dart
Normal file
31
lib/components/getting_started/blur_card.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ void useDeepLinking(WidgetRef ref) {
|
||||
final spotify = ref.watch(spotifyProvider);
|
||||
final queryClient = useQueryClient();
|
||||
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useEffect(() {
|
||||
void uriListener(List<SharedFile> files) async {
|
||||
for (final file in files) {
|
||||
|
@ -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/cli/cli.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/utils/persisted_state_notifier.dart';
|
||||
import 'package:system_theme/system_theme.dart';
|
||||
@ -68,6 +69,9 @@ Future<void> main(List<String> rawArgs) async {
|
||||
DiscordRPC.initialize();
|
||||
}
|
||||
|
||||
await KVStoreService.initialize();
|
||||
KVStoreService.doneGettingStarted = false;
|
||||
|
||||
final hiveCacheDir =
|
||||
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
|
||||
|
||||
@ -184,6 +188,7 @@ class SpotubeState extends ConsumerState<Spotube> {
|
||||
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
|
||||
final paletteColor =
|
||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
useDisableBatteryOptimizations();
|
||||
useInitSysTray(ref);
|
||||
|
93
lib/pages/getting_started/getting_started.dart
Normal file
93
lib/pages/getting_started/getting_started.dart
Normal 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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
54
lib/pages/getting_started/sections/greeting.dart
Normal file
54
lib/pages/getting_started/sections/greeting.dart
Normal 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"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
154
lib/pages/getting_started/sections/playback.dart
Normal file
154
lib/pages/getting_started/sections/playback.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
129
lib/pages/getting_started/sections/region.dart
Normal file
129
lib/pages/getting_started/sections/region.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
131
lib/pages/getting_started/sections/support.dart
Normal file
131
lib/pages/getting_started/sections/support.dart
Normal 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");
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
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:spotube/collections/spotube_icons.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';
|
||||
|
||||
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
|
||||
Widget build(BuildContext context, ref) {
|
||||
@ -24,9 +29,7 @@ class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
});
|
||||
}, []);
|
||||
|
||||
return SectionCardWithHeading(
|
||||
heading: context.l10n.appearance,
|
||||
children: [
|
||||
final children = [
|
||||
AdaptiveSelectTile<LayoutMode>(
|
||||
secondary: const Icon(SpotubeIcons.dashboard),
|
||||
title: Text(context.l10n.layout_mode),
|
||||
@ -104,7 +107,23 @@ class SettingsAppearanceSection extends HookConsumerWidget {
|
||||
value: preferences.albumColorSync,
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
15
lib/services/kv_store/kv_store.dart
Normal file
15
lib/services/kv_store/kv_store.dart
Normal 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);
|
||||
}
|
@ -119,7 +119,9 @@ abstract class PersistedStateNotifier<T> extends StateNotifier<T> {
|
||||
Future<void> _load() async {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user