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