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/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("/");
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
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 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) {
|
||||||
|
@ -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);
|
||||||
|
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/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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user