Files
syncrow-app/lib/features/menu/bloc/profile_bloc/profile_bloc.dart
2025-02-16 20:55:13 +04:00

309 lines
10 KiB
Dart

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<ProfileEvent, ProfileState> {
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<RegionModel> allRegions = [];
List<TimeZone> allTimeZone = [];
ProfileBloc() : super(InitialState()) {
on<InitialProfileEvent>(_fetchUserInfo);
on<TimeZoneInitialEvent>(_fetchTimeZone);
on<RegionInitialEvent>(_fetchRegion);
on<SaveNameEvent>(saveName);
on<SelectImageEvent>(_selectImage);
on<ChangeNameEvent>(_changeName);
on<SelectTimeZoneEvent>(selectTimeZone);
on<SearchRegionEvent>(searchRegion);
on<SearchTimeZoneEvent>(searchTimeZone);
on<SelectRegionEvent>(selectRegion);
}
Future<void> saveName(SaveNameEvent event, Emitter<ProfileState> 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<ProfileState> 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<ProfileState> 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<ProfileState> 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<ProfileState> 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<ProfileState> 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<void> searchRegion(
SearchRegionEvent event, Emitter<ProfileState> 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<void> searchTimeZone(
SearchTimeZoneEvent event, Emitter<ProfileState> 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<ProfileState> emit) async {
try {
emit(LoadingInitialState());
allRegions = await ProfileApi.fetchRegion();
emit(RegionsLoadedState(regions: allRegions));
} catch (e) {
emit(FailedState(errorMessage: e.toString()));
}
}
Future<void> _selectImage(
SelectImageEvent event, Emitter<ProfileState> 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<void> _saveImage() async {
List<int> 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';
}
return null;
}
Future<bool> _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;
}
}
}
}