411 lines
13 KiB
Dart
411 lines
13 KiB
Dart
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';
|
||
|
||
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)
|
||
? NetworkImage(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(),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|