Files
IKAR/lib/frontend/screens/profile/edit_profile_screen.dart
T

360 lines
11 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:material_symbols_icons/symbols.dart';
import '../../../core/storage/app_database.dart';
import '../../../core/utils/image_utils.dart';
import '../../../l10n/app_localizations.dart';
import '../../../main.dart' show accountModule, fileUploader, QlyraApp;
import '../../widgets/connection_status.dart';
import '../../widgets/custom_notification.dart';
import '../../widgets/attachment/avatar_editor.dart';
import '../../widgets/hint_bubble.dart';
import '../../widgets/qlyra_avatar.dart';
import '../../widgets/small_spinner.dart';
class EditProfileScreen extends StatefulWidget {
const EditProfileScreen({super.key});
@override
State<EditProfileScreen> createState() => _EditProfileScreenState();
}
class _EditProfileScreenState extends State<EditProfileScreen> {
final _firstNameController = TextEditingController();
final _firstNameFieldKey = GlobalKey();
final _lastNameController = TextEditingController();
final _bioController = TextEditingController();
bool _isLoading = true;
bool _isSaving = false;
String? _avatarUrl;
int? _photoId;
@override
void initState() {
super.initState();
_loadProfile();
}
@override
void dispose() {
_firstNameController.dispose();
_lastNameController.dispose();
_bioController.dispose();
super.dispose();
}
Future<void> _loadProfile() async {
final profile = await AppDatabase.loadActiveProfile();
if (!mounted) return;
if (profile != null) {
_firstNameController.text = profile.firstName;
_lastNameController.text = profile.lastName ?? '';
_bioController.text = profile.description ?? '';
_avatarUrl = profile.baseUrl;
_photoId = profile.photoId;
setState(() => _isLoading = false);
} else {
setState(() => _isLoading = false);
}
}
Future<void> _saveProfile() async {
if (_isSaving) return;
final firstName = _firstNameController.text.trim();
if (firstName.isEmpty) {
if (mounted) {
showHintBubble(
_firstNameFieldKey.currentContext ?? context,
AppLocalizations.of(context)!.editProfileNameEmpty,
);
}
return;
}
final lastName = _lastNameController.text.trim();
setState(() => _isSaving = true);
try {
final newProfile = await accountModule.updateProfile(
firstName,
lastName.isEmpty ? null : lastName,
description: _bioController.text.trim(),
);
_avatarUrl = newProfile.baseUrl;
_photoId = newProfile.photoId;
_bioController.text = newProfile.description ?? '';
if (!mounted) return;
QlyraApp.stateOf(context)?.notifyProfileUpdate();
if (mounted) {
showCustomNotification(
context,
AppLocalizations.of(context)!.editProfileSaved,
);
setState(() => _isSaving = false);
}
} catch (e) {
if (!mounted) return;
showCustomNotification(
context,
AppLocalizations.of(context)!.devicesGenericError('$e'),
);
setState(() => _isSaving = false);
}
}
Future<void> _changeAvatar() async {
if (_isSaving) return;
final path = (await pickAvatarImage(context))?.path;
if (path == null) return;
if (await File(path).length() > kMaxAvatarBytes) {
if (mounted) {
showCustomNotification(
context,
AppLocalizations.of(context)!.groupSettingsPhotoTooLarge,
);
}
return;
}
if (!mounted) return;
setState(() => _isSaving = true);
try {
final processed = await compressAvatar(await File(path).readAsBytes());
if (processed == null) {
if (!mounted) return;
showCustomNotification(
context,
AppLocalizations.of(context)!.contactLocalPhotoFailed,
);
setState(() => _isSaving = false);
return;
}
final url = await accountModule.getAvatarUploadUrl();
final token = await fileUploader.uploadImage(
Uri.parse(url),
processed,
filename: 'avatar.jpg',
);
if (token == null) {
if (!mounted) return;
showCustomNotification(
context,
AppLocalizations.of(context)!.editProfileAvatarUploadFailed,
);
setState(() => _isSaving = false);
return;
}
final newProfile = await accountModule.updateProfileAvatar(token);
if (!mounted) return;
setState(() {
_avatarUrl = newProfile.baseUrl;
_photoId = newProfile.photoId;
_isSaving = false;
});
QlyraApp.stateOf(context)?.notifyProfileUpdate();
showCustomNotification(
context,
AppLocalizations.of(context)!.editProfileAvatarUpdated,
);
} catch (e) {
if (!mounted) return;
showCustomNotification(
context,
AppLocalizations.of(context)!.devicesGenericError('$e'),
);
setState(() => _isSaving = false);
}
}
Future<void> _removeAvatar() async {
if (_isSaving || _photoId == null) return;
setState(() => _isSaving = true);
try {
final newProfile = await accountModule.removeProfilePhoto(_photoId!);
_avatarUrl = newProfile.baseUrl;
_photoId = newProfile.photoId;
if (!mounted) return;
QlyraApp.stateOf(context)?.notifyProfileUpdate();
if (mounted) {
showCustomNotification(
context,
AppLocalizations.of(context)!.editProfilePhotoDeleted,
);
setState(() => _isSaving = false);
}
} catch (e) {
if (!mounted) return;
showCustomNotification(
context,
AppLocalizations.of(context)!.devicesGenericError('$e'),
);
setState(() => _isSaving = false);
}
}
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
final l10n = AppLocalizations.of(context);
return Scaffold(
backgroundColor: cs.surface,
appBar: AppBar(
backgroundColor: cs.surface,
elevation: 0,
leading: IconButton(
icon: Icon(Symbols.arrow_back, color: cs.onSurface),
onPressed: () => Navigator.pop(context),
),
title: ConnectionTitleText(
l10n?.editProfileTitle ?? 'Edit Profile',
style: TextStyle(color: cs.onSurface, fontWeight: FontWeight.w600),
),
actions: [
TextButton(
onPressed: _isLoading || _isSaving ? null : _saveProfile,
child: _isSaving
? const SmallSpinner(size: 16)
: Text(
l10n?.editProfileSave ?? 'Save',
style: TextStyle(
color: cs.primary,
fontWeight: FontWeight.w600,
),
),
),
],
),
body: _isLoading
? const Center(child: SmallSpinner(size: 36))
: ListView(
padding: const EdgeInsets.all(16),
children: [
Center(
child: Stack(
children: [
Container(
width: 88,
height: 88,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: cs.primary.withValues(alpha: 0.5),
width: 2.5,
),
),
child: QlyraAvatar(
name: _firstNameController.text,
imageUrl: _avatarUrl,
size: 88,
fontSize: 32,
),
),
Positioned(
bottom: 0,
right: 0,
child: Container(
decoration: BoxDecoration(
color: cs.primary,
shape: BoxShape.circle,
),
child: IconButton(
icon: Icon(
Symbols.camera_alt,
color: cs.onPrimary,
size: 20,
),
onPressed: _changeAvatar,
),
),
),
],
),
),
if (_photoId != null) ...[
const SizedBox(height: 8),
Center(
child: TextButton(
onPressed: _removeAvatar,
child: Text(
l10n?.editProfileRemovePhoto ?? 'Remove photo',
style: TextStyle(color: cs.error),
),
),
),
],
const SizedBox(height: 24),
_buildTextField(
l10n?.editProfileFirstName ?? 'First name',
_firstNameController,
cs,
enabled: !_isSaving,
fieldKey: _firstNameFieldKey,
),
const SizedBox(height: 12),
_buildTextField(
l10n?.editProfileLastName ?? 'Last name',
_lastNameController,
cs,
enabled: !_isSaving,
),
const SizedBox(height: 12),
_buildTextField(
l10n?.editProfileBio ?? 'About me',
_bioController,
cs,
enabled: !_isSaving,
minLines: 2,
maxLines: 5,
),
const SizedBox(height: 120),
],
),
);
}
Widget _buildTextField(
String label,
TextEditingController controller,
ColorScheme cs, {
bool enabled = true,
int? minLines,
int maxLines = 1,
Key? fieldKey,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 4, bottom: 6),
child: Text(
label,
style: TextStyle(color: cs.onSurfaceVariant, fontSize: 13),
),
),
TextField(
key: fieldKey,
controller: controller,
enabled: enabled,
minLines: minLines,
maxLines: maxLines,
keyboardType: maxLines == 1
? TextInputType.text
: TextInputType.multiline,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
filled: true,
fillColor: cs.surfaceContainerHigh,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
],
);
}
}