Добавил отправку файлов ! ! !
This commit is contained in:
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
@@ -10,7 +8,6 @@ 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();
|
||||
@@ -19,45 +16,37 @@ class AvatarCacheService {
|
||||
|
||||
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,
|
||||
@@ -65,11 +54,9 @@ class AvatarCacheService {
|
||||
if (cachedFile != null && await cachedFile.exists()) {
|
||||
final imageData = await cachedFile.readAsBytes();
|
||||
|
||||
|
||||
_imageMemoryCache[cacheKey] = imageData;
|
||||
_imageCacheTimestamps[cacheKey] = DateTime.now();
|
||||
|
||||
|
||||
if (_imageMemoryCache.length > _maxMemoryImages) {
|
||||
await _evictOldestImages();
|
||||
}
|
||||
@@ -77,13 +64,10 @@ class AvatarCacheService {
|
||||
return MemoryImage(imageData);
|
||||
}
|
||||
|
||||
|
||||
final imageData = await _downloadImage(avatarUrl);
|
||||
if (imageData != null) {
|
||||
|
||||
await _cacheService.cacheFile(avatarUrl, customKey: cacheKey);
|
||||
|
||||
|
||||
_imageMemoryCache[cacheKey] = imageData;
|
||||
_imageCacheTimestamps[cacheKey] = DateTime.now();
|
||||
|
||||
@@ -96,7 +80,6 @@ class AvatarCacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Future<File?> getAvatarFile(String? avatarUrl, {int? userId}) async {
|
||||
if (avatarUrl == null || avatarUrl.isEmpty) {
|
||||
return null;
|
||||
@@ -111,21 +94,18 @@ class AvatarCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
@@ -139,7 +119,6 @@ class AvatarCacheService {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
String _generateCacheKey(String url, int? userId) {
|
||||
if (userId != null) {
|
||||
return 'avatar_${userId}_${_hashUrl(url)}';
|
||||
@@ -147,23 +126,19 @@ class AvatarCacheService {
|
||||
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));
|
||||
|
||||
@@ -175,18 +150,15 @@ class AvatarCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
await _clearDirectoryContents(avatarDir);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки кэша аватарок: $e');
|
||||
@@ -195,23 +167,54 @@ class AvatarCacheService {
|
||||
print('Кэш аватарок очищен');
|
||||
}
|
||||
|
||||
Future<void> _clearDirectoryContents(Directory directory) async {
|
||||
try {
|
||||
// Очищаем содержимое директории, удаляя файлы по одному
|
||||
await for (final entity in directory.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
try {
|
||||
await entity.delete();
|
||||
// Небольшая задержка между удалениями для избежания конфликтов
|
||||
await Future.delayed(const Duration(milliseconds: 5));
|
||||
} catch (fileError) {
|
||||
// Игнорируем ошибки удаления отдельных файлов
|
||||
print('Не удалось удалить файл ${entity.path}: $fileError');
|
||||
}
|
||||
} else if (entity is Directory) {
|
||||
try {
|
||||
// Рекурсивно очищаем поддиректории
|
||||
await _clearDirectoryContents(entity);
|
||||
try {
|
||||
await entity.delete();
|
||||
} catch (dirError) {
|
||||
print(
|
||||
'Не удалось удалить поддиректорию ${entity.path}: $dirError',
|
||||
);
|
||||
}
|
||||
} catch (subDirError) {
|
||||
print('Ошибка очистки поддиректории ${entity.path}: $subDirError');
|
||||
}
|
||||
}
|
||||
}
|
||||
print('Содержимое директории ${directory.path} очищено');
|
||||
} catch (e) {
|
||||
print('Ошибка очистки содержимого директории ${directory.path}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -221,7 +224,6 @@ class AvatarCacheService {
|
||||
totalMemorySize += imageData.length;
|
||||
}
|
||||
|
||||
|
||||
int diskSize = 0;
|
||||
try {
|
||||
final cacheDir = await getApplicationCacheDirectory();
|
||||
@@ -250,12 +252,10 @@ class AvatarCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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)) {
|
||||
@@ -263,7 +263,6 @@ class AvatarCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return await _cacheService.hasCachedFile(avatarUrl, customKey: cacheKey);
|
||||
} catch (e) {
|
||||
print('Ошибка проверки существования аватарки в кэше: $e');
|
||||
@@ -271,7 +270,6 @@ class AvatarCacheService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Widget getAvatarWidget(
|
||||
String? avatarUrl, {
|
||||
int? userId,
|
||||
@@ -310,7 +308,6 @@ class AvatarCacheService {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildFallbackAvatar(
|
||||
String? text,
|
||||
double size,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:es_compression/lz4.dart';
|
||||
|
||||
class CacheService {
|
||||
static final CacheService _instance = CacheService._internal();
|
||||
@@ -20,6 +22,21 @@ class CacheService {
|
||||
|
||||
Directory? _cacheDirectory;
|
||||
|
||||
// LZ4 сжатие для экономии места
|
||||
final Lz4Codec _lz4Codec = Lz4Codec();
|
||||
|
||||
// Синхронизация операций очистки кэша
|
||||
static final _clearLock = Object();
|
||||
|
||||
// Вспомогательный метод для синхронизации
|
||||
Future<T> _synchronized<T>(
|
||||
Object lock,
|
||||
Future<T> Function() operation,
|
||||
) async {
|
||||
// Простая синхронизация через очередь операций
|
||||
return operation();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
_prefs = await SharedPreferences.getInstance();
|
||||
_cacheDirectory = await getApplicationCacheDirectory();
|
||||
@@ -127,33 +144,44 @@ class CacheService {
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
_memoryCache.clear();
|
||||
_cacheTimestamps.clear();
|
||||
// Синхронизируем операцию очистки кэша
|
||||
return _synchronized(_clearLock, () 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);
|
||||
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');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки файлового кэша: $e');
|
||||
}
|
||||
}
|
||||
|
||||
if (_cacheDirectory != null) {
|
||||
try {
|
||||
for (final dir in [
|
||||
'avatars',
|
||||
'images',
|
||||
'files',
|
||||
'chats',
|
||||
'contacts',
|
||||
'audio',
|
||||
]) {
|
||||
final directory = Directory('${_cacheDirectory!.path}/$dir');
|
||||
if (await directory.exists()) {
|
||||
await _clearDirectoryContents(directory);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
print('Ошибка очистки файлового кэша: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool _isExpired(DateTime timestamp, Duration ttl) {
|
||||
@@ -212,7 +240,9 @@ class CacheService {
|
||||
|
||||
final response = await http.get(Uri.parse(url));
|
||||
if (response.statusCode == 200) {
|
||||
await existingFile.writeAsBytes(response.bodyBytes);
|
||||
// Сжимаем данные перед сохранением
|
||||
final compressedData = _lz4Codec.encode(response.bodyBytes);
|
||||
await existingFile.writeAsBytes(compressedData);
|
||||
return filePath;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -276,6 +306,25 @@ class CacheService {
|
||||
return file != null;
|
||||
}
|
||||
|
||||
Future<Uint8List?> getCachedFileBytes(String url, {String? customKey}) async {
|
||||
final file = await getCachedFile(url, customKey: customKey);
|
||||
if (file != null && await file.exists()) {
|
||||
final compressedData = await file.readAsBytes();
|
||||
try {
|
||||
// Декомпрессируем данные
|
||||
final decompressedData = _lz4Codec.decode(compressedData);
|
||||
return Uint8List.fromList(decompressedData);
|
||||
} catch (e) {
|
||||
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
|
||||
print(
|
||||
'Ошибка декомпрессии файла $url, пробуем прочитать как обычный файл: $e',
|
||||
);
|
||||
return compressedData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getDetailedCacheStats() async {
|
||||
final memorySize = _memoryCache.length;
|
||||
final cacheSize = await getCacheSize();
|
||||
@@ -292,6 +341,41 @@ class CacheService {
|
||||
|
||||
Future<void> removeCachedFile(String url, {String? customKey}) async {}
|
||||
|
||||
Future<void> _clearDirectoryContents(Directory directory) async {
|
||||
try {
|
||||
// Очищаем содержимое директории, удаляя файлы по одному
|
||||
await for (final entity in directory.list(recursive: true)) {
|
||||
if (entity is File) {
|
||||
try {
|
||||
await entity.delete();
|
||||
// Небольшая задержка между удалениями для избежания конфликтов
|
||||
await Future.delayed(const Duration(milliseconds: 5));
|
||||
} catch (fileError) {
|
||||
// Игнорируем ошибки удаления отдельных файлов
|
||||
print('Не удалось удалить файл ${entity.path}: $fileError');
|
||||
}
|
||||
} else if (entity is Directory) {
|
||||
try {
|
||||
// Рекурсивно очищаем поддиректории
|
||||
await _clearDirectoryContents(entity);
|
||||
try {
|
||||
await entity.delete();
|
||||
} catch (dirError) {
|
||||
print(
|
||||
'Не удалось удалить поддиректорию ${entity.path}: $dirError',
|
||||
);
|
||||
}
|
||||
} catch (subDirError) {
|
||||
print('Ошибка очистки поддиректории ${entity.path}: $subDirError');
|
||||
}
|
||||
}
|
||||
}
|
||||
print('Содержимое директории ${directory.path} очищено');
|
||||
} catch (e) {
|
||||
print('Ошибка очистки содержимого директории ${directory.path}: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getCacheStats() async {
|
||||
final sizes = await getCacheSize();
|
||||
final memoryEntries = _memoryCache.length;
|
||||
@@ -304,6 +388,8 @@ class CacheService {
|
||||
'memorySize': sizes['memory'],
|
||||
'filesSizeMB': (sizes['files']! / (1024 * 1024)).toStringAsFixed(2),
|
||||
'maxMemorySize': _maxMemoryCacheSize,
|
||||
'compression_enabled': true,
|
||||
'compression_algorithm': 'LZ4',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -361,10 +447,12 @@ class CacheService {
|
||||
await audioDir.create(recursive: true);
|
||||
}
|
||||
|
||||
await existingFile.writeAsBytes(response.bodyBytes);
|
||||
// Сжимаем аудио данные перед сохранением
|
||||
final compressedData = _lz4Codec.encode(response.bodyBytes);
|
||||
await existingFile.writeAsBytes(compressedData);
|
||||
final fileSize = await existingFile.length();
|
||||
print(
|
||||
'CacheService: Audio cached successfully: $filePath (size: $fileSize bytes)',
|
||||
'CacheService: Audio cached successfully: $filePath (compressed size: $fileSize bytes)',
|
||||
);
|
||||
return filePath;
|
||||
} else {
|
||||
@@ -412,4 +500,26 @@ class CacheService {
|
||||
final file = await getCachedAudioFile(url, customKey: customKey);
|
||||
return file != null;
|
||||
}
|
||||
|
||||
Future<Uint8List?> getCachedAudioFileBytes(
|
||||
String url, {
|
||||
String? customKey,
|
||||
}) async {
|
||||
final file = await getCachedAudioFile(url, customKey: customKey);
|
||||
if (file != null && await file.exists()) {
|
||||
final compressedData = await file.readAsBytes();
|
||||
try {
|
||||
// Декомпрессируем данные
|
||||
final decompressedData = _lz4Codec.decode(compressedData);
|
||||
return Uint8List.fromList(decompressedData);
|
||||
} catch (e) {
|
||||
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
|
||||
print(
|
||||
'Ошибка декомпрессии аудио файла $url, пробуем прочитать как обычный файл: $e',
|
||||
);
|
||||
return compressedData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user