Initial Commit
This commit is contained in:
339
lib/services/avatar_cache_service.dart
Normal file
339
lib/services/avatar_cache_service.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:gwid/services/cache_service.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class AvatarCacheService {
|
||||
static final AvatarCacheService _instance = AvatarCacheService._internal();
|
||||
factory AvatarCacheService() => _instance;
|
||||
AvatarCacheService._internal();
|
||||
|
||||
final CacheService _cacheService = CacheService();
|
||||
|
||||
|
||||
final Map<String, Uint8List> _imageMemoryCache = {};
|
||||
final Map<String, DateTime> _imageCacheTimestamps = {};
|
||||
|
||||
|
||||
static const Duration _imageTTL = Duration(days: 7);
|
||||
static const int _maxMemoryImages = 50;
|
||||
static const int _maxImageSizeMB = 5;
|
||||
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _cacheService.initialize();
|
||||
print('AvatarCacheService инициализирован');
|
||||
}
|
||||
|
||||
|
||||
Future<ImageProvider?> getAvatar(String? avatarUrl, {int? userId}) async {
|
||||
if (avatarUrl == null || avatarUrl.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
final cacheKey = _generateCacheKey(avatarUrl, userId);
|
||||
|
||||
|
||||
if (_imageMemoryCache.containsKey(cacheKey)) {
|
||||
final timestamp = _imageCacheTimestamps[cacheKey];
|
||||
if (timestamp != null && !_isExpired(timestamp, _imageTTL)) {
|
||||
final imageData = _imageMemoryCache[cacheKey]!;
|
||||
return MemoryImage(imageData);
|
||||
} else {
|
||||
|
||||
_imageMemoryCache.remove(cacheKey);
|
||||
_imageCacheTimestamps.remove(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
final cachedFile = await _cacheService.getCachedFile(
|
||||
avatarUrl,
|
||||
customKey: cacheKey,
|
||||
);
|
||||
if (cachedFile != null && await cachedFile.exists()) {
|
||||
final imageData = await cachedFile.readAsBytes();
|
||||
|
||||
|
||||
_imageMemoryCache[cacheKey] = imageData;
|
||||
_imageCacheTimestamps[cacheKey] = DateTime.now();
|
||||
|
||||
|
||||
if (_imageMemoryCache.length > _maxMemoryImages) {
|
||||
await _evictOldestImages();
|
||||
}
|
||||
|
||||
return MemoryImage(imageData);
|
||||
}
|
||||
|
||||
|
||||
final imageData = await _downloadImage(avatarUrl);
|
||||
if (imageData != null) {
|
||||
|
||||
await _cacheService.cacheFile(avatarUrl, customKey: cacheKey);
|
||||
|
||||
|
||||
_imageMemoryCache[cacheKey] = imageData;
|
||||
_imageCacheTimestamps[cacheKey] = DateTime.now();
|
||||
|
||||
return MemoryImage(imageData);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения аватарки: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<File?> getAvatarFile(String? avatarUrl, {int? userId}) async {
|
||||
if (avatarUrl == null || avatarUrl.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final cacheKey = _generateCacheKey(avatarUrl, userId);
|
||||
return await _cacheService.getCachedFile(avatarUrl, customKey: cacheKey);
|
||||
} catch (e) {
|
||||
print('Ошибка получения файла аватарки: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> preloadAvatars(List<String> avatarUrls) async {
|
||||
final futures = avatarUrls.map((url) => getAvatar(url));
|
||||
await Future.wait(futures);
|
||||
print('Предзагружено ${avatarUrls.length} аватарок');
|
||||
}
|
||||
|
||||
|
||||
Future<Uint8List?> _downloadImage(String url) async {
|
||||
try {
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
final imageData = response.bodyBytes;
|
||||
|
||||
|
||||
if (imageData.length > _maxImageSizeMB * 1024 * 1024) {
|
||||
print('Изображение слишком большое: ${imageData.length} байт');
|
||||
return null;
|
||||
}
|
||||
|
||||
return imageData;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка загрузки изображения $url: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String _generateCacheKey(String url, int? userId) {
|
||||
if (userId != null) {
|
||||
return 'avatar_${userId}_${_hashUrl(url)}';
|
||||
}
|
||||
return 'avatar_${_hashUrl(url)}';
|
||||
}
|
||||
|
||||
|
||||
String _hashUrl(String url) {
|
||||
final bytes = utf8.encode(url);
|
||||
final digest = sha256.convert(bytes);
|
||||
return digest.toString().substring(0, 16);
|
||||
}
|
||||
|
||||
|
||||
bool _isExpired(DateTime timestamp, Duration ttl) {
|
||||
return DateTime.now().difference(timestamp) > ttl;
|
||||
}
|
||||
|
||||
|
||||
Future<void> _evictOldestImages() async {
|
||||
if (_imageMemoryCache.isEmpty) return;
|
||||
|
||||
|
||||
final sortedEntries = _imageCacheTimestamps.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
final toRemove = (sortedEntries.length * 0.2).ceil();
|
||||
for (int i = 0; i < toRemove && i < sortedEntries.length; i++) {
|
||||
final key = sortedEntries[i].key;
|
||||
_imageMemoryCache.remove(key);
|
||||
_imageCacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> clearAvatarCache() async {
|
||||
_imageMemoryCache.clear();
|
||||
_imageCacheTimestamps.clear();
|
||||
|
||||
|
||||
try {
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
final avatarDir = Directory('${cacheDir.path}/avatars');
|
||||
if (await avatarDir.exists()) {
|
||||
await avatarDir.delete(recursive: true);
|
||||
await avatarDir.create(recursive: true);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки кэша аватарок: $e');
|
||||
}
|
||||
|
||||
print('Кэш аватарок очищен');
|
||||
}
|
||||
|
||||
|
||||
Future<void> removeAvatarFromCache(String avatarUrl, {int? userId}) async {
|
||||
try {
|
||||
final cacheKey = _generateCacheKey(avatarUrl, userId);
|
||||
|
||||
|
||||
_imageMemoryCache.remove(cacheKey);
|
||||
_imageCacheTimestamps.remove(cacheKey);
|
||||
|
||||
|
||||
await _cacheService.removeCachedFile(avatarUrl, customKey: cacheKey);
|
||||
} catch (e) {
|
||||
print('Ошибка удаления аватарки из кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getAvatarCacheStats() async {
|
||||
try {
|
||||
final memoryImages = _imageMemoryCache.length;
|
||||
int totalMemorySize = 0;
|
||||
|
||||
for (final imageData in _imageMemoryCache.values) {
|
||||
totalMemorySize += imageData.length;
|
||||
}
|
||||
|
||||
|
||||
int diskSize = 0;
|
||||
try {
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
final avatarDir = Directory('${cacheDir.path}/avatars');
|
||||
if (await avatarDir.exists()) {
|
||||
await for (final entity in avatarDir.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
diskSize += await entity.length();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка подсчета размера файлового кэша: $e');
|
||||
}
|
||||
|
||||
return {
|
||||
'memoryImages': memoryImages,
|
||||
'memorySizeMB': (totalMemorySize / (1024 * 1024)).toStringAsFixed(2),
|
||||
'diskSizeMB': (diskSize / (1024 * 1024)).toStringAsFixed(2),
|
||||
'maxMemoryImages': _maxMemoryImages,
|
||||
'maxImageSizeMB': _maxImageSizeMB,
|
||||
};
|
||||
} catch (e) {
|
||||
print('Ошибка получения статистики кэша аватарок: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<bool> hasAvatarInCache(String avatarUrl, {int? userId}) async {
|
||||
try {
|
||||
final cacheKey = _generateCacheKey(avatarUrl, userId);
|
||||
|
||||
|
||||
if (_imageMemoryCache.containsKey(cacheKey)) {
|
||||
final timestamp = _imageCacheTimestamps[cacheKey];
|
||||
if (timestamp != null && !_isExpired(timestamp, _imageTTL)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return await _cacheService.hasCachedFile(avatarUrl, customKey: cacheKey);
|
||||
} catch (e) {
|
||||
print('Ошибка проверки существования аватарки в кэше: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Widget getAvatarWidget(
|
||||
String? avatarUrl, {
|
||||
int? userId,
|
||||
double size = 40,
|
||||
String? fallbackText,
|
||||
Color? backgroundColor,
|
||||
Color? textColor,
|
||||
}) {
|
||||
if (avatarUrl == null || avatarUrl.isEmpty) {
|
||||
return _buildFallbackAvatar(
|
||||
fallbackText,
|
||||
size,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
);
|
||||
}
|
||||
|
||||
return FutureBuilder<ImageProvider?>(
|
||||
future: getAvatar(avatarUrl, userId: userId),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData && snapshot.data != null) {
|
||||
return CircleAvatar(
|
||||
radius: size / 2,
|
||||
backgroundImage: snapshot.data,
|
||||
backgroundColor: backgroundColor,
|
||||
);
|
||||
} else {
|
||||
return _buildFallbackAvatar(
|
||||
fallbackText,
|
||||
size,
|
||||
backgroundColor,
|
||||
textColor,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildFallbackAvatar(
|
||||
String? text,
|
||||
double size,
|
||||
Color? backgroundColor,
|
||||
Color? textColor,
|
||||
) {
|
||||
return CircleAvatar(
|
||||
radius: size / 2,
|
||||
backgroundColor: backgroundColor ?? Colors.grey[300],
|
||||
child: text != null && text.isNotEmpty
|
||||
? Text(
|
||||
text[0].toUpperCase(),
|
||||
style: TextStyle(
|
||||
color: textColor ?? Colors.white,
|
||||
fontSize: size * 0.4,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.person,
|
||||
size: size * 0.6,
|
||||
color: textColor ?? Colors.white,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
332
lib/services/cache_service.dart
Normal file
332
lib/services/cache_service.dart
Normal file
@@ -0,0 +1,332 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
|
||||
class CacheService {
|
||||
static final CacheService _instance = CacheService._internal();
|
||||
factory CacheService() => _instance;
|
||||
CacheService._internal();
|
||||
|
||||
|
||||
final Map<String, dynamic> _memoryCache = {};
|
||||
final Map<String, DateTime> _cacheTimestamps = {};
|
||||
|
||||
|
||||
static const Duration _defaultTTL = Duration(hours: 24);
|
||||
static const int _maxMemoryCacheSize = 1000;
|
||||
|
||||
|
||||
SharedPreferences? _prefs;
|
||||
|
||||
|
||||
Directory? _cacheDirectory;
|
||||
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_cacheDirectory = await getApplicationCacheDirectory();
|
||||
|
||||
|
||||
await _createCacheDirectories();
|
||||
|
||||
print('CacheService инициализирован');
|
||||
}
|
||||
|
||||
|
||||
Future<void> _createCacheDirectories() async {
|
||||
if (_cacheDirectory == null) return;
|
||||
|
||||
final directories = ['avatars', 'images', 'files', 'chats', 'contacts'];
|
||||
|
||||
for (final dir in directories) {
|
||||
final directory = Directory('${_cacheDirectory!.path}/$dir');
|
||||
if (!await directory.exists()) {
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<T?> get<T>(String key, {Duration? ttl}) async {
|
||||
|
||||
if (_memoryCache.containsKey(key)) {
|
||||
final timestamp = _cacheTimestamps[key];
|
||||
if (timestamp != null && !_isExpired(timestamp, ttl ?? _defaultTTL)) {
|
||||
return _memoryCache[key] as T?;
|
||||
} else {
|
||||
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
final cacheKey = 'cache_$key';
|
||||
final cachedData = _prefs!.getString(cacheKey);
|
||||
|
||||
if (cachedData != null) {
|
||||
final Map<String, dynamic> data = jsonDecode(cachedData);
|
||||
final timestamp = DateTime.fromMillisecondsSinceEpoch(
|
||||
data['timestamp'],
|
||||
);
|
||||
final value = data['value'];
|
||||
|
||||
if (!_isExpired(timestamp, ttl ?? _defaultTTL)) {
|
||||
|
||||
_memoryCache[key] = value;
|
||||
_cacheTimestamps[key] = timestamp;
|
||||
return value as T?;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения данных из кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> set<T>(String key, T value, {Duration? ttl}) async {
|
||||
final timestamp = DateTime.now();
|
||||
|
||||
|
||||
_memoryCache[key] = value;
|
||||
_cacheTimestamps[key] = timestamp;
|
||||
|
||||
|
||||
if (_memoryCache.length > _maxMemoryCacheSize) {
|
||||
await _evictOldestMemoryCache();
|
||||
}
|
||||
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
final cacheKey = 'cache_$key';
|
||||
final data = {
|
||||
'value': value,
|
||||
'timestamp': timestamp.millisecondsSinceEpoch,
|
||||
'ttl': (ttl ?? _defaultTTL).inMilliseconds,
|
||||
};
|
||||
|
||||
await _prefs!.setString(cacheKey, jsonEncode(data));
|
||||
} catch (e) {
|
||||
print('Ошибка сохранения данных в кэш: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> remove(String key) async {
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
final cacheKey = 'cache_$key';
|
||||
await _prefs!.remove(cacheKey);
|
||||
} catch (e) {
|
||||
print('Ошибка удаления данных из кэша: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> clear() async {
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
|
||||
if (_prefs != null) {
|
||||
try {
|
||||
|
||||
final keys = _prefs!.getKeys().where((key) => key.startsWith('cache_'));
|
||||
for (final key in keys) {
|
||||
await _prefs!.remove(key);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (_cacheDirectory != null) {
|
||||
try {
|
||||
for (final dir in ['avatars', 'images', 'files', 'chats', 'contacts']) {
|
||||
final directory = Directory('${_cacheDirectory!.path}/$dir');
|
||||
if (await directory.exists()) {
|
||||
await directory.delete(recursive: true);
|
||||
await directory.create(recursive: true);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки файлового кэша: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
bool _isExpired(DateTime timestamp, Duration ttl) {
|
||||
return DateTime.now().difference(timestamp) > ttl;
|
||||
}
|
||||
|
||||
|
||||
Future<void> _evictOldestMemoryCache() async {
|
||||
if (_memoryCache.isEmpty) return;
|
||||
|
||||
|
||||
final sortedEntries = _cacheTimestamps.entries.toList()
|
||||
..sort((a, b) => a.value.compareTo(b.value));
|
||||
|
||||
final toRemove = (sortedEntries.length * 0.2).ceil();
|
||||
for (int i = 0; i < toRemove && i < sortedEntries.length; i++) {
|
||||
final key = sortedEntries[i].key;
|
||||
_memoryCache.remove(key);
|
||||
_cacheTimestamps.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, int>> getCacheSize() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
|
||||
|
||||
int filesSize = 0;
|
||||
if (_cacheDirectory != null) {
|
||||
try {
|
||||
await for (final entity in _cacheDirectory!.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
filesSize += await entity.length();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка подсчета размера файлового кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'memory': memorySize,
|
||||
'database': 0, // Нет SQLite базы данных
|
||||
'files': filesSize,
|
||||
'total': filesSize,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Future<String?> cacheFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) return null;
|
||||
|
||||
try {
|
||||
|
||||
final fileName = _generateFileName(url, customKey);
|
||||
final filePath = '${_cacheDirectory!.path}/images/$fileName';
|
||||
|
||||
|
||||
final existingFile = File(filePath);
|
||||
if (await existingFile.exists()) {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
|
||||
await existingFile.writeAsBytes(response.bodyBytes);
|
||||
return filePath;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования файла $url: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<File?> getCachedFile(String url, {String? customKey}) async {
|
||||
if (_cacheDirectory == null) return null;
|
||||
|
||||
try {
|
||||
final fileName = _generateFileName(url, customKey);
|
||||
final filePath = '${_cacheDirectory!.path}/images/$fileName';
|
||||
|
||||
final file = File(filePath);
|
||||
if (await file.exists()) {
|
||||
return file;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированного файла: $e');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String _generateFileName(String url, String? customKey) {
|
||||
final key = customKey ?? url;
|
||||
final hash = key.hashCode.abs().toString().substring(0, 16);
|
||||
final extension = _getFileExtension(url);
|
||||
return '$hash$extension';
|
||||
}
|
||||
|
||||
|
||||
String _getFileExtension(String url) {
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
final path = uri.path;
|
||||
final extension = path.substring(path.lastIndexOf('.'));
|
||||
return extension.isNotEmpty && extension.length < 10 ? extension : '.jpg';
|
||||
} catch (e) {
|
||||
return '.jpg';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<bool> hasCachedFile(String url, {String? customKey}) async {
|
||||
final file = await getCachedFile(url, customKey: customKey);
|
||||
return file != null;
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getDetailedCacheStats() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
final cacheSize = await getCacheSize();
|
||||
|
||||
return {
|
||||
'memory': {'items': memorySize, 'max_items': _maxMemoryCacheSize},
|
||||
'filesystem': {
|
||||
'total_size': cacheSize['total'],
|
||||
'files_size': cacheSize['files'],
|
||||
},
|
||||
'timestamp': DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Future<void> removeCachedFile(String url, {String? customKey}) async {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getCacheStats() async {
|
||||
final sizes = await getCacheSize();
|
||||
final memoryEntries = _memoryCache.length;
|
||||
final diskEntries =
|
||||
_prefs?.getKeys().where((key) => key.startsWith('cache_')).length ?? 0;
|
||||
|
||||
return {
|
||||
'memoryEntries': memoryEntries,
|
||||
'diskEntries': diskEntries,
|
||||
'memorySize': sizes['memory'],
|
||||
'filesSizeMB': (sizes['files']! / (1024 * 1024)).toStringAsFixed(2),
|
||||
'maxMemorySize': _maxMemoryCacheSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
334
lib/services/chat_cache_service.dart
Normal file
334
lib/services/chat_cache_service.dart
Normal file
@@ -0,0 +1,334 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'package:gwid/models/contact.dart';
|
||||
import 'package:gwid/models/message.dart';
|
||||
import 'package:gwid/services/cache_service.dart';
|
||||
|
||||
class ChatCacheService {
|
||||
static final ChatCacheService _instance = ChatCacheService._internal();
|
||||
factory ChatCacheService() => _instance;
|
||||
ChatCacheService._internal();
|
||||
|
||||
final CacheService _cacheService = CacheService();
|
||||
|
||||
|
||||
Future<void> initialize() async {
|
||||
await _cacheService.initialize();
|
||||
print('ChatCacheService инициализирован');
|
||||
}
|
||||
|
||||
|
||||
static const String _chatsKey = 'cached_chats';
|
||||
static const String _contactsKey = 'cached_contacts';
|
||||
static const String _messagesKey = 'cached_messages';
|
||||
static const String _chatMessagesKey = 'cached_chat_messages';
|
||||
|
||||
|
||||
static const Duration _chatsTTL = Duration(hours: 1);
|
||||
static const Duration _contactsTTL = Duration(hours: 6);
|
||||
static const Duration _messagesTTL = Duration(hours: 2);
|
||||
|
||||
|
||||
Future<void> cacheChats(List<Map<String, dynamic>> chats) async {
|
||||
try {
|
||||
await _cacheService.set(_chatsKey, chats, ttl: _chatsTTL);
|
||||
print('Кэшировано ${chats.length} чатов');
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования чатов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<List<Map<String, dynamic>>?> getCachedChats() async {
|
||||
try {
|
||||
final cached = await _cacheService.get<List<dynamic>>(
|
||||
_chatsKey,
|
||||
ttl: _chatsTTL,
|
||||
);
|
||||
if (cached != null) {
|
||||
return cached.cast<Map<String, dynamic>>();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированных чатов: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> cacheContacts(List<Contact> contacts) async {
|
||||
try {
|
||||
final contactsData = contacts
|
||||
.map(
|
||||
(contact) => {
|
||||
'id': contact.id,
|
||||
'name': contact.name,
|
||||
'firstName': contact.firstName,
|
||||
'lastName': contact.lastName,
|
||||
'photoBaseUrl': contact.photoBaseUrl,
|
||||
'isBlocked': contact.isBlocked,
|
||||
'isBlockedByMe': contact.isBlockedByMe,
|
||||
'accountStatus': contact.accountStatus,
|
||||
'status': contact.status,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
|
||||
await _cacheService.set(_contactsKey, contactsData, ttl: _contactsTTL);
|
||||
print('Кэшировано ${contacts.length} контактов');
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования контактов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<List<Contact>?> getCachedContacts() async {
|
||||
try {
|
||||
final cached = await _cacheService.get<List<dynamic>>(
|
||||
_contactsKey,
|
||||
ttl: _contactsTTL,
|
||||
);
|
||||
if (cached != null) {
|
||||
return cached.map((data) => Contact.fromJson(data)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированных контактов: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> cacheChatMessages(int chatId, List<Message> messages) async {
|
||||
try {
|
||||
final key = '$_chatMessagesKey$chatId';
|
||||
final messagesData = messages
|
||||
.map(
|
||||
(message) => {
|
||||
'id': message.id,
|
||||
'sender': message.senderId,
|
||||
'text': message.text,
|
||||
'time': message.time,
|
||||
'status': message.status,
|
||||
'updateTime': message.updateTime,
|
||||
'attaches': message.attaches,
|
||||
'cid': message.cid,
|
||||
'reactionInfo': message.reactionInfo,
|
||||
'link': message.link,
|
||||
},
|
||||
)
|
||||
.toList();
|
||||
|
||||
await _cacheService.set(key, messagesData, ttl: _messagesTTL);
|
||||
print('Кэшировано ${messages.length} сообщений для чата $chatId');
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования сообщений для чата $chatId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<List<Message>?> getCachedChatMessages(int chatId) async {
|
||||
try {
|
||||
final key = '$_chatMessagesKey$chatId';
|
||||
final cached = await _cacheService.get<List<dynamic>>(
|
||||
key,
|
||||
ttl: _messagesTTL,
|
||||
);
|
||||
if (cached != null) {
|
||||
return cached.map((data) => Message.fromJson(data)).toList();
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированных сообщений для чата $chatId: $e');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> addMessageToCache(int chatId, Message message) async {
|
||||
try {
|
||||
final cached = await getCachedChatMessages(chatId);
|
||||
|
||||
if (cached != null) {
|
||||
|
||||
final updatedMessages = [message, ...cached];
|
||||
await cacheChatMessages(chatId, updatedMessages);
|
||||
} else {
|
||||
|
||||
await cacheChatMessages(chatId, [message]);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка добавления сообщения в кэш: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> updateMessageInCache(int chatId, Message updatedMessage) async {
|
||||
try {
|
||||
final cached = await getCachedChatMessages(chatId);
|
||||
|
||||
if (cached != null) {
|
||||
final updatedMessages = cached.map((message) {
|
||||
if (message.id == updatedMessage.id) {
|
||||
return updatedMessage;
|
||||
}
|
||||
return message;
|
||||
}).toList();
|
||||
|
||||
await cacheChatMessages(chatId, updatedMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка обновления сообщения в кэше: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> removeMessageFromCache(int chatId, String messageId) async {
|
||||
try {
|
||||
final cached = await getCachedChatMessages(chatId);
|
||||
|
||||
if (cached != null) {
|
||||
final updatedMessages = cached
|
||||
.where((message) => message.id != messageId)
|
||||
.toList();
|
||||
await cacheChatMessages(chatId, updatedMessages);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка удаления сообщения из кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> cacheChatInfo(int chatId, Map<String, dynamic> chatInfo) async {
|
||||
try {
|
||||
final key = 'chat_info_$chatId';
|
||||
await _cacheService.set(key, chatInfo, ttl: _chatsTTL);
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования информации о чате $chatId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>?> getCachedChatInfo(int chatId) async {
|
||||
try {
|
||||
final key = 'chat_info_$chatId';
|
||||
return await _cacheService.get<Map<String, dynamic>>(key, ttl: _chatsTTL);
|
||||
} catch (e) {
|
||||
print('Ошибка получения кэшированной информации о чате $chatId: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> cacheLastMessage(int chatId, Message? lastMessage) async {
|
||||
try {
|
||||
final key = 'last_message_$chatId';
|
||||
if (lastMessage != null) {
|
||||
final messageData = {
|
||||
'id': lastMessage.id,
|
||||
'sender': lastMessage.senderId,
|
||||
'text': lastMessage.text,
|
||||
'time': lastMessage.time,
|
||||
'status': lastMessage.status,
|
||||
'updateTime': lastMessage.updateTime,
|
||||
'attaches': lastMessage.attaches,
|
||||
'cid': lastMessage.cid,
|
||||
'reactionInfo': lastMessage.reactionInfo,
|
||||
'link': lastMessage.link,
|
||||
};
|
||||
await _cacheService.set(key, messageData, ttl: _chatsTTL);
|
||||
} else {
|
||||
await _cacheService.remove(key);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка кэширования последнего сообщения для чата $chatId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Message?> getCachedLastMessage(int chatId) async {
|
||||
try {
|
||||
final key = 'last_message_$chatId';
|
||||
final cached = await _cacheService.get<Map<String, dynamic>>(
|
||||
key,
|
||||
ttl: _chatsTTL,
|
||||
);
|
||||
if (cached != null) {
|
||||
return Message.fromJson(cached);
|
||||
}
|
||||
} catch (e) {
|
||||
print(
|
||||
'Ошибка получения кэшированного последнего сообщения для чата $chatId: $e',
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<void> clearChatCache(int chatId) async {
|
||||
try {
|
||||
final keys = [
|
||||
'$_chatMessagesKey$chatId',
|
||||
'chat_info_$chatId',
|
||||
'last_message_$chatId',
|
||||
];
|
||||
|
||||
for (final key in keys) {
|
||||
await _cacheService.remove(key);
|
||||
}
|
||||
|
||||
print('Кэш для чата $chatId очищен');
|
||||
} catch (e) {
|
||||
print('Ошибка очистки кэша для чата $chatId: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<void> clearAllChatCache() async {
|
||||
try {
|
||||
await _cacheService.remove(_chatsKey);
|
||||
await _cacheService.remove(_contactsKey);
|
||||
await _cacheService.remove(_messagesKey);
|
||||
|
||||
|
||||
|
||||
print('Весь кэш чатов очищен');
|
||||
} catch (e) {
|
||||
print('Ошибка очистки всего кэша чатов: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<Map<String, dynamic>> getChatCacheStats() async {
|
||||
try {
|
||||
final cacheStats = await _cacheService.getCacheStats();
|
||||
final chats = await getCachedChats();
|
||||
final contacts = await getCachedContacts();
|
||||
|
||||
return {
|
||||
'cachedChats': chats?.length ?? 0,
|
||||
'cachedContacts': contacts?.length ?? 0,
|
||||
'cacheStats': cacheStats,
|
||||
};
|
||||
} catch (e) {
|
||||
print('Ошибка получения статистики кэша чатов: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Future<bool> isCacheValid(String cacheType) async {
|
||||
try {
|
||||
switch (cacheType) {
|
||||
case 'chats':
|
||||
return await _cacheService.get(_chatsKey, ttl: _chatsTTL) != null;
|
||||
case 'contacts':
|
||||
return await _cacheService.get(_contactsKey, ttl: _contactsTTL) !=
|
||||
null;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка проверки актуальности кэша: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
121
lib/services/version_checker.dart
Normal file
121
lib/services/version_checker.dart
Normal file
@@ -0,0 +1,121 @@
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class VersionChecker {
|
||||
|
||||
|
||||
|
||||
|
||||
static Future<String> getLatestVersion() async {
|
||||
try {
|
||||
|
||||
final html = await _fetchPage('https://web.max.ru/');
|
||||
|
||||
|
||||
final mainChunkUrl = _extractMainChunkUrl(html);
|
||||
print('[INFO] Загружаем главный chunk: $mainChunkUrl');
|
||||
|
||||
|
||||
final mainChunkCode = await _fetchPage(mainChunkUrl);
|
||||
|
||||
|
||||
final chunkPaths = _extractChunkPaths(mainChunkCode);
|
||||
|
||||
|
||||
for (final path in chunkPaths) {
|
||||
if (path.contains('/chunks/')) {
|
||||
final url = _buildChunkUrl(path);
|
||||
print('[INFO] Загружаем chunk: $url');
|
||||
|
||||
try {
|
||||
final jsCode = await _fetchPage(url);
|
||||
final version = _extractVersion(jsCode);
|
||||
|
||||
if (version != null) {
|
||||
print('[SUCCESS] Версия: $version из $url');
|
||||
return version;
|
||||
}
|
||||
} catch (e) {
|
||||
print('[WARN] Не удалось скачать $url: $e');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception('Версия не найдена ни в одном из чанков');
|
||||
} catch (e) {
|
||||
throw Exception('Не удалось проверить версию: $e');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static Future<String> _fetchPage(String url) async {
|
||||
final response = await http
|
||||
.get(Uri.parse(url))
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('Ошибка загрузки $url (${response.statusCode})');
|
||||
}
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
|
||||
static String _extractMainChunkUrl(String html) {
|
||||
final parts = html.split('import(');
|
||||
if (parts.length < 3) {
|
||||
throw Exception('Не найден import() в HTML');
|
||||
}
|
||||
|
||||
final mainChunkImport = parts[2]
|
||||
.split(')')[0]
|
||||
.replaceAll('"', '')
|
||||
.replaceAll("'", '');
|
||||
|
||||
return 'https://web.max.ru$mainChunkImport';
|
||||
}
|
||||
|
||||
|
||||
static List<String> _extractChunkPaths(String mainChunkCode) {
|
||||
final firstLine = mainChunkCode.split('\n')[0];
|
||||
final arrayContent = firstLine.split('[')[1].split(']')[0];
|
||||
|
||||
return arrayContent.split(',');
|
||||
}
|
||||
|
||||
|
||||
static String _buildChunkUrl(String path) {
|
||||
final cleanPath = path.substring(3, path.length - 1);
|
||||
return 'https://web.max.ru/_app/immutable$cleanPath';
|
||||
}
|
||||
|
||||
|
||||
static String? _extractVersion(String jsCode) {
|
||||
const wsAnchor = 'wss://ws-api.oneme.ru/websocket';
|
||||
final pos = jsCode.indexOf(wsAnchor);
|
||||
|
||||
if (pos == -1) {
|
||||
print('[INFO] ws-якорь не найден');
|
||||
return null;
|
||||
}
|
||||
|
||||
print('[INFO] Найден ws-якорь на позиции $pos');
|
||||
|
||||
|
||||
final snippet = jsCode.substring(pos, (pos + 2000).clamp(0, jsCode.length));
|
||||
|
||||
print('[INFO] Анализируем snippet (первые 500 символов):');
|
||||
print('${snippet.substring(0, 500.clamp(0, snippet.length))}...\n');
|
||||
|
||||
|
||||
final versionRegex = RegExp(r'[:=]\s*"(\d{1,2}\.\d{1,2}\.\d{1,2})"');
|
||||
final match = versionRegex.firstMatch(snippet);
|
||||
|
||||
if (match != null) {
|
||||
return match.group(1);
|
||||
}
|
||||
|
||||
print('[INFO] Версия не найдена в snippet');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user