Chepuhagram/lib/presentation/screens/user_profile_screen.dart

511 lines
16 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:chepuhagram/domain/services/api_service.dart';
import 'package:chepuhagram/data/datasources/ws_client.dart';
import 'package:provider/provider.dart';
import '/core/constants.dart';
import 'dart:io';
class UserProfileScreen extends StatefulWidget {
final int userId;
final String username;
final String name;
final VoidCallback? onClose;
const UserProfileScreen({
super.key,
required this.userId,
required this.username,
required this.name,
this.onClose,
});
@override
State<UserProfileScreen> createState() => _UserProfileScreenState();
}
class _UserProfileScreenState extends State<UserProfileScreen> {
Map<String, dynamic>? _userData;
StreamSubscription<dynamic>? _socketSubscription;
bool _isLoading = true;
String? _error;
Duration? offset;
Timer? _onlineTimer;
String? firstName;
String? lastName;
@override
void initState() {
super.initState();
_loadUserData();
startOnlineUpdates();
DateTime now = DateTime.now();
offset = now.timeZoneOffset;
final socketService = Provider.of<SocketService>(context, listen: false);
_socketSubscription = socketService.messages.listen(_handleIncomingMessage);
}
void startOnlineUpdates() {
_onlineTimer = Timer.periodic(const Duration(minutes: 1), (_) {
_loadUserData();
});
}
Future<void> _loadUserData() async {
try {
final api = ApiService();
final data = await api.getUserById(widget.userId);
final prefs = await SharedPreferences.getInstance();
firstName = prefs.getString('firstname_${widget.userId}');
lastName = prefs.getString('lastname_${widget.userId}');
if (mounted) {
setState(() {
_userData = data;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString().contains('SocketFailed')
? 'Соединение разорвано'
: e.toString().replaceAll('Exception: ', '');
_isLoading = false;
});
}
}
}
@override
void dispose() {
_onlineTimer?.cancel();
_socketSubscription?.cancel();
super.dispose();
}
String _formatLastSeen(String? lastSeenStr) {
if (lastSeenStr == null) return 'Был(а) недавно';
final lastSeen = DateTime.tryParse(lastSeenStr);
if (lastSeen == null) return 'Был(а) недавно';
// Применяем локальный офсет часового пояса, если необходимо
final localLastSeen = offset != null ? lastSeen.add(offset!) : lastSeen;
final now = DateTime.now();
final difference = now.difference(localLastSeen);
if (difference.inMinutes < 1) {
return 'Был(а) только что';
} else if (difference.inMinutes < 60) {
return 'Был(а) ${difference.inMinutes} ${_pluralize(difference.inMinutes, "минуту", "минуты", "минут")} назад';
} else if (difference.inHours < 24) {
return 'Был(а) ${difference.inHours} ${_pluralize(difference.inHours, "час", "часа", "часов")} назад';
} else if (difference.inDays < 7) {
return 'Был(а) ${difference.inDays} ${_pluralize(difference.inDays, "день", "дня", "дней")} назад';
} else if (difference.inDays < 30) {
final weeks = (difference.inDays / 7).floor();
return 'Был(а) $weeks ${_pluralize(weeks, "неделю", "недели", "недель")} назад';
} else {
return 'Был(а) давно';
}
}
String _pluralize(int count, String form1, String form2, String form5) {
final mod10 = count % 10;
final mod100 = count % 100;
if (mod10 == 1 && mod100 != 11) {
return form1;
} else if (mod10 >= 2 && mod10 <= 4 && (mod100 < 10 || mod100 >= 20)) {
return form2;
} else {
return form5;
}
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.background,
body: SafeArea(
child: Stack(
children: [
// Основное содержимое экрана
_buildMainContent(colorScheme),
if (Platform.isWindows) ...[
Positioned(
top: 12,
right: 16,
child: ClipOval(
child: Material(
child: IconButton(
icon: const Icon(Icons.close_rounded),
color: colorScheme.onSurfaceVariant,
onPressed: () {
if (widget.onClose != null) {
widget.onClose!();
} else if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
),
),
),
),
] else if (Platform.isAndroid) ...[
Positioned(
top: 12,
left: 16,
child: ClipOval(
child: Material(
child: IconButton(
icon: const Icon(Icons.arrow_back),
color: colorScheme.onSurfaceVariant,
onPressed: () {
if (widget.onClose != null) {
widget.onClose!();
} else if (Navigator.canPop(context)) {
Navigator.pop(context);
}
},
),
),
),
),
],
],
),
),
);
}
Widget _buildMainContent(ColorScheme colorScheme) {
if (_isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (_error != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
_error!,
style: TextStyle(
color: colorScheme.error,
fontSize: 16,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
);
}
return _buildUserInfo();
}
Widget _buildUserInfo() {
if (_userData == null) return const SizedBox.shrink();
final colorScheme = Theme.of(context).colorScheme;
final String displayFN = firstName ?? _userData?['first_name'] ?? '';
final String displayLN = lastName ?? _userData?['last_name'] ?? '';
final String combinedName = '$displayFN $displayLN'.trim();
final String username = _userData?['username'] ?? '';
final rawAvatarUrl = _userData?['avatar_url']?.toString();
final avatarUrl = rawAvatarUrl != null && rawAvatarUrl.startsWith('/')
? '${AppConstants.baseUrl}$rawAvatarUrl'
: rawAvatarUrl;
return ListView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.only(top: 44, bottom: 24),
children: [
Center(
child: Container(
width: 110,
height: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.primaryContainer.withOpacity(0.5),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
image: (avatarUrl != null && _userData?['show_avatar'] == true)
? DecorationImage(
image: NetworkImage(avatarUrl),
fit: BoxFit.cover,
)
: null,
),
child: (avatarUrl == null || _userData?['show_avatar'] != true)
? Center(
child: Text(
combinedName.isNotEmpty
? combinedName[0].toUpperCase()
: '?',
style: TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: colorScheme.primary,
),
),
)
: null,
),
),
const SizedBox(height: 20),
Center(
child: InkWell(
onTap: () => _editUserName(displayFN, displayLN),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
combinedName.isNotEmpty ? combinedName : 'Без имени',
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 6),
Icon(
Icons.edit_rounded,
size: 16,
color: colorScheme.outline,
),
],
),
),
),
),
if (username.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
'@$username',
style: TextStyle(
color: colorScheme.primary,
fontSize: 15,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 6),
_buildOnlineStatus(),
const SizedBox(height: 24),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Container(
decoration: BoxDecoration(
color: colorScheme.surfaceVariant.withOpacity(0.2),
borderRadius: BorderRadius.circular(24),
border: Border.all(
color: colorScheme.outlineVariant.withOpacity(0.1),
),
),
child: Column(
children: [
_buildInfoRow(
Icons.fingerprint_rounded,
_userData!['id'].toString(),
'ID пользователя',
true,
),
if (_userData!['about'] != null &&
_userData!['about'].toString().isNotEmpty)
_buildInfoRow(
Icons.info_outline_rounded,
_userData!['about'],
'О себе',
true,
),
if (_userData!['phone'] != null &&
_userData!['phone'].toString().isNotEmpty)
_buildInfoRow(
Icons.phone_android_rounded,
_userData!['phone'],
'Номер телефона',
true,
),
if (_userData!['email'] != null &&
_userData!['email'].toString().isNotEmpty)
_buildInfoRow(
Icons.mail_outline_rounded,
_userData!['email'],
'Электронная почта',
true,
),
_buildInfoRow(
Icons.key_rounded,
_userData!['public_key'] ?? 'Отсутствует',
'Публичный E2EE ключ',
false,
maxLines: 2,
),
],
),
),
),
],
);
}
Widget _buildOnlineStatus() {
if (_userData?['online'] == true) {
return const Text(
'В сети',
style: TextStyle(
color: Colors.green,
fontSize: 13,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
);
}
// Получаем строку последнего онлайна из данных сервера
final String? lastSeenStr = _userData?['last_online']?.toString();
return Text(
_formatLastSeen(lastSeenStr),
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: 13,
),
textAlign: TextAlign.center,
);
}
Widget _buildInfoRow(
IconData icon,
String value,
String label,
bool showDivider, {
int maxLines = 1,
}) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 2,
),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: colorScheme.primary.withOpacity(0.06),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: colorScheme.primary, size: 18),
),
title: Text(
value,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
label,
style: TextStyle(fontSize: 12, color: colorScheme.outline),
),
),
if (showDivider)
Divider(
height: 1,
indent: 68,
color: colorScheme.outlineVariant.withOpacity(0.15),
),
],
);
}
Future<void> _editUserName(String firstname, String lastname) async {
final firstnameController = TextEditingController(text: firstname);
final lastnameController = TextEditingController(text: lastname);
final result = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text('Задать локальное имя'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: firstnameController,
decoration: const InputDecoration(labelText: 'Имя'),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 8),
TextField(
controller: lastnameController,
decoration: const InputDecoration(labelText: 'Фамилия'),
textCapitalization: TextCapitalization.words,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Сбросить'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Сохранить'),
),
],
),
);
final prefs = await SharedPreferences.getInstance();
if (result == true) {
await prefs.setString(
'firstname_${widget.userId}',
firstnameController.text.trim(),
);
await prefs.setString(
'lastname_${widget.userId}',
lastnameController.text.trim(),
);
} else if (result == false) {
await prefs.remove('firstname_${widget.userId}');
await prefs.remove('lastname_${widget.userId}');
}
_loadUserData();
}
void _handleIncomingMessage(Map<String, dynamic> data) {
if (data['type'] == 'user_online' && data['user_id'] == widget.userId) {
if (mounted) setState(() => _userData?['online'] = true);
}
if (data['type'] == 'user_offline' && data['user_id'] == widget.userId) {
if (mounted) {
setState(() {
_userData?['online'] = false;
_userData?['last_online'] = DateTime.now().toIso8601String();
});
}
}
if (data['type'] == 'user_updated' && data['user_id'] == widget.userId) {
_loadUserData();
}
}
}