Improved MacOS support and organized screens and utils
This commit is contained in:
513
lib/screens/manage_account_screen.dart
Normal file
513
lib/screens/manage_account_screen.dart
Normal file
@@ -0,0 +1,513 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gwid/api/api_service.dart';
|
||||
import 'package:gwid/models/profile.dart';
|
||||
import 'package:gwid/screens/phone_entry_screen.dart';
|
||||
import 'package:gwid/services/profile_cache_service.dart';
|
||||
import 'package:gwid/services/local_profile_manager.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class ManageAccountScreen extends StatefulWidget {
|
||||
final Profile? myProfile;
|
||||
const ManageAccountScreen({super.key, this.myProfile});
|
||||
|
||||
@override
|
||||
State<ManageAccountScreen> createState() => _ManageAccountScreenState();
|
||||
}
|
||||
|
||||
class _ManageAccountScreenState extends State<ManageAccountScreen> {
|
||||
late final TextEditingController _firstNameController;
|
||||
late final TextEditingController _lastNameController;
|
||||
late final TextEditingController _descriptionController;
|
||||
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
|
||||
final ProfileCacheService _profileCache = ProfileCacheService();
|
||||
final LocalProfileManager _profileManager = LocalProfileManager();
|
||||
|
||||
Profile? _actualProfile;
|
||||
String? _localAvatarPath;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeProfileData();
|
||||
}
|
||||
|
||||
Future<void> _initializeProfileData() async {
|
||||
await _profileManager.initialize();
|
||||
|
||||
_actualProfile = await _profileManager.getActualProfile(widget.myProfile);
|
||||
|
||||
_firstNameController = TextEditingController(
|
||||
text: _actualProfile?.firstName ?? '',
|
||||
);
|
||||
_lastNameController = TextEditingController(
|
||||
text: _actualProfile?.lastName ?? '',
|
||||
);
|
||||
_descriptionController = TextEditingController(
|
||||
text: _actualProfile?.description ?? '',
|
||||
);
|
||||
final localPath = await _profileManager.getLocalAvatarPath();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_localAvatarPath = localPath;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveProfile() async {
|
||||
if (!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final firstName = _firstNameController.text.trim();
|
||||
final lastName = _lastNameController.text.trim();
|
||||
final description = _descriptionController.text.trim();
|
||||
|
||||
final userId = _actualProfile?.id ?? widget.myProfile?.id ?? 0;
|
||||
final photoBaseUrl =
|
||||
_actualProfile?.photoBaseUrl ?? widget.myProfile?.photoBaseUrl;
|
||||
final photoId = _actualProfile?.photoId ?? widget.myProfile?.photoId ?? 0;
|
||||
|
||||
await _profileCache.saveProfileData(
|
||||
userId: userId,
|
||||
firstName: firstName,
|
||||
lastName: lastName,
|
||||
description: description.isEmpty ? null : description,
|
||||
photoBaseUrl: photoBaseUrl,
|
||||
photoId: photoId,
|
||||
);
|
||||
|
||||
_actualProfile = await _profileManager.getActualProfile(widget.myProfile);
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Профиль сохранен локально"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Ошибка сохранения: $e"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _logout() async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Выйти из аккаунта?'),
|
||||
content: const Text('Вы уверены, что хотите выйти из аккаунта?'),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Отмена'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade400,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('Выйти'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true && mounted) {
|
||||
try {
|
||||
await ApiService.instance.logout();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushAndRemoveUntil(
|
||||
MaterialPageRoute(builder: (context) => const PhoneEntryScreen()),
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Ошибка выхода: $e'),
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickAndUpdateProfilePhoto() async {
|
||||
try {
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? image = await picker.pickImage(
|
||||
source: ImageSource.gallery,
|
||||
maxWidth: 1024,
|
||||
maxHeight: 1024,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (image == null) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
File imageFile = File(image.path);
|
||||
|
||||
final userId = _actualProfile?.id ?? widget.myProfile?.id ?? 0;
|
||||
if (userId != 0) {
|
||||
final localPath = await _profileCache.saveAvatar(imageFile, userId);
|
||||
|
||||
if (localPath != null && mounted) {
|
||||
setState(() {
|
||||
_localAvatarPath = localPath;
|
||||
});
|
||||
_actualProfile = await _profileManager.getActualProfile(
|
||||
widget.myProfile,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Фотография профиля сохранена"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Ошибка загрузки фото: $e"),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text("Изменить профиль"),
|
||||
centerTitle: true,
|
||||
scrolledUnderElevation: 0,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _saveProfile,
|
||||
child: const Text(
|
||||
"Сохранить",
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildAvatarSection(theme),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Основная информация",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _firstNameController,
|
||||
maxLength: 60, // Ограничение по символам
|
||||
decoration: _buildInputDecoration(
|
||||
"Имя",
|
||||
Icons.person_outline,
|
||||
).copyWith(counterText: ""), // Скрываем счетчик
|
||||
validator: (value) =>
|
||||
value!.isEmpty ? 'Введите ваше имя' : null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _lastNameController,
|
||||
maxLength: 60, // Ограничение по символам
|
||||
decoration: _buildInputDecoration(
|
||||
"Фамилия",
|
||||
Icons.person_outline,
|
||||
).copyWith(counterText: ""), // Скрываем счетчик
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Дополнительно",
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
TextFormField(
|
||||
controller: _descriptionController,
|
||||
maxLines: 4,
|
||||
maxLength: 400,
|
||||
decoration: _buildInputDecoration(
|
||||
"О себе",
|
||||
Icons.edit_note_outlined,
|
||||
alignLabel: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (widget.myProfile != null)
|
||||
Card(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildInfoTile(
|
||||
icon: Icons.phone_outlined,
|
||||
title: "Телефон",
|
||||
subtitle: widget.myProfile!.formattedPhone,
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildTappableInfoTile(
|
||||
icon: Icons.tag,
|
||||
title: "Ваш ID",
|
||||
subtitle: widget.myProfile!.id.toString(),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: widget.myProfile!.id.toString(),
|
||||
),
|
||||
);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('ID скопирован в буфер обмена'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
_buildLogoutButton(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvatarSection(ThemeData theme) {
|
||||
ImageProvider? avatarImage;
|
||||
|
||||
if (_localAvatarPath != null) {
|
||||
avatarImage = FileImage(File(_localAvatarPath!));
|
||||
} else if (_actualProfile?.photoBaseUrl != null) {
|
||||
if (_actualProfile!.photoBaseUrl!.startsWith('file://')) {
|
||||
final path = _actualProfile!.photoBaseUrl!.replaceFirst('file://', '');
|
||||
avatarImage = FileImage(File(path));
|
||||
} else {
|
||||
avatarImage = NetworkImage(_actualProfile!.photoBaseUrl!);
|
||||
}
|
||||
} else if (widget.myProfile?.photoBaseUrl != null) {
|
||||
avatarImage = NetworkImage(widget.myProfile!.photoBaseUrl!);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: _pickAndUpdateProfilePhoto,
|
||||
child: Stack(
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 60,
|
||||
backgroundColor: theme.colorScheme.secondaryContainer,
|
||||
backgroundImage: avatarImage,
|
||||
child: avatarImage == null
|
||||
? Icon(
|
||||
Icons.person,
|
||||
size: 60,
|
||||
color: theme.colorScheme.onSecondaryContainer,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
if (_isLoading)
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
color: Colors.white,
|
||||
strokeWidth: 3,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: 4,
|
||||
right: 4,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.camera_alt, color: Colors.white, size: 20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _buildInputDecoration(
|
||||
String label,
|
||||
IconData icon, {
|
||||
bool alignLabel = false,
|
||||
}) {
|
||||
final prefixIcon = (label == "О себе")
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(bottom: 60), // Смещаем иконку вверх
|
||||
child: Icon(icon),
|
||||
)
|
||||
: Icon(icon);
|
||||
|
||||
return InputDecoration(
|
||||
labelText: label,
|
||||
prefixIcon: prefixIcon,
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
||||
alignLabelWithHint: alignLabel,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
}) {
|
||||
return ListTile(
|
||||
leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(subtitle),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTappableInfoTile({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: ListTile(
|
||||
leading: Icon(icon, color: Theme.of(context).colorScheme.primary),
|
||||
title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)),
|
||||
subtitle: Text(subtitle),
|
||||
trailing: const Icon(Icons.copy_outlined, size: 20),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogoutButton() {
|
||||
return OutlinedButton.icon(
|
||||
icon: const Icon(Icons.logout),
|
||||
label: const Text('Выйти из аккаунта'),
|
||||
onPressed: _logout,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: Colors.red.shade400,
|
||||
side: BorderSide(color: Colors.red.shade200),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_firstNameController.dispose();
|
||||
_lastNameController.dispose();
|
||||
_descriptionController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user