From 0ef2e7dd67f2509aa1ef126474132ed4a9e68ca1 Mon Sep 17 00:00:00 2001 From: mahesh jamdade Date: Wed, 3 Jan 2024 12:38:31 +0530 Subject: [PATCH] add acount deletion --- .gitignore | 1 + ios/Flutter/Flutter.podspec | 18 + lib/base_home.dart | 11 +- lib/constants/const.dart | 2 + lib/constants/strings.dart | 10 + lib/main.dart | 11 +- lib/models/user.dart | 18 + lib/models/user.g.dart | 12 +- lib/navbar/profile/edit.dart | 335 ++++++++++------ lib/navbar/profile/profile.dart | 50 ++- lib/navbar/profile/report.dart | 22 +- lib/navbar/profile/settings.dart | 21 +- lib/pages/collections/list_collections.dart | 1 - lib/pages/login.dart | 50 ++- lib/services/services/database.dart | 4 +- lib/services/services/user.dart | 21 + lib/widgets/button.dart | 27 +- lib/widgets/drawer.dart | 419 ++++++++++---------- lib/widgets/widgets.dart | 15 +- 19 files changed, 633 insertions(+), 415 deletions(-) create mode 100644 ios/Flutter/Flutter.podspec diff --git a/.gitignore b/.gitignore index 58fbcdb..e78d7d8 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ doc/api/ .buildlog/ .history .svn/ +.env # IntelliJ related *.iml diff --git a/ios/Flutter/Flutter.podspec b/ios/Flutter/Flutter.podspec new file mode 100644 index 0000000..29758b7 --- /dev/null +++ b/ios/Flutter/Flutter.podspec @@ -0,0 +1,18 @@ +# +# This podspec is NOT to be published. It is only used as a local source! +# This is a generated file; do not edit or check into version control. +# + +Pod::Spec.new do |s| + s.name = 'Flutter' + s.version = '1.0.0' + s.summary = 'A UI toolkit for beautiful and fast apps.' + s.homepage = 'https://flutter.dev' + s.license = { :type => 'BSD' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } + s.ios.deployment_target = '11.0' + # Framework linking is handled by Flutter tooling, not CocoaPods. + # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. + s.vendored_frameworks = 'path/to/nothing' +end diff --git a/lib/base_home.dart b/lib/base_home.dart index ae266b3..8805e84 100644 --- a/lib/base_home.dart +++ b/lib/base_home.dart @@ -10,8 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:vocabhub/constants/constants.dart'; import 'package:vocabhub/models/user.dart'; import 'package:vocabhub/navbar/navbar.dart'; -import 'package:vocabhub/navbar/profile/about.dart'; -import 'package:vocabhub/navbar/profile/settings.dart'; +import 'package:vocabhub/navbar/profile/edit.dart'; import 'package:vocabhub/navbar/search/search_view.dart'; import 'package:vocabhub/pages/addword.dart'; import 'package:vocabhub/pages/login.dart'; @@ -117,7 +116,7 @@ class _AdaptiveLayoutState extends ConsumerState { WidgetsBinding.instance.addPostFrameCallback((_) { NavbarNotifier.showSnackBar(context, message, actionLabel: action, - bottom: kNavbarHeight * 1.2, + bottom: 50, onActionPressed: onActionPressed, duration: persist ? Duration(days: 1) : Duration(seconds: 3), onClosed: () { if (mounted) { @@ -170,9 +169,8 @@ class _AdaptiveLayoutState extends ConsumerState { if (user!.isLoggedIn) { _routes.addAll({ 3: { - UserProfile.route: UserProfile(), - SettingsPage.route: SettingsPage(), - AboutVocabhub.route: AboutVocabhub(), + UserProfileNavigator.route: UserProfileNavigator(), + EditProfile.route: EditProfile(), } }); if (items.length < 4) { @@ -244,6 +242,7 @@ class _AdaptiveLayoutState extends ConsumerState { return isExiting; } }, + shouldPopToBaseRoute: true, isDesktop: !SizeUtils.isMobile, // destinationAnimationCurve: Curves.fastOutSlowIn, destinationAnimationDuration: SizeUtils.isDesktop ? 0 : 0, diff --git a/lib/constants/const.dart b/lib/constants/const.dart index a731c69..862ce58 100644 --- a/lib/constants/const.dart +++ b/lib/constants/const.dart @@ -53,12 +53,14 @@ class Constants { static const NOTE_COLUMN = 'notes'; static const STATE_COLUMN = 'state'; static const CREATED_AT_COLUMN = 'created_at'; + static const UPDATED_AT_COLUMN = 'updated_at'; /// USER TABLE COLUMNS static const USERID_COLUMN = 'id'; static const USER_NAME_COLUMN = 'name'; static const USER_EMAIL_COLUMN = 'email'; static const USERNAME_COLUMN = 'username'; + static const DELETED_COLUMN = 'deleted'; static const USER_BOOKMARKS_COLUMN = 'bookmarks'; static const USER_CREATED_AT_COLUMN = 'created_at'; static const USER_LOGGEDIN_COLUMN = 'isLoggedIn'; diff --git a/lib/constants/strings.dart b/lib/constants/strings.dart index 757b105..d928840 100644 --- a/lib/constants/strings.dart +++ b/lib/constants/strings.dart @@ -60,3 +60,13 @@ String onDeviceCollectionsString = String onDeviceCollectionsString2 = "Note The collections are retained on your device and will not be synced."; + +String userNameConstraints = + 'Username should contain letters, numbers and underscores with minimum 3 characters'; + +String accountDeleted = 'This account is deleted. Please contact support at'; + +String registration_Failed = 'failed to register new user'; + +String accountActivationEmail = + 'mailto:${Constants.FEEDBACK_EMAIL_TO}?subject=Sign In Failure&body=Hi, I am facing issues while signing in. Please help me out.'; diff --git a/lib/main.dart b/lib/main.dart index f3a4a79..2543983 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,6 +16,9 @@ import 'package:vocabhub/controller/auth_controller.dart'; import 'package:vocabhub/controller/collections_controller.dart'; import 'package:vocabhub/controller/controllers.dart'; import 'package:vocabhub/models/user.dart'; +import 'package:vocabhub/navbar/profile/about.dart'; +import 'package:vocabhub/navbar/profile/report.dart'; +import 'package:vocabhub/navbar/profile/settings.dart'; import 'package:vocabhub/navbar/profile/webview.dart'; import 'package:vocabhub/pages/notifications/notifications.dart'; import 'package:vocabhub/pages/splashscreen.dart'; @@ -40,10 +43,7 @@ final appProvider = showFAB: true, hasUpdate: false, ))); -// final appThemeProvider = StateNotifierProvider( -// (ref) => VocabThemeNotifier(ref)); -// same as above comment final appThemeProvider = StateNotifierProvider(VocabThemeNotifier.new); @@ -166,7 +166,6 @@ class _VocabAppState extends ConsumerState { dashboardController.disposeService(); exploreController.disposeService(); // pushNotificationService.disposeService(); - super.dispose(); } @@ -217,6 +216,10 @@ class _VocabAppState extends ConsumerState { title: Constants.PRIVACY_POLICY_TITLE, url: Constants.PRIVACY_POLICY, ), + ReportABug.route: (context) => ReportABug(), + AboutVocabhub.route: (context) => AboutVocabhub(), + SettingsPage.route: (context) => SettingsPage(), + ViewBugReports.route: (context) => ViewBugReports(), }, themeMode: appThemeController.isDark ? ThemeMode.dark : ThemeMode.light, home: SplashScreen(), diff --git a/lib/models/user.dart b/lib/models/user.dart index 8f8617d..e1632a9 100644 --- a/lib/models/user.dart +++ b/lib/models/user.dart @@ -17,7 +17,9 @@ class UserModel extends ChangeNotifier { String username; // push notification token String token; + bool isDeleted; DateTime? created_at; + DateTime? updated_at; UserModel({ this.name = '', @@ -29,6 +31,8 @@ class UserModel extends ChangeNotifier { this.token = '', this.username = '', this.created_at, + this.updated_at, + this.isDeleted = false, this.isLoggedIn = false, }); @@ -45,7 +49,9 @@ class UserModel extends ChangeNotifier { isAdmin: w.isAdmin, username: w.username, token: w.token, + isDeleted: w.isDeleted, created_at: w.created_at, + updated_at: w.updated_at, isLoggedIn: w.isLoggedIn, ); } @@ -61,6 +67,8 @@ class UserModel extends ChangeNotifier { String? username, String? token, DateTime? created_at, + DateTime? updated_at, + bool? isDeleted, List? bookmarks, }) { return UserModel( @@ -72,7 +80,9 @@ class UserModel extends ChangeNotifier { isAdmin: isAdmin ?? this.isAdmin, username: username ?? this.username, token: token ?? this.token, + isDeleted: isDeleted ?? this.isDeleted, created_at: created_at ?? this.created_at, + updated_at: updated_at ?? this.updated_at, isLoggedIn: isLoggedIn ?? this.isLoggedIn, ); } @@ -85,9 +95,11 @@ class UserModel extends ChangeNotifier { idToken: '', accessToken: '', created_at: DateTime.now(), + updated_at: DateTime.now(), username: '', token: '', isAdmin: false, + isDeleted: false, isLoggedIn: false); } @@ -132,6 +144,11 @@ class UserModel extends ChangeNotifier { notifyListeners(); } + set setIsDeleted(bool m) { + isDeleted = m; + notifyListeners(); + } + UserModel get user => this; // updates local state and also stores in local storage @@ -146,6 +163,7 @@ class UserModel extends ChangeNotifier { this.token = user.token; this.created_at = user.created_at; this.isLoggedIn = user.isLoggedIn; + this.isDeleted = user.isDeleted; authController.setUser(this); notifyListeners(); } diff --git a/lib/models/user.g.dart b/lib/models/user.g.dart index c7de6c6..cb3e834 100644 --- a/lib/models/user.g.dart +++ b/lib/models/user.g.dart @@ -15,15 +15,13 @@ UserModel _$UserModelFromJson(Map json) => UserModel( accessToken: json['accessToken'] as String?, token: json['token'] as String? ?? '', username: json['username'] as String? ?? '', - created_at: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), + created_at: json['created_at'] == null ? null : DateTime.parse(json['created_at'] as String), + updated_at: json['updated_at'] == null ? null : DateTime.parse(json['updated_at'] as String), isLoggedIn: json['isLoggedIn'] as bool? ?? false, + isDeleted: json['deleted'] as bool? ?? false, ); Map _$UserModelToJson(UserModel instance) => { - 'idToken': instance.idToken, - 'accessToken': instance.accessToken, 'email': instance.email, 'name': instance.name, 'avatarUrl': instance.avatarUrl, @@ -31,5 +29,9 @@ Map _$UserModelToJson(UserModel instance) => { 'isAdmin': instance.isAdmin, 'username': instance.username, 'token': instance.token, + 'deleted': instance.isDeleted, 'created_at': instance.created_at?.toIso8601String(), + 'updated_at': instance.updated_at?.toIso8601String(), + 'idToken': instance.idToken, + 'accessToken': instance.accessToken, }; diff --git a/lib/navbar/profile/edit.dart b/lib/navbar/profile/edit.dart index 91357ae..ff2cee2 100644 --- a/lib/navbar/profile/edit.dart +++ b/lib/navbar/profile/edit.dart @@ -3,19 +3,21 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:navbar_router/navbar_router.dart'; import 'package:vocabhub/constants/constants.dart'; import 'package:vocabhub/models/user.dart'; +import 'package:vocabhub/pages/addword.dart'; +import 'package:vocabhub/pages/login.dart'; import 'package:vocabhub/services/services.dart'; import 'package:vocabhub/utils/extensions.dart'; import 'package:vocabhub/utils/utility.dart'; import 'package:vocabhub/widgets/button.dart'; import 'package:vocabhub/widgets/circle_avatar.dart'; import 'package:vocabhub/widgets/responsive.dart'; +import 'package:vocabhub/widgets/widgets.dart'; class EditProfile extends StatefulWidget { - final UserModel? user; + static const String route = '/edit-profile'; final VoidCallback? onClose; - static const String route = '/'; - EditProfile({Key? key, required this.user, this.onClose}) : super(key: key); + EditProfile({Key? key, this.onClose}) : super(key: key); @override State createState() => _EditProfileState(); @@ -25,7 +27,7 @@ class _EditProfileState extends State { @override Widget build(BuildContext context) { return ResponsiveBuilder( - desktopBuilder: (context) => EditProfileDesktop(), + desktopBuilder: (context) => EditProfileMobile(), mobileBuilder: (context) => EditProfileMobile( onClose: widget.onClose, ), @@ -55,10 +57,10 @@ class _EditProfileMobileState extends ConsumerState { }); } - ValueNotifier _validNotifier = ValueNotifier(null); + ValueNotifier _validNotifier = + ValueNotifier(Response(state: RequestState.none)); ValueNotifier _responseNotifier = ValueNotifier(Response(state: RequestState.none)); - String error = ''; TextEditingController _nameController = TextEditingController(); TextEditingController _usernameController = TextEditingController(); TextEditingController _emailController = TextEditingController(); @@ -71,148 +73,233 @@ class _EditProfileMobileState extends ConsumerState { super.dispose(); } + Future showDeleteConfirmation(Function onDelete) async { + // drafts found in local storage + await showDialog( + context: context, + builder: (x) => VocabAlert( + title: 'Are you sure you want to delete your account?', + subtitle: 'Note: This action cannot be undone', + actionTitle1: 'Confirm Account Deletion', + actionTitle2: 'Cancel', + onAction1: () { + onDelete(); + }, + onAction2: () async { + Navigator.of(context).pop(); + })); + } + Future validateUsername(String username) async { - _validNotifier.value = null; + _validNotifier.value = Response(state: RequestState.active, didSucced: true); RegExp usernamePattern = new RegExp(r"^[a-zA-Z0-9_]{3,}$"); if (username.length < 3 || !usernamePattern.hasMatch(username)) { - error = 'Username should contain letters, numbers and underscores with minimum 3 characters'; - _validNotifier.value = false; + _validNotifier.value = + Response(data: userNameConstraints, state: RequestState.error, didSucced: false); return; } else { final bool isUsernameValid = await UserService.isUsernameValid(username); if (isUsernameValid) { - error = 'Username is available'; + _validNotifier.value = + Response(state: RequestState.done, data: 'Username is available', didSucced: true); } else { - error = 'Username is not available'; + _validNotifier.value = + Response(state: RequestState.error, data: 'Username is not available', didSucced: true); } - _validNotifier.value = isUsernameValid; } } @override Widget build(BuildContext context) { UserModel user = ref.watch(userNotifierProvider); + final apptheme = ref.read(appThemeProvider); final colorScheme = Theme.of(context).colorScheme; return Scaffold( backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, appBar: AppBar( title: Text('Edit Profile'), backgroundColor: Colors.transparent, ), - body: ValueListenableBuilder( - valueListenable: _responseNotifier, - builder: (BuildContext context, Response request, Widget? child) { - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + body: GestureDetector( + onTap: () { + removeFocus(context); + }, + child: ValueListenableBuilder( + valueListenable: _responseNotifier, + builder: (BuildContext context, Response request, Widget? child) { + return Column( children: [ - Align( - alignment: Alignment.center, - child: Padding( - padding: 16.0.topHorizontalPadding, - child: CircleAvatar( - radius: 46, - backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.2), - child: CircularAvatar( - url: '${user.avatarUrl}', - name: user.name.initals(), - radius: 40, - )), - ), - ), - // Align( - // alignment: Alignment.center, - // child: TextButton( - // child: Text('Edit Avatar'), - // onPressed: () {}, - // ), - // ), - 24.0.vSpacer(), - VHTextfield( - hint: 'Name', - controller: _nameController, - isReadOnly: true, - ), - ValueListenableBuilder( - valueListenable: _validNotifier, - builder: (BuildContext context, bool? isValid, Widget? child) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - VHTextfield( - hint: 'Username', - isReadOnly: request.state == RequestState.active, - controller: _usernameController, - onChanged: (username) { - user = ref.watch(userNotifierProvider); - if (user.username == username) { - _validNotifier.value = null; - return; - } - validateUsername(username); - }, - ), - isValid == null - ? SizedBox.shrink() - : Padding( - padding: 16.0.bottomLeftPadding, - child: Text(error, - style: TextStyle( - color: - isValid ? colorScheme.primary : colorScheme.error)), + Expanded( + child: ListView( + // crossAxisAlignment: CrossAxisAlignment.start, + // mainAxisSize: MainAxisSize.max, + children: [ + Align( + alignment: Alignment.center, + child: Padding( + padding: 16.0.topHorizontalPadding, + child: CircleAvatar( + radius: 46, + backgroundColor: + Theme.of(context).colorScheme.primary.withOpacity(0.2), + child: CircularAvatar( + url: '${user.avatarUrl}', + name: user.name.initals(), + radius: 40, + )), + ), + ), + // Align( + // alignment: Alignment.center, + // child: TextButton( + // child: Text('Edit Avatar'), + // onPressed: () {}, + // ), + // ), + 24.0.vSpacer(), + VHTextfield( + hint: 'Name', + controller: _nameController, + isReadOnly: true, + ), + ValueListenableBuilder( + valueListenable: _validNotifier, + builder: (BuildContext context, Response response, Widget? child) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + VHTextfield( + hint: 'Username', + isReadOnly: request.state == RequestState.active, + controller: _usernameController, + onChanged: (username) { + if (username.isEmpty) { + _validNotifier.value = Response( + state: RequestState.error, + data: 'Username cannot be empty', + didSucced: true); + } else { + if (user.username == username) { + _validNotifier.value = + Response(state: RequestState.none, didSucced: true); + return; + } +// wait few seconds before validating username + Future.delayed(Duration(milliseconds: 300), () { + validateUsername(username); + }); + } + }, ), - ], - ); - }), - VHTextfield( - hint: 'Email', - controller: _emailController, - isReadOnly: true, - ), - VHTextfield( - hint: 'Joined', - controller: _joinedController, - isReadOnly: true, - ), - Align( - alignment: Alignment.center, - child: Padding( - padding: 16.0.allPadding, - child: VHButton( - height: 48, - width: 200, - isLoading: request.state == RequestState.active, - onTap: () async { - FocusScope.of(context).unfocus(); - if (_validNotifier.value == null || !_validNotifier.value!) return; - _responseNotifier.value = - Response(didSucced: false, state: RequestState.active); - final userName = _usernameController.text.trim(); - final editedUser = user!.copyWith(username: userName); - final success = await UserService.updateUser(editedUser); - if (success) { - _responseNotifier.value = - Response(state: RequestState.done, didSucced: true); - _validNotifier.value = null; - user.setUser(editedUser); - NavbarNotifier.showSnackBar(context, 'success updating user! '); - } else { - _responseNotifier.value = - Response(state: RequestState.done, didSucced: false); - NavbarNotifier.showSnackBar(context, 'error updating user! '); - } - Future.delayed(Duration(seconds: 2), () { - widget.onClose!(); - Navigate.popView(context); - }); - }, - label: 'Save'), + response.state == RequestState.none || + response.state == RequestState.active + ? SizedBox.shrink() + : Padding( + padding: 16.0.bottomLeftPadding, + child: Row( + children: [ + Flexible( + child: Text(response.data as String, + style: TextStyle( + color: response.state != RequestState.error + ? colorScheme.primary + : Colors.red)), + ), + ], + ), + ), + response.state == RequestState.active + ? LoadingWidget(radius: 24, width: 1.5) + : SizedBox.shrink() + ], + ); + }), + VHTextfield( + hint: 'Email', + controller: _emailController, + isReadOnly: true, + ), + VHTextfield( + hint: 'Joined', + controller: _joinedController, + isReadOnly: true, + ), + Align( + alignment: Alignment.center, + child: Padding( + padding: 16.0.allPadding, + child: VHButton( + height: 48, + width: 200, + isLoading: request.state == RequestState.active, + onTap: () async { + FocusScope.of(context).unfocus(); + if (_validNotifier.value.state == RequestState.none) { + NavbarNotifier.showSnackBar( + context, 'Nothing to update, closing...'); + Future.delayed(Duration(seconds: 2), () { + widget.onClose!(); + Navigate.popView(context); + }); + return; + } + if (_validNotifier.value.state == RequestState.error) return; + _responseNotifier.value = + Response(didSucced: false, state: RequestState.active); + final userName = _usernameController.text.trim(); + final editedUser = user.copyWith(username: userName); + final success = await UserService.updateUser(editedUser); + if (success) { + _responseNotifier.value = + Response(state: RequestState.done, didSucced: true); + user.setUser(editedUser); + NavbarNotifier.showSnackBar(context, 'success updating user! '); + } else { + _responseNotifier.value = + Response(state: RequestState.done, didSucced: false); + NavbarNotifier.showSnackBar(context, 'error updating user! '); + } + Future.delayed(Duration(seconds: 2), () { + widget.onClose!(); + Navigate.popView(context); + }); + }, + label: 'Save'), + ), + ), + Align( + alignment: Alignment.center, + child: Padding( + padding: 80.0.bottomPadding, + child: VHButton( + height: 48, + width: 200, + label: 'Delete Account', + backgroundColor: Colors.transparent, + foregroundColor: apptheme.isDark ? Colors.white : Colors.black, + // isLoading: request.state == RequestState.active, + onTap: () async { + FocusScope.of(context).unfocus(); + showDeleteConfirmation(() async { + showCircularIndicator(context); + await UserService.deleteUser(user); + showToast('You will be logged out!'); + await Future.delayed(Duration(seconds: 2)); + stopCircularIndicator(context); + Navigate.pushAndPopAll(context, AppSignIn()); + }); + // show a dialog to confirm + }), + ), + ) + ], ), - ) + ), ], - ), - ); - }), + ); + }), + ), ); } } @@ -295,7 +382,9 @@ class _VHTextfieldState extends State { ), ), Card( - color: Colors.transparent, + color: widget.isReadOnly + ? Theme.of(context).colorScheme.background + : Theme.of(context).colorScheme.surface, margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 5), diff --git a/lib/navbar/profile/profile.dart b/lib/navbar/profile/profile.dart index 4d9c476..40f27c1 100644 --- a/lib/navbar/profile/profile.dart +++ b/lib/navbar/profile/profile.dart @@ -19,9 +19,44 @@ import 'package:vocabhub/widgets/icon.dart'; import 'package:vocabhub/widgets/responsive.dart'; import 'package:vocabhub/widgets/widgets.dart'; +class UserProfileNavigator extends StatefulWidget { + static const String route = '/'; + const UserProfileNavigator({super.key}); + + @override + State createState() => _UserProfileNavigatorState(); +} + +class _UserProfileNavigatorState extends State { + @override + Widget build(BuildContext context) { + return Navigator( + initialRoute: UserProfile.route, + onGenerateRoute: (settings) { + switch (settings.name) { + case UserProfile.route: + return MaterialPageRoute(builder: (context) => UserProfile()); + case EditProfile.route: + return MaterialPageRoute( + builder: (context) => EditProfile( + onClose: () {}, + )); + case SettingsPage.route: + return MaterialPageRoute(builder: (context) => SettingsPage()); + + default: + return MaterialPageRoute( + builder: (context) => ErrorPage( + onRetry: () {}, errorMessage: 'Oh no! You have landed on an unknown planet ')); + } + }, + ); + } +} + /// when specidying readOnly as true, email must be provided class UserProfile extends ConsumerStatefulWidget { - static const String route = '/'; + static const String route = '/profile'; final bool isReadOnly; final String email; UserProfile({Key? key, this.isReadOnly = false, this.email = ''}) : super(key: key); @@ -267,14 +302,11 @@ class _UserProfileMobileState extends ConsumerState { size: 30, onTap: () { Navigator.of(context, rootNavigator: true) - .push(PageRoutes.sharedAxis( - EditProfile( - user: user, - onClose: () async { - setState(() {}); - }, - ), - SharedAxisTransitionType.scaled)); + .push(PageRoutes.sharedAxis(EditProfile( + onClose: () async { + setState(() {}); + }, + ), SharedAxisTransitionType.scaled)); }, )) ], diff --git a/lib/navbar/profile/report.dart b/lib/navbar/profile/report.dart index 509b887..70f2f05 100644 --- a/lib/navbar/profile/report.dart +++ b/lib/navbar/profile/report.dart @@ -35,6 +35,8 @@ class _ReportABugState extends State { } class ViewBugReports extends StatefulWidget { + static const route = '/view-bug-reports'; + const ViewBugReports({super.key}); @override @@ -386,12 +388,12 @@ class _ReportABugMobileState extends ConsumerState { child: Column( children: [ AppBar( - title: const Text('Report a bug'), + title: const Text('Feedback'), backgroundColor: Colors.transparent, ), 24.0.vSpacer(), VHTextfield( - hint: 'Description of the bug', + hint: 'Describe your feedback', hasLabel: false, controller: _feedBackcontroller, maxLines: 8, @@ -405,8 +407,7 @@ class _ReportABugMobileState extends ConsumerState { _responseNotifier.value.copyWith(state: RequestState.active); final String description = _feedBackcontroller.text.trim(); if (description.isEmpty) { - NavbarNotifier.showSnackBar( - context, 'You must enter a description of the bug', + NavbarNotifier.showSnackBar(context, 'Feedback cannot be empty', bottom: 0); _responseNotifier.value = _responseNotifier.value.copyWith(state: RequestState.done); @@ -427,7 +428,7 @@ class _ReportABugMobileState extends ConsumerState { state: RequestState.done, message: 'Report sent successfully'); _responseNotifier.value = _responseNotifier.value.copyWith(state: RequestState.done); - NavbarNotifier.showSnackBar(context, 'Thanks for reporting the bug'); + NavbarNotifier.showSnackBar(context, 'Thank you for your feedback'); pushNotificationService.sendNotification(Constants.reportPayLoad( 'A new bug report', '${user.name}: $description')); _feedBackcontroller.clear(); @@ -435,12 +436,13 @@ class _ReportABugMobileState extends ConsumerState { Navigator.pop(context); } else { _responseNotifier.value = _responseNotifier.value.copyWith( - state: RequestState.done, message: 'Error sending report'); - NavbarNotifier.showSnackBar(context, 'Error sending report! Try again'); + state: RequestState.done, message: 'Error sending feedback'); + NavbarNotifier.showSnackBar( + context, 'Error sending feedback! Try again'); } } catch (e) { - _responseNotifier.value = _responseNotifier.value - .copyWith(state: RequestState.done, message: 'Error sending report'); + _responseNotifier.value = _responseNotifier.value.copyWith( + state: RequestState.done, message: 'Error sending feedback'); NavbarNotifier.showSnackBar(context, 'Something went wrong, try agcain'); } }, @@ -449,7 +451,7 @@ class _ReportABugMobileState extends ConsumerState { Padding( padding: 16.0.allPadding, child: Text( - 'Note: We may contact you for more information about the bug you reported.'), + 'Note: We may contact you for more information about the feedback you reported.'), ), 24.0.vSpacer(), ], diff --git a/lib/navbar/profile/settings.dart b/lib/navbar/profile/settings.dart index 75e4c43..d9b3e08 100644 --- a/lib/navbar/profile/settings.dart +++ b/lib/navbar/profile/settings.dart @@ -105,11 +105,12 @@ class _SettingsPageMobileState extends ConsumerState { AppBar( backgroundColor: Colors.transparent, title: const Text('Settings'), + automaticallyImplyLeading: !SizeUtils.isDesktop, ), settingTile( 'About', onTap: () { - Navigate.push(context, AboutVocabhub()); + Navigate.pushNamed(context, isRootNavigator: true, AboutVocabhub.route); }, ), hLine(), @@ -335,12 +336,9 @@ class _SettingsPageMobileState extends ConsumerState { ); }), hLine(), - settingTile( - 'Report a bug', - onTap: () { - Navigate.push(context, ReportABug()); - }, - ), + settingTile('Share a feedback', onTap: () { + Navigate.pushNamed(context, ReportABug.route); + }, leadingIcon: Icons.feedback), hLine(), !user.isAdmin ? const SizedBox.shrink() @@ -355,6 +353,7 @@ class _SettingsPageMobileState extends ConsumerState { ? const SizedBox.shrink() : settingTile( 'My Bug Reports', + leadingIcon: Icons.bug_report, onTap: () { Navigate.push( context, @@ -368,16 +367,16 @@ class _SettingsPageMobileState extends ConsumerState { ), hLine(), !user.isAdmin ? const SizedBox.shrink() : hLine(), - settingTile(Constants.PRIVACY_POLICY_TITLE, onTap: () { + settingTile(Constants.PRIVACY_POLICY_TITLE, leadingIcon: Icons.privacy_tip, onTap: () { Navigate.pushNamed(context, WebViewPage.routeName, isRootNavigator: true); }), hLine(), - settingTile('Contact Us', onTap: () { + settingTile('Contact Us', leadingIcon: Icons.contact_mail, onTap: () { launchUrl(Uri.parse('mailto:${Constants.FEEDBACK_EMAIL_TO}'), mode: LaunchMode.externalApplication); }), hLine(), - settingTile('Licenses', onTap: () { + settingTile('Licenses', leadingIcon: Icons.verified_user, onTap: () { showLicensePage( context: context, applicationLegalese: "© 2022 ${Constants.ORGANIZATION}", @@ -385,7 +384,7 @@ class _SettingsPageMobileState extends ConsumerState { ); }), hLine(), - settingTile('Logout', trailingIcon: Icons.logout, onTap: () async { + settingTile('Logout', leadingIcon: Icons.logout, onTap: () async { user.loggedIn = false; authController.logout(context); Navigate.pushAndPopAll(context, AppSignIn()); diff --git a/lib/pages/collections/list_collections.dart b/lib/pages/collections/list_collections.dart index b261151..9ac36a4 100644 --- a/lib/pages/collections/list_collections.dart +++ b/lib/pages/collections/list_collections.dart @@ -252,7 +252,6 @@ class CollectionDetailsSheet extends ConsumerStatefulWidget { class _CollectionDetailsSheetState extends ConsumerState { @override Widget build(BuildContext context) { - final collections = ref.watch(collectionNotifier).collections; final collection = widget.collection ?? VHCollection.init(); return Column( children: [ diff --git a/lib/pages/login.dart b/lib/pages/login.dart index a1d65a2..bb2458e 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,4 +1,3 @@ -import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:navbar_router/navbar_router.dart'; @@ -21,7 +20,6 @@ class AppSignIn extends ConsumerStatefulWidget { class _AppSignInState extends ConsumerState { AuthService auth = AuthService(); - FirebaseMessaging _messaging = FirebaseMessaging.instance; Future _handleSignIn(BuildContext context) async { final _userNotifier = ref.read(userNotifierProvider); try { @@ -47,9 +45,23 @@ class _AppSignInState extends ConsumerState { _userNotifier.loggedIn = false; NavbarNotifier.showSnackBar(context, '$signInFailure'); _requestNotifier.value = Response(state: RequestState.done); - throw 'failed to register new user'; + throw '$registration_Failed'; } } else { + if (existingUser.isDeleted) { + _requestNotifier.value = Response(state: RequestState.done); + NavbarNotifier.showSnackBar( + context, + '$accountDeleted ${Constants.FEEDBACK_EMAIL_TO}', + actionLabel: 'Contact Support', + showCloseIcon: false, + duration: Duration(seconds: 10), + onActionPressed: () => launchURL( + accountActivationEmail, + ), + ); + return; + } existingUser.loggedIn = true; _userNotifier.setUser(existingUser); user = user!.copyWith( @@ -59,6 +71,7 @@ class _AppSignInState extends ConsumerState { data: { Constants.USER_LOGGEDIN_COLUMN: true, Constants.USER_TOKEN_COLUMN: token, + Constants.DELETED_COLUMN: false }, email: existingUser.email, ); @@ -69,10 +82,10 @@ class _AppSignInState extends ConsumerState { } else { NavbarNotifier.showSnackBar(context, '$signInFailure'); _requestNotifier.value = Response(state: RequestState.done); - throw 'failed to register new user'; + throw '$registration_Failed'; } } catch (error) { - NavbarNotifier.showSnackBar(context, error.toString()); + NavbarNotifier.showSnackBar(context, bottom: 0, error.toString()); _requestNotifier.value = Response(state: RequestState.done); _userNotifier.loggedIn = false; } @@ -180,19 +193,22 @@ class _AppSignInState extends ConsumerState { ); }, mobileBuilder: (x) { - return Container( - child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ - 200.0.vSpacer(), - // _heading('Hi!'), - _heading('Welcome!'), - Expanded(child: Container()), - _signInButton(), + return Scaffold( + backgroundColor: Colors.transparent, + body: Container( + child: Column(crossAxisAlignment: CrossAxisAlignment.center, children: [ + 200.0.vSpacer(), + // _heading('Hi!'), + _heading('Welcome!'), + Expanded(child: Container()), + _signInButton(), - 20.0.vSpacer(), - _skipButton(), - Expanded(child: Container()), - 100.0.vSpacer(), - ]), + 20.0.vSpacer(), + _skipButton(), + Expanded(child: Container()), + 100.0.vSpacer(), + ]), + ), ); })); }); diff --git a/lib/services/services/database.dart b/lib/services/services/database.dart index 8a707ff..c8c6e15 100644 --- a/lib/services/services/database.dart +++ b/lib/services/services/database.dart @@ -237,8 +237,8 @@ class DatabaseService { /// updates a row in the table /// update `tableName` where `columnName` = `colValue` /// with `data` - static Future updateRow( - {required String colValue, + static Future updateRow( + {required T colValue, required Map data, String columnName = '${Constants.ID_COLUMN}', String tableName = '${Constants.VOCAB_TABLE_NAME}'}) async { diff --git a/lib/services/services/user.dart b/lib/services/services/user.dart index 9dfdda8..213b007 100644 --- a/lib/services/services/user.dart +++ b/lib/services/services/user.dart @@ -74,6 +74,27 @@ class UserService { } } + static Future deleteUser(UserModel user) async { + try { + final response = await DatabaseService.updateByColumn( + searchValue: user.email, + data: { + Constants.DELETED_COLUMN: true, + Constants.USER_LOGGEDIN_COLUMN: false, + Constants.UPDATED_AT_COLUMN: DateTime.now().toIso8601String() + }, + searchColumn: Constants.USER_EMAIL_COLUMN, + tableName: _tableName); + if (response.status == 200) { + return true; + } else { + return false; + } + } catch (_) { + return false; + } + } + /// ```Select * from words;``` static Future> findAllUsers() async { List users = []; diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 3cba2bb..3751b3b 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -41,19 +41,20 @@ class _VHButtonState extends State { @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; - return ElevatedButton( - style: (widget.onTap == null) - ? null - : ButtonStyle( - minimumSize: MaterialStateProperty.resolveWith( - (states) => Size(widget.width ?? 120, widget.height)), - maximumSize: MaterialStateProperty.resolveWith( - (states) => Size(widget.width ?? 120, widget.height)), - foregroundColor: MaterialStateColor.resolveWith( - (states) => widget.foregroundColor ?? colorScheme.onSecondary), - backgroundColor: MaterialStateColor.resolveWith( - (states) => widget.backgroundColor ?? colorScheme.secondary, - )), + final style = (widget.onTap == null) + ? null + : ButtonStyle( + minimumSize: MaterialStateProperty.resolveWith( + (states) => Size(widget.width ?? 120, widget.height)), + maximumSize: MaterialStateProperty.resolveWith( + (states) => Size(widget.width ?? 120, widget.height)), + foregroundColor: MaterialStateColor.resolveWith( + (states) => widget.foregroundColor ?? colorScheme.onSecondary), + backgroundColor: MaterialStateColor.resolveWith( + (states) => widget.backgroundColor ?? colorScheme.secondary, + )); + return OutlinedButton( + style: style, onPressed: widget.isLoading || (widget.onTap == null) ? null : widget.onTap, child: widget.isLoading ? CircularProgressIndicator( diff --git a/lib/widgets/drawer.dart b/lib/widgets/drawer.dart index 2e03674..642a620 100644 --- a/lib/widgets/drawer.dart +++ b/lib/widgets/drawer.dart @@ -1,221 +1,209 @@ -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:navbar_router/navbar_router.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:vocabhub/constants/constants.dart'; -import 'package:vocabhub/models/models.dart'; -import 'package:vocabhub/pages/addword.dart'; -import 'package:vocabhub/services/appstate.dart'; -import 'package:vocabhub/services/services.dart'; -import 'package:vocabhub/themes/vocab_theme.dart'; -import 'package:vocabhub/utils/extensions.dart'; -import 'package:vocabhub/utils/utility.dart'; -import 'package:vocabhub/widgets/circle_avatar.dart'; -import 'package:vocabhub/widgets/widgets.dart'; -import 'package:vocabhub/widgets/wordscount.dart'; +import 'package:vocabhub/utils/utils.dart'; bool isAnimated = false; -class DrawerBuilder extends ConsumerStatefulWidget { - final Function(String)? onMenuTap; +// class DrawerBuilder extends ConsumerStatefulWidget { +// final Function(String)? onMenuTap; - const DrawerBuilder({Key? key, this.onMenuTap}) : super(key: key); +// const DrawerBuilder({Key? key, this.onMenuTap}) : super(key: key); - @override - _DrawerBuilderState createState() => _DrawerBuilderState(); -} +// @override +// _DrawerBuilderState createState() => _DrawerBuilderState(); +// } -class _DrawerBuilderState extends ConsumerState { - Widget subTitle(String text) { - return Text( - '$text', - style: VocabTheme.listSubtitleStyle, - ); - } +// class _DrawerBuilderState extends ConsumerState { +// Widget subTitle(String text) { +// return Text( +// '$text', +// style: VocabTheme.listSubtitleStyle, +// ); +// } - Widget title(String text) { - return Text( - '$text', - style: Theme.of(context).textTheme.headlineSmall, - ); - } +// Widget title(String text) { +// return Text( +// '$text', +// style: Theme.of(context).textTheme.headlineSmall, +// ); +// } - @override - void dispose() { - isAnimated = true; - super.dispose(); - } +// @override +// void dispose() { +// isAnimated = true; +// super.dispose(); +// } - Widget _avatar(UserModel user) { - if (user.email.isEmpty) { - return CircularAvatar( - url: '${Constants.PROFILE_AVATAR_ASSET}', - radius: 35, - ); - } else { - return CircularAvatar( - name: getInitial('${user.name}'), - url: user.avatarUrl, - radius: 35, - onTap: null, - ); - } - } +// Widget _avatar(UserModel user) { +// if (user.email.isEmpty) { +// return CircularAvatar( +// url: '${Constants.PROFILE_AVATAR_ASSET}', +// radius: 35, +// ); +// } else { +// return CircularAvatar( +// name: getInitial('${user.name}'), +// url: user.avatarUrl, +// radius: 35, +// onTap: null, +// ); +// } +// } - Future downloadFile() async { - try { - final success = await VocabStoreService().downloadFile(); - if (success) { - NavbarNotifier.showSnackBar(context, 'Downloaded successfully!'); - } else { - NavbarNotifier.showSnackBar(context, 'Failed to Download'); - } - } catch (x) { - NavbarNotifier.showSnackBar(context, '$x'); - } - } +// Future downloadFile() async { +// try { +// final success = await VocabStoreService().downloadFile(); +// if (success) { +// NavbarNotifier.showSnackBar(context, 'Downloaded successfully!'); +// } else { +// NavbarNotifier.showSnackBar(context, 'Failed to Download'); +// } +// } catch (x) { +// NavbarNotifier.showSnackBar(context, '$x'); +// } +// } - @override - Widget build(BuildContext context) { - final userProvider = ref.watch(userNotifierProvider); - Widget trailingIcon(IconData data) { - return Icon( - data, - ); - } +// @override +// Widget build(BuildContext context) { +// final userProvider = ref.watch(userNotifierProvider); +// Widget trailingIcon(IconData data) { +// return Icon( +// data, +// ); +// } - final appThemeController = ref.watch(appThemeProvider); - final bool isAdmin = (userProvider.isLoggedIn && userProvider.isAdmin); +// final appThemeController = ref.watch(appThemeProvider); +// final bool isAdmin = (userProvider.isLoggedIn && userProvider.isAdmin); - return Drawer( - child: Container( - child: Column( - children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), - height: 150, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - _avatar(userProvider), - (userProvider.isLoggedIn ? 20.0 : 30.0).hSpacer(), - Flexible( - child: GestureDetector( - onTap: () { - if (!userProvider.isLoggedIn) { - Navigate.popView(context); - widget.onMenuTap?.call('Sign In'); - } - }, - child: Text(userProvider.isLoggedIn ? '${userProvider.name}' : 'Sign In', - style: Theme.of(context) - .textTheme - .headlineMedium! - .copyWith(fontWeight: FontWeight.w500))), - ), - ], - ), - ), - hLine(), - ListTile( - onTap: () { - launchURL(Constants.REPORT_URL); - }, - subtitle: subTitle( - 'Report a bug or Request a feature', - ), - trailing: trailingIcon(Icons.bug_report), - title: title( - 'Report', - ), - ), - hLine(), - ListTile( - onTap: () { - Navigate.popView(context); - Navigate.push(context, AddWordForm(), transitionType: TransitionType.btt); - }, - trailing: trailingIcon( - Icons.add, - ), - title: title( - 'Add a word', - ), - subtitle: subTitle('Can\'t find a word?'), - ), - hLine(), - ListTile( - subtitle: subTitle('The code to this app is Open Sourced'), - onTap: () { - launchURL(Constants.SOURCE_CODE_URL); - }, - title: title( - 'Source code', - ), - trailing: Image.asset( - appThemeController.isDark ? GITHUB_WHITE_ASSET_PATH : GITHUB_ASSET_PATH, - width: 26, - ), - ), - isAdmin ? hLine() : SizedBox(), - isAdmin - ? ListTile( - subtitle: subTitle('Downlod the data as json'), - onTap: () async { - await Navigate.popView(context); - downloadFile(); - }, - title: title( - 'Download file', - ), - trailing: trailingIcon(Icons.download), - ) - : SizedBox.shrink(), - hLine(), - ListTile( - onTap: () { - launchURL(Constants.PRIVACY_POLICY); - }, - trailing: trailingIcon(Icons.privacy_tip), - title: title( - 'Privacy Policy', - ), - subtitle: subTitle(''), - ), - hLine(), - userProvider.isLoggedIn - ? ListTile( - onTap: () { - Navigate.popView(context); - widget.onMenuTap!('Sign Out'); - }, - trailing: trailingIcon(Icons.exit_to_app), - title: title( - 'Sign Out', - ), - subtitle: subTitle(''), - ) - : Container(), - !userProvider.isLoggedIn ? Container() : hLine(), - Expanded(child: Container()), - hLine(), - WordsCountAnimator( - isAnimated: isAnimated, - ), - 20.0.hSpacer(), - if (kIsWeb) storeRedirect(context), - Container( - height: 60, - alignment: Alignment.center, - child: VersionBuilder(), - ) - ], - ), - ), - ); - } -} +// return Drawer( +// child: Container( +// child: Column( +// children: [ +// Container( +// padding: EdgeInsets.symmetric(vertical: 16, horizontal: 8), +// height: 150, +// child: Row( +// crossAxisAlignment: CrossAxisAlignment.center, +// children: [ +// _avatar(userProvider), +// (userProvider.isLoggedIn ? 20.0 : 30.0).hSpacer(), +// Flexible( +// child: GestureDetector( +// onTap: () { +// if (!userProvider.isLoggedIn) { +// Navigate.popView(context); +// widget.onMenuTap?.call('Sign In'); +// } +// }, +// child: Text(userProvider.isLoggedIn ? '${userProvider.name}' : 'Sign In', +// style: Theme.of(context) +// .textTheme +// .headlineMedium! +// .copyWith(fontWeight: FontWeight.w500))), +// ), +// ], +// ), +// ), +// hLine(), +// ListTile( +// onTap: () { +// launchURL(Constants.REPORT_URL); +// }, +// subtitle: subTitle( +// 'Report a bug or Request a feature', +// ), +// trailing: trailingIcon(Icons.bug_report), +// title: title( +// 'Report', +// ), +// ), +// hLine(), +// ListTile( +// onTap: () { +// Navigate.popView(context); +// Navigate.push(context, AddWordForm(), transitionType: TransitionType.btt); +// }, +// trailing: trailingIcon( +// Icons.add, +// ), +// title: title( +// 'Add a word', +// ), +// subtitle: subTitle('Can\'t find a word?'), +// ), +// hLine(), +// ListTile( +// subtitle: subTitle('The code to this app is Open Sourced'), +// onTap: () { +// launchURL(Constants.SOURCE_CODE_URL); +// }, +// title: title( +// 'Source code', +// ), +// trailing: Image.asset( +// appThemeController.isDark ? GITHUB_WHITE_ASSET_PATH : GITHUB_ASSET_PATH, +// width: 26, +// ), +// ), +// isAdmin ? hLine() : SizedBox(), +// isAdmin +// ? ListTile( +// subtitle: subTitle('Downlod the data as json'), +// onTap: () async { +// await Navigate.popView(context); +// downloadFile(); +// }, +// title: title( +// 'Download file', +// ), +// trailing: trailingIcon(Icons.download), +// ) +// : SizedBox.shrink(), +// hLine(), +// ListTile( +// onTap: () { +// launchURL(Constants.PRIVACY_POLICY); +// }, +// trailing: trailingIcon(Icons.privacy_tip), +// title: title( +// 'Privacy Policy', +// ), +// subtitle: subTitle(''), +// ), +// hLine(), +// userProvider.isLoggedIn +// ? ListTile( +// onTap: () { +// Navigate.popView(context); +// widget.onMenuTap!('Sign Out'); +// }, +// trailing: trailingIcon(Icons.exit_to_app), +// title: title( +// 'Sign Out', +// ), +// subtitle: subTitle(''), +// ) +// : Container(), +// !userProvider.isLoggedIn ? Container() : hLine(), +// Expanded(child: Container()), +// hLine(), +// WordsCountAnimator( +// isAnimated: isAnimated, +// ), +// 20.0.hSpacer(), +// if (kIsWeb) storeRedirect(context), +// Container( +// height: 60, +// alignment: Alignment.center, +// child: VersionBuilder(), +// ) +// ], +// ), +// ), +// ); +// } +// } class VersionBuilder extends StatelessWidget { final String version; @@ -234,15 +222,24 @@ class VersionBuilder extends StatelessWidget { child: FutureBuilder( future: getAppDetails(), builder: (BuildContext context, AsyncSnapshot snapshot) { - return snapshot.data == null - ? Text('${Constants.VERSION}', style: Theme.of(context).textTheme.bodySmall) - : Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text('v', style: Theme.of(context).textTheme.bodySmall), - Text(snapshot.data!, style: Theme.of(context).textTheme.bodySmall), - ], - ); + return Column( + children: [ + snapshot.data == null + ? Text('${Constants.VERSION}', style: Theme.of(context).textTheme.bodySmall) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('v', style: Theme.of(context).textTheme.bodySmall), + Text(snapshot.data!, style: Theme.of(context).textTheme.bodySmall), + ], + ), + 16.0.vSpacer(), + Text('Made with ❤️ in India', style: Theme.of(context).textTheme.bodySmall), + // copyRightText(), + Text('© 2022 ${Constants.ORGANIZATION}', + style: Theme.of(context).textTheme.bodySmall), + ], + ); }), )); } diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index ed5090b..2406e8e 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:vocabhub/constants/const.dart'; import 'package:vocabhub/navbar/profile/settings.dart'; import 'package:vocabhub/services/analytics.dart'; -import 'package:vocabhub/services/services.dart'; import 'package:vocabhub/utils/size_utils.dart'; import 'package:vocabhub/utils/utility.dart'; @@ -25,10 +24,18 @@ void stopCircularIndicator(BuildContext context) { class LoadingWidget extends StatelessWidget { final Color? color; - const LoadingWidget({Key? key, this.color}) : super(key: key); + final double? radius; + final double? width; + const LoadingWidget({Key? key, this.color, this.width, this.radius}) : super(key: key); @override Widget build(BuildContext context) { - return Center(child: CircularProgressIndicator()); + return Center( + child: SizedBox( + height: radius ?? 40, + width: radius ?? 40, + child: CircularProgressIndicator( + strokeWidth: width ?? 3, + ))); } } @@ -218,9 +225,11 @@ Widget settingTile(String label, {String? description, Function? onTap, IconData? trailingIcon, + IconData? leadingIcon, double titleSize = 18, double verticalPadding = 24.0}) { return ListTile( + leading: leadingIcon != null ? Icon(leadingIcon) : null, minVerticalPadding: verticalPadding, title: Text( '$label',