fix: port 7 web features and repair the endless-loading screens
Root cause behind most "stuck loading" reports: the backend changed shape (schedules became an array, location_preferences a string) while the mobile parser still hard-cast to Map/int. The TypeError was swallowed by a silent catch that returned null, and screens only handled the success state, so a parse failure rendered as a permanent spinner. Same class of bug appeared across service lists via non-null map lookups and a total absence of request timeouts. Ported from prosappweb: - in-app suggestions (POST /suggestions) - policies/terms from GET /settings/policies - configurable appointment length (slot_duration_minutes) - block/unblock calendar slots (POST /services/block) - GPS city detection on the profile (Nominatim) - server-side professional search with haversine distance - retry cooldown after a rejected professional application Reliability: - parse schedules array (day_of_week 0=Mon) and string location_preferences - read times as wall clock, so 08:00 stays 08:00 across timezones - carry minutes into hours in TimeOfDay.add; a minute-based step used to loop forever and freeze the calendar (covered by test/time_slots_test.dart) - semver update check instead of string equality, which blocked every build that did not exactly match the configured version - request timeouts across all repositories - surface HTTP >= 400 instead of reporting failed writes as success - error states with retry instead of an indefinite shimmer Includes pre-existing uncommitted work from the UI redesign. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
06a89df690
commit
8631e6f729
@@ -1,7 +1,10 @@
|
||||
import 'dart:io';
|
||||
import 'package:city_repository/city_repository.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:injector/injector.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl_phone_field/helpers.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@@ -15,6 +18,19 @@ import 'package:prosappco/screens/profile/components/profile_item.dart';
|
||||
import 'package:prosappco/screens/profile/profile_register_email_screen.dart';
|
||||
import 'package:prosappco/screens/profile/profile_register_phone_screen.dart';
|
||||
import 'package:prosappco/screens/profile/profile_update_password_screen.dart';
|
||||
import 'package:prosappco/utils/nominatim_geocoder.dart';
|
||||
|
||||
const _kPrimary = Color(0xFF1565C0);
|
||||
|
||||
extension _Th on BuildContext {
|
||||
ThemeData get _t => Theme.of(this);
|
||||
Color get onSurface => _t.colorScheme.onSurface;
|
||||
Color get muted => _t.colorScheme.onSurface.withOpacity(0.55);
|
||||
Color get card => _t.cardColor;
|
||||
bool get isDark => _t.brightness == Brightness.dark;
|
||||
Color get shadowSm =>
|
||||
isDark ? Colors.transparent : Colors.black.withOpacity(0.05);
|
||||
}
|
||||
|
||||
class ProfileScreen extends StatefulWidget {
|
||||
const ProfileScreen({super.key});
|
||||
@@ -27,14 +43,13 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _cityController = TextEditingController();
|
||||
final TextEditingController _emailController = TextEditingController();
|
||||
final TextEditingController _newEmailController = TextEditingController();
|
||||
final TextEditingController _phoneController = TextEditingController();
|
||||
final TextEditingController _birthdayController = TextEditingController();
|
||||
final TextEditingController _genderController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
|
||||
XFile? _imageFile;
|
||||
bool isLoading = false;
|
||||
bool _isLocating = false;
|
||||
|
||||
late final AuthBloc authBloc;
|
||||
|
||||
@@ -49,44 +64,97 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
_nameController.dispose();
|
||||
_cityController.dispose();
|
||||
_emailController.dispose();
|
||||
_newEmailController.dispose();
|
||||
_phoneController.dispose();
|
||||
_birthdayController.dispose();
|
||||
_genderController.dispose();
|
||||
_passwordController.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _useMyLocation() async {
|
||||
setState(() => _isLocating = true);
|
||||
try {
|
||||
bool serviceEnabled = await Geolocator.isLocationServiceEnabled();
|
||||
if (!serviceEnabled) throw Exception('Location services disabled');
|
||||
|
||||
LocationPermission permission = await Geolocator.checkPermission();
|
||||
if (permission == LocationPermission.denied) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
if (permission == LocationPermission.denied ||
|
||||
permission == LocationPermission.deniedForever) {
|
||||
throw Exception('Location permission denied');
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition();
|
||||
final cityName = await NominatimGeocoder.reverseGeocodeCity(
|
||||
position.latitude, position.longitude);
|
||||
|
||||
if (cityName == null) throw Exception('City not resolved');
|
||||
|
||||
final cities =
|
||||
await Injector.appInstance.get<CityRepository>().getCities();
|
||||
final normalized = removeDiacritics(cityName.toLowerCase().trim());
|
||||
|
||||
CityUi? match;
|
||||
for (final c in cities) {
|
||||
if (removeDiacritics(c.cityName.toLowerCase().trim()) == normalized) {
|
||||
match = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
for (final c in cities) {
|
||||
final cName = removeDiacritics(c.cityName.toLowerCase());
|
||||
if (cName.contains(normalized) || normalized.contains(cName)) {
|
||||
match = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
if (match != null) {
|
||||
setState(() => _cityController.text = match!.cityName);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text(
|
||||
'No encontramos tu ciudad en la lista, selecciónala manualmente'),
|
||||
));
|
||||
}
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('No se pudo obtener tu ubicación, selecciona tu ciudad manualmente'),
|
||||
));
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLocating = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<AuthBloc>(
|
||||
create: (context) => authBloc,
|
||||
create: (_) => authBloc,
|
||||
child: BlocListener<ProfileBloc, ProfileState>(
|
||||
listener: (context, state) {
|
||||
if (state is UpdateUserInfoLoading) {
|
||||
setState(() {
|
||||
isLoading = true;
|
||||
});
|
||||
setState(() => isLoading = true);
|
||||
} else if (state is UpdateUserInfoSuccess) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Información actualizada'),
|
||||
));
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Información actualizada')));
|
||||
setState(() => isLoading = false);
|
||||
} else if (state is UpdateUserInfoFailure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Error al actualizar la información'),
|
||||
));
|
||||
setState(() {
|
||||
isLoading = false;
|
||||
});
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Error al actualizar')));
|
||||
setState(() => isLoading = false);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Perfil'),
|
||||
title: const Text('Mi perfil'),
|
||||
backgroundColor: _kPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
),
|
||||
body: BlocBuilder<MyUserBloc, MyUserState>(
|
||||
builder: (context, state) {
|
||||
@@ -102,195 +170,21 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 40, vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
pictureWidget(state, context),
|
||||
const SizedBox(height: 30),
|
||||
TextFormField(
|
||||
controller: _nameController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Nombre',
|
||||
prefixIcon: Icon(Icons.person),
|
||||
hintText: 'Nombre (obligatorio)',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
)),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red, width: 2.0),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Por favor, ingrese su nombre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
TextFormField(
|
||||
controller: _cityController,
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final cityName = await Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (BuildContext context) {
|
||||
return const CityListScreen();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
if (cityName != null) {
|
||||
_cityController.text = cityName;
|
||||
}
|
||||
},
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Ciudad',
|
||||
prefixIcon: Icon(Icons.near_me_rounded),
|
||||
hintText: 'Selecciona tu ciudad',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
)),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(color: Colors.red),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderSide: BorderSide(
|
||||
color: Colors.red, width: 2.0),
|
||||
),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Por favor, ingrese su nombre';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
_birthdayController.text.isEmpty &&
|
||||
_genderController.text.isEmpty
|
||||
? Column(
|
||||
children: [
|
||||
const SizedBox(height: 20.0),
|
||||
BirthdayPicker(
|
||||
onDateSelected: (birthDay) {
|
||||
_birthdayController.text =
|
||||
DateFormat('dd/MM/yyyy')
|
||||
.format(birthDay);
|
||||
},
|
||||
controller: _birthdayController,
|
||||
),
|
||||
const SizedBox(height: 20.0),
|
||||
GenderDropdown(
|
||||
controller: _genderController,
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox(),
|
||||
const SizedBox(height: 20),
|
||||
ProfileItem(
|
||||
title: 'Configurar inicio de sesión con correo',
|
||||
subtitle: _emailController.text,
|
||||
leading: Icons.email_rounded,
|
||||
onTap: () {
|
||||
if (state.user!.name == null ||
|
||||
state.user!.name == '') {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Por favor, ingrese su nombre')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.user!.city == null ||
|
||||
state.user!.city == '') {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Por favor, ingrese su ciudad')));
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) => _emailController
|
||||
.text.isEmpty
|
||||
? ProfileRegisterEmailScreen()
|
||||
: ProfileUpdatePasswordScreen(
|
||||
email: _emailController.text)),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ProfileItem(
|
||||
title:
|
||||
'Configurar inicio de sesión con celular',
|
||||
subtitle: _phoneController.text,
|
||||
leading: Icons.phone_iphone_rounded,
|
||||
onTap: () {
|
||||
if (state.user!.name == null ||
|
||||
state.user!.name == '') {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Por favor, ingrese su nombre')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.user!.city == null ||
|
||||
state.user!.city == '') {
|
||||
ScaffoldMessenger.of(context)
|
||||
.clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text(
|
||||
'Por favor, ingrese su nombre')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_phoneController.text.isEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (context) =>
|
||||
const ProfileRegisterPhoneScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_avatarSection(state, context),
|
||||
const SizedBox(height: 8),
|
||||
_formSection(state, context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
),
|
||||
const Divider(height: 1, thickness: 0.5),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 10,
|
||||
horizontal: 15,
|
||||
),
|
||||
child: saveButton(state, context),
|
||||
vertical: 12, horizontal: 16),
|
||||
child: _saveButton(state, context),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -304,29 +198,220 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget saveButton(MyUserState state, BuildContext context) {
|
||||
Widget _avatarSection(MyUserState state, BuildContext context) {
|
||||
return Container(
|
||||
color: _kPrimary,
|
||||
padding: const EdgeInsets.only(bottom: 28),
|
||||
child: Center(
|
||||
child: Stack(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () async {
|
||||
final picker = ImagePicker();
|
||||
final image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
maxHeight: 500,
|
||||
maxWidth: 500,
|
||||
imageQuality: 40,
|
||||
);
|
||||
if (image != null) setState(() => _imageFile = image);
|
||||
},
|
||||
child: Hero(
|
||||
tag: 'picture-profile',
|
||||
child: _avatarContainer(state),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 30,
|
||||
height: 30,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.15), blurRadius: 4)
|
||||
],
|
||||
),
|
||||
child:
|
||||
const Icon(Icons.camera_alt_outlined, size: 16, color: _kPrimary),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _avatarContainer(MyUserState state) {
|
||||
ImageProvider<Object>? imageProvider;
|
||||
if (_imageFile?.path != null && _imageFile!.path.isNotEmpty) {
|
||||
imageProvider = FileImage(File(_imageFile!.path));
|
||||
} else if (state.user?.picture != null &&
|
||||
state.user!.picture!.isNotEmpty) {
|
||||
imageProvider = NetworkImage(state.user!.picture!);
|
||||
}
|
||||
|
||||
return Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
image: imageProvider == null
|
||||
? null
|
||||
: DecorationImage(image: imageProvider, fit: BoxFit.cover),
|
||||
),
|
||||
child: imageProvider == null
|
||||
? const Icon(CupertinoIcons.person, color: Colors.white70, size: 44)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _formSection(MyUserState state, BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
_field(
|
||||
controller: _nameController,
|
||||
label: 'Nombre',
|
||||
icon: Icons.person_outline_rounded,
|
||||
validator: (v) =>
|
||||
(v == null || v.isEmpty) ? 'Ingresa tu nombre' : null,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
TextFormField(
|
||||
controller: _cityController,
|
||||
readOnly: true,
|
||||
onTap: () async {
|
||||
final cityName = await Navigator.push<String>(
|
||||
context,
|
||||
CupertinoPageRoute(builder: (_) => const CityListScreen()),
|
||||
);
|
||||
if (cityName != null) _cityController.text = cityName;
|
||||
},
|
||||
decoration: _inputDeco(
|
||||
'Ciudad',
|
||||
Icons.location_city_outlined,
|
||||
hint: 'Selecciona tu ciudad',
|
||||
suffixIcon: _isLocating
|
||||
? const Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.my_location_outlined),
|
||||
tooltip: 'Usar mi ubicación',
|
||||
onPressed: _useMyLocation,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_birthdayController.text.isEmpty &&
|
||||
_genderController.text.isEmpty) ...[
|
||||
const SizedBox(height: 14),
|
||||
BirthdayPicker(
|
||||
onDateSelected: (d) => _birthdayController.text =
|
||||
DateFormat('dd/MM/yyyy').format(d),
|
||||
controller: _birthdayController,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
GenderDropdown(controller: _genderController),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: context.card,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [BoxShadow(color: context.shadowSm, blurRadius: 8)],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ProfileItem(
|
||||
title: 'Inicio de sesión con correo',
|
||||
subtitle: _emailController.text,
|
||||
leading: Icons.email_outlined,
|
||||
onTap: () {
|
||||
if (!_validateProfile(state, context)) return;
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) => _emailController.text.isEmpty
|
||||
? ProfileRegisterEmailScreen()
|
||||
: ProfileUpdatePasswordScreen(
|
||||
email: _emailController.text),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 0.5,
|
||||
color: context.onSurface.withOpacity(0.08)),
|
||||
ProfileItem(
|
||||
title: 'Inicio de sesión con celular',
|
||||
subtitle: _phoneController.text,
|
||||
leading: Icons.phone_iphone_rounded,
|
||||
onTap: () {
|
||||
if (!_validateProfile(state, context)) return;
|
||||
if (_phoneController.text.isEmpty) {
|
||||
Navigator.push(
|
||||
context,
|
||||
CupertinoPageRoute(
|
||||
builder: (_) =>
|
||||
const ProfileRegisterPhoneScreen()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
bool _validateProfile(MyUserState state, BuildContext context) {
|
||||
if (state.user!.name == null || state.user!.name == '') {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Por favor, ingresa tu nombre')));
|
||||
return false;
|
||||
}
|
||||
if (state.user!.city == null || state.user!.city == '') {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Por favor, ingresa tu ciudad')));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Widget _saveButton(MyUserState state, BuildContext context) {
|
||||
return ElevatedButton(
|
||||
onPressed: () {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isLoading) return;
|
||||
if (_nameController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Por favor, ingrese su nombre')));
|
||||
|
||||
const SnackBar(content: Text('Por favor, ingresa tu nombre')));
|
||||
return;
|
||||
}
|
||||
|
||||
if (_cityController.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Por favor, ingrese su ciudad')));
|
||||
|
||||
const SnackBar(content: Text('Por favor, ingresa tu ciudad')));
|
||||
return;
|
||||
}
|
||||
|
||||
final myUser = state.user!.copyWith(
|
||||
name: _nameController.text,
|
||||
city: _cityController.text,
|
||||
@@ -336,102 +421,62 @@ class _ProfileScreenState extends State<ProfileScreen> {
|
||||
birthday: _birthdayController.text,
|
||||
gender: _genderController.text,
|
||||
);
|
||||
|
||||
context
|
||||
.read<ProfileBloc>()
|
||||
.add(UpdateUserInfo(myUser: myUser, filePicture: _imageFile?.path));
|
||||
|
||||
ScaffoldMessenger.of(context).clearSnackBars();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Información actualizada...')),
|
||||
);
|
||||
|
||||
const SnackBar(content: Text('Actualizando información...')));
|
||||
Navigator.pop(context);
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _kPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 15),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
elevation: 0,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: isLoading
|
||||
? const CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
)
|
||||
: const Text(
|
||||
'Actualizar',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white))
|
||||
: const Text('Guardar cambios',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600)),
|
||||
);
|
||||
}
|
||||
|
||||
static InputDecoration _inputDeco(String label, IconData icon,
|
||||
{String? hint, Widget? suffixIcon}) {
|
||||
return InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
prefixIcon: Icon(icon),
|
||||
suffixIcon: suffixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: Colors.grey.shade300),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: _kPrimary, width: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget pictureWidget(MyUserState state, BuildContext context) {
|
||||
final pictureUrl = state.user?.picture;
|
||||
final pathImageFile = _imageFile?.path;
|
||||
|
||||
ImageProvider<Object>? imageProvider;
|
||||
|
||||
if (pathImageFile != null && pathImageFile.isNotEmpty) {
|
||||
imageProvider = FileImage(File(pathImageFile));
|
||||
} else if (pictureUrl != null && pictureUrl.isNotEmpty) {
|
||||
imageProvider = NetworkImage(pictureUrl);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
maxHeight: 500,
|
||||
maxWidth: 500,
|
||||
imageQuality: 40,
|
||||
);
|
||||
|
||||
if (image != null) {
|
||||
setState(() {
|
||||
_imageFile = image;
|
||||
});
|
||||
}
|
||||
},
|
||||
child: Hero(
|
||||
tag: 'picture-profile',
|
||||
child: pictureContainerWidget(imageProvider),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget pictureContainerWidget(ImageProvider<Object>? imageProvider) {
|
||||
final image = imageProvider == null
|
||||
? null
|
||||
: DecorationImage(
|
||||
image: imageProvider,
|
||||
fit: BoxFit.contain,
|
||||
);
|
||||
|
||||
final widget = image == null
|
||||
? Icon(
|
||||
CupertinoIcons.person,
|
||||
color: Colors.grey.shade400,
|
||||
size: 40,
|
||||
)
|
||||
: null;
|
||||
|
||||
return Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
shape: BoxShape.circle,
|
||||
image: image,
|
||||
),
|
||||
child: widget,
|
||||
Widget _field({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
decoration: _inputDeco(label, icon),
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user