From 8a4a158af53181f81cd7bf4eff198ced1b5144a0 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 16 Jul 2024 14:05:54 +0300 Subject: [PATCH 01/13] profile changes --- android/app/src/main/AndroidManifest.xml | 9 +- ios/Podfile | 7 + ios/Podfile.lock | 16 +- ios/Runner/Info.plist | 2 + lib/features/menu/bloc/menu_cubit.dart | 2 + .../menu/bloc/profile_bloc/profile_bloc.dart | 400 ++++++++++++++++++ .../menu/bloc/profile_bloc/profile_event.dart | 75 ++++ .../menu/bloc/profile_bloc/profile_state.dart | 69 +++ .../menu/bloc/profile_bloc/region_model.dart | 25 ++ .../bloc/profile_bloc/time_zone_model.dart | 27 ++ lib/features/menu/view/menu_view.dart | 3 +- .../widgets/create_home/create_home_view.dart | 3 - .../view/widgets/profile/profile_tab.dart | 11 +- .../view/widgets/profile/profile_view.dart | 287 ++++++++----- .../view/widgets/profile/region_page.dart | 100 +++++ .../profile/time_zone_screen_page.dart | 101 +++++ lib/services/api/profile_api.dart | 59 +++ pubspec.lock | 132 +++++- pubspec.yaml | 2 + 19 files changed, 1199 insertions(+), 131 deletions(-) create mode 100644 lib/features/menu/bloc/profile_bloc/profile_bloc.dart create mode 100644 lib/features/menu/bloc/profile_bloc/profile_event.dart create mode 100644 lib/features/menu/bloc/profile_bloc/profile_state.dart create mode 100644 lib/features/menu/bloc/profile_bloc/region_model.dart create mode 100644 lib/features/menu/bloc/profile_bloc/time_zone_model.dart create mode 100644 lib/features/menu/view/widgets/profile/region_page.dart create mode 100644 lib/features/menu/view/widgets/profile/time_zone_screen_page.dart create mode 100644 lib/services/api/profile_api.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9287665..ad16c69 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,12 +2,17 @@ + + + + + + android:allowBackup="false"> + NSPhotoLibraryUsageDescription + We need access to your photo library to allow you to select and upload photos. CADisableMinimumFrameDurationOnPhone CFBundleDevelopmentRegion diff --git a/lib/features/menu/bloc/menu_cubit.dart b/lib/features/menu/bloc/menu_cubit.dart index a51a41e..d78e0e0 100644 --- a/lib/features/menu/bloc/menu_cubit.dart +++ b/lib/features/menu/bloc/menu_cubit.dart @@ -7,6 +7,8 @@ class MenuCubit extends Cubit { static MenuCubit of(context) => BlocProvider.of(context); + String name = ''; + // List menuLists = [ // MenuListModel( // label: 'Home Management', diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart new file mode 100644 index 0000000..31562a7 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -0,0 +1,400 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; +import 'package:syncrow_app/utils/helpers/snack_bar.dart'; +import 'package:image_picker/image_picker.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class ProfileBloc extends Bloc { + final String userId; + bool isSaving = false; + bool editName = false; + final FocusNode focusNode = FocusNode(); + File? image; + final ImagePicker _picker = ImagePicker(); + String timeZoneSelected = ''; + String regionSelected = ''; + final TextEditingController searchController = TextEditingController(); + final TextEditingController nameController = TextEditingController(text: 'asd alsdkasdd '); + List? timeZoneList; + List? regionList; + + ProfileBloc({required this.userId}) : super(InitialState()) { + on(_fetchUserInfo); + on(_fetchTimeZone); + on(_fetchRegion); + on(saveName); + on(_selectImage); + on(_changeName); + on(selectTimeZone); + on(searchRegion); + } + + + List> timeZone= + [ + {"name": "GMT-12:00", "offset": "-12:00","id":'1'}, + + {"name": "GMT-11:00", "offset": "-11:00","id":'2'}, + + {"name": "GMT-10:00", "offset": "-10:00","id":'3'}, + + {"name": "GMT-09:00", "offset": "-09:00","id":'4'}, + + {"name": "GMT-08:00", "offset": "-08:00","id":'5'}, + + {"name": "GMT-07:00", "offset": "-07:00","id":'6'}, + + {"name": "GMT-06:00", "offset": "-06:00","id":'7'}, + + {"name": "GMT-05:00", "offset": "-05:00","id":'8'}, + + {"name": "GMT-04:00", "offset": "-04:00","id":'9'}, + + ]; + + + List> region= + [ + {"name": "region 1", "id":'1'}, + {"name": "region 2", "id":'2'}, + {"name": "region 3","id":'3'}, + {"name": "region 4", "id":'4'}, + {"name": "region 5","id":'5'}, + {"name": "region 6","id":'6'}, + {"name": "region 7","id":'7'}, + {"name": "region 8", "id":'8'}, + ]; + + + + void _changeName(ChangeNameEvent event, Emitter emit) { + emit(LoadingInitialState()); + editName = event.value!; + if (editName) { + Future.delayed(const Duration(milliseconds: 500), () { + focusNode.requestFocus(); + }); + }else { + focusNode.unfocus(); + } + emit(NameEditingState(editName: editName)); + } + + void _fetchUserInfo(InitialEvent event, Emitter emit) async { + try { + emit(LoadingInitialState()); + // var response = await ProfileApi.fetchUserInfo(userId); + // List statusModelList = []; + // for (var status in response['status']) { + // statusModelList.add(StatusModel.fromJson(status)); + // } + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future _fetchTimeZone( + TimeZoneInitialEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + timeZoneList = timeZone.map((zone) => TimeZone.fromJson(zone)).toList(); + // List statusModelList = []; + // for (var status in response['status']) { + // statusModelList.add(StatusModel.fromJson(status)); + // } + emit(UpdateState(timeZoneList: timeZoneList!)); // Make sure UpdateState accepts the list + return timeZoneList; + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future selectTimeZone(SelectTimeZoneEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + timeZoneSelected= event.val; + CustomSnackBar.displaySnackBar('Save Successfully'); + Navigator.pop(event.context); + return timeZoneList; + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future selectRegion(SelectRegionEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + regionSelected= event.val; + CustomSnackBar.displaySnackBar('Save Successfully'); + Navigator.pop(event.context); + return timeZoneList; + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future searchRegion(SearchRegionEvent event, Emitter emit) async { + final query = event.query.toLowerCase(); + if(event.query.isEmpty){ + regionList = region.map((zone) => RegionModel.fromJson(zone)).toList(); + }else{ + final filteredRegions = regionList?.where((region) { + return region.name.toLowerCase().contains(query); + }).toList() ?? []; + regionList = filteredRegions;// Assume this fetches the regions + emit(RegionsLoadedState(regions: filteredRegions)); + } + } + + void _fetchRegion(RegionInitialEvent event, Emitter emit) async { + try { + emit(LoadingInitialState()); + regionList = region.map((zone) => RegionModel.fromJson(zone)).toList(); + emit(UpdateState(timeZoneList: timeZoneList!)); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future _selectImage(SelectImageEvent event, Emitter emit) async { + if (await _requestPermission()) { + emit(ChangeImageState()); + final pickedFile = await _picker.pickImage(source: ImageSource.gallery); + if (pickedFile != null) { + image = File(pickedFile.path); + final bytes = image!.readAsBytesSync().lengthInBytes; + final kb = bytes / 1024; + final mb = kb / 1024; + if(mb>1){ + image=null; + CustomSnackBar.displaySnackBar('Image size must be 1 MB or less'); + }else{ + await saveImage(); + } + } else { + print('No image selected.'); + } + emit(ImageSelectedState()); + } else { + _showPermissionDeniedDialog(event.context); + } + } + + Future saveImage() async { + emit(ChangeImageState()); + List imageBytes = image!.readAsBytesSync(); + String base64Image = base64Encode(imageBytes); + print(base64Image); + emit(ImageSelectedState()); + } + + Future _requestPermission() async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + if (Platform.isAndroid ) { + PermissionStatus status = await Permission.photos.status; + if(androidInfo.version.sdkInt<= 33){ + if (status.isDenied) { + PermissionStatus status = await Permission.storage.request(); + if (status.isGranted) { + return true; + } else { + return false; + } + } + }else{ + if (status.isGranted) { + return true; + } else if (status.isDenied) { + PermissionStatus status = await Permission.photos.request(); + if (status.isGranted) { + return true; + } else { + return false; + } + } + } + return false; + } else { + SharedPreferences sharedPreferences = + await SharedPreferences.getInstance(); + bool firstClick = sharedPreferences.getBool('firstPermission') ?? true; + await sharedPreferences.setBool('firstPermission', false); + if (firstClick == false) { + var status = await Permission.photos.status; + return status.isGranted; + } else { + return true; + } + } + } + + void _showPermissionDeniedDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Permission Denied'), + content: const Text( + 'Photo access is required to select an image. Please allow photo access in the app settings.'), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + openAppSettings(); + }, + child: const Text('Settings'), + ), + ], + ); + }, + ); + } + + Future saveName(SaveNameEvent event, Emitter emit) async { + if (_validateInputs()) return; + try { + isSaving = true; + emit(LoadingSaveState()); + Navigator.of(event.context).pop(true); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveState()); + } catch (_) { + } finally { + isSaving = false; + } + } + + bool _validateInputs() { + if (nameController.text.length < 7) { + CustomSnackBar.displaySnackBar('Password less than 7'); + return true; + } + if (nameController.text.isEmpty) { + CustomSnackBar.displaySnackBar('Password required'); + return true; + } + if (nameController.text.isEmpty) { + CustomSnackBar.displaySnackBar('Password name required'); + return true; + } + return false; + } + + void showRenameDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + child: SizedBox( + height: 200, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.only( + top: 25, + ), + child: Text( + 'Change Name', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ), + const Spacer(), + const Spacer(), + Row( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.grey, + width: 0.5, + ), + top: BorderSide( + color: Colors.grey, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: const Text( + 'Cancel', + style: TextStyle( + fontSize: 18, + color: Colors.black, + ), + ), + ), + ), + ), + const VerticalDivider( + thickness: 1, + width: 1, + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: Colors.grey, + width: 0.5, + ), + top: BorderSide( + color: Colors.grey, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + // Handle save action + }, + child: const Text( + 'Save', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + ), + ), + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/menu/bloc/profile_bloc/profile_event.dart b/lib/features/menu/bloc/profile_bloc/profile_event.dart new file mode 100644 index 0000000..a8e41d4 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_event.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +class InitialEvent extends ProfileEvent {} + +class TimeZoneInitialEvent extends ProfileEvent {} + +class ChangeNameEvent extends ProfileEvent { + final bool? value; + const ChangeNameEvent({ this.value}); +} + +class RegionInitialEvent extends ProfileEvent {} + + +class UpdateLockEvent extends ProfileEvent { + final bool value; + const UpdateLockEvent({required this.value}); + @override + List get props => [value]; +} + +class SaveNameEvent extends ProfileEvent { + final BuildContext context; + const SaveNameEvent({required this.context}); + @override + List get props => [context]; +} + + +class SelectImageEvent extends ProfileEvent { + final BuildContext context; + final bool isSelected; + const SelectImageEvent({required this.context,required this.isSelected}); + @override + List get props => [context,isSelected]; +} + + +class ToggleRepeatEvent extends ProfileEvent {} + +class SelectTimeZoneEvent extends ProfileEvent { + final String val; + final BuildContext context; + const SelectTimeZoneEvent({required this.val,required this.context}); + @override + List get props => [val]; +} + +class SelectRegionEvent extends ProfileEvent { + final String val; + final BuildContext context; + const SelectRegionEvent({required this.val,required this.context}); + @override + List get props => [val,context]; +} + + +class SearchRegionEvent extends ProfileEvent { + final String query; + + const SearchRegionEvent({required this.query}); + @override + List get props => [query]; +} + + + diff --git a/lib/features/menu/bloc/profile_bloc/profile_state.dart b/lib/features/menu/bloc/profile_bloc/profile_state.dart new file mode 100644 index 0000000..3f46c07 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_state.dart @@ -0,0 +1,69 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_app/features/devices/model/smart_door_model.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; + +class ProfileState extends Equatable { + const ProfileState(); + + @override + List get props => []; +} + +class InitialState extends ProfileState {} + +class LoadingInitialState extends ProfileState {} + +class UpdateState extends ProfileState { + final List timeZoneList; + + UpdateState({required this.timeZoneList}); +} + +class NameEditingState extends ProfileState { + final bool editName; + + NameEditingState({required this.editName}); +} + +class LoadingNewSate extends ProfileState { + final SmartDoorModel smartDoorModel; + const LoadingNewSate({required this.smartDoorModel}); + + @override + List get props => [smartDoorModel]; +} + +class FailedState extends ProfileState { + final String errorMessage; + + const FailedState({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} + +class GeneratePasswordState extends ProfileState {} + +class ImageSelectedState extends ProfileState {} + +class ImageTooLargeState extends ProfileState {} + +class IsRepeatState extends ProfileState {} + +class IsStartEndState extends ProfileState {} + +class ChangeStartTimeState extends ProfileState {} + +class ChangeEndTimeState extends ProfileState {} + +class ChangeImageState extends ProfileState {} + +class SaveState extends ProfileState {} + +class LoadingSaveState extends ProfileState {} +class RegionsLoadedState extends ProfileState { + final List regions; + + const RegionsLoadedState({required this.regions}); +} \ No newline at end of file diff --git a/lib/features/menu/bloc/profile_bloc/region_model.dart b/lib/features/menu/bloc/profile_bloc/region_model.dart new file mode 100644 index 0000000..5548167 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/region_model.dart @@ -0,0 +1,25 @@ + + +class RegionModel { + final String name; + final String id; + + RegionModel({ + required this.name, + required this.id, + }); + + factory RegionModel.fromJson(Map json) { + return RegionModel( + name: json['name'], + id: json['id'].toString(), // Ensure id is a String + ); + } + + Map toJson() { + return { + 'name': name, + 'id': id, + }; + } +} diff --git a/lib/features/menu/bloc/profile_bloc/time_zone_model.dart b/lib/features/menu/bloc/profile_bloc/time_zone_model.dart new file mode 100644 index 0000000..e31c4a1 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/time_zone_model.dart @@ -0,0 +1,27 @@ +class TimeZone { + final String name; + final String offset; + final String id; + + TimeZone({ + required this.name, + required this.offset, + required this.id, + }); + + factory TimeZone.fromJson(Map json) { + return TimeZone( + name: json['name'], + offset: json['offset'], + id: json['id'].toString(), // Ensure id is a String + ); + } + + Map toJson() { + return { + 'name': name, + 'offset': offset, + 'id': id, + }; + } +} diff --git a/lib/features/menu/view/menu_view.dart b/lib/features/menu/view/menu_view.dart index 0d3dc83..a636ec1 100644 --- a/lib/features/menu/view/menu_view.dart +++ b/lib/features/menu/view/menu_view.dart @@ -20,11 +20,12 @@ class MenuView extends StatelessWidget { builder: (context, state) { return BlocBuilder( builder: (context, state) { + final profileBloc = BlocProvider.of(context); return SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ - const ProfileTab(), + ProfileTab(name:profileBloc.name), for (var section in menuSections) MenuList( section: section, diff --git a/lib/features/menu/view/widgets/create_home/create_home_view.dart b/lib/features/menu/view/widgets/create_home/create_home_view.dart index 201f80b..e11bede 100644 --- a/lib/features/menu/view/widgets/create_home/create_home_view.dart +++ b/lib/features/menu/view/widgets/create_home/create_home_view.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/create_unit_bloc/create_unit_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/create_unit_bloc/create_unit_event.dart'; import 'package:syncrow_app/features/menu/bloc/create_unit_bloc/create_unit_state.dart'; @@ -11,8 +10,6 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_large.dart import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; import 'package:syncrow_app/generated/assets.dart'; -import 'package:syncrow_app/navigation/navigation_service.dart'; -import 'package:syncrow_app/navigation/routing_constants.dart'; import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; diff --git a/lib/features/menu/view/widgets/profile/profile_tab.dart b/lib/features/menu/view/widgets/profile/profile_tab.dart index 2d86229..21117fc 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -6,9 +6,8 @@ import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dar import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; class ProfileTab extends StatelessWidget { - const ProfileTab({ - super.key, - }); + final String? name; + const ProfileTab({super.key,this.name}); @override Widget build(BuildContext context) { @@ -22,18 +21,18 @@ class ProfileTab extends StatelessWidget { ), ); }, - child: const Stack( + child: Stack( children: [ Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SizedBox(height: 20), + const SizedBox(height: 20), DefaultContainer( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ BodyMedium( - text: "Karim", + text: name!, fontWeight: FontWeight.bold, ), BodySmall(text: "Syncrow Account") diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 91a346e..94fbe33 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -1,129 +1,186 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; +import 'package:syncrow_app/features/menu/view/widgets/profile/region_page.dart'; +import 'package:syncrow_app/features/menu/view/widgets/profile/time_zone_screen_page.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; -import 'package:syncrow_app/features/shared_widgets/text_widgets/title_medium.dart'; -import 'package:syncrow_app/utils/context_extension.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; class ProfileView extends StatelessWidget { const ProfileView({super.key}); - @override Widget build(BuildContext context) { - return DefaultScaffold( - title: 'Profile Page', - child: Column( - children: [ - //profile pic - const SizedBox.square( - dimension: 120, - child: CircleAvatar( - backgroundColor: Colors.white, - child: SizedBox.square( - dimension: 115, - child: CircleAvatar( - backgroundColor: Colors.grey, - child: FlutterLogo(), - ), + return BlocProvider( + create: (BuildContext context) => ProfileBloc(userId: ''), + child: BlocConsumer(listener: (context, state) { + if (state is FailedState) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, ), + ); + } + }, builder: (context, state) { + final profileBloc = BlocProvider.of(context); + return DefaultScaffold( + title: 'Syncrow Account', + child: Column( + children: [ + SizedBox(height: MediaQuery.of(context).size.height*0.05,), + InkWell( + onTap: () { + profileBloc.add(SelectImageEvent(context: context, isSelected: false)); + }, + child: SizedBox.square( + dimension: 125, + child: CircleAvatar( + backgroundColor: Colors.white, + child: SizedBox.square( + dimension: 120, + child: CircleAvatar( + backgroundColor: Colors.grey, + backgroundImage: profileBloc.image == null + ? null + : FileImage(profileBloc.image!), + ), + ), + ), + ), + ), + const SizedBox(height: 20), + SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IntrinsicWidth( + child: TextFormField( + maxLength: 30, + style: const TextStyle( + color: Colors.black, + ), + textAlign: TextAlign.center, + focusNode: profileBloc.focusNode, + controller: profileBloc.nameController, + enabled: profileBloc.editName, + onEditingComplete: () { + profileBloc.add(const ChangeNameEvent(value: false)); + }, + decoration: const InputDecoration( + hintText: "Your Name", + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', // Hides the character count + ), + ), + ), + const SizedBox(width: 5), + InkWell( + onTap: () { + profileBloc.add(const ChangeNameEvent(value: true)); + }, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Icon( + Icons.edit_outlined, + size: 20, + color: ColorsManager.textPrimaryColor, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 10), + //Info + DefaultContainer( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 5, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // const BodyMedium(text: 'Email '), + // Flexible( + // child: TextField( + // textAlign: TextAlign.end, + // decoration: InputDecoration( + // hintText: ' Test@test.com', + // hintStyle: + // context.bodyMedium.copyWith(color: Colors.grey), + // border: InputBorder.none, + // ), + // ), + // ), + // ], + // ), + // Container( + // height: 1, + // color: ColorsManager.greyColor, + // ), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const RegionPage(), + )); + }, + child: const Padding( + padding: EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BodyMedium(text: 'Region '), + Flexible( + child: BodyMedium(text: 'United Arab Emirates') + ), + ], + ), + )), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => + const TimeZoneScreenPage(), + )); + }, + child: const Padding( + padding: EdgeInsets.only(top: 15, bottom: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + BodyMedium(text: 'Time Zone '), + Flexible( + child: BodyMedium(text: 'GMT +4') + ), + ], + ), + ), + ), + ], + )), + ], ), - ), - const SizedBox(height: 20), - //name - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const TitleMedium(text: 'Karim'), - const SizedBox( - width: 5, - ), - InkWell( - onTap: () { - //TODO: Implement edit name - }, - child: const Icon( - Icons.edit_outlined, - size: 20, - color: ColorsManager.textPrimaryColor, - ), - ), - ], - ), - const SizedBox(height: 10), - //Info - DefaultContainer( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BodyMedium(text: 'Email '), - Flexible( - child: TextField( - textAlign: TextAlign.end, - decoration: InputDecoration( - hintText: ' Test@test.com', - hintStyle: - context.bodyMedium.copyWith(color: Colors.grey), - border: InputBorder.none, - ), - ), - ), - ], - ), - Container( - height: 1, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BodyMedium(text: 'Region '), - Flexible( - child: TextField( - textAlign: TextAlign.end, - decoration: InputDecoration( - hintText: 'United Arab Emirates', - hintStyle: - context.bodyMedium.copyWith(color: Colors.grey), - border: InputBorder.none, - ), - ), - ), - ], - ), - Container( - height: 1, - color: ColorsManager.greyColor, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const BodyMedium(text: 'Time Zone '), - Flexible( - child: TextField( - textAlign: TextAlign.end, - decoration: InputDecoration( - hintText: 'GMT +4', - hintStyle: - context.bodyMedium.copyWith(color: Colors.grey), - border: InputBorder.none, - ), - ), - ), - ], - ), - ], - )), - ], - ), - ); + ); + })); } } diff --git a/lib/features/menu/view/widgets/profile/region_page.dart b/lib/features/menu/view/widgets/profile/region_page.dart new file mode 100644 index 0000000..91321e9 --- /dev/null +++ b/lib/features/menu/view/widgets/profile/region_page.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; +import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class RegionPage extends StatelessWidget { + const RegionPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => ProfileBloc(userId: '')..add(RegionInitialEvent()), + child: BlocConsumer(listener: (context, state) { + if (state is FailedState) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + final profileBloc = BlocProvider.of(context); + final regionList = profileBloc.regionList ?? []; // Safeguard against null + return DefaultScaffold( + padding: const EdgeInsets.all(0), + title: 'Region', + child: state is LoadingInitialState + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + TextFormField( + controller:profileBloc.searchController , + onChanged: (value) { + profileBloc.add(SearchRegionEvent(query: value)); + }, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.search), + hintText: 'Search', + fillColor: ColorsManager.textGray, + ), + ), + const SizedBox(height: 10), + Expanded( + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.onPrimaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: ListView.builder( + itemCount: regionList.length, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + profileBloc.add(SelectRegionEvent( + val: regionList[index].id, + context: context)); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SizedBox( + height: 45, + child: ListTile( + contentPadding: EdgeInsets.zero, + // trailing: BodyMedium( + // text: regionList[index].offset, + // fontSize: 13, + // fontColor: ColorsManager.textGray,), + leading: BodyMedium( + fontSize: 15, + text: regionList[index].name,),), + ), + ), + const Divider(color: ColorsManager.textGray), // Divider between items + ], + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + })); + } +} diff --git a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart new file mode 100644 index 0000000..22c3b5f --- /dev/null +++ b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; +import 'package:syncrow_app/features/shared_widgets/default_scaffold.dart'; +import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; +import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; + +class TimeZoneScreenPage extends StatelessWidget { + const TimeZoneScreenPage({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => ProfileBloc(userId: '')..add(TimeZoneInitialEvent()), + child: + BlocConsumer(listener: (context, state) { + if (state is FailedState) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + backgroundColor: Colors.red, + ), + ); + } + }, + builder: (context, state) { + final profileBloc = BlocProvider.of(context); + final timeZoneList = profileBloc.timeZoneList ?? []; // Safeguard against null + return DefaultScaffold( + padding: const EdgeInsets.all(0), + title: 'Time Zone', + child: state is LoadingInitialState + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + TextFormField( + decoration: const InputDecoration( + prefixIcon: Icon(Icons.search), + hintText: 'Search', + fillColor: ColorsManager.textGray, + ), + ), + const SizedBox(height: 10), + Expanded( + child: Container( + decoration: const BoxDecoration( + color: ColorsManager.onPrimaryColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: ListView.builder( + itemCount: timeZoneList.length, + itemBuilder: (context, index) { + return InkWell( + onTap: () { + + profileBloc.add(SelectTimeZoneEvent( + val: timeZoneList[index].id, + context: context)); + }, + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: SizedBox( + height: 45, + child: ListTile( + contentPadding: EdgeInsets.zero, + trailing: BodyMedium( + text: timeZoneList[index].offset, + fontSize: 13, + fontColor: ColorsManager.textGray,), + leading: BodyMedium( + fontSize: 15, + text: timeZoneList[index].name,),), + ), + ), + const Divider(color: ColorsManager.textGray), // Divider between items + ], + ), + ), + ); + }, + ), + ), + ), + ], + ), + ); + }) + ); + } +} diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart new file mode 100644 index 0000000..a133ef7 --- /dev/null +++ b/lib/services/api/profile_api.dart @@ -0,0 +1,59 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:syncrow_app/features/devices/model/device_category_model.dart'; +import 'package:syncrow_app/features/devices/model/device_control_model.dart'; +import 'package:syncrow_app/features/devices/model/device_model.dart'; +import 'package:syncrow_app/features/devices/model/function_model.dart'; +import 'package:syncrow_app/services/api/api_links_endpoints.dart'; +import 'package:syncrow_app/services/api/http_service.dart'; + +import '../../features/devices/model/create_temporary_password_model.dart'; + +class ProfileApi { + static final HTTPService _httpService = HTTPService(); + static Future> saveName( + String newName, String userId) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.controlDevice.replaceAll('{deviceUuid}', userId), + body: newName, + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future> saveImage( + File newName, String userId) async { + try { + final response = await _httpService.post( + path: ApiEndpoints.controlDevice.replaceAll('{deviceUuid}', userId), + body: newName, + showServerMessage: false, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future fetchUserInfo(String userId) async { + final response = await _httpService.get( + path: ApiEndpoints.groupBySpace.replaceAll('{unitUuid}', userId), + // queryParameters: params, + showServerMessage: false, + expectedResponseModel: (json) => DevicesCategoryModel.fromJsonList(json), + ); + return response; + } + + +} diff --git a/pubspec.lock b/pubspec.lock index ec31228..6530155 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + url: "https://pub.dev" + source: hosted + version: "10.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: @@ -169,6 +185,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" firebase_analytics: dependency: "direct main" description: @@ -307,6 +355,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + url: "https://pub.dev" + source: hosted + version: "2.0.20" flutter_secure_storage: dependency: "direct main" description: @@ -405,6 +461,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: "4161e1f843d8480d2e9025ee22411778c3c9eb7e40076dcf2da23d8242b7b51c" + url: "https://pub.dev" + source: hosted + version: "0.8.12+3" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + url: "https://pub.dev" + source: hosted + version: "3.0.4" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: "direct main" description: @@ -986,6 +1106,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.2.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" xdg_directories: dependency: transitive description: @@ -1003,5 +1131,5 @@ packages: source: hosted version: "6.5.0" sdks: - dart: ">=3.3.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0dbf15a..4049dc7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,8 @@ dependencies: smooth_page_indicator: ^1.1.0 uuid: ^4.4.0 time_picker_spinner: ^1.0.0 + image_picker: ^1.1.2 + device_info_plus: ^10.1.0 dev_dependencies: flutter_lints: ^3.0.1 From e5e839248de89437ef878cdc557c52a90597d1af Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 23 Jul 2024 16:16:31 +0300 Subject: [PATCH 02/13] profile --- lib/features/auth/bloc/auth_cubit.dart | 34 +- lib/features/auth/model/user_model.dart | 42 ++- lib/features/menu/bloc/menu_cubit.dart | 81 ----- .../menu/bloc/profile_bloc/profile_bloc.dart | 326 ++++++------------ .../menu/bloc/profile_bloc/profile_event.dart | 17 +- .../menu/bloc/profile_bloc/profile_state.dart | 24 +- .../menu/bloc/profile_bloc/region_model.dart | 8 +- .../bloc/profile_bloc/time_zone_model.dart | 6 +- lib/features/menu/view/menu_view.dart | 2 +- .../view/widgets/profile/profile_tab.dart | 120 ++++--- .../view/widgets/profile/profile_view.dart | 186 +++++----- .../view/widgets/profile/region_page.dart | 2 +- .../profile/time_zone_screen_page.dart | 7 +- lib/features/splash/view/splash_view.dart | 1 + lib/my_app.dart | 2 + lib/services/api/api_links_endpoints.dart | 13 +- lib/services/api/profile_api.dart | 102 ++++-- 17 files changed, 457 insertions(+), 516 deletions(-) diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 2b9f14b..6e2b730 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -1,3 +1,4 @@ + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -8,6 +9,7 @@ import 'package:syncrow_app/features/auth/model/user_model.dart'; import 'package:syncrow_app/navigation/navigation_service.dart'; import 'package:syncrow_app/navigation/routing_constants.dart'; import 'package:syncrow_app/services/api/authentication_api.dart'; +import 'package:syncrow_app/services/api/profile_api.dart'; import 'package:syncrow_app/utils/helpers/shared_preferences_helper.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:syncrow_app/utils/resource_manager/strings_manager.dart'; @@ -178,12 +180,18 @@ class AuthCubit extends Cubit { if (token.accessTokenIsNotEmpty) { debugPrint('token: ${token.accessToken}'); FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken); - + await storage.write( + key: Token.loginAccessTokenKey, + value: token.accessToken + ); + await fetchUserInfo(); const FlutterSecureStorage().write( key: UserModel.userUuidKey, - value: Token.decodeToken(token.accessToken)['uuid'].toString()); - + value: Token.decodeToken(token.accessToken)['uuid'].toString() + ).then((value) async { + var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + user = await ProfileApi.fetchUserInfo(uuid); + },); user = UserModel.fromToken(token); emailController.clear(); passwordController.clear(); @@ -277,8 +285,7 @@ class AuthCubit extends Cubit { try { emit(AuthTokenLoading()); const storage = FlutterSecureStorage(); - final firstLaunch = - await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; + final firstLaunch = await SharedPreferencesHelper.readBoolFromSP(StringsManager.firstLaunch) ?? true; if (firstLaunch) { storage.deleteAll(); @@ -311,6 +318,7 @@ class AuthCubit extends Cubit { } } + sendToForgetPassword({required String password}) async { try { emit(AuthForgetPassLoading()); @@ -320,4 +328,18 @@ class AuthCubit extends Cubit { emit(AuthForgetPassError(message: 'Something went wrong')); } } + + + Future fetchUserInfo() async { + try { + emit(AuthLoading()); + var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + user = await ProfileApi.fetchUserInfo(uuid); + emit(AuthLoginSuccess()); + + } catch (e) { + return; + } + } + } diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart index 5491d73..ab127dc 100644 --- a/lib/features/auth/model/user_model.dart +++ b/lib/features/auth/model/user_model.dart @@ -4,34 +4,42 @@ class UserModel { static String userUuidKey = 'userUuid'; final String? uuid; final String? email; - final String? name; - final String? photoUrl; - + final String? firstName; + final String? lastName; + final String? profilePicture; final String? phoneNumber; - final bool? isEmailVerified; - + final String? regionName; + final String? timeZone; final bool? isAgreementAccepted; UserModel({ required this.uuid, required this.email, - required this.name, - required this.photoUrl, + required this.firstName, + required this.lastName, + required this.profilePicture, required this.phoneNumber, required this.isEmailVerified, required this.isAgreementAccepted, + required this.regionName, // Add this line + required this.timeZone, // Add this line + }); factory UserModel.fromJson(Map json) { return UserModel( - uuid: json['id'], + uuid: json['uuid'], email: json['email'], - name: json['name'], - photoUrl: json['photoUrl'], + firstName: json['firstName'], + lastName: json['lastName'], + profilePicture: json['profilePicture'], phoneNumber: json['phoneNumber'], isEmailVerified: json['isEmailVerified'], isAgreementAccepted: json['isAgreementAccepted'], + regionName: json['region']?['regionName'], // Extract regionName + timeZone: json['timeZone']?['timeZoneOffset'], // Extract regionName + ); } @@ -44,11 +52,16 @@ class UserModel { return UserModel( uuid: tempJson['uuid'].toString(), email: tempJson['email'], - name: null, - photoUrl: null, + lastName: tempJson['lastName'], + firstName:tempJson['firstName'] , + profilePicture: tempJson['profilePicture'], phoneNumber: null, isEmailVerified: null, isAgreementAccepted: null, + regionName: tempJson['region']?['regionName'], + timeZone: tempJson['timezone']?['timeZoneOffset'], + + ); } @@ -56,8 +69,9 @@ class UserModel { return { 'id': uuid, 'email': email, - 'name': name, - 'photoUrl': photoUrl, + 'lastName': lastName, + 'firstName': firstName, + 'photoUrl': profilePicture, 'phoneNumber': phoneNumber, 'isEmailVerified': isEmailVerified, 'isAgreementAccepted': isAgreementAccepted, diff --git a/lib/features/menu/bloc/menu_cubit.dart b/lib/features/menu/bloc/menu_cubit.dart index d78e0e0..068d947 100644 --- a/lib/features/menu/bloc/menu_cubit.dart +++ b/lib/features/menu/bloc/menu_cubit.dart @@ -9,85 +9,4 @@ class MenuCubit extends Cubit { String name = ''; - // List menuLists = [ - // MenuListModel( - // label: 'Home Management', - // listItems: [ - // ListItemModel( - // label: 'Create a Home', - // ), - // ListItemModel( - // label: 'Join a Home', - // ), - // ListItemModel( - // label: 'Manage Your Home', - // ), - // ], - // ), - // MenuListModel( - // label: 'General Settings', - // listItems: [ - // ListItemModel( - // label: 'Voice Assistant', - // ), - // ListItemModel( - // label: 'Temperature unit', - // ), - // ListItemModel( - // label: 'Touch tone on panel', - // ), - // ListItemModel( - // label: 'Language', - // ), - // ListItemModel( - // label: 'Network diagnosis', - // ), - // ListItemModel( - // label: 'Clear cache', - // ), - // ], - // ), - // MenuListModel( - // label: 'Messages Center', - // listItems: [ - // ListItemModel( - // label: 'Alerts', - // ), - // ListItemModel( - // label: 'Messages', - // ), - // ListItemModel( - // label: 'FAQs', - // ), - // ListItemModel( - // label: 'Help & Feedback', - // ), - // ], - // ), - // MenuListModel( - // label: 'Security and Privacy', - // listItems: [ - // ListItemModel( - // label: 'Security', - // ), - // ListItemModel( - // label: 'privacy', - // ), - // ], - // ), - // MenuListModel( - // label: 'Legal Information', - // listItems: [ - // ListItemModel( - // label: 'About', - // ), - // ListItemModel( - // label: 'Privacy Policy', - // ), - // ListItemModel( - // label: 'User Agreement', - // ), - // ], - // ), - // ]; } diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 31562a7..821aeb5 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -1,19 +1,21 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; +import 'package:syncrow_app/services/api/profile_api.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; class ProfileBloc extends Bloc { - final String userId; bool isSaving = false; bool editName = false; final FocusNode focusNode = FocusNode(); @@ -22,12 +24,13 @@ class ProfileBloc extends Bloc { String timeZoneSelected = ''; String regionSelected = ''; final TextEditingController searchController = TextEditingController(); - final TextEditingController nameController = TextEditingController(text: 'asd alsdkasdd '); + final TextEditingController nameController = TextEditingController(text: '${AuthCubit.user!.firstName} ${AuthCubit.user!.lastName}'); List? timeZoneList; List? regionList; - ProfileBloc({required this.userId}) : super(InitialState()) { - on(_fetchUserInfo); + + ProfileBloc() : super(InitialState()) { + on(_fetchUserInfo); on(_fetchTimeZone); on(_fetchRegion); on(saveName); @@ -35,46 +38,18 @@ class ProfileBloc extends Bloc { on(_changeName); on(selectTimeZone); on(searchRegion); + on(searchTimeZone); + on(selectRegion); } - - List> timeZone= - [ - {"name": "GMT-12:00", "offset": "-12:00","id":'1'}, - - {"name": "GMT-11:00", "offset": "-11:00","id":'2'}, - - {"name": "GMT-10:00", "offset": "-10:00","id":'3'}, - - {"name": "GMT-09:00", "offset": "-09:00","id":'4'}, - - {"name": "GMT-08:00", "offset": "-08:00","id":'5'}, - - {"name": "GMT-07:00", "offset": "-07:00","id":'6'}, - - {"name": "GMT-06:00", "offset": "-06:00","id":'7'}, - - {"name": "GMT-05:00", "offset": "-05:00","id":'8'}, - - {"name": "GMT-04:00", "offset": "-04:00","id":'9'}, - - ]; - - - List> region= - [ - {"name": "region 1", "id":'1'}, - {"name": "region 2", "id":'2'}, - {"name": "region 3","id":'3'}, - {"name": "region 4", "id":'4'}, - {"name": "region 5","id":'5'}, - {"name": "region 6","id":'6'}, - {"name": "region 7","id":'7'}, - {"name": "region 8", "id":'8'}, - ]; - - - + Uint8List? decodeBase64Image(String? base64String) { + if (base64String != null) { + final startIndex = base64String.indexOf('base64,') + 7; + final pureBase64String = base64String.substring(startIndex); + return base64.decode(pureBase64String); + } + return null; + } void _changeName(ChangeNameEvent event, Emitter emit) { emit(LoadingInitialState()); editName = event.value!; @@ -88,30 +63,21 @@ class ProfileBloc extends Bloc { emit(NameEditingState(editName: editName)); } - void _fetchUserInfo(InitialEvent event, Emitter emit) async { + void _fetchUserInfo(InitialProfileEvent event, Emitter emit) async { try { emit(LoadingInitialState()); - // var response = await ProfileApi.fetchUserInfo(userId); - // List statusModelList = []; - // for (var status in response['status']) { - // statusModelList.add(StatusModel.fromJson(status)); - // } + AuthCubit.user = await ProfileApi.fetchUserInfo(AuthCubit.user!.uuid); } catch (e) { emit(FailedState(errorMessage: e.toString())); return; } } - Future _fetchTimeZone( - TimeZoneInitialEvent event, Emitter emit) async { + Future _fetchTimeZone(TimeZoneInitialEvent event, Emitter emit) async { emit(LoadingInitialState()); try { - timeZoneList = timeZone.map((zone) => TimeZone.fromJson(zone)).toList(); - // List statusModelList = []; - // for (var status in response['status']) { - // statusModelList.add(StatusModel.fromJson(status)); - // } - emit(UpdateState(timeZoneList: timeZoneList!)); // Make sure UpdateState accepts the list + timeZoneList = await ProfileApi.fetchTimeZone(); + emit(UpdateState(timeZoneList: timeZoneList!)); return timeZoneList; } catch (e) { emit(FailedState(errorMessage: e.toString())); @@ -119,26 +85,25 @@ class ProfileBloc extends Bloc { } } + Future selectTimeZone(SelectTimeZoneEvent event, Emitter emit) async { - emit(LoadingInitialState()); try { - timeZoneSelected= event.val; + emit(LoadingInitialState()); + timeZoneSelected = event.val; + await ProfileApi.saveTimeZone(regionUuid: event.val); CustomSnackBar.displaySnackBar('Save Successfully'); - Navigator.pop(event.context); - return timeZoneList; + emit(SaveState()); } catch (e) { emit(FailedState(errorMessage: e.toString())); - return; } } Future selectRegion(SelectRegionEvent event, Emitter emit) async { - emit(LoadingInitialState()); try { - regionSelected= event.val; + emit(LoadingInitialState()); + await ProfileApi.saveRegion(regionUuid:event.val ); CustomSnackBar.displaySnackBar('Save Successfully'); - Navigator.pop(event.context); - return timeZoneList; + emit(SaveState()); } catch (e) { emit(FailedState(errorMessage: e.toString())); return; @@ -146,26 +111,43 @@ class ProfileBloc extends Bloc { } Future searchRegion(SearchRegionEvent event, Emitter emit) async { + emit(LoadingInitialState()); final query = event.query.toLowerCase(); - if(event.query.isEmpty){ - regionList = region.map((zone) => RegionModel.fromJson(zone)).toList(); - }else{ + if(event.query.isNotEmpty){ final filteredRegions = regionList?.where((region) { return region.name.toLowerCase().contains(query); }).toList() ?? []; regionList = filteredRegions;// Assume this fetches the regions emit(RegionsLoadedState(regions: filteredRegions)); + }else{ + regionList = await ProfileApi.fetchRegion(); + emit(RegionsLoadedState(regions: regionList!)); } } + Future searchTimeZone(SearchTimeZoneEvent event, Emitter emit) async { + emit(LoadingInitialState()); + final query = event.query.toLowerCase(); + if(event.query.isNotEmpty){ + final filtered = timeZoneList?.where((region) { + return region.name.toLowerCase().contains(query); + }).toList() ?? []; + timeZoneList = filtered; + emit(TimeZoneLoadedState(regions: filtered)); + }else{ + timeZoneList = await ProfileApi.fetchTimeZone(); + emit(UpdateState(timeZoneList: timeZoneList!)); + } + } + + void _fetchRegion(RegionInitialEvent event, Emitter emit) async { try { emit(LoadingInitialState()); - regionList = region.map((zone) => RegionModel.fromJson(zone)).toList(); - emit(UpdateState(timeZoneList: timeZoneList!)); + regionList = await ProfileApi.fetchRegion(); + emit(RegionsLoadedState(regions: regionList!)); } catch (e) { emit(FailedState(errorMessage: e.toString())); - return; } } @@ -182,7 +164,7 @@ class ProfileBloc extends Bloc { image=null; CustomSnackBar.displaySnackBar('Image size must be 1 MB or less'); }else{ - await saveImage(); + await _saveImage(); } } else { print('No image selected.'); @@ -193,54 +175,15 @@ class ProfileBloc extends Bloc { } } - Future saveImage() async { + Future _saveImage() async { emit(ChangeImageState()); List imageBytes = image!.readAsBytesSync(); String base64Image = base64Encode(imageBytes); print(base64Image); + var response = await ProfileApi.saveImage(base64Image); emit(ImageSelectedState()); } - Future _requestPermission() async { - DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); - AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; - if (Platform.isAndroid ) { - PermissionStatus status = await Permission.photos.status; - if(androidInfo.version.sdkInt<= 33){ - if (status.isDenied) { - PermissionStatus status = await Permission.storage.request(); - if (status.isGranted) { - return true; - } else { - return false; - } - } - }else{ - if (status.isGranted) { - return true; - } else if (status.isDenied) { - PermissionStatus status = await Permission.photos.request(); - if (status.isGranted) { - return true; - } else { - return false; - } - } - } - return false; - } else { - SharedPreferences sharedPreferences = - await SharedPreferences.getInstance(); - bool firstClick = sharedPreferences.getBool('firstPermission') ?? true; - await sharedPreferences.setBool('firstPermission', false); - if (firstClick == false) { - var status = await Permission.photos.status; - return status.isGranted; - } else { - return true; - } - } - } void _showPermissionDeniedDialog(BuildContext context) { showDialog( @@ -272,129 +215,72 @@ class ProfileBloc extends Bloc { Future saveName(SaveNameEvent event, Emitter emit) async { if (_validateInputs()) return; try { + add(const ChangeNameEvent(value: false)); isSaving = true; emit(LoadingSaveState()); + final fullName = nameController.text; + final nameParts = fullName.split(' '); + final firstName = nameParts[0]; + final lastName = nameParts.length > 1 ? nameParts[1] : ''; + var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); + add(InitialProfileEvent()); + AuthCubit.get(event.context).fetchUserInfo(); Navigator.of(event.context).pop(true); CustomSnackBar.displaySnackBar('Save Successfully'); emit(SaveState()); } catch (_) { + // Handle the error } finally { isSaving = false; } } bool _validateInputs() { - if (nameController.text.length < 7) { - CustomSnackBar.displaySnackBar('Password less than 7'); - return true; - } - if (nameController.text.isEmpty) { - CustomSnackBar.displaySnackBar('Password required'); - return true; - } - if (nameController.text.isEmpty) { - CustomSnackBar.displaySnackBar('Password name required'); + if (nameController.text.length < 2) { + CustomSnackBar.displaySnackBar('Name Must More than 2 '); return true; } return false; } - void showRenameDialog(BuildContext context) { - showDialog( - context: context, - builder: (BuildContext context) { - return Dialog( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - child: SizedBox( - height: 200, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Padding( - padding: EdgeInsets.only( - top: 25, - ), - child: Text( - 'Change Name', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - ), - ), - ), - const Spacer(), - const Spacer(), - Row( - children: [ - Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: Colors.grey, - width: 0.5, - ), - top: BorderSide( - color: Colors.grey, - width: 0.5, - ), - ), - ), - child: TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: const Text( - 'Cancel', - style: TextStyle( - fontSize: 18, - color: Colors.black, - ), - ), - ), - ), - ), - const VerticalDivider( - thickness: 1, - width: 1, - ), - Expanded( - child: Container( - decoration: const BoxDecoration( - border: Border( - right: BorderSide( - color: Colors.grey, - width: 0.5, - ), - top: BorderSide( - color: Colors.grey, - width: 0.5, - ), - ), - ), - child: TextButton( - onPressed: () { - // Handle save action - }, - child: const Text( - 'Save', - style: TextStyle( - fontSize: 18, - color: Colors.grey, - ), - ), - ), - ), - ), - ], - ), - ], - ), - ), - ); - }, - ); + Future _requestPermission() async { + DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); + if (Platform.isAndroid ) { + AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; + PermissionStatus status = await Permission.photos.status; + if(androidInfo.version.sdkInt<= 33){ + if (status.isDenied) { + PermissionStatus status = await Permission.storage.request(); + if (status.isGranted) { + return true; + } else { + return false; + } + } + }else{ + if (status.isGranted) { + return true; + } else if (status.isDenied) { + PermissionStatus status = await Permission.photos.request(); + if (status.isGranted) { + return true; + } else { + return false; + } + } + } + return false; + } else { + SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); + bool firstClick = sharedPreferences.getBool('firstPermission') ?? true; + await sharedPreferences.setBool('firstPermission', false); + if (firstClick == false) { + var status = await Permission.photos.status; + return status.isGranted; + } else { + return true; + } + } } + } diff --git a/lib/features/menu/bloc/profile_bloc/profile_event.dart b/lib/features/menu/bloc/profile_bloc/profile_event.dart index a8e41d4..15d9336 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_event.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_event.dart @@ -8,7 +8,7 @@ abstract class ProfileEvent extends Equatable { List get props => []; } -class InitialEvent extends ProfileEvent {} +class InitialProfileEvent extends ProfileEvent {} class TimeZoneInitialEvent extends ProfileEvent {} @@ -20,13 +20,6 @@ class ChangeNameEvent extends ProfileEvent { class RegionInitialEvent extends ProfileEvent {} -class UpdateLockEvent extends ProfileEvent { - final bool value; - const UpdateLockEvent({required this.value}); - @override - List get props => [value]; -} - class SaveNameEvent extends ProfileEvent { final BuildContext context; const SaveNameEvent({required this.context}); @@ -71,5 +64,13 @@ class SearchRegionEvent extends ProfileEvent { List get props => [query]; } +class SearchTimeZoneEvent extends ProfileEvent { + final String query; + + const SearchTimeZoneEvent({required this.query}); + @override + List get props => [query]; +} + diff --git a/lib/features/menu/bloc/profile_bloc/profile_state.dart b/lib/features/menu/bloc/profile_bloc/profile_state.dart index 3f46c07..19b1f5c 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_state.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_state.dart @@ -1,5 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_app/features/devices/model/smart_door_model.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; @@ -26,13 +25,7 @@ class NameEditingState extends ProfileState { NameEditingState({required this.editName}); } -class LoadingNewSate extends ProfileState { - final SmartDoorModel smartDoorModel; - const LoadingNewSate({required this.smartDoorModel}); - @override - List get props => [smartDoorModel]; -} class FailedState extends ProfileState { final String errorMessage; @@ -43,20 +36,9 @@ class FailedState extends ProfileState { List get props => [errorMessage]; } -class GeneratePasswordState extends ProfileState {} class ImageSelectedState extends ProfileState {} -class ImageTooLargeState extends ProfileState {} - -class IsRepeatState extends ProfileState {} - -class IsStartEndState extends ProfileState {} - -class ChangeStartTimeState extends ProfileState {} - -class ChangeEndTimeState extends ProfileState {} - class ChangeImageState extends ProfileState {} class SaveState extends ProfileState {} @@ -66,4 +48,10 @@ class RegionsLoadedState extends ProfileState { final List regions; const RegionsLoadedState({required this.regions}); +} + +class TimeZoneLoadedState extends ProfileState { + final List regions; + + const TimeZoneLoadedState({required this.regions}); } \ No newline at end of file diff --git a/lib/features/menu/bloc/profile_bloc/region_model.dart b/lib/features/menu/bloc/profile_bloc/region_model.dart index 5548167..fd6306a 100644 --- a/lib/features/menu/bloc/profile_bloc/region_model.dart +++ b/lib/features/menu/bloc/profile_bloc/region_model.dart @@ -11,15 +11,15 @@ class RegionModel { factory RegionModel.fromJson(Map json) { return RegionModel( - name: json['name'], - id: json['id'].toString(), // Ensure id is a String + name: json['regionName'], + id: json['uuid'].toString(), // Ensure id is a String ); } Map toJson() { return { - 'name': name, - 'id': id, + 'regionName': name, + 'uuid': id, }; } } diff --git a/lib/features/menu/bloc/profile_bloc/time_zone_model.dart b/lib/features/menu/bloc/profile_bloc/time_zone_model.dart index e31c4a1..b37049b 100644 --- a/lib/features/menu/bloc/profile_bloc/time_zone_model.dart +++ b/lib/features/menu/bloc/profile_bloc/time_zone_model.dart @@ -11,9 +11,9 @@ class TimeZone { factory TimeZone.fromJson(Map json) { return TimeZone( - name: json['name'], - offset: json['offset'], - id: json['id'].toString(), // Ensure id is a String + name: json['cityName'], + offset: json['timeZoneOffset'], + id: json['uuid'].toString(), // Ensure id is a String ); } diff --git a/lib/features/menu/view/menu_view.dart b/lib/features/menu/view/menu_view.dart index a636ec1..018db84 100644 --- a/lib/features/menu/view/menu_view.dart +++ b/lib/features/menu/view/menu_view.dart @@ -25,7 +25,7 @@ class MenuView extends StatelessWidget { physics: const BouncingScrollPhysics(), child: Column( children: [ - ProfileTab(name:profileBloc.name), + ProfileTab(), for (var section in menuSections) MenuList( section: section, diff --git a/lib/features/menu/view/widgets/profile/profile_tab.dart b/lib/features/menu/view/widgets/profile/profile_tab.dart index 21117fc..27bc5ba 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -1,62 +1,98 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/view/widgets/profile/profile_view.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; -import 'package:syncrow_app/features/shared_widgets/syncrow_logo.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; class ProfileTab extends StatelessWidget { final String? name; - const ProfileTab({super.key,this.name}); + const ProfileTab({super.key, this.name}); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: InkWell( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => const ProfileView(), - ), - ); - }, - child: Stack( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, + return BlocBuilder( + builder: (context, state) { + final user = AuthCubit.user; + Uint8List? imageBytes; + if (user!.profilePicture != null) { + final base64String = user.profilePicture!; + final startIndex = base64String.indexOf('base64,') + 7; + final pureBase64String = base64String.substring(startIndex); + imageBytes = base64.decode(pureBase64String); + } + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: InkWell( + onTap: () { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => const ProfileView(), + ), + ) + .then((result) { + if (result == true) { + context.read().fetchUserInfo(); + } + }); + }, + child: Stack( children: [ - const SizedBox(height: 20), - DefaultContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BodyMedium( - text: name!, - fontWeight: FontWeight.bold, + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 20), + DefaultContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + BodyMedium( + text: '${user.firstName!} ', + fontWeight: FontWeight.bold, + ), + BodyMedium( + text: user.lastName!, + fontWeight: FontWeight.bold, + ), + ], + ), + const BodySmall(text: "Syncrow Account"), + ], ), - BodySmall(text: "Syncrow Account") - ], + ), + ], + ), + Positioned( + right: 20, + top: 0, + child: CircleAvatar( + radius: 38, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 37, + backgroundColor: Colors.grey, + child: ClipOval( + child: Image.memory( + imageBytes!, + fit: BoxFit.cover, + width: 110, // You can adjust the width and height as needed + height: 110, + )), + ), ), ), ], ), - Positioned( - right: 20, - top: 0, - child: CircleAvatar( - radius: 38, - backgroundColor: Colors.white, - child: CircleAvatar( - radius: 37, - backgroundColor: Colors.grey, - child: SyncrowLogo(), - ), - ), - ), - ], - ), - ), + ), + ); + }, ); } } + diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 94fbe33..4fcc965 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; @@ -13,29 +14,29 @@ import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; class ProfileView extends StatelessWidget { const ProfileView({super.key}); + @override Widget build(BuildContext context) { + + return BlocProvider( - create: (BuildContext context) => ProfileBloc(userId: ''), - child: BlocConsumer(listener: (context, state) { - if (state is FailedState) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.errorMessage), - backgroundColor: Colors.red, - ), - ); - } - }, builder: (context, state) { + create: (BuildContext context) => ProfileBloc()..add(InitialProfileEvent()), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { final profileBloc = BlocProvider.of(context); + Uint8List? imageBytes = profileBloc.decodeBase64Image(AuthCubit.user!.profilePicture); return DefaultScaffold( title: 'Syncrow Account', child: Column( children: [ - SizedBox(height: MediaQuery.of(context).size.height*0.05,), + SizedBox( + height: MediaQuery.of(context).size.height * 0.05, + ), InkWell( onTap: () { - profileBloc.add(SelectImageEvent(context: context, isSelected: false)); + profileBloc.add( + SelectImageEvent(context: context, isSelected: false)); }, child: SizedBox.square( dimension: 125, @@ -48,6 +49,18 @@ class ProfileView extends StatelessWidget { backgroundImage: profileBloc.image == null ? null : FileImage(profileBloc.image!), + child: profileBloc.image != null + ? null + :imageBytes != null + ? ClipOval( + child: Image.memory( + imageBytes, + fit: BoxFit.cover, + width: 110, + height: 110, + ), + ) + : null, ), ), ), @@ -70,7 +83,7 @@ class ProfileView extends StatelessWidget { controller: profileBloc.nameController, enabled: profileBloc.editName, onEditingComplete: () { - profileBloc.add(const ChangeNameEvent(value: false)); + profileBloc.add(SaveNameEvent(context: context)); }, decoration: const InputDecoration( hintText: "Your Name", @@ -98,89 +111,80 @@ class ProfileView extends StatelessWidget { ), ), const SizedBox(height: 10), - //Info + // Info DefaultContainer( - padding: const EdgeInsets.symmetric( - horizontal: 25, - vertical: 5, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - // Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, - // children: [ - // const BodyMedium(text: 'Email '), - // Flexible( - // child: TextField( - // textAlign: TextAlign.end, - // decoration: InputDecoration( - // hintText: ' Test@test.com', - // hintStyle: - // context.bodyMedium.copyWith(color: Colors.grey), - // border: InputBorder.none, - // ), - // ), - // ), - // ], - // ), - // Container( - // height: 1, - // color: ColorsManager.greyColor, - // ), - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const RegionPage(), - )); - }, - child: const Padding( - padding: EdgeInsets.only(top: 20, bottom: 20), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BodyMedium(text: 'Region '), - Flexible( - child: BodyMedium(text: 'United Arab Emirates') - ), - ], - ), - )), - Container( - height: 1, - color: ColorsManager.greyColor, - ), - InkWell( - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => - const TimeZoneScreenPage(), - )); - }, - child: const Padding( - padding: EdgeInsets.only(top: 15, bottom: 15), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - BodyMedium(text: 'Time Zone '), - Flexible( - child: BodyMedium(text: 'GMT +4') - ), - ], + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 5, + ), + child: + BlocBuilder( + builder: (context, state) { + final user = AuthCubit.user; + + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const RegionPage(), ), + ).then((result) { + context.read().fetchUserInfo(); + + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'Region '), + Flexible(child: BodyMedium(text: user!.regionName??'No Region')), + ], ), ), - ], - )), + ), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TimeZoneScreenPage(), + ), + ).then((result) { + context.read().fetchUserInfo(); + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'Time Zone '), + Flexible( + child: BodyMedium(text: user.timeZone??"No Time Zone"), + ), + ], + ), + ), + ), + ], + );}), + ), ], ), ); - })); + }, + ), + ); } } diff --git a/lib/features/menu/view/widgets/profile/region_page.dart b/lib/features/menu/view/widgets/profile/region_page.dart index 91321e9..b209d99 100644 --- a/lib/features/menu/view/widgets/profile/region_page.dart +++ b/lib/features/menu/view/widgets/profile/region_page.dart @@ -13,7 +13,7 @@ class RegionPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => ProfileBloc(userId: '')..add(RegionInitialEvent()), + create: (BuildContext context) => ProfileBloc()..add(RegionInitialEvent()), child: BlocConsumer(listener: (context, state) { if (state is FailedState) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart index 22c3b5f..fe80b0f 100644 --- a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart +++ b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart @@ -15,7 +15,7 @@ class TimeZoneScreenPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => ProfileBloc(userId: '')..add(TimeZoneInitialEvent()), + create: (BuildContext context) => ProfileBloc()..add(TimeZoneInitialEvent()), child: BlocConsumer(listener: (context, state) { if (state is FailedState) { @@ -38,6 +38,10 @@ class TimeZoneScreenPage extends StatelessWidget { : Column( children: [ TextFormField( + controller:profileBloc.searchController , + onChanged: (value) { + profileBloc.add(SearchTimeZoneEvent(query: value)); + }, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), hintText: 'Search', @@ -59,7 +63,6 @@ class TimeZoneScreenPage extends StatelessWidget { itemBuilder: (context, index) { return InkWell( onTap: () { - profileBloc.add(SelectTimeZoneEvent( val: timeZoneList[index].id, context: context)); diff --git a/lib/features/splash/view/splash_view.dart b/lib/features/splash/view/splash_view.dart index bc81f87..ca254e8 100644 --- a/lib/features/splash/view/splash_view.dart +++ b/lib/features/splash/view/splash_view.dart @@ -18,6 +18,7 @@ class _SplashViewState extends State { @override void initState() { AuthCubit.get(context).getTokenAndValidate(); + AuthCubit.get(context).fetchUserInfo(); super.initState(); } diff --git a/lib/my_app.dart b/lib/my_app.dart index e8e4f4c..a49fec7 100644 --- a/lib/my_app.dart +++ b/lib/my_app.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/scene/bloc/create_scene/create_scene_bloc.dart'; import 'package:syncrow_app/navigation/navigation_service.dart'; import 'package:syncrow_app/utils/resource_manager/color_manager.dart'; @@ -26,6 +27,7 @@ class MyApp extends StatelessWidget { BlocProvider(create: (context) => AuthCubit()), BlocProvider(create: (context) => CreateSceneBloc()), BlocProvider(create: (context) => SceneBloc()), + BlocProvider(create: (context) => ProfileBloc()), ], child: MaterialApp( diff --git a/lib/services/api/api_links_endpoints.dart b/lib/services/api/api_links_endpoints.dart index d32bdba..8190be8 100644 --- a/lib/services/api/api_links_endpoints.dart +++ b/lib/services/api/api_links_endpoints.dart @@ -1,5 +1,5 @@ abstract class ApiEndpoints { - static const String baseUrl = 'https://syncrow.azurewebsites.net'; + static const String baseUrl = 'https://syncrow-dev.azurewebsites.net'; // static const String baseUrl = 'http://100.107.182.63:4001'; //Localhost ////////////////////////////////////// Authentication /////////////////////////////// @@ -151,4 +151,15 @@ abstract class ApiEndpoints { //multiple-time offline static const String deleteTemporaryPassword = '$baseUrl/door-lock/temporary-password/{doorLockUuid}/{passwordId}'; + + + //user + + static const String getUser = '$baseUrl/user/{userUuid}'; + static const String saveRegion = '$baseUrl/user/region/{userUuid}'; + static const String saveTimeZone = '$baseUrl/user/timezone/{userUuid}'; + static const String saveName = '$baseUrl/user/name/{userUuid}'; + static const String sendPicture = '$baseUrl/user/profile-picture/{userUuid}'; + static const String getRegion = '$baseUrl/region'; + static const String getTimezone = '$baseUrl/timezone'; } diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart index a133ef7..a9bfd00 100644 --- a/lib/services/api/profile_api.dart +++ b/lib/services/api/profile_api.dart @@ -1,23 +1,22 @@ import 'dart:async'; -import 'dart:io'; -import 'package:syncrow_app/features/devices/model/device_category_model.dart'; -import 'package:syncrow_app/features/devices/model/device_control_model.dart'; -import 'package:syncrow_app/features/devices/model/device_model.dart'; -import 'package:syncrow_app/features/devices/model/function_model.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/auth/model/user_model.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; +import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'package:syncrow_app/services/api/http_service.dart'; -import '../../features/devices/model/create_temporary_password_model.dart'; - class ProfileApi { static final HTTPService _httpService = HTTPService(); - static Future> saveName( - String newName, String userId) async { + + static Future> saveName({String? firstName, String? lastName,}) async { try { - final response = await _httpService.post( - path: ApiEndpoints.controlDevice.replaceAll('{deviceUuid}', userId), - body: newName, - showServerMessage: false, + final response = await _httpService.put( + path: ApiEndpoints.saveName.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + body: { + "firstName": firstName, + "lastName": lastName + }, expectedResponseModel: (json) { return json; }, @@ -28,13 +27,29 @@ class ProfileApi { } } - static Future> saveImage( - File newName, String userId) async { + static Future saveRegion({String? regionUuid,}) async { try { - final response = await _httpService.post( - path: ApiEndpoints.controlDevice.replaceAll('{deviceUuid}', userId), - body: newName, - showServerMessage: false, + final response = await _httpService.put( + path: ApiEndpoints.saveRegion.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + body: { + "regionUuid": regionUuid, + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + static Future saveTimeZone({String? regionUuid,}) async { + try { + final response = await _httpService.put( + path: ApiEndpoints.saveTimeZone.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + body: { + "timezoneUuid": regionUuid, + }, expectedResponseModel: (json) { return json; }, @@ -45,15 +60,54 @@ class ProfileApi { } } - static Future fetchUserInfo(String userId) async { + static Future> saveImage(String image) async { + try { + final response = await _httpService.put( + path: ApiEndpoints.sendPicture.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + body: { + "profilePicture": 'data:image/png;base64,$image' + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future fetchUserInfo(userId) async { final response = await _httpService.get( - path: ApiEndpoints.groupBySpace.replaceAll('{unitUuid}', userId), - // queryParameters: params, - showServerMessage: false, - expectedResponseModel: (json) => DevicesCategoryModel.fromJsonList(json), + path: ApiEndpoints.getUser.replaceAll('{userUuid}', userId!), + showServerMessage: true, + expectedResponseModel: (json) =>UserModel.fromJson(json) ); return response; } + static Future> fetchRegion() async { + final response = await _httpService.get( + path: ApiEndpoints.getRegion, + showServerMessage: true, + expectedResponseModel: (json) { + return (json as List).map((zone) => RegionModel.fromJson(zone)).toList(); + } + ); + return response as List; + } + + + static Future> fetchTimeZone() async { + final response = await _httpService.get( + path: ApiEndpoints.getTimezone, + showServerMessage: true, + expectedResponseModel: (json) { + return (json as List).map((zone) => TimeZone.fromJson(zone)).toList(); + } + ); + return response as List; + } + } From 104477506640e5e9104c4024dcef456106ded0f9 Mon Sep 17 00:00:00 2001 From: mohammad Date: Tue, 23 Jul 2024 16:28:53 +0300 Subject: [PATCH 03/13] profile --- lib/features/menu/bloc/profile_bloc/profile_bloc.dart | 6 +++--- lib/features/menu/view/widgets/profile/profile_view.dart | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 821aeb5..7b0f77a 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -67,6 +67,7 @@ class ProfileBloc extends Bloc { try { emit(LoadingInitialState()); AuthCubit.user = await ProfileApi.fetchUserInfo(AuthCubit.user!.uuid); + emit(SaveState()); } catch (e) { emit(FailedState(errorMessage: e.toString())); return; @@ -176,7 +177,7 @@ class ProfileBloc extends Bloc { } Future _saveImage() async { - emit(ChangeImageState()); + emit(LoadingInitialState()); List imageBytes = image!.readAsBytesSync(); String base64Image = base64Encode(imageBytes); print(base64Image); @@ -184,7 +185,6 @@ class ProfileBloc extends Bloc { emit(ImageSelectedState()); } - void _showPermissionDeniedDialog(BuildContext context) { showDialog( context: context, @@ -217,7 +217,7 @@ class ProfileBloc extends Bloc { try { add(const ChangeNameEvent(value: false)); isSaving = true; - emit(LoadingSaveState()); + emit(LoadingInitialState()); final fullName = nameController.text; final nameParts = fullName.split(' '); final firstName = nameParts[0]; diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 4fcc965..ad0e30c 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -28,7 +28,10 @@ class ProfileView extends StatelessWidget { Uint8List? imageBytes = profileBloc.decodeBase64Image(AuthCubit.user!.profilePicture); return DefaultScaffold( title: 'Syncrow Account', - child: Column( + child: + state is LoadingInitialState + ? const Center(child: CircularProgressIndicator()): + Column( children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.05, From 69b901afed57360d894ec7ee26ff3ae5d92462b1 Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 24 Jul 2024 16:03:22 +0300 Subject: [PATCH 04/13] profile page with HomeCubit --- lib/features/app_layout/bloc/home_cubit.dart | 17 ++- lib/features/app_layout/bloc/home_state.dart | 6 + lib/features/auth/bloc/auth_cubit.dart | 16 +- lib/features/auth/model/user_model.dart | 24 +-- .../menu/bloc/profile_bloc/profile_bloc.dart | 59 ++++---- .../view/widgets/profile/profile_tab.dart | 142 +++++++++--------- .../view/widgets/profile/profile_view.dart | 28 ++-- .../view/widgets/profile/region_page.dart | 3 +- lib/features/splash/view/splash_view.dart | 1 - lib/services/api/profile_api.dart | 16 +- 10 files changed, 153 insertions(+), 159 deletions(-) diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index eee72a1..3d44449 100644 --- a/lib/features/app_layout/bloc/home_cubit.dart +++ b/lib/features/app_layout/bloc/home_cubit.dart @@ -22,6 +22,7 @@ import 'package:syncrow_app/features/scene/view/scene_view.dart'; import 'package:syncrow_app/generated/assets.dart'; import 'package:syncrow_app/navigation/navigation_service.dart'; import 'package:syncrow_app/services/api/devices_api.dart'; +import 'package:syncrow_app/services/api/profile_api.dart'; import 'package:syncrow_app/services/api/spaces_api.dart'; import 'package:syncrow_app/utils/helpers/custom_page_route.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; @@ -33,6 +34,7 @@ part 'home_state.dart'; class HomeCubit extends Cubit { HomeCubit._() : super(HomeInitial()) { checkIfNotificationPermissionGranted(); + fetchUserInfo(); if (selectedSpace == null) { fetchUnitsByUserId(); // .then((value) { @@ -42,7 +44,7 @@ class HomeCubit extends Cubit { // }); } } - + static UserModel? user; static HomeCubit? _instance; static HomeCubit getInstance() { // If an instance already exists, return it @@ -50,6 +52,19 @@ class HomeCubit extends Cubit { return _instance!; } + + Future fetchUserInfo() async { + try { + print('fetchUserInfo'); + var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + user = await ProfileApi().fetchUserInfo(uuid); + emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info + + } catch (e) { + return; + } + } + void emitSafe(HomeState newState) { final cubit = this; if (!cubit.isClosed) { diff --git a/lib/features/app_layout/bloc/home_state.dart b/lib/features/app_layout/bloc/home_state.dart index 9c20d4e..1125e01 100644 --- a/lib/features/app_layout/bloc/home_state.dart +++ b/lib/features/app_layout/bloc/home_state.dart @@ -58,3 +58,9 @@ class RoomSelected extends HomeState { class RoomUnSelected extends HomeState {} class NavChangePage extends HomeState {} +// Define new state classes +class HomeUserInfoLoaded extends HomeState { + final UserModel user; + + HomeUserInfoLoaded(this.user); +} diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 6e2b730..77b2d6c 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -184,14 +184,10 @@ class AuthCubit extends Cubit { key: Token.loginAccessTokenKey, value: token.accessToken ); - await fetchUserInfo(); const FlutterSecureStorage().write( key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString() - ).then((value) async { - var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); - user = await ProfileApi.fetchUserInfo(uuid); - },); + ); user = UserModel.fromToken(token); emailController.clear(); passwordController.clear(); @@ -330,16 +326,6 @@ class AuthCubit extends Cubit { } - Future fetchUserInfo() async { - try { - emit(AuthLoading()); - var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); - user = await ProfileApi.fetchUserInfo(uuid); - emit(AuthLoginSuccess()); - } catch (e) { - return; - } - } } diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart index ab127dc..fd68fb4 100644 --- a/lib/features/auth/model/user_model.dart +++ b/lib/features/auth/model/user_model.dart @@ -1,3 +1,6 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; import 'package:syncrow_app/features/auth/model/token.dart'; class UserModel { @@ -6,7 +9,7 @@ class UserModel { final String? email; final String? firstName; final String? lastName; - final String? profilePicture; + final Uint8List? profilePicture; final String? phoneNumber; final bool? isEmailVerified; final String? regionName; @@ -33,38 +36,39 @@ class UserModel { email: json['email'], firstName: json['firstName'], lastName: json['lastName'], - profilePicture: json['profilePicture'], + profilePicture: UserModel.decodeBase64Image(json['profilePicture']), phoneNumber: json['phoneNumber'], isEmailVerified: json['isEmailVerified'], isAgreementAccepted: json['isAgreementAccepted'], regionName: json['region']?['regionName'], // Extract regionName timeZone: json['timeZone']?['timeZoneOffset'], // Extract regionName - ); } - //uuid to json - //from token - factory UserModel.fromToken(Token token) { + factory UserModel.fromToken(Token token) { Map tempJson = Token.decodeToken(token.accessToken); - return UserModel( uuid: tempJson['uuid'].toString(), email: tempJson['email'], lastName: tempJson['lastName'], firstName:tempJson['firstName'] , - profilePicture: tempJson['profilePicture'], + profilePicture: UserModel.decodeBase64Image(tempJson['profilePicture']), phoneNumber: null, isEmailVerified: null, isAgreementAccepted: null, regionName: tempJson['region']?['regionName'], timeZone: tempJson['timezone']?['timeZoneOffset'], - - ); } + static Uint8List? decodeBase64Image(String? base64String) { + if (base64String != null) { + return base64.decode(base64String); + } + return null; + } + Map toJson() { return { 'id': uuid, diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 7b0f77a..3e7e76e 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -1,11 +1,10 @@ import 'dart:convert'; import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; @@ -16,6 +15,7 @@ import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; class ProfileBloc extends Bloc { + bool isSaving = false; bool editName = false; final FocusNode focusNode = FocusNode(); @@ -24,7 +24,7 @@ class ProfileBloc extends Bloc { String timeZoneSelected = ''; String regionSelected = ''; final TextEditingController searchController = TextEditingController(); - final TextEditingController nameController = TextEditingController(text: '${AuthCubit.user!.firstName} ${AuthCubit.user!.lastName}'); + final TextEditingController nameController = TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); List? timeZoneList; List? regionList; @@ -42,14 +42,30 @@ class ProfileBloc extends Bloc { on(selectRegion); } - Uint8List? decodeBase64Image(String? base64String) { - if (base64String != null) { - final startIndex = base64String.indexOf('base64,') + 7; - final pureBase64String = base64String.substring(startIndex); - return base64.decode(pureBase64String); + Future saveName(SaveNameEvent event, Emitter emit) async { + if (_validateInputs()) return; + try { + add(const ChangeNameEvent(value: false)); + isSaving = true; + emit(LoadingInitialState()); + final fullName = nameController.text; + final nameParts = fullName.split(' '); + final firstName = nameParts[0]; + final lastName = nameParts.length > 1 ? nameParts[1] : ''; + var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); + add(InitialProfileEvent()); + final homeCubit = event.context.read(); + await homeCubit.fetchUserInfo(); + Navigator.of(event.context).pop(true); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveState()); + } catch (_) { + // Handle the error + } finally { + isSaving = false; } - return null; } + void _changeName(ChangeNameEvent event, Emitter emit) { emit(LoadingInitialState()); editName = event.value!; @@ -66,7 +82,7 @@ class ProfileBloc extends Bloc { void _fetchUserInfo(InitialProfileEvent event, Emitter emit) async { try { emit(LoadingInitialState()); - AuthCubit.user = await ProfileApi.fetchUserInfo(AuthCubit.user!.uuid); + HomeCubit.user = await ProfileApi().fetchUserInfo(HomeCubit.user!.uuid); emit(SaveState()); } catch (e) { emit(FailedState(errorMessage: e.toString())); @@ -212,28 +228,7 @@ class ProfileBloc extends Bloc { ); } - Future saveName(SaveNameEvent event, Emitter emit) async { - if (_validateInputs()) return; - try { - add(const ChangeNameEvent(value: false)); - isSaving = true; - emit(LoadingInitialState()); - final fullName = nameController.text; - final nameParts = fullName.split(' '); - final firstName = nameParts[0]; - final lastName = nameParts.length > 1 ? nameParts[1] : ''; - var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); - add(InitialProfileEvent()); - AuthCubit.get(event.context).fetchUserInfo(); - Navigator.of(event.context).pop(true); - CustomSnackBar.displaySnackBar('Save Successfully'); - emit(SaveState()); - } catch (_) { - // Handle the error - } finally { - isSaving = false; - } - } + bool _validateInputs() { if (nameController.text.length < 2) { diff --git a/lib/features/menu/view/widgets/profile/profile_tab.dart b/lib/features/menu/view/widgets/profile/profile_tab.dart index 27bc5ba..19c091a 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -1,98 +1,90 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/menu/view/widgets/profile/profile_view.dart'; import 'package:syncrow_app/features/shared_widgets/default_container.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_medium.dart'; import 'package:syncrow_app/features/shared_widgets/text_widgets/body_small.dart'; class ProfileTab extends StatelessWidget { - final String? name; - const ProfileTab({super.key, this.name}); - + const ProfileTab({super.key,}); @override Widget build(BuildContext context) { - return BlocBuilder( + return BlocBuilder( builder: (context, state) { - final user = AuthCubit.user; - Uint8List? imageBytes; - if (user!.profilePicture != null) { - final base64String = user.profilePicture!; - final startIndex = base64String.indexOf('base64,') + 7; - final pureBase64String = base64String.substring(startIndex); - imageBytes = base64.decode(pureBase64String); - } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: InkWell( - onTap: () { - Navigator.of(context) - .push( - MaterialPageRoute( - builder: (context) => const ProfileView(), - ), - ) - .then((result) { - if (result == true) { - context.read().fetchUserInfo(); - } - }); - }, - child: Stack( + return _buildProfileContent(context ); + }, + ); + } + + Widget _buildProfileContent(BuildContext context) { + final homeCubit = context.read(); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: InkWell( + onTap: () { + Navigator.of(context) + .push( + MaterialPageRoute( + builder: (context) => const ProfileView(), + ), + ).then((result) { + context.read().fetchUserInfo(); + }); + }, + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const SizedBox(height: 20), - DefaultContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + const SizedBox(height: 20), + DefaultContainer( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( children: [ - Row( - children: [ - BodyMedium( - text: '${user.firstName!} ', - fontWeight: FontWeight.bold, - ), - BodyMedium( - text: user.lastName!, - fontWeight: FontWeight.bold, - ), - ], + BodyMedium( + text: '${HomeCubit.user!.firstName ?? ''} ', + fontWeight: FontWeight.bold, + ), + BodyMedium( + text: HomeCubit.user!.lastName ?? '', + fontWeight: FontWeight.bold, ), - const BodySmall(text: "Syncrow Account"), ], ), - ), - ], - ), - Positioned( - right: 20, - top: 0, - child: CircleAvatar( - radius: 38, - backgroundColor: Colors.white, - child: CircleAvatar( - radius: 37, - backgroundColor: Colors.grey, - child: ClipOval( - child: Image.memory( - imageBytes!, - fit: BoxFit.cover, - width: 110, // You can adjust the width and height as needed - height: 110, - )), - ), + const BodySmall(text: "Syncrow Account"), + ], ), ), ], ), - ), - ); - }, + Positioned( + right: 20, + top: 0, + child: CircleAvatar( + radius: 38, + backgroundColor: Colors.white, + child: CircleAvatar( + radius: 37, + backgroundColor: Colors.grey, + child: ClipOval( + child: HomeCubit.user?.profilePicture != null + ? Image.memory( + HomeCubit.user!.profilePicture!, + fit: BoxFit.cover, + width: 110, + height: 110, + ) + : Icon(Icons.person, size: 70), // Fallback if no image + ), + ), + ), + ), + ], + ), + ), ); } } - diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index ad0e30c..760bbce 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; @@ -18,14 +19,12 @@ class ProfileView extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( create: (BuildContext context) => ProfileBloc()..add(InitialProfileEvent()), child: BlocConsumer( listener: (context, state) {}, builder: (context, state) { final profileBloc = BlocProvider.of(context); - Uint8List? imageBytes = profileBloc.decodeBase64Image(AuthCubit.user!.profilePicture); return DefaultScaffold( title: 'Syncrow Account', child: @@ -54,10 +53,10 @@ class ProfileView extends StatelessWidget { : FileImage(profileBloc.image!), child: profileBloc.image != null ? null - :imageBytes != null + :HomeCubit.user!.profilePicture != null ? ClipOval( child: Image.memory( - imageBytes, + HomeCubit.user!.profilePicture!, fit: BoxFit.cover, width: 110, height: 110, @@ -121,11 +120,8 @@ class ProfileView extends StatelessWidget { vertical: 5, ), child: - BlocBuilder( - builder: (context, state) { - final user = AuthCubit.user; - return Column( + Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -137,17 +133,16 @@ class ProfileView extends StatelessWidget { builder: (context) => const RegionPage(), ), ).then((result) { - context.read().fetchUserInfo(); - + profileBloc.add(InitialProfileEvent()); }); }, - child: Padding( + child: Padding( padding: const EdgeInsets.only(top: 20, bottom: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const BodyMedium(text: 'Region '), - Flexible(child: BodyMedium(text: user!.regionName??'No Region')), + Flexible(child: BodyMedium(text: HomeCubit.user!.regionName ?? 'No Region')), ], ), ), @@ -164,24 +159,25 @@ class ProfileView extends StatelessWidget { builder: (context) => const TimeZoneScreenPage(), ), ).then((result) { - context.read().fetchUserInfo(); + profileBloc.add(InitialProfileEvent()); }); }, - child: Padding( + child: Padding( padding: const EdgeInsets.only(top: 15, bottom: 15), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const BodyMedium(text: 'Time Zone '), Flexible( - child: BodyMedium(text: user.timeZone??"No Time Zone"), + child: BodyMedium(text: HomeCubit.user!.timeZone ?? "No Time Zone"), ), ], ), ), ), ], - );}), + ) + ), ], ), diff --git a/lib/features/menu/view/widgets/profile/region_page.dart b/lib/features/menu/view/widgets/profile/region_page.dart index b209d99..16fef45 100644 --- a/lib/features/menu/view/widgets/profile/region_page.dart +++ b/lib/features/menu/view/widgets/profile/region_page.dart @@ -61,8 +61,7 @@ class RegionPage extends StatelessWidget { return InkWell( onTap: () { profileBloc.add(SelectRegionEvent( - val: regionList[index].id, - context: context)); + val: regionList[index].id, context: context)); }, child: Padding( padding: const EdgeInsets.all(10.0), diff --git a/lib/features/splash/view/splash_view.dart b/lib/features/splash/view/splash_view.dart index ca254e8..bc81f87 100644 --- a/lib/features/splash/view/splash_view.dart +++ b/lib/features/splash/view/splash_view.dart @@ -18,7 +18,6 @@ class _SplashViewState extends State { @override void initState() { AuthCubit.get(context).getTokenAndValidate(); - AuthCubit.get(context).fetchUserInfo(); super.initState(); } diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart index a9bfd00..5666923 100644 --- a/lib/services/api/profile_api.dart +++ b/lib/services/api/profile_api.dart @@ -1,5 +1,5 @@ import 'dart:async'; -import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; +import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/auth/model/user_model.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; @@ -12,7 +12,7 @@ class ProfileApi { static Future> saveName({String? firstName, String? lastName,}) async { try { final response = await _httpService.put( - path: ApiEndpoints.saveName.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + path: ApiEndpoints.saveName.replaceAll('{userUuid}', HomeCubit.user!.uuid!), body: { "firstName": firstName, "lastName": lastName @@ -30,7 +30,7 @@ class ProfileApi { static Future saveRegion({String? regionUuid,}) async { try { final response = await _httpService.put( - path: ApiEndpoints.saveRegion.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + path: ApiEndpoints.saveRegion.replaceAll('{userUuid}', HomeCubit.user!.uuid!), body: { "regionUuid": regionUuid, }, @@ -46,7 +46,7 @@ class ProfileApi { static Future saveTimeZone({String? regionUuid,}) async { try { final response = await _httpService.put( - path: ApiEndpoints.saveTimeZone.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + path: ApiEndpoints.saveTimeZone.replaceAll('{userUuid}', HomeCubit.user!.uuid!), body: { "timezoneUuid": regionUuid, }, @@ -63,7 +63,7 @@ class ProfileApi { static Future> saveImage(String image) async { try { final response = await _httpService.put( - path: ApiEndpoints.sendPicture.replaceAll('{userUuid}', AuthCubit.user!.uuid!), + path: ApiEndpoints.sendPicture.replaceAll('{userUuid}', HomeCubit.user!.uuid!), body: { "profilePicture": 'data:image/png;base64,$image' }, @@ -77,11 +77,13 @@ class ProfileApi { } } - static Future fetchUserInfo(userId) async { + Future fetchUserInfo(userId) async { final response = await _httpService.get( path: ApiEndpoints.getUser.replaceAll('{userUuid}', userId!), showServerMessage: true, - expectedResponseModel: (json) =>UserModel.fromJson(json) + expectedResponseModel: (json) { + return UserModel.fromJson(json); + } ); return response; } From 2b8767aa4bcc71c1b7f6d70bc3443fc17a8b773e Mon Sep 17 00:00:00 2001 From: mohammad Date: Wed, 24 Jul 2024 16:05:35 +0300 Subject: [PATCH 05/13] profile page with HomeCubit --- lib/features/app_layout/bloc/home_cubit.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index 3d44449..a964a53 100644 --- a/lib/features/app_layout/bloc/home_cubit.dart +++ b/lib/features/app_layout/bloc/home_cubit.dart @@ -55,7 +55,6 @@ class HomeCubit extends Cubit { Future fetchUserInfo() async { try { - print('fetchUserInfo'); var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await ProfileApi().fetchUserInfo(uuid); emit(HomeUserInfoLoaded(user!)); // Emit state after fetching user info From 378f4396d852288ec18ac1e3254041688841c67b Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 25 Jul 2024 15:19:10 +0300 Subject: [PATCH 06/13] profile page --- lib/features/menu/bloc/profile_bloc/profile_bloc.dart | 7 +++---- lib/features/menu/view/widgets/profile/profile_view.dart | 4 +--- lib/features/menu/view/widgets/profile/region_page.dart | 8 ++++++-- .../menu/view/widgets/profile/time_zone_screen_page.dart | 6 +++++- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 3e7e76e..7fa0889 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -54,8 +54,7 @@ class ProfileBloc extends Bloc { final lastName = nameParts.length > 1 ? nameParts[1] : ''; var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); add(InitialProfileEvent()); - final homeCubit = event.context.read(); - await homeCubit.fetchUserInfo(); + await HomeCubit.getInstance().fetchUserInfo(); Navigator.of(event.context).pop(true); CustomSnackBar.displaySnackBar('Save Successfully'); emit(SaveState()); @@ -102,7 +101,6 @@ class ProfileBloc extends Bloc { } } - Future selectTimeZone(SelectTimeZoneEvent event, Emitter emit) async { try { emit(LoadingInitialState()); @@ -135,6 +133,7 @@ class ProfileBloc extends Bloc { return region.name.toLowerCase().contains(query); }).toList() ?? []; regionList = filteredRegions;// Assume this fetches the regions + emit(RegionsLoadedState(regions: filteredRegions)); }else{ regionList = await ProfileApi.fetchRegion(); @@ -243,7 +242,7 @@ class ProfileBloc extends Bloc { if (Platform.isAndroid ) { AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; PermissionStatus status = await Permission.photos.status; - if(androidInfo.version.sdkInt<= 33){ + if(androidInfo.version.sdkInt<33){ if (status.isDenied) { PermissionStatus status = await Permission.storage.request(); if (status.isGranted) { diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 760bbce..50d6699 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; -import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; @@ -37,8 +36,7 @@ class ProfileView extends StatelessWidget { ), InkWell( onTap: () { - profileBloc.add( - SelectImageEvent(context: context, isSelected: false)); + profileBloc.add(SelectImageEvent(context: context, isSelected: false)); }, child: SizedBox.square( dimension: 125, diff --git a/lib/features/menu/view/widgets/profile/region_page.dart b/lib/features/menu/view/widgets/profile/region_page.dart index 16fef45..9f84576 100644 --- a/lib/features/menu/view/widgets/profile/region_page.dart +++ b/lib/features/menu/view/widgets/profile/region_page.dart @@ -23,6 +23,11 @@ class RegionPage extends StatelessWidget { ), ); } + if (state is SaveState) { + new Future.delayed(const Duration(milliseconds: 500), () { + Navigator.of(context).pop(true); + }); + } }, builder: (context, state) { final profileBloc = BlocProvider.of(context); @@ -60,8 +65,7 @@ class RegionPage extends StatelessWidget { itemBuilder: (context, index) { return InkWell( onTap: () { - profileBloc.add(SelectRegionEvent( - val: regionList[index].id, context: context)); + profileBloc.add(SelectRegionEvent(val: regionList[index].id, context: context)); }, child: Padding( padding: const EdgeInsets.all(10.0), diff --git a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart index fe80b0f..7594b05 100644 --- a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart +++ b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart @@ -25,7 +25,11 @@ class TimeZoneScreenPage extends StatelessWidget { backgroundColor: Colors.red, ), ); - } + }if (state is SaveState) { + new Future.delayed(const Duration(milliseconds: 500), () { + Navigator.of(context).pop(true); + }); + } }, builder: (context, state) { final profileBloc = BlocProvider.of(context); From 76b867d4b960524b3cdd55fea151ba2d545ef25b Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 25 Jul 2024 16:19:02 +0300 Subject: [PATCH 07/13] profile page --- .../menu/bloc/profile_bloc/profile_bloc.dart | 38 +++++++++++++++++-- .../view/widgets/profile/profile_view.dart | 38 ++++++++++--------- lib/services/api/profile_api.dart | 3 +- 3 files changed, 57 insertions(+), 22 deletions(-) diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 7fa0889..91b452b 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -45,6 +45,7 @@ class ProfileBloc extends Bloc { Future saveName(SaveNameEvent event, Emitter emit) async { if (_validateInputs()) return; try { + add(const ChangeNameEvent(value: false)); isSaving = true; emit(LoadingInitialState()); @@ -55,7 +56,7 @@ class ProfileBloc extends Bloc { var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); add(InitialProfileEvent()); await HomeCubit.getInstance().fetchUserInfo(); - Navigator.of(event.context).pop(true); + // Navigator.of(event.context).pop(true); CustomSnackBar.displaySnackBar('Save Successfully'); emit(SaveState()); } catch (_) { @@ -229,14 +230,45 @@ class ProfileBloc extends Bloc { + bool _validateInputs() { - if (nameController.text.length < 2) { - CustomSnackBar.displaySnackBar('Name Must More than 2 '); + final nameError = fullNameValidator(nameController.text); + if (nameError != null) { + CustomSnackBar.displaySnackBar(nameError); return true; } return false; } + + String? fullNameValidator(String? value) { + if (value == null) return 'Full name is required'; + + final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); + + if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { + return 'Full name must be between 2 and 30 characters long'; + } + + // Test if it contains anything but alphanumeric spaces and single quote + + if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { + return 'Only alphanumeric characters, space, dash and single quote are allowed'; + } + + final parts = withoutExtraSpaces.split(' '); + + if (parts.length < 2) return 'Full name must contain first and last names'; + + if (parts.length > 3) return 'Full name can at most contain 3 parts'; + + if (parts.any((part) => part.length < 2 || part.length > 30)) { + return 'Full name parts must be between 2 and 30 characters long'; + } + return null; + } + + Future _requestPermission() async { DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); if (Platform.isAndroid ) { diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 50d6699..22082b5 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; +import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; @@ -73,23 +74,26 @@ class ProfileView extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ IntrinsicWidth( - child: TextFormField( - maxLength: 30, - style: const TextStyle( - color: Colors.black, - ), - textAlign: TextAlign.center, - focusNode: profileBloc.focusNode, - controller: profileBloc.nameController, - enabled: profileBloc.editName, - onEditingComplete: () { - profileBloc.add(SaveNameEvent(context: context)); - }, - decoration: const InputDecoration( - hintText: "Your Name", - border: InputBorder.none, - fillColor: Colors.white10, - counterText: '', // Hides the character count + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: TextFormField( + maxLength: 30, + style: const TextStyle( + color: Colors.black, + ), + textAlign: TextAlign.center, + focusNode: profileBloc.focusNode, + controller: profileBloc.nameController, + enabled: profileBloc.editName, + onEditingComplete: () { + profileBloc.add(SaveNameEvent(context: context)); + }, + decoration: const InputDecoration( + hintText: "Your Name", + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), ), ), ), diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart index 5666923..3b617ce 100644 --- a/lib/services/api/profile_api.dart +++ b/lib/services/api/profile_api.dart @@ -43,6 +43,7 @@ class ProfileApi { rethrow; } } + static Future saveTimeZone({String? regionUuid,}) async { try { final response = await _httpService.put( @@ -99,7 +100,6 @@ class ProfileApi { return response as List; } - static Future> fetchTimeZone() async { final response = await _httpService.get( path: ApiEndpoints.getTimezone, @@ -111,5 +111,4 @@ class ProfileApi { return response as List; } - } From 00199dda90eb0bcaf1df4bfc029ec7a7520b2cfb Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 25 Jul 2024 16:27:09 +0300 Subject: [PATCH 08/13] profile page fix bugs --- lib/features/menu/bloc/profile_bloc/profile_bloc.dart | 4 ++-- lib/features/menu/bloc/profile_bloc/profile_state.dart | 4 ++-- .../menu/{bloc/profile_bloc => model}/region_model.dart | 0 .../menu/{bloc/profile_bloc => model}/time_zone_model.dart | 0 lib/services/api/profile_api.dart | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename lib/features/menu/{bloc/profile_bloc => model}/region_model.dart (100%) rename lib/features/menu/{bloc/profile_bloc => model}/time_zone_model.dart (100%) diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index 91b452b..d12a9de 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -7,8 +7,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; +import 'package:syncrow_app/features/menu/model/region_model.dart'; +import 'package:syncrow_app/features/menu/model/time_zone_model.dart'; import 'package:syncrow_app/services/api/profile_api.dart'; import 'package:syncrow_app/utils/helpers/snack_bar.dart'; import 'package:image_picker/image_picker.dart'; diff --git a/lib/features/menu/bloc/profile_bloc/profile_state.dart b/lib/features/menu/bloc/profile_bloc/profile_state.dart index 19b1f5c..c5eacb5 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_state.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_state.dart @@ -1,6 +1,6 @@ import 'package:equatable/equatable.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; +import 'package:syncrow_app/features/menu/model/region_model.dart'; +import 'package:syncrow_app/features/menu/model/time_zone_model.dart'; class ProfileState extends Equatable { const ProfileState(); diff --git a/lib/features/menu/bloc/profile_bloc/region_model.dart b/lib/features/menu/model/region_model.dart similarity index 100% rename from lib/features/menu/bloc/profile_bloc/region_model.dart rename to lib/features/menu/model/region_model.dart diff --git a/lib/features/menu/bloc/profile_bloc/time_zone_model.dart b/lib/features/menu/model/time_zone_model.dart similarity index 100% rename from lib/features/menu/bloc/profile_bloc/time_zone_model.dart rename to lib/features/menu/model/time_zone_model.dart diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart index 3b617ce..c942f68 100644 --- a/lib/services/api/profile_api.dart +++ b/lib/services/api/profile_api.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; import 'package:syncrow_app/features/auth/model/user_model.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/region_model.dart'; -import 'package:syncrow_app/features/menu/bloc/profile_bloc/time_zone_model.dart'; +import 'package:syncrow_app/features/menu/model/region_model.dart'; +import 'package:syncrow_app/features/menu/model/time_zone_model.dart'; import 'package:syncrow_app/services/api/api_links_endpoints.dart'; import 'package:syncrow_app/services/api/http_service.dart'; From b9432c3de39f7bab001bf93b319afaec23e54bd1 Mon Sep 17 00:00:00 2001 From: mohammad Date: Thu, 25 Jul 2024 16:28:46 +0300 Subject: [PATCH 09/13] profile page fix bugs --- lib/features/menu/view/widgets/profile/profile_view.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 22082b5..4047b89 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -46,7 +46,7 @@ class ProfileView extends StatelessWidget { child: SizedBox.square( dimension: 120, child: CircleAvatar( - backgroundColor: Colors.grey, + backgroundColor: Colors.white, backgroundImage: profileBloc.image == null ? null : FileImage(profileBloc.image!), @@ -57,8 +57,8 @@ class ProfileView extends StatelessWidget { child: Image.memory( HomeCubit.user!.profilePicture!, fit: BoxFit.cover, - width: 110, - height: 110, + width: 120, + height: 120, ), ) : null, From 6c5acc3a084a9121d07f1d2f193c43e928f8d059 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 28 Jul 2024 09:12:15 +0300 Subject: [PATCH 10/13] profile page fix bugs --- .../menu/bloc/profile_bloc/profile_bloc.dart | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart index d12a9de..603fe20 100644 --- a/lib/features/menu/bloc/profile_bloc/profile_bloc.dart +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -15,7 +15,6 @@ import 'package:image_picker/image_picker.dart'; import 'package:permission_handler/permission_handler.dart'; class ProfileBloc extends Bloc { - bool isSaving = false; bool editName = false; final FocusNode focusNode = FocusNode(); @@ -24,7 +23,8 @@ class ProfileBloc extends Bloc { String timeZoneSelected = ''; String regionSelected = ''; final TextEditingController searchController = TextEditingController(); - final TextEditingController nameController = TextEditingController(text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); + final TextEditingController nameController = TextEditingController( + text: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); List? timeZoneList; List? regionList; @@ -45,7 +45,6 @@ class ProfileBloc extends Bloc { Future saveName(SaveNameEvent event, Emitter emit) async { if (_validateInputs()) return; try { - add(const ChangeNameEvent(value: false)); isSaving = true; emit(LoadingInitialState()); @@ -56,11 +55,10 @@ class ProfileBloc extends Bloc { var response = await ProfileApi.saveName(firstName: firstName, lastName: lastName); add(InitialProfileEvent()); await HomeCubit.getInstance().fetchUserInfo(); - // Navigator.of(event.context).pop(true); CustomSnackBar.displaySnackBar('Save Successfully'); emit(SaveState()); - } catch (_) { - // Handle the error + } catch (e) { + emit(FailedState(errorMessage: e.toString())); } finally { isSaving = false; } @@ -133,8 +131,7 @@ class ProfileBloc extends Bloc { final filteredRegions = regionList?.where((region) { return region.name.toLowerCase().contains(query); }).toList() ?? []; - regionList = filteredRegions;// Assume this fetches the regions - + regionList = filteredRegions; emit(RegionsLoadedState(regions: filteredRegions)); }else{ regionList = await ProfileApi.fetchRegion(); From 9162dd1033baa236db4e1af1c6b63cafa03d9071 Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 28 Jul 2024 10:42:29 +0300 Subject: [PATCH 11/13] profile page fix bugs --- .../view/widgets/profile/profile_view.dart | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/lib/features/menu/view/widgets/profile/profile_view.dart b/lib/features/menu/view/widgets/profile/profile_view.dart index 4047b89..bac4502 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_app/features/app_layout/bloc/home_cubit.dart'; -import 'package:syncrow_app/features/auth/bloc/auth_cubit.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_bloc.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_event.dart'; import 'package:syncrow_app/features/menu/bloc/profile_bloc/profile_state.dart'; @@ -115,18 +114,30 @@ class ProfileView extends StatelessWidget { ), ), const SizedBox(height: 10), - // Info DefaultContainer( padding: const EdgeInsets.symmetric( horizontal: 25, vertical: 5, ), - child: - - Column( + child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'email '), + Flexible(child: BodyMedium(text: HomeCubit.user!.email ?? 'No Email')), + ], + ), + ), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( onTap: () { Navigator.push( From 9502c745540e1e60672dfb20083170e8e1fe929d Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 28 Jul 2024 11:12:06 +0300 Subject: [PATCH 12/13] profile page fix bugs --- .../view/widgets/profile/region_page.dart | 24 ++++----- .../profile/time_zone_screen_page.dart | 51 +++++++++---------- 2 files changed, 36 insertions(+), 39 deletions(-) diff --git a/lib/features/menu/view/widgets/profile/region_page.dart b/lib/features/menu/view/widgets/profile/region_page.dart index 9f84576..6a5ae74 100644 --- a/lib/features/menu/view/widgets/profile/region_page.dart +++ b/lib/features/menu/view/widgets/profile/region_page.dart @@ -60,39 +60,37 @@ class RegionPage extends StatelessWidget { topRight: Radius.circular(20), ), ), - child: ListView.builder( + child:Padding( + padding: const EdgeInsets.only(bottom: 10.0,top: 10.0,left: 15,right: 15), + child: ListView.builder( itemCount: regionList.length, itemBuilder: (context, index) { return InkWell( onTap: () { profileBloc.add(SelectRegionEvent(val: regionList[index].id, context: context)); }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( + child:Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const EdgeInsets.symmetric(horizontal: 10,), child: SizedBox( - height: 45, child: ListTile( contentPadding: EdgeInsets.zero, - // trailing: BodyMedium( - // text: regionList[index].offset, - // fontSize: 13, - // fontColor: ColorsManager.textGray,), leading: BodyMedium( fontSize: 15, - text: regionList[index].name,),), + text: regionList[index].name, + ), + ), ), ), const Divider(color: ColorsManager.textGray), // Divider between items ], ), - ), ); }, - ), + )), ), ), ], diff --git a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart index 7594b05..6fd50b0 100644 --- a/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart +++ b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart @@ -62,41 +62,40 @@ class TimeZoneScreenPage extends StatelessWidget { topRight: Radius.circular(20), ), ), - child: ListView.builder( + child: Padding( + padding: const EdgeInsets.only(bottom: 10.0,top: 10.0,left: 15,right: 15), + child: ListView.builder( itemCount: timeZoneList.length, itemBuilder: (context, index) { return InkWell( onTap: () { - profileBloc.add(SelectTimeZoneEvent( - val: timeZoneList[index].id, - context: context)); + profileBloc.add(SelectTimeZoneEvent(val: timeZoneList[index].id, context: context)); }, - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: SizedBox( - height: 45, - child: ListTile( - contentPadding: EdgeInsets.zero, - trailing: BodyMedium( - text: timeZoneList[index].offset, - fontSize: 13, - fontColor: ColorsManager.textGray,), - leading: BodyMedium( - fontSize: 15, - text: timeZoneList[index].name,),), - ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10,), + child: SizedBox( + height: 45, + child: ListTile( + contentPadding: EdgeInsets.zero, + trailing: BodyMedium( + text: timeZoneList[index].offset, + fontSize: 13, + fontColor: ColorsManager.textGray,), + leading: BodyMedium( + fontSize: 15, + text: timeZoneList[index].name,),), ), - const Divider(color: ColorsManager.textGray), // Divider between items - ], - ), + ), + const Divider(color: ColorsManager.textGray), // Divider between items + ], ), ); }, - ), + )), ), ), ], From 38d571a2c9d6eda447983f0ef3c77a57dcb7a08a Mon Sep 17 00:00:00 2001 From: mohammad Date: Sun, 28 Jul 2024 11:20:28 +0300 Subject: [PATCH 13/13] profile page fix bugs --- .../view/widgets/profile/profile_tab.dart | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/features/menu/view/widgets/profile/profile_tab.dart b/lib/features/menu/view/widgets/profile/profile_tab.dart index 19c091a..d3e2dd3 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -20,7 +20,7 @@ class ProfileTab extends StatelessWidget { Widget _buildProfileContent(BuildContext context) { final homeCubit = context.read(); return Padding( - padding: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.symmetric(vertical: 10,), child: InkWell( onTap: () { Navigator.of(context) @@ -39,23 +39,27 @@ class ProfileTab extends StatelessWidget { children: [ const SizedBox(height: 20), DefaultContainer( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - BodyMedium( - text: '${HomeCubit.user!.firstName ?? ''} ', - fontWeight: FontWeight.bold, - ), - BodyMedium( - text: HomeCubit.user!.lastName ?? '', - fontWeight: FontWeight.bold, - ), - ], - ), - const BodySmall(text: "Syncrow Account"), - ], + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + BodyMedium( + text: '${HomeCubit.user!.firstName ?? ''} ', + fontWeight: FontWeight.bold, + ), + BodyMedium( + text: HomeCubit.user!.lastName ?? '', + fontWeight: FontWeight.bold, + ), + ], + ), + SizedBox(height: 5,), + const BodySmall(text: "Syncrow Account"), + ], + ), ), ), ],