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/app_layout/bloc/home_cubit.dart b/lib/features/app_layout/bloc/home_cubit.dart index eee72a1..a964a53 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,18 @@ class HomeCubit extends Cubit { return _instance!; } + + Future fetchUserInfo() async { + try { + 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 efe9794..5f05563 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'; @@ -179,12 +181,14 @@ 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 + ); const FlutterSecureStorage().write( key: UserModel.userUuidKey, - value: Token.decodeToken(token.accessToken)['uuid'].toString()); - + value: Token.decodeToken(token.accessToken)['uuid'].toString() + ); user = UserModel.fromToken(token); emailController.clear(); passwordController.clear(); @@ -278,8 +282,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(); @@ -312,6 +315,7 @@ class AuthCubit extends Cubit { } } + sendToForgetPassword({required String password}) async { try { emit(AuthForgetPassLoading()); @@ -321,4 +325,8 @@ class AuthCubit extends Cubit { emit(AuthForgetPassError(message: 'Something went wrong')); } } + + + + } diff --git a/lib/features/auth/model/user_model.dart b/lib/features/auth/model/user_model.dart index 5491d73..fd68fb4 100644 --- a/lib/features/auth/model/user_model.dart +++ b/lib/features/auth/model/user_model.dart @@ -1,63 +1,81 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; import 'package:syncrow_app/features/auth/model/token.dart'; 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 Uint8List? 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: 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'], - name: null, - photoUrl: null, + lastName: tempJson['lastName'], + firstName:tempJson['firstName'] , + 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, '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 a51a41e..068d947 100644 --- a/lib/features/menu/bloc/menu_cubit.dart +++ b/lib/features/menu/bloc/menu_cubit.dart @@ -7,85 +7,6 @@ class MenuCubit extends Cubit { static MenuCubit of(context) => BlocProvider.of(context); - // 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', - // ), - // ], - // ), - // ]; + String name = ''; + } 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..603fe20 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_bloc.dart @@ -0,0 +1,309 @@ +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/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/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'; +import 'package:permission_handler/permission_handler.dart'; + +class ProfileBloc extends Bloc { + 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: '${HomeCubit.user!.firstName} ${HomeCubit.user!.lastName}'); + List? timeZoneList; + List? regionList; + + + ProfileBloc() : super(InitialState()) { + on(_fetchUserInfo); + on(_fetchTimeZone); + on(_fetchRegion); + on(saveName); + on(_selectImage); + on(_changeName); + on(selectTimeZone); + on(searchRegion); + on(searchTimeZone); + on(selectRegion); + } + + 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()); + await HomeCubit.getInstance().fetchUserInfo(); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveState()); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + } finally { + isSaving = false; + } + } + + 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(InitialProfileEvent event, Emitter emit) async { + try { + emit(LoadingInitialState()); + HomeCubit.user = await ProfileApi().fetchUserInfo(HomeCubit.user!.uuid); + emit(SaveState()); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future _fetchTimeZone(TimeZoneInitialEvent event, Emitter emit) async { + emit(LoadingInitialState()); + try { + timeZoneList = await ProfileApi.fetchTimeZone(); + emit(UpdateState(timeZoneList: timeZoneList!)); + return timeZoneList; + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future selectTimeZone(SelectTimeZoneEvent event, Emitter emit) async { + try { + emit(LoadingInitialState()); + timeZoneSelected = event.val; + await ProfileApi.saveTimeZone(regionUuid: event.val); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveState()); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + } + } + + Future selectRegion(SelectRegionEvent event, Emitter emit) async { + try { + emit(LoadingInitialState()); + await ProfileApi.saveRegion(regionUuid:event.val ); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(SaveState()); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + return; + } + } + + Future searchRegion(SearchRegionEvent event, Emitter emit) async { + emit(LoadingInitialState()); + final query = event.query.toLowerCase(); + if(event.query.isNotEmpty){ + final filteredRegions = regionList?.where((region) { + return region.name.toLowerCase().contains(query); + }).toList() ?? []; + regionList = filteredRegions; + 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 = await ProfileApi.fetchRegion(); + emit(RegionsLoadedState(regions: regionList!)); + } catch (e) { + emit(FailedState(errorMessage: e.toString())); + } + } + + 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(LoadingInitialState()); + List imageBytes = image!.readAsBytesSync(); + String base64Image = base64Encode(imageBytes); + print(base64Image); + var response = await ProfileApi.saveImage(base64Image); + emit(ImageSelectedState()); + } + + 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'), + ), + ], + ); + }, + ); + } + + + + + bool _validateInputs() { + 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 ) { + 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 new file mode 100644 index 0000000..15d9336 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_event.dart @@ -0,0 +1,76 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/cupertino.dart'; + +abstract class ProfileEvent extends Equatable { + const ProfileEvent(); + + @override + List get props => []; +} + +class InitialProfileEvent extends ProfileEvent {} + +class TimeZoneInitialEvent extends ProfileEvent {} + +class ChangeNameEvent extends ProfileEvent { + final bool? value; + const ChangeNameEvent({ this.value}); +} + +class RegionInitialEvent extends ProfileEvent {} + + +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]; +} + +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 new file mode 100644 index 0000000..c5eacb5 --- /dev/null +++ b/lib/features/menu/bloc/profile_bloc/profile_state.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.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(); + + @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 FailedState extends ProfileState { + final String errorMessage; + + const FailedState({required this.errorMessage}); + + @override + List get props => [errorMessage]; +} + + +class ImageSelectedState 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}); +} + +class TimeZoneLoadedState extends ProfileState { + final List regions; + + const TimeZoneLoadedState({required this.regions}); +} \ No newline at end of file diff --git a/lib/features/menu/model/region_model.dart b/lib/features/menu/model/region_model.dart new file mode 100644 index 0000000..fd6306a --- /dev/null +++ b/lib/features/menu/model/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['regionName'], + id: json['uuid'].toString(), // Ensure id is a String + ); + } + + Map toJson() { + return { + 'regionName': name, + 'uuid': id, + }; + } +} diff --git a/lib/features/menu/model/time_zone_model.dart b/lib/features/menu/model/time_zone_model.dart new file mode 100644 index 0000000..b37049b --- /dev/null +++ b/lib/features/menu/model/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['cityName'], + offset: json['timeZoneOffset'], + id: json['uuid'].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 298d1f7..9b2cf56 100644 --- a/lib/features/menu/view/menu_view.dart +++ b/lib/features/menu/view/menu_view.dart @@ -21,11 +21,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(), 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..d3e2dd3 100644 --- a/lib/features/menu/view/widgets/profile/profile_tab.dart +++ b/lib/features/menu/view/widgets/profile/profile_tab.dart @@ -1,43 +1,65 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/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 { - const ProfileTab({ - super.key, - }); - + const ProfileTab({super.key,}); @override Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return _buildProfileContent(context ); + }, + ); + } + + 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).push( + Navigator.of(context) + .push( MaterialPageRoute( builder: (context) => const ProfileView(), ), - ); + ).then((result) { + context.read().fetchUserInfo(); + }); }, - 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", - fontWeight: FontWeight.bold, - ), - 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"), + ], + ), ), ), ], @@ -51,7 +73,16 @@ class ProfileTab extends StatelessWidget { child: CircleAvatar( radius: 37, backgroundColor: Colors.grey, - child: SyncrowLogo(), + 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 91a346e..bac4502 100644 --- a/lib/features/menu/view/widgets/profile/profile_view.dart +++ b/lib/features/menu/view/widgets/profile/profile_view.dart @@ -1,9 +1,15 @@ 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/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 { @@ -11,118 +17,185 @@ class ProfileView extends StatelessWidget { @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()..add(InitialProfileEvent()), + child: BlocConsumer( + listener: (context, state) {}, + builder: (context, state) { + final profileBloc = BlocProvider.of(context); + return DefaultScaffold( + title: 'Syncrow Account', + child: + state is LoadingInitialState + ? const Center(child: CircularProgressIndicator()): + 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.white, + backgroundImage: profileBloc.image == null + ? null + : FileImage(profileBloc.image!), + child: profileBloc.image != null + ? null + :HomeCubit.user!.profilePicture != null + ? ClipOval( + child: Image.memory( + HomeCubit.user!.profilePicture!, + fit: BoxFit.cover, + width: 120, + height: 120, + ), + ) + : null, + ), + ), + ), + ), + ), + const SizedBox(height: 20), + SizedBox( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IntrinsicWidth( + 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: '', + ), + ), + ), + ), + 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), + DefaultContainer( + padding: const EdgeInsets.symmetric( + horizontal: 25, + vertical: 5, + ), + 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( + context, + MaterialPageRoute( + builder: (context) => const RegionPage(), + ), + ).then((result) { + profileBloc.add(InitialProfileEvent()); + }); + }, + child: Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const BodyMedium(text: 'Region '), + Flexible(child: BodyMedium(text: HomeCubit.user!.regionName ?? 'No Region')), + ], + ), + ), + ), + Container( + height: 1, + color: ColorsManager.greyColor, + ), + InkWell( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const TimeZoneScreenPage(), + ), + ).then((result) { + profileBloc.add(InitialProfileEvent()); + }); + }, + 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: HomeCubit.user!.timeZone ?? "No Time Zone"), + ), + ], + ), + ), + ), + ], + ) + + ), + ], ), - ), - 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..6a5ae74 --- /dev/null +++ b/lib/features/menu/view/widgets/profile/region_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 RegionPage extends StatelessWidget { + const RegionPage({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => ProfileBloc()..add(RegionInitialEvent()), + child: BlocConsumer(listener: (context, state) { + if (state is FailedState) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + 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); + 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: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:Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10,), + child: SizedBox( + child: ListTile( + contentPadding: EdgeInsets.zero, + 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..6fd50b0 --- /dev/null +++ b/lib/features/menu/view/widgets/profile/time_zone_screen_page.dart @@ -0,0 +1,107 @@ +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()..add(TimeZoneInitialEvent()), + child: + BlocConsumer(listener: (context, state) { + if (state is FailedState) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage), + 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); + 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( + controller:profileBloc.searchController , + onChanged: (value) { + profileBloc.add(SearchTimeZoneEvent(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: 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)); + }, + 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 + ], + ), + ); + }, + )), + ), + ), + ], + ), + ); + }) + ); + } +} 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 f02bc99..3d42813 100644 --- a/lib/services/api/api_links_endpoints.dart +++ b/lib/services/api/api_links_endpoints.dart @@ -150,4 +150,14 @@ abstract class ApiEndpoints { //multiple-time offline static const String deleteTemporaryPassword = '/door-lock/temporary-password/{doorLockUuid}/{passwordId}'; + + //user + + static const String getUser = '/user/{userUuid}'; + static const String saveRegion = '/user/region/{userUuid}'; + static const String saveTimeZone = '/user/timezone/{userUuid}'; + static const String saveName = '/user/name/{userUuid}'; + static const String sendPicture = '/user/profile-picture/{userUuid}'; + static const String getRegion = '/region'; + static const String getTimezone = '/timezone'; } diff --git a/lib/services/api/profile_api.dart b/lib/services/api/profile_api.dart new file mode 100644 index 0000000..c942f68 --- /dev/null +++ b/lib/services/api/profile_api.dart @@ -0,0 +1,114 @@ +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/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'; + +class ProfileApi { + static final HTTPService _httpService = HTTPService(); + + static Future> saveName({String? firstName, String? lastName,}) async { + try { + final response = await _httpService.put( + path: ApiEndpoints.saveName.replaceAll('{userUuid}', HomeCubit.user!.uuid!), + body: { + "firstName": firstName, + "lastName": lastName + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future saveRegion({String? regionUuid,}) async { + try { + final response = await _httpService.put( + path: ApiEndpoints.saveRegion.replaceAll('{userUuid}', HomeCubit.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}', HomeCubit.user!.uuid!), + body: { + "timezoneUuid": regionUuid, + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future> saveImage(String image) async { + try { + final response = await _httpService.put( + path: ApiEndpoints.sendPicture.replaceAll('{userUuid}', HomeCubit.user!.uuid!), + body: { + "profilePicture": 'data:image/png;base64,$image' + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + Future fetchUserInfo(userId) async { + final response = await _httpService.get( + path: ApiEndpoints.getUser.replaceAll('{userUuid}', userId!), + showServerMessage: true, + expectedResponseModel: (json) { + return 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; + } + +} 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 7a130bf..ccc0d42 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