mirror of
https://github.com/SyncrowIOT/syncrow-app.git
synced 2025-08-25 18:39:39 +00:00
320 lines
11 KiB
Dart
320 lines
11 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';
|
|
}
|
|
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<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;
|
|
}
|
|
}
|
|
}
|
|
}
|