добавлены анимации на экране сообщений, добавлено редактирование профиля (локально), изменена панель сообщений

добавлен баг с незагрузкой аватарок в чатах
This commit is contained in:
needle10
2025-11-27 20:06:11 +03:00
parent ad943e0936
commit 9745370613
26 changed files with 3782 additions and 1008 deletions

View File

@@ -0,0 +1,243 @@
import 'dart:async';
import 'dart:io';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:convert';
class ContactLocalNamesService {
static final ContactLocalNamesService _instance =
ContactLocalNamesService._internal();
factory ContactLocalNamesService() => _instance;
ContactLocalNamesService._internal();
final Map<int, Map<String, dynamic>> _cache = {};
final _changesController = StreamController<int>.broadcast();
Stream<int> get changes => _changesController.stream;
bool _initialized = false;
Future<void> initialize() async {
if (_initialized) return;
try {
final prefs = await SharedPreferences.getInstance();
final keys = prefs.getKeys();
for (final key in keys) {
if (key.startsWith('contact_')) {
final contactIdStr = key.replaceFirst('contact_', '');
final contactId = int.tryParse(contactIdStr);
if (contactId != null) {
final data = prefs.getString(key);
if (data != null) {
try {
final decoded = jsonDecode(data) as Map<String, dynamic>;
final avatarPath = decoded['avatarPath'] as String?;
if (avatarPath != null) {
final file = File(avatarPath);
if (!await file.exists()) {
decoded.remove('avatarPath');
}
}
_cache[contactId] = decoded;
} catch (e) {
print(
'Ошибка парсинга локальных данных для контакта $contactId: $e',
);
}
}
}
}
}
_initialized = true;
print(
'✅ ContactLocalNamesService: загружено ${_cache.length} локальных имен',
);
} catch (e) {
print('❌ Ошибка инициализации ContactLocalNamesService: $e');
}
}
Map<String, dynamic>? getContactData(int contactId) {
return _cache[contactId];
}
String getDisplayName({
required int contactId,
String? originalName,
String? originalFirstName,
String? originalLastName,
}) {
final localData = _cache[contactId];
if (localData != null) {
final firstName = localData['firstName'] as String?;
final lastName = localData['lastName'] as String?;
if (firstName != null && firstName.isNotEmpty ||
lastName != null && lastName.isNotEmpty) {
final fullName = '${firstName ?? ''} ${lastName ?? ''}'.trim();
if (fullName.isNotEmpty) {
return fullName;
}
}
}
if (originalFirstName != null || originalLastName != null) {
final fullName = '${originalFirstName ?? ''} ${originalLastName ?? ''}'
.trim();
if (fullName.isNotEmpty) {
return fullName;
}
}
return originalName ?? 'ID $contactId';
}
String? getDisplayDescription({
required int contactId,
String? originalDescription,
}) {
final localData = _cache[contactId];
if (localData != null) {
final notes = localData['notes'] as String?;
if (notes != null && notes.isNotEmpty) {
return notes;
}
}
return originalDescription;
}
Future<String?> saveContactAvatar(File imageFile, int contactId) async {
try {
final directory = await getApplicationDocumentsDirectory();
final avatarDir = Directory('${directory.path}/contact_avatars');
if (!await avatarDir.exists()) {
await avatarDir.create(recursive: true);
}
final fileName = 'contact_$contactId.jpg';
final savePath = '${avatarDir.path}/$fileName';
await imageFile.copy(savePath);
final localData = _cache[contactId] ?? {};
localData['avatarPath'] = savePath;
_cache[contactId] = localData;
final prefs = await SharedPreferences.getInstance();
final key = 'contact_$contactId';
await prefs.setString(key, jsonEncode(localData));
_changesController.add(contactId);
print('✅ Локальный аватар контакта сохранен: $savePath');
return savePath;
} catch (e) {
print('❌ Ошибка сохранения локального аватара контакта: $e');
return null;
}
}
String? getContactAvatarPath(int contactId) {
final localData = _cache[contactId];
if (localData != null) {
return localData['avatarPath'] as String?;
}
return null;
}
String? getDisplayAvatar({
required int contactId,
String? originalAvatarUrl,
}) {
final localAvatarPath = getContactAvatarPath(contactId);
if (localAvatarPath != null) {
final file = File(localAvatarPath);
if (file.existsSync()) {
return 'file://$localAvatarPath';
} else {
final localData = _cache[contactId];
if (localData != null) {
localData.remove('avatarPath');
_cache[contactId] = localData;
}
}
}
return originalAvatarUrl;
}
Future<void> removeContactAvatar(int contactId) async {
try {
final localData = _cache[contactId];
if (localData != null) {
final avatarPath = localData['avatarPath'] as String?;
if (avatarPath != null) {
final file = File(avatarPath);
if (await file.exists()) {
await file.delete();
}
}
localData.remove('avatarPath');
_cache[contactId] = localData;
final prefs = await SharedPreferences.getInstance();
final key = 'contact_$contactId';
await prefs.setString(key, jsonEncode(localData));
_changesController.add(contactId);
print('✅ Локальный аватар контакта удален');
}
} catch (e) {
print('❌ Ошибка удаления локального аватара контакта: $e');
}
}
Future<void> saveContactData(int contactId, Map<String, dynamic> data) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = 'contact_$contactId';
await prefs.setString(key, jsonEncode(data));
_cache[contactId] = data;
_changesController.add(contactId);
print('✅ Сохранены локальные данные для контакта $contactId');
} catch (e) {
print('❌ Ошибка сохранения локальных данных контакта: $e');
}
}
Future<void> clearContactData(int contactId) async {
try {
final prefs = await SharedPreferences.getInstance();
final key = 'contact_$contactId';
await prefs.remove(key);
_cache.remove(contactId);
_changesController.add(contactId);
print('✅ Очищены локальные данные для контакта $contactId');
} catch (e) {
print('❌ Ошибка очистки локальных данных контакта: $e');
}
}
void clearCache() {
_cache.clear();
}
void dispose() {
_changesController.close();
}
}

