diff --git a/assets/icon/Integrate-with-Samsung-IAP-05212021.pdf b/assets/icon/Integrate-with-Samsung-IAP-05212021.pdf
deleted file mode 100644
index 38e5078..0000000
Binary files a/assets/icon/Integrate-with-Samsung-IAP-05212021.pdf and /dev/null differ
diff --git a/assets/icon/app_icon.jpg b/assets/icon/app_icon.jpg
deleted file mode 100644
index b214607..0000000
Binary files a/assets/icon/app_icon.jpg and /dev/null differ
diff --git a/assets/icon/google.svg b/assets/icon/google.svg
new file mode 100644
index 0000000..d5616b3
--- /dev/null
+++ b/assets/icon/google.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/error/something_went_wrong.png b/assets/images/error/something_went_wrong.png
deleted file mode 100644
index 53e8b63..0000000
Binary files a/assets/images/error/something_went_wrong.png and /dev/null differ
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..7e7e7f6
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/lib/app.dart b/lib/app.dart
index 7111d33..cb1f695 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:varanasi_mobile_app/features/library/cubit/library_cubit.dart';
+import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
import 'package:varanasi_mobile_app/features/user-library/cubit/user_library_cubit.dart';
import 'package:varanasi_mobile_app/utils/constants/constants.dart';
import 'package:varanasi_mobile_app/utils/router.dart';
@@ -36,6 +37,7 @@ class Varanasi extends StatelessWidget {
create: (ctx) => MediaPlayerCubit()..init(),
),
BlocProvider(lazy: false, create: (ctx) => LibraryCubit()),
+ BlocProvider(lazy: false, create: (_) => SessionCubit()..init()),
],
child: Builder(builder: (context) {
final scheme = context.select(
diff --git a/lib/cubits/player/player_cubit.dart b/lib/cubits/player/player_cubit.dart
index 9b53db0..ebaf061 100644
--- a/lib/cubits/player/player_cubit.dart
+++ b/lib/cubits/player/player_cubit.dart
@@ -111,7 +111,10 @@ class MediaPlayerCubit extends AppCubit
media.moreInfoUrl,
options: CommonOptions(
transformer: (response) {
- final data = response as List;
+ if (response is! List) {
+ throw Exception('Invalid response');
+ }
+ final data = response;
if (data.isEmpty) {
throw Exception('No data found');
}
diff --git a/lib/features/home/bloc/home_state.dart b/lib/features/home/bloc/home_state.dart
index 716fe00..0b19d3c 100644
--- a/lib/features/home/bloc/home_state.dart
+++ b/lib/features/home/bloc/home_state.dart
@@ -1,6 +1,6 @@
part of 'home_bloc.dart';
-abstract class HomeState extends Equatable {
+sealed class HomeState extends Equatable {
const HomeState();
}
diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart
index 2bdc875..d3b17e9 100644
--- a/lib/features/home/ui/home_screen.dart
+++ b/lib/features/home/ui/home_screen.dart
@@ -54,10 +54,10 @@ class HomePage extends StatelessWidget {
],
),
body: TriStateVisibility(
- state: switch (state.runtimeType) {
- HomeErrorState => TriState.error,
- HomeLoadingState => TriState.loading,
- _ => TriState.loaded,
+ state: switch (state) {
+ (HomeErrorState _) => TriState.error,
+ (HomeLoadingState _) => TriState.loading,
+ (_) => TriState.loaded,
},
loadingChild: const HomePageLoader(),
errorChild: switch (state) {
diff --git a/lib/features/search/data/search_result/result.dart b/lib/features/search/data/search_result/result.dart
index 863575c..83420e1 100644
--- a/lib/features/search/data/search_result/result.dart
+++ b/lib/features/search/data/search_result/result.dart
@@ -83,7 +83,7 @@ class Result extends PlayableMedia with EquatableMixin {
String? get artworkUrl => image?.lastOrNull?.link ?? '';
@override
- String get itemId => itemType.isSong
+ String get itemId => itemType.isSong && itemUrl.isNotEmpty
? (itemUrl.split('/').lastOrNull ?? id ?? '')
: (id ?? '');
@@ -98,7 +98,4 @@ class Result extends PlayableMedia with EquatableMixin {
@override
String get itemUrl => url ?? '';
-
- @override
- bool get preferLinkOverId => itemType.isSong || itemType.isAlbum;
}
diff --git a/lib/features/search/data/top_search_result/top_search.dart b/lib/features/search/data/top_search_result/top_search.dart
index 4314c8a..6995b68 100644
--- a/lib/features/search/data/top_search_result/top_search.dart
+++ b/lib/features/search/data/top_search_result/top_search.dart
@@ -70,7 +70,9 @@ class TopSearch extends PlayableMedia with EquatableMixin {
String? get artworkUrl => image?.lastOrNull?.link;
@override
- String get itemId => itemUrl.split('/').lastOrNull ?? id ?? '';
+ String get itemId => itemUrl.isEmpty
+ ? (id ?? '')
+ : (itemUrl.split('/').lastOrNull ?? id ?? '');
@override
String get itemSubtitle => "Top Search • $description";
@@ -83,7 +85,4 @@ class TopSearch extends PlayableMedia with EquatableMixin {
@override
String get itemUrl => url ?? '';
-
- @override
- bool get preferLinkOverId => true;
}
diff --git a/lib/features/session/cubit/session_cubit.dart b/lib/features/session/cubit/session_cubit.dart
index 0bc711d..3c947c1 100644
--- a/lib/features/session/cubit/session_cubit.dart
+++ b/lib/features/session/cubit/session_cubit.dart
@@ -2,13 +2,10 @@ import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:firebase_auth/firebase_auth.dart';
-import 'package:go_router/go_router.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:varanasi_mobile_app/utils/app_cubit.dart';
-import 'package:varanasi_mobile_app/utils/extensions/router.dart';
-import 'package:varanasi_mobile_app/utils/helpers/get_app_context.dart';
+import 'package:varanasi_mobile_app/utils/app_snackbar.dart';
import 'package:varanasi_mobile_app/utils/logger.dart';
-import 'package:varanasi_mobile_app/utils/routes.dart';
part 'session_state.dart';
@@ -23,22 +20,9 @@ class SessionCubit extends AppCubit {
@override
FutureOr init() {
- _auth.userChanges().listen((user) {
- if (user == null) {
- emit(UnAuthenticated());
- } else {
- emit(Authenticated(user: user));
- }
- });
- stream.distinct().listen((state) {
- if (state is! Authenticated) {
- return appContext.go(AppRoutes.authentication.path);
- }
- if ([AppRoutes.authentication.path]
- .contains(appContext.routerState.matchedLocation)) {
- return appContext.go(AppRoutes.home.path);
- }
- });
+ _auth.userChanges().map((user) {
+ return user == null ? UnAuthenticated() : Authenticated(user: user);
+ }).listen(emit);
}
Future continueWithGoogle() async {
@@ -52,8 +36,78 @@ class SessionCubit extends AppCubit {
);
final userCredential = await _auth.signInWithCredential(credential);
_logger.d(userCredential.user?.toString());
+ } on FirebaseAuthException catch (e) {
+ _handleException(e);
} catch (e) {
+ await _googleSignIn.signOut();
_logger.d(e.toString());
}
}
+
+ Future signInWithEmailAndPassword({
+ required String email,
+ required String password,
+ }) async {
+ emit(Authenticating());
+ try {
+ final userCredential = await _auth.signInWithEmailAndPassword(
+ email: email,
+ password: password,
+ );
+ _logger.d(userCredential.user?.toString());
+ } on FirebaseAuthException catch (e) {
+ _handleException(e);
+ } catch (e) {
+ _logger.d(e.toString());
+ }
+ }
+
+ Future signUpWithEmailAndPassword({
+ required String email,
+ required String password,
+ required String name,
+ }) async {
+ emit(Authenticating());
+ try {
+ final userCredential = await _auth.createUserWithEmailAndPassword(
+ email: email,
+ password: password,
+ );
+ await userCredential.user?.updateDisplayName(name);
+ _logger.d(userCredential.user?.toString());
+ } on FirebaseAuthException catch (e) {
+ _handleException(e);
+ } catch (e) {
+ _logger.d(e.toString());
+ }
+ }
+
+ Future signOut() async {
+ emit(Authenticating());
+ try {
+ await _googleSignIn.signOut();
+ await _auth.signOut();
+ } catch (e) {
+ _logger.d(e.toString());
+ }
+ }
+
+ void _handleException(FirebaseAuthException exception) {
+ final message = switch (exception.code) {
+ 'invalid-email' => 'The email address is not valid.',
+ 'user-disabled' =>
+ 'The user corresponding to the given email has been disabled.',
+ 'user-not-found' =>
+ 'The user corresponding to the given email does not exist.',
+ 'wrong-password' =>
+ 'The password is invalid for the given email, or the account corresponding to the email does not have a password set.',
+ 'email-already-in-use' =>
+ 'The email address is already in use by another account.',
+ 'operation-not-allowed' =>
+ 'Indicates that Email & Password accounts are not enabled.',
+ 'weak-password' => 'The password must be 6 characters long or more.',
+ (_) => 'An undefined Error happened.'
+ };
+ AppSnackbar.show(message);
+ }
}
diff --git a/lib/features/session/ui/auth_page.dart b/lib/features/session/ui/auth_page.dart
index d452b31..a730576 100644
--- a/lib/features/session/ui/auth_page.dart
+++ b/lib/features/session/ui/auth_page.dart
@@ -1,81 +1,77 @@
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:go_router/go_router.dart';
import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
import 'package:varanasi_mobile_app/gen/assets.gen.dart';
import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
import 'package:varanasi_mobile_app/utils/helpers/ressponsive_sizer.dart';
+import 'package:varanasi_mobile_app/utils/routes.dart';
class AuthPage extends StatelessWidget {
const AuthPage({super.key});
@override
Widget build(BuildContext context) {
- return AnnotatedRegion(
- value: const SystemUiOverlayStyle(statusBarBrightness: Brightness.dark),
- child: Scaffold(
- body: SafeArea(
- child: Align(
- alignment: Alignment.center,
- child: SizedBox(
- width: Device.width * 0.8,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- Assets.icon.appIconMonotone.svg(
- placeholderBuilder: (ctx) =>
- const SizedBox(width: 48, height: 48),
+ return Scaffold(
+ body: SafeArea(
+ child: Align(
+ alignment: Alignment.center,
+ child: SizedBox(
+ width: Device.width * 0.8,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Assets.icon.appIconMonotone.svg(
+ placeholderBuilder: (ctx) =>
+ const SizedBox(width: 48, height: 48),
+ ),
+ const SizedBox(height: 20),
+ Text(
+ "Millions of Songs.\nForever Free.",
+ style: context.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
),
- const SizedBox(height: 20),
- Text(
- "Millions of Songs.\nForever Free.",
- style: context.textTheme.titleLarge?.copyWith(
- fontWeight: FontWeight.bold,
- ),
- textAlign: TextAlign.center,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 36),
+ FilledButton.tonal(
+ style: _buildButtonStyles(),
+ onPressed: () => context.pushNamed(AppRoutes.signup.name),
+ child: _buildText(context, "Sign up for free"),
+ ),
+ const SizedBox(height: 8),
+ OutlinedButton.icon(
+ icon: Assets.icon.google.svg(width: 24, height: 24),
+ onPressed: context.read().continueWithGoogle,
+ style: _buildButtonStyles(),
+ label: Center(
+ child: _buildText(context, "Continue with Google"),
),
- const SizedBox(height: 36),
- FilledButton.tonal(
- onPressed: () {},
- child: const Text(
- "Sign up for free",
- style: TextStyle(
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- OutlinedButton.icon(
- icon: const Icon(Icons.facebook),
- onPressed: context.read().continueWithGoogle,
- style: OutlinedButton.styleFrom(
- foregroundColor: context.colorScheme.onBackground,
- ),
- label: const Center(
- child: Text(
- "Continue with Google",
- style: TextStyle(
- fontWeight: FontWeight.w800,
- ),
- ),
- ),
- ),
- TextButton(
- style: TextButton.styleFrom(
- foregroundColor: context.colorScheme.onBackground,
- ),
- onPressed: () {},
- child: const Text(
- "Log in",
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- )
- ],
- ),
+ ),
+ const SizedBox(height: 8),
+ TextButton(
+ onPressed: () => context.pushNamed(AppRoutes.login.name),
+ child: _buildText(context, "Log in"),
+ )
+ ],
),
),
),
),
);
}
+
+ ButtonStyle _buildButtonStyles() {
+ return OutlinedButton.styleFrom(
+ padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
+ );
+ }
+
+ Text _buildText(BuildContext context, String text) {
+ return Text(
+ text,
+ style: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
+ );
+ }
}
diff --git a/lib/features/session/ui/login_page.dart b/lib/features/session/ui/login_page.dart
new file mode 100644
index 0000000..ac6cce2
--- /dev/null
+++ b/lib/features/session/ui/login_page.dart
@@ -0,0 +1,170 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
+import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
+import 'package:varanasi_mobile_app/utils/extensions/media_query.dart';
+import 'package:varanasi_mobile_app/widgets/input_field.dart';
+
+class LoginPage extends StatefulWidget {
+ const LoginPage({super.key});
+
+ @override
+ State createState() => _LoginPageState();
+}
+
+class _LoginPageState extends State {
+ late final GlobalKey _formKey;
+ late final TextEditingController _emailController;
+ late final TextEditingController _passwordController;
+ bool isFormValid = false;
+ bool isPasswordVisible = false;
+
+ @override
+ void initState() {
+ _formKey = GlobalKey();
+ _emailController = TextEditingController();
+ _passwordController = TextEditingController();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _formKey.currentState?.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: true,
+ title: const Text('Log in'),
+ titleTextStyle: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ body: SizedBox(
+ width: context.width,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: _buildForm(context),
+ ),
+ ),
+ );
+ }
+
+ Form _buildForm(BuildContext context) {
+ return Form(
+ key: _formKey,
+ autovalidateMode: AutovalidateMode.onUserInteraction,
+ onChanged: () {
+ final isValid = _formKey.currentState?.validate() ?? false;
+ if (isValid != isFormValid) {
+ setState(() {
+ isFormValid = isValid;
+ });
+ }
+ },
+ child: AutofillGroup(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Email',
+ style: context.textTheme.titleLarge
+ ?.copyWith(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ FilledInputField(
+ controller: _emailController,
+ context: context,
+ maxLines: 1,
+ autofillHints: const [AutofillHints.email],
+ autofocus: true,
+ inputType: InputType.email,
+ textInputAction: TextInputAction.next,
+ ),
+ const SizedBox(height: 24),
+ Text(
+ 'Password',
+ style: context.textTheme.titleLarge
+ ?.copyWith(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 8),
+ FilledInputField(
+ controller: _passwordController,
+ context: context,
+ autofillHints: const [AutofillHints.password],
+ inputType: InputType.password,
+ maxLines: 1,
+ obscureText: !isPasswordVisible,
+ decoration: InputDecoration(suffixIcon: _buildSuffix()),
+ ),
+ const SizedBox(height: 24),
+ LoginButton(
+ isFormValid: isFormValid,
+ emailController: _emailController,
+ passwordController: _passwordController,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ GestureDetector _buildSuffix() {
+ return GestureDetector(
+ onTap: () => setState(() {
+ isPasswordVisible = !isPasswordVisible;
+ }),
+ child: Icon(
+ isPasswordVisible
+ ? Icons.visibility_outlined
+ : Icons.visibility_off_outlined,
+ size: 24,
+ ),
+ );
+ }
+}
+
+class LoginButton extends StatelessWidget {
+ const LoginButton({
+ super.key,
+ required this.isFormValid,
+ required TextEditingController emailController,
+ required TextEditingController passwordController,
+ }) : _emailController = emailController,
+ _passwordController = passwordController;
+
+ final bool isFormValid;
+ final TextEditingController _emailController;
+ final TextEditingController _passwordController;
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.center,
+ child: FilledButton.tonal(
+ onPressed: isFormValid
+ ? () {
+ context.read().signInWithEmailAndPassword(
+ email: _emailController.text,
+ password: _passwordController.text,
+ );
+ }
+ : null,
+ style: FilledButton.styleFrom(
+ padding: const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 36,
+ ),
+ ),
+ child: const Text(
+ 'Log in',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/session/ui/signup_page.dart b/lib/features/session/ui/signup_page.dart
new file mode 100644
index 0000000..15f8ad5
--- /dev/null
+++ b/lib/features/session/ui/signup_page.dart
@@ -0,0 +1,204 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
+import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
+import 'package:varanasi_mobile_app/widgets/input_field.dart';
+
+class SignupPage extends StatefulWidget {
+ const SignupPage({super.key});
+
+ @override
+ State createState() => _SignupPageState();
+}
+
+class _SignupPageState extends State {
+ late final GlobalKey _formKey;
+ late final TextEditingController _nameController;
+ late final TextEditingController _emailController;
+ late final TextEditingController _passwordController;
+ bool isFormValid = false;
+ bool isPasswordVisible = false;
+
+ @override
+ void initState() {
+ _formKey = GlobalKey();
+ _nameController = TextEditingController();
+ _emailController = TextEditingController();
+ _passwordController = TextEditingController();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ _formKey.currentState?.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(
+ centerTitle: true,
+ title: const Text('Create Account'),
+ titleTextStyle: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ letterSpacing: 0.5,
+ ),
+ ),
+ body: SizedBox(
+ width: context.width,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 24),
+ child: _buildForm(context),
+ ),
+ ),
+ );
+ }
+
+ Form _buildForm(BuildContext context) {
+ return Form(
+ key: _formKey,
+ autovalidateMode: AutovalidateMode.onUserInteraction,
+ onChanged: () {
+ final isValid = _formKey.currentState?.validate() ?? false;
+ if (isValid != isFormValid) {
+ setState(() {
+ isFormValid = isValid;
+ });
+ }
+ },
+ child: AutofillGroup(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ _buildTitle(context, 'What should we call you?'),
+ const SizedBox(height: 8),
+ FilledInputField(
+ controller: _nameController,
+ context: context,
+ maxLines: 1,
+ autofillHints: const [
+ AutofillHints.name,
+ AutofillHints.newUsername
+ ],
+ autofocus: true,
+ inputType: InputType.username,
+ textInputAction: TextInputAction.next,
+ ),
+ const SizedBox(height: 24),
+ _buildTitle(context, 'What\'s your email?'),
+ const SizedBox(height: 8),
+ FilledInputField(
+ controller: _emailController,
+ context: context,
+ maxLines: 1,
+ autofillHints: const [AutofillHints.email],
+ inputType: InputType.email,
+ textInputAction: TextInputAction.next,
+ ),
+ const SizedBox(height: 24),
+ _buildTitle(context, 'Create a password'),
+ const SizedBox(height: 8),
+ FilledInputField(
+ controller: _passwordController,
+ context: context,
+ autofillHints: const [AutofillHints.newPassword],
+ inputType: InputType.password,
+ maxLines: 1,
+ obscureText: !isPasswordVisible,
+ decoration: InputDecoration(suffixIcon: _buildSuffix()),
+ ),
+ const SizedBox(height: 24),
+ LoginButton(
+ isFormValid: isFormValid,
+ emailController: _emailController,
+ passwordController: _passwordController,
+ nameController: _nameController,
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Text _buildTitle(BuildContext context, String text) {
+ return Text(
+ text,
+ style: context.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ );
+ }
+
+ GestureDetector _buildSuffix() {
+ return GestureDetector(
+ onTap: () => setState(() {
+ isPasswordVisible = !isPasswordVisible;
+ }),
+ child: Icon(
+ isPasswordVisible
+ ? Icons.visibility_outlined
+ : Icons.visibility_off_outlined,
+ size: 24,
+ ),
+ );
+ }
+}
+
+class LoginButton extends StatelessWidget {
+ const LoginButton({
+ super.key,
+ required this.isFormValid,
+ required this.emailController,
+ required this.passwordController,
+ required this.nameController,
+ });
+
+ final bool isFormValid;
+ final TextEditingController emailController;
+ final TextEditingController passwordController;
+ final TextEditingController nameController;
+
+ @override
+ Widget build(BuildContext context) {
+ final authenticating = context.select((SessionCubit cubit) =>
+ cubit.state is Authenticating || cubit.state is Authenticated);
+ return Align(
+ alignment: Alignment.center,
+ child: FilledButton.tonal(
+ onPressed: isFormValid && !authenticating
+ ? () {
+ context.read().signUpWithEmailAndPassword(
+ email: emailController.text,
+ password: passwordController.text,
+ name: nameController.text,
+ );
+ }
+ : null,
+ style: FilledButton.styleFrom(
+ padding: authenticating
+ ? const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 12,
+ )
+ : const EdgeInsets.symmetric(
+ vertical: 12,
+ horizontal: 36,
+ ),
+ ),
+ child: Visibility(
+ visible: !authenticating,
+ replacement: const SizedBox(
+ width: 24,
+ height: 24,
+ child: CircularProgressIndicator(),
+ ),
+ child: const Text(
+ 'Create Account',
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/features/settings/ui/settings_page.dart b/lib/features/settings/ui/settings_page.dart
index c205b3e..a192a24 100644
--- a/lib/features/settings/ui/settings_page.dart
+++ b/lib/features/settings/ui/settings_page.dart
@@ -5,6 +5,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:settings_ui/settings_ui.dart';
import 'package:varanasi_mobile_app/cubits/config/config_cubit.dart';
+import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
import 'package:varanasi_mobile_app/flavors.dart';
import 'package:varanasi_mobile_app/models/app_config.dart';
import 'package:varanasi_mobile_app/models/download_url.dart';
@@ -28,6 +29,18 @@ class SettingsPage extends StatelessWidget {
appBar: AppBar(title: const Text("Settings"), centerTitle: true),
body: SettingsList(
sections: [
+ SettingsSection(
+ title: const Text("Account"),
+ tiles: [
+ SettingsTile(
+ title: const Text("Sign out"),
+ leading: const Icon(Icons.logout_outlined),
+ onPressed: (context) {
+ context.read().signOut();
+ },
+ ),
+ ],
+ ),
SettingsSection(
title: const Text("General"),
tiles: [
diff --git a/lib/gen/assets.gen.dart b/lib/gen/assets.gen.dart
index 46290fc..519ec7e 100644
--- a/lib/gen/assets.gen.dart
+++ b/lib/gen/assets.gen.dart
@@ -15,13 +15,6 @@ import 'package:lottie/lottie.dart';
class $AssetsIconGen {
const $AssetsIconGen();
- /// File path: assets/icon/Integrate-with-Samsung-IAP-05212021.pdf
- String get integrateWithSamsungIAP05212021 =>
- 'assets/icon/Integrate-with-Samsung-IAP-05212021.pdf';
-
- /// File path: assets/icon/app_icon.jpg
- AssetGenImage get appIcon => const AssetGenImage('assets/icon/app_icon.jpg');
-
/// File path: assets/icon/app_icon_monotone.svg
SvgGenImage get appIconMonotone =>
const SvgGenImage('assets/icon/app_icon_monotone.svg');
@@ -41,6 +34,9 @@ class $AssetsIconGen {
SvgGenImage get downloading =>
const SvgGenImage('assets/icon/downloading.svg');
+ /// File path: assets/icon/google.svg
+ SvgGenImage get google => const SvgGenImage('assets/icon/google.svg');
+
$AssetsIconNavGen get nav => const $AssetsIconNavGen();
/// File path: assets/icon/shuffle.svg
@@ -48,21 +44,18 @@ class $AssetsIconGen {
/// List of all assets
List get values => [
- integrateWithSamsungIAP05212021,
- appIcon,
appIconMonotone,
circularLoader,
download,
downloadFilled,
downloading,
+ google,
shuffle
];
}
class $AssetsImagesGen {
const $AssetsImagesGen();
-
- $AssetsImagesErrorGen get error => const $AssetsImagesErrorGen();
}
class $AssetsIconNavGen {
@@ -94,17 +87,6 @@ class $AssetsIconNavGen {
[home, homeSelected, library, librarySelected, search, searchSelected];
}
-class $AssetsImagesErrorGen {
- const $AssetsImagesErrorGen();
-
- /// File path: assets/images/error/something_went_wrong.png
- AssetGenImage get somethingWentWrong =>
- const AssetGenImage('assets/images/error/something_went_wrong.png');
-
- /// List of all assets
- List get values => [somethingWentWrong];
-}
-
class Assets {
Assets._();
diff --git a/lib/models/playable_item.dart b/lib/models/playable_item.dart
index 2971803..a3adf1f 100644
--- a/lib/models/playable_item.dart
+++ b/lib/models/playable_item.dart
@@ -2,7 +2,6 @@
import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
-import 'package:flutter/foundation.dart';
import 'package:varanasi_mobile_app/models/app_config.dart';
import 'package:varanasi_mobile_app/models/download_url.dart';
import 'package:varanasi_mobile_app/models/image.dart';
@@ -41,7 +40,8 @@ abstract class PlayableMedia extends Equatable {
String get heroTag => itemId;
String get itemSubtitle;
- bool get preferLinkOverId => false;
+ bool get preferLinkOverId =>
+ (itemType.isSong || itemType.isAlbum) && itemUrl.isNotEmpty;
PlayableMediaType get itemType;
String? get artworkUrl;
@@ -111,16 +111,19 @@ abstract class PlayableMedia extends Equatable {
Uri get moreInfoUrl {
return switch (itemType) {
PlayableMediaType.song when !preferLinkOverId => Uri.parse(
- '${appConfig.endpoint.songs!.id}?id=$itemId&language=hindi,english',
+ '${appConfig.endpoint.songs!.id}?id=$itemId',
),
PlayableMediaType.song => Uri.parse(
- '${appConfig.endpoint.songs!.link}?link=${Uri.encodeComponent(itemUrl)}&language=hindi,english',
+ '${appConfig.endpoint.songs!.link}?link=${Uri.encodeComponent(itemUrl)}',
+ ),
+ PlayableMediaType.album when preferLinkOverId => Uri.parse(
+ '${appConfig.endpoint.albums!.link}?link=$itemUrl',
),
PlayableMediaType.album => Uri.parse(
- '${appConfig.endpoint.albums!.link}?link=$itemUrl&language=hindi,english',
+ '${appConfig.endpoint.albums!.id}?id=$itemId',
),
PlayableMediaType.playlist => Uri.parse(
- '${appConfig.endpoint.playlists!.id}?id=$itemId&language=hindi,english',
+ '${appConfig.endpoint.playlists!.id}?id=$itemId',
),
PlayableMediaType.artist => Uri.parse(
'${appConfig.endpoint.artists?.id}?id=$itemId&language=hindi,english',
@@ -131,7 +134,7 @@ abstract class PlayableMedia extends Equatable {
/// {@template getCacheKey}
/// Returns a unique key for the [PlayableMedia] to be used in the cache.
/// {@endtemplate}
- String get cacheKey => '$itemId-${describeEnum(itemType)}';
+ String get cacheKey => '$itemId-${itemType.name}';
MediaPlaylist toMediaPlaylist() {
return MediaPlaylist(
diff --git a/lib/utils/configs.dart b/lib/utils/configs.dart
index dffdab6..12892f1 100644
--- a/lib/utils/configs.dart
+++ b/lib/utils/configs.dart
@@ -31,9 +31,11 @@ class Songs {
class Albums {
final String link;
+ final String id;
const Albums({
required this.link,
+ required this.id,
});
}
@@ -119,7 +121,7 @@ Config get appConfig {
endpoint: Endpoint(
modules: '/modules',
playlists: Playlists(id: 'playlists'),
- albums: Albums(link: 'albums'),
+ albums: Albums(id: 'albums', link: 'albums'),
songs: Songs(id: 'songs', link: 'songs'),
search: Search(
all: "/search/all",
diff --git a/lib/utils/extensions/extensions.dart b/lib/utils/extensions/extensions.dart
index 5aacfec..ed32590 100644
--- a/lib/utils/extensions/extensions.dart
+++ b/lib/utils/extensions/extensions.dart
@@ -1,5 +1,6 @@
library extensions;
export 'color.dart';
+export 'media_query.dart';
export 'strings.dart';
export 'theme.dart';
diff --git a/lib/utils/generate_greeting.dart b/lib/utils/generate_greeting.dart
index 8be0157..421296e 100644
--- a/lib/utils/generate_greeting.dart
+++ b/lib/utils/generate_greeting.dart
@@ -1,11 +1,12 @@
+const greetings = [
+ 'Good morning!',
+ 'Good afternoon!',
+ 'Good evening!',
+ 'Hello!'
+];
+
String generateGreeting() {
var hour = DateTime.now().hour;
- var greetings = [
- 'Good morning!',
- 'Good afternoon!',
- 'Good evening!',
- 'Hello!'
- ];
if (hour < 12) {
return greetings[0];
} else if (hour < 18) {
diff --git a/lib/utils/input_validators.dart b/lib/utils/input_validators.dart
new file mode 100644
index 0000000..cba1867
--- /dev/null
+++ b/lib/utils/input_validators.dart
@@ -0,0 +1,54 @@
+class InputValidators {
+ static String? validateName(String? value) {
+ // name should be at least 2 characters long
+ if (value == null || value.isEmpty) {
+ return 'Name is required';
+ }
+ if (value.length < 2) {
+ return 'Name should be at least 2 characters long';
+ }
+ // name should not contain any numbers or special characters
+ if (value.contains(RegExp(r'[0-9!@#\$&*~]'))) {
+ return 'Name should not contain any numbers or special characters';
+ }
+ return null;
+ }
+
+ static String? validateEmail(String? value) {
+ // email should be in the format of an email address
+ // make sure emails in format `john.doe+1@gmail.com` and `john.doe@gmail.com` both are accepted
+ final regex = RegExp(
+ r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$');
+ if (value == null || value.isEmpty) {
+ return 'Email is required';
+ }
+ if (!regex.hasMatch(value)) {
+ return 'Enter a valid email address';
+ }
+ return null;
+ }
+
+ static String? validatePassword(String? value) {
+ // password should be at least 8 characters long, contain at least one uppercase letter, one lowercase letter, one number and one special character
+ // do separate checks for each requirement
+ if (value == null || value.isEmpty) {
+ return 'Password is required';
+ }
+ if (value.length < 8) {
+ return 'Password should be at least 8 characters long';
+ }
+ if (!value.contains(RegExp(r'[A-Z]'))) {
+ return 'Password should contain at least one uppercase letter';
+ }
+ if (!value.contains(RegExp(r'[a-z]'))) {
+ return 'Password should contain at least one lowercase letter';
+ }
+ if (!value.contains(RegExp(r'[0-9]'))) {
+ return 'Password should contain at least one number';
+ }
+ if (!value.contains(RegExp(r'[!@#\$&*~]'))) {
+ return 'Password should contain at least one special character';
+ }
+ return null;
+ }
+}
diff --git a/lib/utils/router.dart b/lib/utils/router.dart
index d031662..af77279 100644
--- a/lib/utils/router.dart
+++ b/lib/utils/router.dart
@@ -1,3 +1,7 @@
+import 'dart:async';
+
+import 'package:firebase_auth/firebase_auth.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'package:varanasi_mobile_app/features/home/bloc/home_bloc.dart';
@@ -7,7 +11,10 @@ import 'package:varanasi_mobile_app/features/library/ui/library_screen.dart';
import 'package:varanasi_mobile_app/features/library/ui/library_search_page.dart';
import 'package:varanasi_mobile_app/features/search/cubit/search_cubit.dart';
import 'package:varanasi_mobile_app/features/search/ui/search_page.dart';
+import 'package:varanasi_mobile_app/features/session/cubit/session_cubit.dart';
import 'package:varanasi_mobile_app/features/session/ui/auth_page.dart';
+import 'package:varanasi_mobile_app/features/session/ui/login_page.dart';
+import 'package:varanasi_mobile_app/features/session/ui/signup_page.dart';
import 'package:varanasi_mobile_app/features/settings/ui/settings_page.dart';
import 'package:varanasi_mobile_app/features/user-library/data/user_library.dart';
import 'package:varanasi_mobile_app/features/user-library/ui/user_library_page.dart';
@@ -19,9 +26,43 @@ import 'package:varanasi_mobile_app/widgets/transitions/fade_transition_page.dar
import 'keys.dart';
+class StreamListener extends ChangeNotifier {
+ /// Creates a [StreamListener].
+ ///
+ /// Every time the [Stream] receives an event this [ChangeNotifier] will
+ /// notify its listeners.
+ StreamListener(Stream stream) {
+ notifyListeners();
+ _subscription = stream.asBroadcastStream().listen((_) => notifyListeners());
+ }
+
+ late final StreamSubscription _subscription;
+
+ @override
+ void dispose() {
+ _subscription.cancel();
+ super.dispose();
+ }
+}
+
final routerConfig = GoRouter(
initialLocation: AppRoutes.home.path,
navigatorKey: rootNavigatorKey,
+ refreshListenable: StreamListener(FirebaseAuth.instance.userChanges()),
+ redirect: (context, state) {
+ final session = context.read().state;
+ final allowedLoggedOutRoutes = [
+ AppRoutes.authentication.name,
+ AppRoutes.login.name,
+ AppRoutes.signup.name,
+ ].map(state.namedLocation);
+ final isInsideAuth = allowedLoggedOutRoutes.contains(state.matchedLocation);
+ return switch (session) {
+ (UnAuthenticated _) when !isInsideAuth => AppRoutes.authentication.path,
+ (Authenticated _) when isInsideAuth => AppRoutes.home.path,
+ _ => null,
+ };
+ },
routes: [
StatefulShellRoute.indexedStack(
parentNavigatorKey: rootNavigatorKey,
@@ -117,6 +158,18 @@ final routerConfig = GoRouter(
name: AppRoutes.authentication.name,
path: AppRoutes.authentication.path,
builder: (context, state) => AuthPage(key: state.pageKey),
+ routes: [
+ GoRoute(
+ name: AppRoutes.login.name,
+ path: AppRoutes.login.path,
+ builder: (context, state) => LoginPage(key: state.pageKey),
+ ),
+ GoRoute(
+ path: AppRoutes.signup.path,
+ name: AppRoutes.signup.name,
+ builder: (context, state) => SignupPage(key: state.pageKey),
+ )
+ ],
),
],
);
diff --git a/lib/utils/routes.dart b/lib/utils/routes.dart
index 19a17ef..c0d4a8e 100644
--- a/lib/utils/routes.dart
+++ b/lib/utils/routes.dart
@@ -5,6 +5,8 @@ class AppRoutes {
static const Route library = (name: 'media-library', path: '/media-library');
static const Route librarySearch = (name: 'library-search', path: 'search');
static const Route authentication = (name: 'auth', path: '/auth');
+ static const Route login = (name: 'login', path: 'login');
+ static const Route signup = (name: 'signup', path: 'signup');
static const Route splash = (name: 'splash', path: '/splash');
// home specific routes
diff --git a/lib/widgets/animated_overflow_text.dart b/lib/widgets/animated_overflow_text.dart
index 4e1385a..14e3887 100644
--- a/lib/widgets/animated_overflow_text.dart
+++ b/lib/widgets/animated_overflow_text.dart
@@ -26,7 +26,6 @@ class AnimatedText extends StatelessWidget {
this.wrapWords = true,
this.overflow,
this.overflowReplacement,
- this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : textSpan = null;
@@ -51,7 +50,6 @@ class AnimatedText extends StatelessWidget {
this.wrapWords = true,
this.overflow,
this.overflowReplacement,
- this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
}) : data = null;
@@ -174,18 +172,6 @@ class AnimatedText extends StatelessWidget {
/// displayed instead.
final Widget? overflowReplacement;
- /// The number of font pixels for each logical pixel.
- ///
- /// For example, if the text scale factor is 1.5, text will be 50% larger than
- /// the specified font size.
- ///
- /// This property also affects [minFontSize], [maxFontSize] and [presetFontSizes].
- ///
- /// The value given to the constructor as textScaleFactor. If null, will
- /// use the [MediaQueryData.textScaleFactor] obtained from the ambient
- /// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
- final double? textScaleFactor;
-
/// An optional maximum number of lines for the text to span, wrapping if necessary.
/// If the text exceeds the given number of lines, it will be resized according
/// to the specified bounds and if necessary truncated according to [overflow].
@@ -249,7 +235,6 @@ class AnimatedText extends StatelessWidget {
overflow: overflow,
overflowReplacement:
overflowReplacement ?? buildDefaultOverfowReplacement(),
- textScaleFactor: textScaleFactor,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
);
@@ -273,7 +258,6 @@ class AnimatedText extends StatelessWidget {
overflow: overflow,
overflowReplacement:
overflowReplacement ?? buildDefaultOverfowReplacement(),
- textScaleFactor: textScaleFactor,
maxLines: maxLines,
semanticsLabel: semanticsLabel,
);
diff --git a/lib/widgets/error/error_page.dart b/lib/widgets/error/error_page.dart
index dd3d43d..3a5fd1e 100644
--- a/lib/widgets/error/error_page.dart
+++ b/lib/widgets/error/error_page.dart
@@ -18,8 +18,8 @@ class ErrorPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return switch (error.runtimeType) {
- NetworkException => NetworkErrorPage(
+ return switch (error) {
+ (NetworkException _) => NetworkErrorPage(
error: error as NetworkException,
retryCallback: retryCallback,
),
diff --git a/lib/widgets/error/network_error.dart b/lib/widgets/error/network_error.dart
index 788ae5f..da1b278 100644
--- a/lib/widgets/error/network_error.dart
+++ b/lib/widgets/error/network_error.dart
@@ -1,7 +1,6 @@
import 'package:flutter/material.dart';
-import 'package:varanasi_mobile_app/gen/assets.gen.dart';
import 'package:varanasi_mobile_app/utils/exceptions/network_exception.dart';
-import 'package:varanasi_mobile_app/utils/extensions/ressponsive_sizer.dart';
+import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
import 'error_page.dart';
@@ -18,23 +17,29 @@ class NetworkErrorPage extends ErrorPage {
@override
Widget build(BuildContext context) {
return Scaffold(
- body: Stack(
- children: [
- Assets.images.error.somethingWentWrong.image(
- fit: BoxFit.cover,
- height: double.infinity,
- width: double.infinity,
- ),
- Positioned(
- bottom: 10.h,
- left: 16.w,
- right: 16.w,
- child: ElevatedButton(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(
+ "Something went wrong",
+ style: context.textTheme.titleLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Text(
+ "Please check your internet connection and try again",
+ style: context.textTheme.titleSmall,
+ textAlign: TextAlign.center,
+ ),
+ const SizedBox(height: 8),
+ FilledButton.tonal(
onPressed: retryCallback,
child: Text(retryText),
- ),
- )
- ],
+ )
+ ],
+ ),
),
);
}
diff --git a/lib/widgets/filled_input_field.dart b/lib/widgets/filled_input_field.dart
new file mode 100644
index 0000000..7462c3b
--- /dev/null
+++ b/lib/widgets/filled_input_field.dart
@@ -0,0 +1,72 @@
+part of 'input_field.dart';
+
+class FilledInputField extends InputFormField {
+ FilledInputField({
+ super.key,
+ super.controller,
+ super.initialValue,
+ super.focusNode,
+ InputDecoration decoration = const InputDecoration(),
+ super.keyboardType,
+ super.textCapitalization = TextCapitalization.none,
+ super.textInputAction,
+ super.style,
+ super.strutStyle,
+ super.textDirection,
+ super.textAlign,
+ super.textAlignVertical,
+ super.autofocus,
+ super.readOnly,
+ super.toolbarOptions,
+ super.showCursor,
+ super.obscuringCharacter = '•',
+ super.obscureText,
+ super.autocorrect,
+ super.smartDashesType,
+ super.smartQuotesType,
+ super.enableSuggestions,
+ super.maxLengthEnforcement,
+ super.maxLines,
+ super.minLines,
+ super.expands,
+ super.maxLength,
+ super.onChanged,
+ super.onTap,
+ super.onEditingComplete,
+ super.onFieldSubmitted,
+ super.inputFormatters,
+ super.enabled,
+ super.cursorWidth,
+ super.cursorHeight,
+ super.cursorRadius,
+ super.cursorColor,
+ super.keyboardAppearance,
+ super.scrollPadding,
+ super.enableInteractiveSelection,
+ super.selectionControls,
+ super.buildCounter,
+ super.scrollPhysics,
+ super.autofillHints,
+ super.autovalidateMode,
+ super.scrollController,
+ super.enableIMEPersonalizedLearning,
+ super.mouseCursor,
+ super.restorationId,
+ super.onSaved,
+ super.validator,
+ super.inputType,
+ required BuildContext context,
+ }) : super(
+ decoration: decoration.copyWith(
+ filled: true,
+ isDense: true,
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: context.colorScheme.primary),
+ ),
+ border: OutlineInputBorder(
+ borderSide: BorderSide.none,
+ borderRadius: BorderRadius.circular(4),
+ ),
+ ),
+ );
+}
diff --git a/lib/widgets/input_field.dart b/lib/widgets/input_field.dart
index 23ec8eb..dbadce6 100644
--- a/lib/widgets/input_field.dart
+++ b/lib/widgets/input_field.dart
@@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
+import 'package:varanasi_mobile_app/utils/extensions/extensions.dart';
+import 'package:varanasi_mobile_app/utils/input_validators.dart';
+
+part 'filled_input_field.dart';
class InputFormField extends TextFormField {
InputFormField({
@@ -53,9 +57,11 @@ class InputFormField extends TextFormField {
super.mouseCursor,
super.restorationId,
super.onSaved,
- super.validator,
+ InputValidator? validator,
InputType inputType = InputType.none,
- }) : super(obscureText: obscureText ?? inputType == InputType.password);
+ }) : super(
+ validator: validator ?? _getInputValidator(inputType),
+ obscureText: obscureText ?? inputType == InputType.password);
InputFormField.custom({
super.key,
@@ -110,9 +116,10 @@ class InputFormField extends TextFormField {
super.mouseCursor,
super.restorationId,
super.onSaved,
- super.validator,
+ InputValidator? validator,
InputType inputType = InputType.none,
}) : super(
+ validator: validator ?? _getInputValidator(inputType),
keyboardType: keyboardType ?? _getTextInputType(inputType),
obscureText: obscureText ?? inputType == InputType.password,
decoration: size == InputFieldSize.medium
@@ -171,9 +178,10 @@ class InputFormField extends TextFormField {
super.mouseCursor,
super.restorationId,
super.onSaved,
- super.validator,
+ InputValidator? validator,
InputType inputType = InputType.none,
}) : super(
+ validator: validator ?? _getInputValidator(inputType),
keyboardType: keyboardType ?? _getTextInputType(inputType),
obscureText: obscureText ?? inputType == InputType.password,
decoration:
@@ -207,3 +215,11 @@ TextInputType? _getTextInputType(InputType inputType) {
return null;
}
}
+
+typedef InputValidator = String? Function(String? value);
+
+InputValidator? _getInputValidator(InputType inputType) => switch (inputType) {
+ InputType.email => InputValidators.validateEmail,
+ InputType.password => InputValidators.validatePassword,
+ _ => null,
+ };
diff --git a/lib/widgets/music_visualizer.dart b/lib/widgets/music_visualizer.dart
index 88eafaa..d6a5d94 100644
--- a/lib/widgets/music_visualizer.dart
+++ b/lib/widgets/music_visualizer.dart
@@ -52,14 +52,14 @@ class MiniMusicVisualizer extends StatelessWidget {
class VisualComponent extends StatefulWidget {
const VisualComponent({
- Key? key,
+ super.key,
required this.duration,
required this.color,
required this.curve,
this.width,
this.height,
required this.animating,
- }) : super(key: key);
+ });
final int duration;
final Color color;
diff --git a/lib/widgets/page_with_navbar.dart b/lib/widgets/page_with_navbar.dart
index cbc353f..552a9b8 100644
--- a/lib/widgets/page_with_navbar.dart
+++ b/lib/widgets/page_with_navbar.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:go_router/go_router.dart';
@@ -31,50 +32,53 @@ class PageWithNavbar extends HookWidget {
final queue = context
.select((MediaPlayerCubit cubit) => cubit.state.queueState.queue);
final showPlayer = controller != null && queue.isNotEmpty;
- return Scaffold(
- body: Stack(
- children: [
- Positioned.fill(
- bottom: showPlayer ? 56 : 0,
- child: child,
- ),
- if (showPlayer)
- SlidingUpPanel(
- controller: controller,
- renderPanelSheet: false,
- backdropEnabled: true,
- color: Colors.transparent,
- borderRadius: BorderRadius.circular(8),
- collapsed: MiniPlayer(panelController: controller),
- minHeight: 56,
- maxHeight: context.height,
- panel: position > 0
- ? Player(panelController: controller)
- : const SizedBox.shrink(),
- onPanelSlide: (pos) => positionState.value = pos,
+ return AnnotatedRegion(
+ value: SystemUiOverlayStyle.light,
+ child: Scaffold(
+ body: Stack(
+ children: [
+ Positioned.fill(
+ bottom: showPlayer ? 56 : 0,
+ child: child,
),
- ],
- ),
- bottomNavigationBar: SizedBox(
- height: currentHeight,
- child: Transform.translate(
- offset: Offset(0.0, currentHeight * position * 0.5),
- child: AnimatedOpacity(
- duration: const Duration(milliseconds: 200),
- opacity: opacity,
- child: OverflowBox(
- maxHeight: bottomNavHeight,
- child: NavigationBar(
- height: bottomNavHeight,
- indicatorColor: Colors.transparent,
- selectedIndex: child.currentIndex,
- onDestinationSelected: (value) {
- child.goBranch(
- value,
- initialLocation: child.currentIndex == value,
- );
- },
- destinations: navItems.map(_createDestination).toList(),
+ if (showPlayer)
+ SlidingUpPanel(
+ controller: controller,
+ renderPanelSheet: false,
+ backdropEnabled: true,
+ color: Colors.transparent,
+ borderRadius: BorderRadius.circular(8),
+ collapsed: MiniPlayer(panelController: controller),
+ minHeight: 56,
+ maxHeight: context.height,
+ panel: position > 0
+ ? Player(panelController: controller)
+ : const SizedBox.shrink(),
+ onPanelSlide: (pos) => positionState.value = pos,
+ ),
+ ],
+ ),
+ bottomNavigationBar: SizedBox(
+ height: currentHeight,
+ child: Transform.translate(
+ offset: Offset(0.0, currentHeight * position * 0.5),
+ child: AnimatedOpacity(
+ duration: const Duration(milliseconds: 200),
+ opacity: opacity,
+ child: OverflowBox(
+ maxHeight: bottomNavHeight,
+ child: NavigationBar(
+ height: bottomNavHeight,
+ indicatorColor: Colors.transparent,
+ selectedIndex: child.currentIndex,
+ onDestinationSelected: (value) {
+ child.goBranch(
+ value,
+ initialLocation: child.currentIndex == value,
+ );
+ },
+ destinations: navItems.map(_createDestination).toList(),
+ ),
),
),
),
diff --git a/pubspec.yaml b/pubspec.yaml
index b78d4e2..61a6f0a 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -141,7 +141,6 @@ flutter:
- assets/icon/app_icon.jpg
- assets/icon/nav/
- assets/icon/
- - shorebird.yaml
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware