Chepuhagram/lib/presentation/screens/user_profile_screen.dart

412 lines
13 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 'package:cached_network_image/cached_network_image.dart';
class UserProfileScreen extends StatefulWidget {
final int userId;
final String username;
final String name;
const UserProfileScreen({
super.key,
required this.userId,
required this.username,
required this.name,
});
@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.containsKey('firstname_${widget.userId}')
? prefs.getString('firstname_${widget.userId}')
: null;
lastName = prefs.containsKey('lastname_${widget.userId}')
? prefs.getString('lastname_${widget.userId}')
: null;
if (mounted) {
setState(() {
_userData = data;
_isLoading = false;
});
}
} catch (e) {
if (mounted) {
setState(() {
_error = e.toString().replaceAll('Exception: ', '');
_isLoading = false;
});
}
}
}
@override
void dispose() {
_onlineTimer?.cancel();
_socketSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Информация о пользователе')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadUserData,
child: const Text('Повторить'),
),
],
),
)
: _buildUserInfo(),
);
}
Widget _buildUserInfo() {
if (_userData == null) return const SizedBox.shrink();
final String displayFN = firstName ?? _userData?['first_name'] ?? '';
final String displayLN = lastName ?? _userData?['last_name'] ?? '';
final String username = _userData?['username'] ?? '';
final rawAvatarUrl = _userData?['avatar_url']?.toString();
final avatarUrl = rawAvatarUrl != null && rawAvatarUrl.startsWith('/')
? '${AppConstants.baseUrl}$rawAvatarUrl'
: rawAvatarUrl;
return ListView(
padding: const EdgeInsets.all(16),
children: [
// Avatar placeholder
Center(
child: CircleAvatar(
radius: 50,
backgroundColor: Theme.of(context).primaryColor.withOpacity(0.1),
backgroundImage:
(avatarUrl != null && _userData?['show_avatar'] == true)
? CachedNetworkImageProvider(avatarUrl)
: null,
child: (avatarUrl == null || _userData?['show_avatar'] != true)
? Text(
(displayFN.isNotEmpty && displayLN.isNotEmpty)
? '${displayFN[0]}${displayLN[0]}'.toUpperCase()
: (displayFN.isNotEmpty)
? displayFN[0].toUpperCase()
: (username.isNotEmpty)
? username[0].toUpperCase()
: '?',
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
),
)
: null,
),
),
const SizedBox(height: 24),
// Name
GestureDetector(
onTap: () => {_editUserName(displayFN, displayLN)},
child: Row(
children: [
const Spacer(),
if ((displayFN.isNotEmpty) || (displayLN.isNotEmpty))
Text(
'$displayFN $displayLN'.trim(),
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(width: 5),
Icon(Icons.edit, color: Theme.of(context).colorScheme.onSurface),
const Spacer(),
],
),
),
const SizedBox(height: 8),
// Username
if (_userData!['username'] != null && _userData!['username'].isNotEmpty)
Text(
'@${_userData!['username']}',
style: Theme.of(
context,
).textTheme.bodyLarge?.copyWith(color: Colors.grey[600]),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
// Last online status
if (_userData!['online'] == true)
const Text(
'Онлайн',
style: TextStyle(fontSize: 12, color: Colors.greenAccent),
textAlign: TextAlign.center,
)
else if (DateTime.tryParse(_userData!['last_online']) != null)
Text(
'Был(а) в сети ${_formatLastOnline(DateTime.tryParse(_userData!['last_online'])!.add(offset != null ? offset! : Duration.zero))}',
style: const TextStyle(
fontSize: 12,
color: Color.fromARGB(255, 161, 161, 161),
),
textAlign: TextAlign.center,
)
else
const Text(
'Был(а) недавно',
style: TextStyle(
fontSize: 12,
color: Color.fromARGB(255, 161, 161, 161),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// User ID
_buildInfoTile('ID пользователя', _userData!['id'].toString()),
// Public Key (if available)
if (_userData!['public_key'] != null)
_buildInfoTile(
'Публичный ключ',
_userData!['public_key'],
maxLines: 3,
),
// About
if (_userData!['about'] != null && _userData!['about'].isNotEmpty)
_buildInfoTile('О себе', _userData!['about'], maxLines: 5),
// Phone
if (_userData!['phone'] != null && _userData!['phone'].isNotEmpty)
_buildInfoTile('Телефон', _userData!['phone']),
// Email
if (_userData!['email'] != null && _userData!['email'].isNotEmpty)
_buildInfoTile('Почта', _userData!['email']),
const SizedBox(height: 16),
if ((_userData!['username'] == null ||
_userData!['username'].isEmpty) &&
(_userData!['first_name'] == null ||
_userData!['first_name'].isEmpty) &&
(_userData!['last_name'] == null ||
_userData!['last_name'].isEmpty) &&
(_userData!['about'] == null || _userData!['about'].isEmpty) &&
(_userData!['phone'] == null || _userData!['phone'].isEmpty) &&
(_userData!['email'] == null || _userData!['email'].isEmpty))
const Text(
'Пользователь скрыл дополнительную информацию',
style: TextStyle(color: Colors.grey),
textAlign: TextAlign.center,
),
],
);
}
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(
title: const Text('Изменить имя пользователя'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: firstnameController,
minLines: 1,
maxLines: 5,
autofocus: true,
decoration: const InputDecoration(hintText: 'Имя'),
textCapitalization: TextCapitalization.words,
),
const SizedBox(height: 8),
TextField(
controller: lastnameController,
minLines: 1,
maxLines: 5,
decoration: const InputDecoration(hintText: 'Фамилия'),
textCapitalization: TextCapitalization.words,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('Сбрость'),
),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
child: const Text('Сохранить'),
),
],
),
);
final prefs = await SharedPreferences.getInstance();
if (result == true) {
if (firstname != firstnameController.text) {
prefs.setString('firstname_${widget.userId}', firstnameController.text);
}
if (lastname != lastnameController.text) {
prefs.setString('lastname_${widget.userId}', lastnameController.text);
}
if (mounted) {
setState(() {});
}
_loadUserData();
} else {
prefs.remove('firstname_${widget.userId}');
prefs.remove('lastname_${widget.userId}');
if (mounted) {
setState(() {});
}
_loadUserData();
}
}
void _handleIncomingMessage(Map<String, dynamic> data) async {
if (data['type'] == 'user_online') {
final userId = int.tryParse(data['user_id']?.toString() ?? '');
if (userId == widget.userId) {
if (mounted) {
setState(() {
_userData = _userData?..['online'] = true;
});
}
}
}
if (data['type'] == 'user_offline') {
final userId = int.tryParse(data['user_id']?.toString() ?? '');
if (userId == widget.userId) {
setState(() {
_userData = _userData?..['online'] = false;
_userData = _userData
?..['last_online'] = DateTime.now().toIso8601String();
});
}
}
if (data['type'] == 'user_updated') {
print('User updated message received, refreshing contact list');
final userId = int.tryParse(data['user_id']?.toString() ?? '');
if (userId != null && userId == widget.userId) {
_loadUserData();
}
}
}
String _formatLastOnline(DateTime lastOnline) {
final now = DateTime.now();
final difference = now.difference(lastOnline);
if (difference.inSeconds < 60) {
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 {
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;
}
}
Widget _buildInfoTile(String label, String value, {int maxLines = 1}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(fontSize: 16),
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
),
const Divider(),
],
),
);
}
}