View File

@@ -0,0 +1,57 @@
import 'package:gwid/models/profile.dart';
import 'package:gwid/services/profile_cache_service.dart';
class LocalProfileManager {
static final LocalProfileManager _instance = LocalProfileManager._internal();
factory LocalProfileManager() => _instance;
LocalProfileManager._internal();
final ProfileCacheService _profileCache = ProfileCacheService();
bool _initialized = false;
Future<void> initialize() async {
if (_initialized) return;
await _profileCache.initialize();
_initialized = true;
}
Future<Profile?> getActualProfile(Profile? serverProfile) async {
await initialize();
final localAvatarPath = await _profileCache.getLocalAvatarPath();
final mergedProfile = await _profileCache.getMergedProfile(serverProfile);
if (mergedProfile != null && localAvatarPath != null) {
return Profile(
id: mergedProfile.id,
phone: mergedProfile.phone,
firstName: mergedProfile.firstName,
lastName: mergedProfile.lastName,
description: mergedProfile.description,
photoBaseUrl: 'file://$localAvatarPath',
photoId: mergedProfile.photoId,
updateTime: mergedProfile.updateTime,
options: mergedProfile.options,
accountStatus: mergedProfile.accountStatus,
profileOptions: mergedProfile.profileOptions,
);
}
return mergedProfile;
}
Future<String?> getLocalAvatarPath() async {
await initialize();
return await _profileCache.getLocalAvatarPath();
}
Future<bool> hasLocalChanges() async {
await initialize();
return await _profileCache.hasLocalChanges();
}
Future<void> clearLocalChanges() async {
await initialize();
await _profileCache.clearProfileCache();
}
}

View File

