From 9386195560f96dc484d26c2b4612ed76b60feb8c Mon Sep 17 00:00:00 2001 From: sidhdhi canopas <122426509+cp-sidhdhi-p@users.noreply.github.com> Date: Fri, 24 May 2024 08:48:34 +0530 Subject: [PATCH] Redesign profile screens (#20) * redesign profile screens * added config for image cropper * use action button --- .../android/app/src/main/AndroidManifest.xml | 5 + khelo/assets/images/ic_bin.svg | 3 + khelo/assets/images/ic_check.svg | 3 + khelo/assets/images/ic_edit.svg | 4 + khelo/assets/images/ic_profile.svg | 3 + khelo/assets/images/ic_profile_thin.svg | 3 + khelo/assets/locales/app_en.arb | 26 +- khelo/lib/components/action_bottom_sheet.dart | 48 +- .../lib/components/profile_image_avatar.dart | 107 ++++ .../lib/domain/formatter/date_formatter.dart | 11 +- khelo/lib/gen/assets.gen.dart | 20 + khelo/lib/ui/flow/main/main_screen.dart | 2 +- khelo/lib/ui/flow/profile/profile_screen.dart | 55 +- .../edit_profile/edit_profile_screen.dart | 602 +++++++++--------- khelo/pubspec.yaml | 2 +- style/lib/text/app_text_field.dart | 22 +- style/lib/theme/colors.dart | 10 + 17 files changed, 542 insertions(+), 384 deletions(-) create mode 100644 khelo/assets/images/ic_bin.svg create mode 100644 khelo/assets/images/ic_check.svg create mode 100644 khelo/assets/images/ic_edit.svg create mode 100644 khelo/assets/images/ic_profile.svg create mode 100644 khelo/assets/images/ic_profile_thin.svg create mode 100644 khelo/lib/components/profile_image_avatar.dart diff --git a/khelo/android/app/src/main/AndroidManifest.xml b/khelo/android/app/src/main/AndroidManifest.xml index 4fb5ecb5..60387f8f 100644 --- a/khelo/android/app/src/main/AndroidManifest.xml +++ b/khelo/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,11 @@ + + + + diff --git a/khelo/assets/images/ic_check.svg b/khelo/assets/images/ic_check.svg new file mode 100644 index 00000000..8fdb4c3c --- /dev/null +++ b/khelo/assets/images/ic_check.svg @@ -0,0 +1,3 @@ + + + diff --git a/khelo/assets/images/ic_edit.svg b/khelo/assets/images/ic_edit.svg new file mode 100644 index 00000000..b965c959 --- /dev/null +++ b/khelo/assets/images/ic_edit.svg @@ -0,0 +1,4 @@ + + + + diff --git a/khelo/assets/images/ic_profile.svg b/khelo/assets/images/ic_profile.svg new file mode 100644 index 00000000..9a9f8a56 --- /dev/null +++ b/khelo/assets/images/ic_profile.svg @@ -0,0 +1,3 @@ + + + diff --git a/khelo/assets/images/ic_profile_thin.svg b/khelo/assets/images/ic_profile_thin.svg new file mode 100644 index 00000000..7a7624cb --- /dev/null +++ b/khelo/assets/images/ic_profile_thin.svg @@ -0,0 +1,3 @@ + + + diff --git a/khelo/assets/locales/app_en.arb b/khelo/assets/locales/app_en.arb index d89d962f..80c8e022 100644 --- a/khelo/assets/locales/app_en.arb +++ b/khelo/assets/locales/app_en.arb @@ -557,24 +557,24 @@ "my_stat_stats_stumping_title": " Stumping", "@_PROFILE": { }, - "tab_profile_title": "profile", - "edit_profile_screen_title": "Edit profile", + "tab_profile_title": "Profile", + "edit_profile_screen_title": "Edit Profile", "edit_profile_select_birth_date_placeholder": "Select your date of birth", - "edit_profile_name_placeholder": "NAME", - "edit_profile_email_placeholder": "EMAIL", - "edit_profile_location_placeholder": "LOCATION", - "edit_profile_dob_placeholder": "DOB", - "edit_profile_gender_placeholder": "GENDER", - "edit_profile_player_role_placeholder": "PLAYER ROLE", - "edit_profile_batting_style_placeholder": "BATTING STYLE", - "edit_profile_bowling_style_placeholder": "BOWLING STYLE", + "edit_profile_name_placeholder": "Name", + "edit_profile_email_placeholder": "Email", + "edit_profile_location_placeholder": "Location", + "edit_profile_dob_placeholder": "Date of birth", + "edit_profile_gender_placeholder": "Gender", + "edit_profile_player_role_placeholder": "Player role", + "edit_profile_batting_style_placeholder": "Batting style", + "edit_profile_bowling_style_placeholder": "Bowling style", "edit_profile_gender_male_title": "Male", "edit_profile_gender_female_title": "Female", "edit_profile_gender_other_title": "Other", - "edit_profile_save_title": "SAVE", + "edit_profile_save_title": "Save", "profile_complete_profile_btn_title": "Complete Profile", - "profile_complete_your_profile_title": "Complete Your Profile", - "profile_complete_profile_description_title": "Enhance your profile by filling in the remaining details. Let's make it more comprehensive and appealing.", + "profile_complete_your_profile_title": "Complete your profile", + "profile_complete_profile_description": "Enhance your profile by filling in the remaining details. Let's make it more comprehensive and appealing.", "image_picker_choose_option_title": "Choose an option.", "image_picker_crop_image_title": "Crop Image", "batting_style_right_hand_bat_title": "Right hand bat", diff --git a/khelo/lib/components/action_bottom_sheet.dart b/khelo/lib/components/action_bottom_sheet.dart index 8488deb8..a8003a71 100644 --- a/khelo/lib/components/action_bottom_sheet.dart +++ b/khelo/lib/components/action_bottom_sheet.dart @@ -17,26 +17,29 @@ Future showActionBottomSheet({ ), ), useRootNavigator: useRootNavigator, + isScrollControlled: true, context: context, - builder: (context) => Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.vertical( - top: Radius.circular(16), + builder: (context) => SingleChildScrollView( + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.vertical( + top: Radius.circular(16), + ), + color: context.colorScheme.surface, ), - color: context.colorScheme.surface, - ), - padding: EdgeInsets.only( - bottom: context.mediaQueryPadding.bottom, - ), - child: ColumnBuilder.separated( - separatorBuilder: (index) => Divider( - height: 0, - thickness: 1, - color: context.colorScheme.outline, + padding: EdgeInsets.only( + bottom: context.mediaQueryPadding.bottom, + ), + child: ColumnBuilder.separated( + separatorBuilder: (index) => Divider( + height: 0, + thickness: 1, + color: context.colorScheme.outline, + ), + itemBuilder: (index) => items[index], + itemCount: items.length, + mainAxisSize: MainAxisSize.min, ), - itemBuilder: (index) => items[index], - itemCount: items.length, - mainAxisSize: MainAxisSize.min, ), ), ); @@ -70,12 +73,13 @@ class BottomSheetAction extends StatelessWidget { visible: icon != null, child: const SizedBox(width: 20), ), - Text( - title, - style: AppTextStyle.subtitle2 - .copyWith(color: context.colorScheme.textPrimary), + Expanded( + child: Text( + title, + style: AppTextStyle.body1 + .copyWith(color: context.colorScheme.textPrimary), + ), ), - const Spacer(), Visibility( visible: child != null, child: child ?? const SizedBox(), diff --git a/khelo/lib/components/profile_image_avatar.dart b/khelo/lib/components/profile_image_avatar.dart new file mode 100644 index 00000000..278386f0 --- /dev/null +++ b/khelo/lib/components/profile_image_avatar.dart @@ -0,0 +1,107 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:khelo/gen/assets.gen.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/indicator/progress_indicator.dart'; + +class ProfileImageAvatar extends StatelessWidget { + final double size; + final String? imageUrl; + final String placeHolderImage; + final bool isLoading; + final Function() onEditButtonTap; + + const ProfileImageAvatar({ + super.key, + required this.size, + this.imageUrl, + required this.placeHolderImage, + required this.isLoading, + required this.onEditButtonTap, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: SizedBox( + height: size, + width: size, + child: Stack( + children: [ + _roundedImageView(context), + _editImageButton(context, onTap: onEditButtonTap) + ], + ), + ), + ); + } + + Widget _roundedImageView(BuildContext context) { + return Container( + height: size, + width: size, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + image: imageUrl != null && !isLoading + ? DecorationImage( + image: CachedNetworkImageProvider(imageUrl!), + fit: BoxFit.cover) + : null, + color: context.colorScheme.primary), + child: _imagePlaceHolder(context), + ); + } + + Widget? _imagePlaceHolder(BuildContext context) { + return imageUrl == null && !isLoading + ? SvgPicture.asset( + Assets.images.icProfileThin, + height: size / 2, + width: size / 2, + colorFilter: ColorFilter.mode( + context.colorScheme.textInversePrimary, + BlendMode.srcATop, + ), + ) + : isLoading + ? AppProgressIndicator(color: context.colorScheme.surface) + : null; + } + + Widget _editImageButton( + BuildContext context, { + required Function() onTap, + }) { + return Align( + alignment: Alignment.bottomRight, + child: OnTapScale( + onTap: onTap, + child: Stack( + alignment: Alignment.center, + children: [ + Container( + height: 32, + width: 32, + decoration: BoxDecoration( + color: context.colorScheme.surface, + border: Border.all(color: context.colorScheme.textPrimary), + shape: BoxShape.circle), + ), + SvgPicture.asset( + Assets.images.icEdit, + height: 18, + width: 18, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcATop, + ), + ) + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/khelo/lib/domain/formatter/date_formatter.dart b/khelo/lib/domain/formatter/date_formatter.dart index d9325908..61cdb334 100644 --- a/khelo/lib/domain/formatter/date_formatter.dart +++ b/khelo/lib/domain/formatter/date_formatter.dart @@ -2,7 +2,7 @@ import 'package:flutter/cupertino.dart'; import 'package:intl/intl.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; -enum DateFormatType { dateAndTime, date, time} +enum DateFormatType { dateAndTime, date, time, shortDate } extension DateFormatter on DateTime { String format(BuildContext context, DateFormatType type) { @@ -14,13 +14,12 @@ extension DateFormatter on DateTime { 'EEE, dd MMM yyyy ${context.is24HourFormat ? 'HH:mm' : 'hh:mm a'}') .format(this); case DateFormatType.date: - return DateFormat( - 'EEE, MMM dd yyyy') - .format(this); + return DateFormat('EEE, MMM dd yyyy').format(this); case DateFormatType.time: - return DateFormat( - context.is24HourFormat ? 'HH:mm' : 'hh:mm a') + return DateFormat(context.is24HourFormat ? 'HH:mm' : 'hh:mm a') .format(this); + case DateFormatType.shortDate: + return DateFormat('dd MMM yyyy').format(this); } } } diff --git a/khelo/lib/gen/assets.gen.dart b/khelo/lib/gen/assets.gen.dart index fcf3f47e..3f4f4000 100644 --- a/khelo/lib/gen/assets.gen.dart +++ b/khelo/lib/gen/assets.gen.dart @@ -19,6 +19,9 @@ class $AssetsImagesGen { AssetGenImage get icBatsman => const AssetGenImage('assets/images/ic_batsman.png'); + /// File path: assets/images/ic_bin.svg + String get icBin => 'assets/images/ic_bin.svg'; + /// File path: assets/images/ic_bowler.png AssetGenImage get icBowler => const AssetGenImage('assets/images/ic_bowler.png'); @@ -27,6 +30,12 @@ class $AssetsImagesGen { AssetGenImage get icCamera => const AssetGenImage('assets/images/ic_camera.png'); + /// File path: assets/images/ic_check.svg + String get icCheck => 'assets/images/ic_check.svg'; + + /// File path: assets/images/ic_edit.svg + String get icEdit => 'assets/images/ic_edit.svg'; + /// File path: assets/images/ic_gallery.png AssetGenImage get icGallery => const AssetGenImage('assets/images/ic_gallery.png'); @@ -47,6 +56,12 @@ class $AssetsImagesGen { AssetGenImage get icOtherOfficial => const AssetGenImage('assets/images/ic_other_official.png'); + /// File path: assets/images/ic_profile.svg + String get icProfile => 'assets/images/ic_profile.svg'; + + /// File path: assets/images/ic_profile_thin.svg + String get icProfileThin => 'assets/images/ic_profile_thin.svg'; + /// File path: assets/images/ic_scorer.png AssetGenImage get icScorer => const AssetGenImage('assets/images/ic_scorer.png'); @@ -73,13 +88,18 @@ class $AssetsImagesGen { List get values => [ icArrowDown, icBatsman, + icBin, icBowler, icCamera, + icCheck, + icEdit, icGallery, icLeatherBall, icMicroPhone, icOtherBall, icOtherOfficial, + icProfile, + icProfileThin, icScorer, icStreamer, icTennisBall, diff --git a/khelo/lib/ui/flow/main/main_screen.dart b/khelo/lib/ui/flow/main/main_screen.dart index 70afe233..635faaec 100644 --- a/khelo/lib/ui/flow/main/main_screen.dart +++ b/khelo/lib/ui/flow/main/main_screen.dart @@ -126,7 +126,7 @@ class _MainScreenState extends ConsumerState TabItem( tabIcon: const Icon(Icons.person_outlined), tabActiveIcon: const Icon(Icons.person), - tabLabel: context.l10n.tab_profile_title, + tabLabel: context.l10n.tab_profile_title.toLowerCase(), route: '', onTap: () { _materialPageController.jumpToPage(3); diff --git a/khelo/lib/ui/flow/profile/profile_screen.dart b/khelo/lib/ui/flow/profile/profile_screen.dart index 33766648..f3ac247d 100644 --- a/khelo/lib/ui/flow/profile/profile_screen.dart +++ b/khelo/lib/ui/flow/profile/profile_screen.dart @@ -54,7 +54,7 @@ class ProfileScreen extends ConsumerWidget { child: Text( context.l10n.common_sign_out_title, style: AppTextStyle.button - .copyWith(color: context.colorScheme.primary), + .copyWith(color: context.colorScheme.alert), )) ], body: Builder( @@ -83,29 +83,29 @@ class ProfileScreen extends ConsumerWidget { child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.containerHigh), - borderRadius: BorderRadius.circular(12)), + color: context.colorScheme.containerNormal, + borderRadius: BorderRadius.circular(16)), child: Row( children: [ Container( - height: 90, - width: 90, + height: 80, + width: 80, alignment: Alignment.center, decoration: BoxDecoration( - color: context.colorScheme.containerLow, - shape: BoxShape.circle, - image: state.currentUser?.profile_img_url != null - ? DecorationImage( - image: CachedNetworkImageProvider( - state.currentUser!.profile_img_url!), - fit: BoxFit.cover) - : null, - border: Border.all(color: context.colorScheme.containerHigh)), + color: context.colorScheme.containerHigh, + shape: BoxShape.circle, + image: state.currentUser?.profile_img_url != null + ? DecorationImage( + image: CachedNetworkImageProvider( + state.currentUser!.profile_img_url!), + fit: BoxFit.cover) + : null, + ), child: state.currentUser?.profile_img_url == null ? Text(state.currentUser?.nameInitial ?? '?', - style: AppTextStyle.header1.copyWith( - color: context.colorScheme.secondary, - fontSize: 40, + textAlign: TextAlign.center, + style: AppTextStyle.header2.copyWith( + color: context.colorScheme.textPrimary, )) : null, ), @@ -115,7 +115,7 @@ class ProfileScreen extends ConsumerWidget { Expanded( child: Text( state.currentUser?.name ?? context.l10n.common_anonymous_title, - style: AppTextStyle.header3 + style: AppTextStyle.subtitle1 .copyWith(color: context.colorScheme.textPrimary), ), ) @@ -130,28 +130,29 @@ class ProfileScreen extends ConsumerWidget { currentUser.batting_style == null || currentUser.bowling_style == null) { return Container( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.all(24), decoration: BoxDecoration( - border: Border.all(color: context.colorScheme.containerHigh), - borderRadius: BorderRadius.circular(12)), + border: Border.all(color: context.colorScheme.outline), + borderRadius: BorderRadius.circular(16)), child: Column( children: [ Text( context.l10n.profile_complete_your_profile_title, - style: AppTextStyle.header1 + textAlign: TextAlign.center, + style: AppTextStyle.header4 .copyWith(color: context.colorScheme.textPrimary), ), const SizedBox( - height: 8, + height: 16, ), Text( - context.l10n.profile_complete_profile_description_title, - style: AppTextStyle.subtitle1 - .copyWith(color: context.colorScheme.textSecondary), + context.l10n.profile_complete_profile_description, textAlign: TextAlign.center, + style: AppTextStyle.body1 + .copyWith(color: context.colorScheme.textSecondary), ), const SizedBox( - height: 24, + height: 16, ), PrimaryButton( onPressed: () => AppRoute.editProfile().push(context), diff --git a/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart b/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart index c0e5bb8c..e25358f8 100644 --- a/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart +++ b/khelo/lib/ui/flow/settings/edit_profile/edit_profile_screen.dart @@ -1,22 +1,28 @@ -import 'package:cached_network_image/cached_network_image.dart'; import 'package:data/api/user/user_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; -import 'package:intl/intl.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:khelo/components/action_bottom_sheet.dart'; import 'package:khelo/components/app_page.dart'; import 'package:khelo/components/confirmation_dialog.dart'; import 'package:khelo/components/error_snackbar.dart'; +import 'package:khelo/components/profile_image_avatar.dart'; import 'package:khelo/domain/extensions/enum_extensions.dart'; import 'package:khelo/domain/extensions/context_extensions.dart'; +import 'package:khelo/domain/formatter/date_formatter.dart'; +import 'package:khelo/gen/assets.gen.dart'; import 'package:khelo/ui/app_route.dart'; import 'package:khelo/ui/flow/settings/edit_profile/edit_profile_view_model.dart'; import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/button/action_button.dart'; import 'package:style/button/bottom_sticky_overlay.dart'; import 'package:style/button/primary_button.dart'; import 'package:style/extensions/context_extensions.dart'; -import 'package:style/indicator/progress_indicator.dart'; +import 'package:style/text/app_text_field.dart'; import 'package:style/text/app_text_style.dart'; +import 'package:style/theme/colors.dart'; import '../../../../components/image_picker_sheet.dart'; @@ -25,15 +31,15 @@ class EditProfileScreen extends ConsumerWidget { const EditProfileScreen({super.key, required this.isToCreateAccount}); - final double profileViewHeight = 130; + final double profileViewHeight = 128; void _observeActionError(BuildContext context, WidgetRef ref) { ref.listen(editProfileStateProvider.select((value) => value.actionError), - (previous, next) { - if (next != null) { - showErrorSnackBar(context: context, error: next); - } - }); + (previous, next) { + if (next != null) { + showErrorSnackBar(context: context, error: next); + } + }); } void _observeIsSaved(BuildContext context, WidgetRef ref) { @@ -58,362 +64,338 @@ class EditProfileScreen extends ConsumerWidget { _observeIsSaved(context, ref); return PopScope( - canPop: !state.isButtonEnable, - onPopInvoked: (didPop) { - notifier.onBackBtnPressed(); - context.pop(); + onPopInvoked: (didPop) async { + await notifier.onBackBtnPressed(); }, child: AppPage( title: context.l10n.edit_profile_screen_title, actions: [ if (!isToCreateAccount) ...[ - IconButton( + actionButton(context, onPressed: () => showConfirmationDialog(context, title: context.l10n.common_delete_title, message: context.l10n.alert_confirm_default_message( context.l10n.common_delete_title.toLowerCase()), confirmBtnText: context.l10n.common_delete_title, onConfirm: notifier.onDeleteTap), - icon: const Icon(Icons.delete_outline)) + icon: SvgPicture.asset( + Assets.images.icBin, + height: 24, + width: 24, + fit: BoxFit.contain, + colorFilter: ColorFilter.mode( + context.colorScheme.primary, BlendMode.srcATop), + )), ] ], - body: Material( - color: Colors.transparent, - child: Builder( - builder: (context) { - return Stack( - children: [ - ListView( - padding: context.mediaQueryPadding + - const EdgeInsets.symmetric( - vertical: 8.0, horizontal: 16.0) + - BottomStickyOverlay.padding, - children: [ - _profileImageView(context, notifier, state), - const SizedBox( - height: 16, - ), - _textInputField( - context, - notifier, - state, - context.l10n.edit_profile_name_placeholder, - state.nameController), - const SizedBox( - height: 16, - ), - _textInputField( - context, - notifier, - state, - context.l10n.edit_profile_email_placeholder, - state.emailController), - const SizedBox( - height: 16, - ), - _textInputField( - context, - notifier, - state, - context.l10n.edit_profile_location_placeholder, - state.locationController), - _sectionTitle( - context, context.l10n.edit_profile_dob_placeholder), - OnTapScale( - onTap: () => _selectDate(context, notifier, state), - child: Text.rich(TextSpan(children: [ - WidgetSpan( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: Icon(Icons.cake_rounded, - color: context.colorScheme.textPrimary), - )), - TextSpan( - text: DateFormat.yMMMMd().format(state.dob), - style: AppTextStyle.subtitle1.copyWith( - color: context.colorScheme.textSecondary)), - ])), - ), - _sectionTitle(context, - context.l10n.edit_profile_gender_placeholder), - _genderOptionView(context, notifier, state), - const SizedBox( - height: 16, - ), - Wrap( - alignment: WrapAlignment.center, - children: [ - DropdownButton( - alignment: Alignment.center, - value: state.playerRole, - dropdownColor: - context.colorScheme.containerLowOnSurface, - isExpanded: false, - hint: Text( - context.l10n - .edit_profile_player_role_placeholder, - style: AppTextStyle.header4.copyWith( - color: context.colorScheme.textDisabled)), - items: PlayerRole.values.map((PlayerRole items) { - return DropdownMenuItem( - value: items, - child: Text( - items.getString(context), - style: AppTextStyle.body1.copyWith( - color: context.colorScheme.textPrimary), - ), - ); - }).toList(), - onChanged: (PlayerRole? newValue) { - if (newValue != null && - newValue != state.playerRole) { - notifier.onPlayerRoleChange(newValue); - } - }), - ], - ), - const SizedBox( - height: 16, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - DropdownButton( - value: state.battingStyle, - dropdownColor: - context.colorScheme.containerLowOnSurface, - hint: Text( - context.l10n - .edit_profile_batting_style_placeholder, - style: AppTextStyle.header4.copyWith( - color: context.colorScheme.textDisabled)), - items: - BattingStyle.values.map((BattingStyle items) { - return DropdownMenuItem( - value: items, - child: Text( - items.getString(context), - style: AppTextStyle.body1.copyWith( - color: context.colorScheme.textPrimary), - ), - ); - }).toList(), - onChanged: (BattingStyle? newValue) { - if (newValue != null && - newValue != state.battingStyle) { - notifier.onBattingStyleChange(newValue); - } - }), - DropdownButton( - value: state.bowlingStyle, - dropdownColor: - context.colorScheme.containerLowOnSurface, - hint: Text( - context.l10n - .edit_profile_bowling_style_placeholder, - style: AppTextStyle.header4.copyWith( - color: context.colorScheme.textDisabled)), - items: - BowlingStyle.values.map((BowlingStyle items) { - return DropdownMenuItem( - value: items, - child: Text( - items.getString(context), - style: AppTextStyle.body1.copyWith( - color: context.colorScheme.textPrimary), - ), - ); - }).toList(), - onChanged: (BowlingStyle? newValue) { - if (newValue != null && - newValue != state.bowlingStyle) { - notifier.onBowlingStyleChange(newValue); - } - }) - ], - ), - const SizedBox( - height: 24, - ), - ], - ), - _stickyButton(context, notifier, state) - ], - ); - }, - ), - ), - ), - ); - } - - Widget _profileImageView( - BuildContext context, - EditProfileViewNotifier notifier, - EditProfileState state, - ) { - return Center( - child: SizedBox( - height: profileViewHeight, - width: profileViewHeight, - child: Stack( - children: [ - Container( - height: profileViewHeight, - width: profileViewHeight, - alignment: Alignment.center, - decoration: BoxDecoration( - shape: BoxShape.circle, - image: state.imageUrl != null && !state.isImageUploading - ? DecorationImage( - image: CachedNetworkImageProvider(state.imageUrl!), - fit: BoxFit.cover) - : null, - color: context.colorScheme.primary), - child: state.imageUrl == null && !state.isImageUploading - ? Icon( - Icons.person, - size: profileViewHeight / 2, - color: context.colorScheme.textSecondary, - ) - : state.isImageUploading - ? AppProgressIndicator( - color: context.colorScheme.surface, - ) - : null, - ), - OnTapScale( - onTap: () async { - final imagePath = - await ImagePickerSheet.show(context, true); - if (imagePath != null) { - notifier.onImageChange(imagePath); - } - }, - child: Align( - alignment: Alignment.bottomRight, - child: Container( - height: profileViewHeight / 3, - width: profileViewHeight / 3, - decoration: BoxDecoration( - color: context.colorScheme.surface, - border: - Border.all(color: context.colorScheme.textSecondary), - shape: BoxShape.circle), - child: Icon( - Icons.edit, - size: profileViewHeight / 5, - color: context.colorScheme.textSecondary, - ), + body: Builder( + builder: (context) { + return Stack( + children: [ + ListView( + padding: context.mediaQueryPadding + + const EdgeInsets.all(16.0) + + BottomStickyOverlay.padding, + children: [ + ProfileImageAvatar( + size: profileViewHeight, + placeHolderImage: Assets.images.icProfileThin, + imageUrl: state.imageUrl, + isLoading: state.isImageUploading, + onEditButtonTap: () async { + final imagePath = await ImagePickerSheet.show( + context, true); + if (imagePath != null) { + notifier.onImageChange(imagePath); + } + }), + const SizedBox(height: 24), + _userContactDetailsView(context, notifier, state), + const SizedBox(height: 24), + _userPersonalDetailsView(context, notifier, state), + const SizedBox(height: 24), + _userPlayStyleView(context, notifier, state), + const SizedBox(height: 24), + ], ), - ), - ), - ], + _stickyButton(context, notifier, state) + ], + ); + }, ), ), ); } - Widget _textInputField( + Widget _userContactDetailsView( BuildContext context, EditProfileViewNotifier notifier, EditProfileState state, - String label, - TextEditingController controller, ) { - return TextField( - controller: controller, - onChanged: (value) => notifier.onValueChange(), - style: - AppTextStyle.header4.copyWith(color: context.colorScheme.textPrimary), - decoration: InputDecoration( - labelText: label, - labelStyle: AppTextStyle.header4 - .copyWith(color: context.colorScheme.textDisabled), - border: const OutlineInputBorder(), - ), - ); - } - - Widget _sectionTitle(BuildContext context, String title) { return Column( - crossAxisAlignment: CrossAxisAlignment.start, children: [ + _textInputField(context, notifier, + placeholderText: context.l10n.edit_profile_name_placeholder, + controller: state.nameController), const SizedBox( - height: 16, - ), - Text( - title, - style: AppTextStyle.body2 - .copyWith(color: context.colorScheme.textSecondary), + height: 8, ), + _textInputField(context, notifier, + placeholderText: context.l10n.edit_profile_email_placeholder, + controller: state.emailController), const SizedBox( height: 8, ), + _textInputField(context, notifier, + placeholderText: context.l10n.edit_profile_location_placeholder, + controller: state.locationController), ], ); } - Future _selectDate( + Widget _textInputField( BuildContext context, - EditProfileViewNotifier notifier, - EditProfileState state, - ) async { - final DateTime? picked = await showDatePicker( - context: context, - helpText: context.l10n.edit_profile_select_birth_date_placeholder, - initialDate: state.dob, - firstDate: DateTime(1920), - lastDate: DateTime.now()); - if (picked != null && picked != state.dob) { - notifier.onDateSelect(selectedDate: picked); - } + EditProfileViewNotifier notifier, { + required String placeholderText, + required TextEditingController controller, + }) { + return AppTextField( + controller: controller, + onChanged: (value) => notifier.onValueChange(), + style: AppTextStyle.subtitle3 + .copyWith(color: context.colorScheme.textPrimary), + borderRadius: BorderRadius.circular(12), + borderType: AppTextFieldBorderType.outline, + backgroundColor: context.colorScheme.containerLow, + borderColor: BorderColor(Colors.transparent, Colors.transparent), + hintText: placeholderText, + hintStyle: AppTextStyle.subtitle3 + .copyWith(color: context.colorScheme.textDisabled), + ); } - Widget _genderOptionView( + Widget _userPersonalDetailsView( BuildContext context, EditProfileViewNotifier notifier, EditProfileState state, ) { return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - _radioBtnCell(context, notifier, state, UserGender.male), - _radioBtnCell(context, notifier, state, UserGender.female), - _radioBtnCell(context, notifier, state, UserGender.other), + Expanded( + child: _selectionDropDownButton( + context, + headerText: context.l10n.edit_profile_dob_placeholder, + title: state.dob.format(context, DateFormatType.shortDate), + placeholder: context.l10n.edit_profile_dob_placeholder, + onTap: () => _selectDate(context, notifier, state), + ), + ), + const SizedBox(width: 16), + Expanded( + child: _selectionDropDownButton( + context, + headerText: context.l10n.edit_profile_gender_placeholder, + title: state.gender?.getString(context), + placeholder: context.l10n.edit_profile_gender_placeholder, + showTrailingIcon: false, + onTap: () { + showActionBottomSheet( + context: context, + items: [UserGender.male, UserGender.female, UserGender.other] + .map((gender) => BottomSheetAction( + title: gender.getString(context), + child: showCheckMark( + context, + showCheck: state.gender == gender, + ), + onTap: () { + context.pop(); + notifier.onGenderSelect(gender: gender); + }, + )) + .toList()); + }, + ), + ), ], ); } - Widget _radioBtnCell( + Widget _userPlayStyleView( BuildContext context, EditProfileViewNotifier notifier, EditProfileState state, - UserGender gender, ) { - return OnTapScale( - onTap: () => notifier.onGenderSelect(gender: gender), - child: Wrap( - direction: Axis.horizontal, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Radio( - value: gender, - groupValue: state.gender, - activeColor: context.colorScheme.primary, - onChanged: (value) { - if (value != null) { - notifier.onGenderSelect(gender: value); - } - }), + return Column( + children: [ + _selectionDropDownButton( + context, + title: state.playerRole?.getString(context), + placeholder: context.l10n.edit_profile_player_role_placeholder, + onTap: () { + showActionBottomSheet( + context: context, + items: PlayerRole.values + .map((role) => BottomSheetAction( + title: role.getString(context), + child: showCheckMark( + context, + showCheck: state.playerRole == role, + ), + onTap: () { + context.pop(); + notifier.onPlayerRoleChange(role); + }, + )) + .toList()); + }, + ), + const SizedBox(height: 24), + _selectionDropDownButton( + context, + title: state.battingStyle?.getString(context), + placeholder: context.l10n.edit_profile_batting_style_placeholder, + onTap: () { + showActionBottomSheet( + context: context, + items: BattingStyle.values + .map((style) => BottomSheetAction( + title: style.getString(context), + child: showCheckMark( + context, + showCheck: state.battingStyle == style, + ), + onTap: () { + context.pop(); + notifier.onBattingStyleChange(style); + }, + )) + .toList()); + }, + ), + const SizedBox(height: 24), + _selectionDropDownButton( + context, + title: state.bowlingStyle?.getString(context), + placeholder: context.l10n.edit_profile_bowling_style_placeholder, + onTap: () { + showActionBottomSheet( + context: context, + items: BowlingStyle.values + .map((style) => BottomSheetAction( + title: style.getString(context), + child: showCheckMark( + context, + showCheck: state.bowlingStyle == style, + ), + onTap: () { + context.pop(); + notifier.onBowlingStyleChange(style); + }, + )) + .toList()); + }, + ), + ], + ); + } + + Widget? showCheckMark( + BuildContext context, { + required bool showCheck, + }) { + return showCheck + ? SvgPicture.asset( + Assets.images.icCheck, + height: 24, + width: 24, + colorFilter: ColorFilter.mode( + context.colorScheme.primary, + BlendMode.srcATop, + ), + ) + : null; + } + + Widget _selectionDropDownButton( + BuildContext context, { + String? headerText, + String? title, + required String placeholder, + bool showTrailingIcon = true, + required Function() onTap, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (headerText != null) ...[ Text( - gender.getString(context), - style: AppTextStyle.button.copyWith( - color: context.colorScheme.textSecondary, fontSize: 16), + headerText, + style: AppTextStyle.caption + .copyWith(color: context.colorScheme.textDisabled), ), + const SizedBox(height: 4), ], - ), + OnTapScale( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: context.colorScheme.outline)), + child: Row( + children: [ + Expanded( + child: Text( + title ?? placeholder, + style: AppTextStyle.subtitle3.copyWith( + color: title != null + ? context.colorScheme.textPrimary + : context.colorScheme.textSecondary), + ), + ), + if (showTrailingIcon) ...[ + SvgPicture.asset( + Assets.images.icArrowDown, + height: 18, + width: 18, + colorFilter: ColorFilter.mode( + context.colorScheme.textPrimary, + BlendMode.srcATop, + ), + ) + ] + ], + ), + ), + ), + ], + ); + } + + Future _selectDate( + BuildContext context, + EditProfileViewNotifier notifier, + EditProfileState state, + ) async { + final DateTime? picked = await showDatePicker( + context: context, + helpText: context.l10n.edit_profile_select_birth_date_placeholder, + initialDate: state.dob, + firstDate: DateTime(1920), + lastDate: DateTime.now(), + builder: (context, child) { + return Theme( + data: context.brightness == Brightness.dark + ? materialThemeDataDark + : materialThemeDataLight, + child: child!, + ); + }, ); + if (picked != null && picked != state.dob) { + notifier.onDateSelect(selectedDate: picked); + } } Widget _stickyButton( diff --git a/khelo/pubspec.yaml b/khelo/pubspec.yaml index bf823427..f001803f 100644 --- a/khelo/pubspec.yaml +++ b/khelo/pubspec.yaml @@ -93,7 +93,7 @@ flutter: uses-material-design: true assets: - - assets/images/. + - assets/images/ # To add assets to your application, add an assets section, like this: # assets: diff --git a/style/lib/text/app_text_field.dart b/style/lib/text/app_text_field.dart index 807db344..8e086af9 100644 --- a/style/lib/text/app_text_field.dart +++ b/style/lib/text/app_text_field.dart @@ -12,10 +12,12 @@ class AppTextField extends StatelessWidget { final int? minLines; final int? maxLength; final TextStyle? style; + final Color? backgroundColor; final bool expands; final bool enabled; final BorderRadius? borderRadius; final AppTextFieldBorderType borderType; + final BorderColor? borderColor; final double borderWidth; final TextInputAction? textInputAction; final String? hintText; @@ -40,10 +42,12 @@ class AppTextField extends StatelessWidget { this.minLines, this.maxLength, this.style, + this.backgroundColor, this.expands = false, this.enabled = true, this.onChanged, this.borderType = AppTextFieldBorderType.underline, + this.borderColor, this.borderWidth = 1, this.textInputAction, this.borderRadius, @@ -85,6 +89,7 @@ class AppTextField extends StatelessWidget { Widget _textField(BuildContext context) => Material( color: Colors.transparent, + borderRadius: borderRadius, child: TextField( controller: controller, onChanged: onChanged, @@ -117,6 +122,8 @@ class AppTextField extends StatelessWidget { isCollapsed: isCollapsed, hintText: hintText, hintStyle: hintStyle, + fillColor: backgroundColor, + filled: backgroundColor != null, focusedBorder: _border(context, true), enabledBorder: _border(context, false), contentPadding: contentPadding ?? @@ -141,8 +148,8 @@ class AppTextField extends StatelessWidget { borderRadius: BorderRadius.circular(8), borderSide: BorderSide( color: focused - ? context.colorScheme.primary - : context.colorScheme.outline, + ? borderColor?.focusColor ?? context.colorScheme.primary + : borderColor?.unFocusColor ?? context.colorScheme.outline, width: borderWidth, ), ); @@ -150,8 +157,8 @@ class AppTextField extends StatelessWidget { return UnderlineInputBorder( borderSide: BorderSide( color: focused - ? context.colorScheme.primary - : context.colorScheme.outline, + ? borderColor?.focusColor ?? context.colorScheme.primary + : borderColor?.unFocusColor ?? context.colorScheme.outline, width: borderWidth, ), ); @@ -163,4 +170,11 @@ enum AppTextFieldBorderType { none, outline, underline, +} + +class BorderColor { + Color? focusColor; + Color? unFocusColor; + + BorderColor(this.focusColor, this.unFocusColor); } \ No newline at end of file diff --git a/style/lib/theme/colors.dart b/style/lib/theme/colors.dart index 3154c712..6d741729 100644 --- a/style/lib/theme/colors.dart +++ b/style/lib/theme/colors.dart @@ -41,6 +41,11 @@ final ThemeData _materialDarkTheme = ThemeData.dark(useMaterial3: true); final ThemeData materialThemeDataLight = _materialLightTheme.copyWith( primaryColor: primaryColor, dividerColor: outlineColor, + datePickerTheme: _materialLightTheme.datePickerTheme.copyWith( + backgroundColor: surfaceLightColor, + headerForegroundColor: textPrimaryLightColor, + dividerColor: outlineColor, + ), colorScheme: _materialLightTheme.colorScheme.copyWith( primary: primaryColor, secondary: secondaryColor, @@ -63,6 +68,11 @@ final ThemeData materialThemeDataLight = _materialLightTheme.copyWith( final ThemeData materialThemeDataDark = _materialDarkTheme.copyWith( primaryColor: primaryColor, dividerColor: outlineColor, + datePickerTheme: _materialDarkTheme.datePickerTheme.copyWith( + backgroundColor: surfaceDarkColor, + headerForegroundColor: textPrimaryDarkColor, + dividerColor: outlineColor, + ), colorScheme: _materialDarkTheme.colorScheme.copyWith( primary: primaryColor, secondary: secondaryColor,