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 allRegions = []; List allTimeZone = []; 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] : ''; 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); HomeCubit.getInstance().project = HomeCubit.user?.project; emit(SaveState()); } catch (e) { emit(FailedState(errorMessage: e.toString())); return; } } Future _fetchTimeZone( TimeZoneInitialEvent event, Emitter emit) async { emit(LoadingInitialState()); try { allTimeZone = await ProfileApi.fetchTimeZone(); emit(TimeZoneLoadedState(timezone: allTimeZone)); return allTimeZone; } 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 (allRegions.isEmpty) { allRegions = await ProfileApi.fetchRegion(); } if (query.isNotEmpty) { final filteredRegions = allRegions.where((region) { return region.name.toLowerCase().contains(query); }).toList(); emit(RegionsLoadedState(regions: filteredRegions)); } else { emit(RegionsLoadedState(regions: allRegions)); } } Future searchTimeZone( SearchTimeZoneEvent event, Emitter emit) async { emit(LoadingInitialState()); final query = event.query.toLowerCase(); if (allTimeZone.isEmpty) { allTimeZone = await ProfileApi.fetchTimeZone(); } if (query.isNotEmpty) { final filteredRegions = allTimeZone.where((region) { return region.name.toLowerCase().contains(query); }).toList(); emit(TimeZoneLoadedState(timezone: filteredRegions)); } else { emit(TimeZoneLoadedState(timezone: allTimeZone)); } } void _fetchRegion( RegionInitialEvent event, Emitter emit) async { try { emit(LoadingInitialState()); allRegions = await ProfileApi.fetchRegion(); emit(RegionsLoadedState(regions: allRegions)); } catch (e) { emit(FailedState(errorMessage: e.toString())); } } Future _selectImage( SelectImageEvent event, Emitter emit) async { try { 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 { emit(LoadingInitialState()); await _saveImage(); emit(ImageSelectedState()); } } emit(ImageSelectedState()); } else { _showPermissionDeniedDialog(event.context); } } catch (_) { emit(const FailedState(errorMessage: 'Something went wrong')); } } Future _saveImage() async { List imageBytes = image!.readAsBytesSync(); String base64Image = base64Encode(imageBytes); await ProfileApi.saveImage(base64Image); } 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 > 2) return 'Full name can at most contain 2 parts'; if (parts.any((part) => part.length < 2 || part.length > 30)) { return 'Full name parts must be between 2 and 30 characters long'; } if (RegExp(r"\s{2,}").hasMatch(value)) { return 'Only one space is allowed between first and last names'; } // Check for leading or trailing spaces if (value != value.trim()) { return 'No leading or trailing spaces allowed'; } // Check if only alphabetic characters and one space are used if (!RegExp(r'^[A-Za-z]+(?: [A-Za-z]+)?$').hasMatch(value)) { return 'Only alphabetic characters and a single space are allowed'; } 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; } } } }