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'; import 'package:cached_network_image/cached_network_image.dart'; 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 createState() => _UserProfileScreenState(); } class _UserProfileScreenState extends State { Map? _userData; StreamSubscription? _socketSubscription; bool _isLoading = true; String? _error; Duration? offset; Timer? _onlineTimer; String? firstName; String? lastName; bool _isAvatarExpanded = false; @override void initState() { super.initState(); _loadUserData(); startOnlineUpdates(); DateTime now = DateTime.now(); offset = now.timeZoneOffset; final socketService = Provider.of(context, listen: false); _socketSubscription = socketService.messages.listen(_handleIncomingMessage); } void startOnlineUpdates() { _onlineTimer = Timer.periodic(const Duration(minutes: 1), (_) { _loadUserData(); }); } Future _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: [ LayoutBuilder( builder: (context, constraints) { // Получаем доступную ширину именно этого экрана/панели final panelWidth = constraints.maxWidth; return Center( child: GestureDetector( onTap: avatarUrl != null && _userData?['show_avatar'] == true ? () => setState(() => _isAvatarExpanded = !_isAvatarExpanded) : null, child: AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, width: _isAvatarExpanded ? panelWidth : 110, height: _isAvatarExpanded ? panelWidth : 110, decoration: BoxDecoration( borderRadius: _isAvatarExpanded ? BorderRadius.zero : BorderRadius.circular(55), color: colorScheme.primaryContainer.withOpacity(0.5), boxShadow: _isAvatarExpanded ? [] : [ BoxShadow( color: Colors.black.withOpacity(0.08), blurRadius: 16, offset: const Offset(0, 8), ), ], image: (avatarUrl != null && _userData?['show_avatar'] == true) ? DecorationImage( image: CachedNetworkImageProvider(avatarUrl), fit: BoxFit.cover, ) : null, ), child: (avatarUrl == null || _userData?['show_avatar'] != true) ? Center( child: Text( combinedName.isNotEmpty ? combinedName[0].toUpperCase() : '?', style: TextStyle( fontSize: _isAvatarExpanded ? 72 : 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( _userData!['id'] == 0 ? 'Системные уведомления' : _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 _editUserName(String firstname, String lastname) async { final firstnameController = TextEditingController(text: firstname); final lastnameController = TextEditingController(text: lastname); final result = await showDialog( 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 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(); } } }