@@ -0,0 +1,241 @@
import 'dart:io';
import 'package:gwid/services/cache_service.dart';
import 'package:gwid/models/profile.dart';
import 'package:path_provider/path_provider.dart';
class ProfileCacheService {
static final ProfileCacheService _instance = ProfileCacheService._internal();
factory ProfileCacheService() => _instance;
ProfileCacheService._internal();
final CacheService _cacheService = CacheService();
static const String _profileKey = 'my_profile_data';
static const String _profileAvatarKey = 'my_profile_avatar';
static const Duration _profileTTL = Duration(days: 30);
bool _initialized = false;
Future<void> initialize() async {
if (_initialized) return;
await _cacheService.initialize();
_initialized = true;
print('✅ ProfileCacheService инициализирован');
}
Future<void> saveProfileData({
required int userId,
required String firstName,
required String lastName,
String? description,
String? photoBaseUrl,
int? photoId,
}) async {
try {
final profileData = {
'userId': userId,
'firstName': firstName,
'lastName': lastName,
'description': description,
'photoBaseUrl': photoBaseUrl,
'photoId': photoId,
'updatedAt': DateTime.now().toIso8601String(),
};
await _cacheService.set(_profileKey, profileData, ttl: _profileTTL);
print('✅ Данные профиля сохранены в кэш: $firstName $lastName');
} catch (e) {
print('❌ Ошибка сохранения профиля в кэш: $e');
}
}
Future<Map<String, dynamic>?> getProfileData() async {
try {
final cached = await _cacheService.get<Map<String, dynamic>>(
_profileKey,
ttl: _profileTTL,
);
if (cached != null) {
print('✅ Данные профиля загружены из кэша');
return cached;
}
} catch (e) {
print('❌ Ошибка загрузки профиля из кэша: $e');
}
return null;
}
Future<String?> saveAvatar(File imageFile, int userId) async {
try {
final directory = await getApplicationDocumentsDirectory();
final avatarDir = Directory('${directory.path}/avatars');
if (!await avatarDir.exists()) {
await avatarDir.create(recursive: true);
}
final fileName = 'profile_$userId.jpg';
final savePath = '${avatarDir.path}/$fileName';
await imageFile.copy(savePath);
await _cacheService.set(_profileAvatarKey, savePath, ttl: _profileTTL);
print('✅ Аватар сохранен локально: $savePath');
return savePath;
} catch (e) {
print('❌ Ошибка сохранения аватара: $e');
return null;
}
}
Future<String?> getLocalAvatarPath() async {
try {
final path = await _cacheService.get<String>(
_profileAvatarKey,
ttl: _profileTTL,
);
if (path != null) {
final file = File(path);
if (await file.exists()) {
print('✅ Локальный аватар найден: $path');
return path;
} else {
await _cacheService.remove(_profileAvatarKey);
}
}
} catch (e) {
print('❌ Ошибка загрузки локального аватара: $e');
}
return null;
}
Future<void> updateProfileFields({
String? firstName,
String? lastName,
String? description,
String? photoBaseUrl,
}) async {
try {
final currentData = await getProfileData();
if (currentData == null) {
print('⚠️ Нет сохраненных данных профиля для обновления');
return;
}
if (firstName != null) currentData['firstName'] = firstName;
if (lastName != null) currentData['lastName'] = lastName;
if (description != null) currentData['description'] = description;
if (photoBaseUrl != null) currentData['photoBaseUrl'] = photoBaseUrl;
currentData['updatedAt'] = DateTime.now().toIso8601String();
await _cacheService.set(_profileKey, currentData, ttl: _profileTTL);
print('✅ Поля профиля обновлены в кэше');
} catch (e) {
print('❌ Ошибка обновления полей профиля: $e');
}
}
Future<void> clearProfileCache() async {
try {
await _cacheService.remove(_profileKey);
await _cacheService.remove(_profileAvatarKey);
final avatarPath = await getLocalAvatarPath();
if (avatarPath != null) {
final file = File(avatarPath);
if (await file.exists()) {
await file.delete();
}
}
print('✅ Кэш профиля очищен');
} catch (e) {
print('❌ Ошибка очистки кэша профиля: $e');
}
}
Future<void> syncWithServerProfile(Profile serverProfile) async {
try {
final cachedData = await getProfileData();
if (cachedData != null) {
print(
'⚠️ Локальные данные профиля уже существуют, пропускаем синхронизацию',
);
return;
}
await saveProfileData(
userId: serverProfile.id,
firstName: serverProfile.firstName,
lastName: serverProfile.lastName,
description: serverProfile.description,
photoBaseUrl: serverProfile.photoBaseUrl,
photoId: serverProfile.photoId,
);
print('✅ Профиль инициализирован с сервера');
} catch (e) {
print('❌ Ошибка синхронизации профиля: $e');
}
}
Future<Profile?> getMergedProfile(Profile? serverProfile) async {
try {
final cachedData = await getProfileData();
if (cachedData == null && serverProfile == null) {
return null;
}
if (cachedData == null && serverProfile != null) {
return serverProfile;
}
if (cachedData != null && serverProfile == null) {
return Profile(
id: cachedData['userId'] ?? 0,
phone: '',
firstName: cachedData['firstName'] ?? '',
lastName: cachedData['lastName'] ?? '',
description: cachedData['description'],
photoBaseUrl: cachedData['photoBaseUrl'],
photoId: cachedData['photoId'] ?? 0,
updateTime: 0,
options: [],
accountStatus: 0,
profileOptions: [],
);
}
return Profile(
id: serverProfile!.id,
phone: serverProfile.phone,
firstName: cachedData!['firstName'] ?? serverProfile.firstName,
lastName: cachedData['lastName'] ?? serverProfile.lastName,
description: cachedData['description'] ?? serverProfile.description,
photoBaseUrl: cachedData['photoBaseUrl'] ?? serverProfile.photoBaseUrl,
photoId: cachedData['photoId'] ?? serverProfile.photoId,
updateTime: serverProfile.updateTime,
options: serverProfile.options,
accountStatus: serverProfile.accountStatus,
profileOptions: serverProfile.profileOptions,
);
} catch (e) {
print('❌ Ошибка получения объединенного профиля: $e');
return serverProfile;
}
}
Future<bool> hasLocalChanges() async {
try {
final cachedData = await getProfileData();
return cachedData != null;
} catch (e) {
return false;
}
}
}