добавлены анимации на экране сообщений, добавлено редактирование профиля (локально), изменена панель сообщений
добавлен баг с незагрузкой аватарок в чатах
This commit is contained in:
243
lib/services/contact_local_names_service.dart
Normal file
243
lib/services/contact_local_names_service.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
57
lib/services/local_profile_manager.dart
Normal file
57
lib/services/local_profile_manager.dart
Normal 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();
|
||||
}
|
||||
}
|
||||
241
lib/services/profile_cache_service.dart
Normal file
241
lib/services/profile_cache_service.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user