From 95b67c7891fc5ce5b70760a603acb7973b525b63 Mon Sep 17 00:00:00 2001 From: jganenok Date: Sat, 22 Nov 2025 12:08:57 +0700 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D1=83=20=D1=84=D0=B0?= =?UTF-8?q?=D0=B9=D0=BB=D0=BE=D0=B2=20!=20!=20!?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/api/api_service_media.dart | 120 +++++++++++------- lib/cache_management_screen.dart | 41 +++++-- lib/chat_screen.dart | 19 +++ lib/image_cache_service.dart | 89 +++++++++----- lib/services/avatar_cache_service.dart | 73 ++++++----- lib/services/cache_service.dart | 162 +++++++++++++++++++++---- 6 files changed, 353 insertions(+), 151 deletions(-) diff --git a/lib/api/api_service_media.dart b/lib/api/api_service_media.dart index 13f9a07..fc86af1 100644 --- a/lib/api/api_service_media.dart +++ b/lib/api/api_service_media.dart @@ -233,49 +233,8 @@ extension ApiServiceMedia on ApiService { await waitUntilOnline(); - final int seq87 = _sendMessage(87, {"count": 1}); - final resp87 = await messages.firstWhere((m) => m['seq'] == seq87); - - if (resp87['payload'] == null || - resp87['payload']['info'] == null || - (resp87['payload']['info'] as List).isEmpty) { - throw Exception('Неверный ответ на Opcode 87: отсутствует "info"'); - } - - final uploadInfo = (resp87['payload']['info'] as List).first; - final String uploadUrl = uploadInfo['url']; - final int fileId = uploadInfo['fileId']; - - print('Получен fileId: $fileId и URL: $uploadUrl'); - - var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); - request.files.add(await http.MultipartFile.fromPath('file', filePath)); - var streamed = await request.send(); - var httpResp = await http.Response.fromStream(streamed); - if (httpResp.statusCode != 200) { - throw Exception( - 'Ошибка загрузки файла: ${httpResp.statusCode} ${httpResp.body}', - ); - } - - print('Файл успешно загружен на сервер.'); - + // Показываем локальное сообщение с файлом сразу, как отправляется final int cid = DateTime.now().millisecondsSinceEpoch; - final payload = { - "chatId": chatId, - "message": { - "text": caption?.trim() ?? "", - "cid": cid, - "elements": [], - "attaches": [ - {"_type": "FILE", "fileId": fileId}, - ], - }, - "notify": true, - }; - - clearChatsCache(); - _emitLocal({ 'ver': 11, 'cmd': 1, @@ -302,8 +261,80 @@ extension ApiServiceMedia on ApiService { }, }); - _sendMessage(64, payload); - print('Сообщение о файле (Opcode 64) отправлено.'); + // Запрашиваем URL для загрузки файла + final int seq87 = _sendMessage(87, {"count": 1}); + final resp87 = await messages.firstWhere((m) => m['seq'] == seq87); + + if (resp87['payload'] == null || + resp87['payload']['info'] == null || + (resp87['payload']['info'] as List).isEmpty) { + throw Exception('Неверный ответ на Opcode 87: отсутствует "info"'); + } + + final uploadInfo = (resp87['payload']['info'] as List).first; + final String uploadUrl = uploadInfo['url']; + final int fileId = uploadInfo['fileId']; + final String token = uploadInfo['token']; + + print('Получен fileId: $fileId, token: $token и URL: $uploadUrl'); + + // Начинаем heartbeat каждые 5 секунд + Timer? heartbeatTimer; + heartbeatTimer = Timer.periodic(const Duration(seconds: 5), (_) { + _sendMessage(65, {"chatId": chatId, "type": "FILE"}); + print('Heartbeat отправлен для загрузки файла'); + }); + + try { + // Загружаем файл + var request = http.MultipartRequest('POST', Uri.parse(uploadUrl)); + request.files.add(await http.MultipartFile.fromPath('file', filePath)); + var streamed = await request.send(); + var httpResp = await http.Response.fromStream(streamed); + if (httpResp.statusCode != 200) { + throw Exception( + 'Ошибка загрузки файла: ${httpResp.statusCode} ${httpResp.body}', + ); + } + + print('Файл успешно загружен на сервер. Ожидаем подтверждение...'); + + // Ждем уведомления о завершении загрузки (opcode 136) + final uploadCompleteMsg = await messages + .timeout(const Duration(seconds: 30)) + .firstWhere( + (msg) => + msg['opcode'] == 136 && msg['payload']['fileId'] == fileId, + ); + + print( + 'Получено подтверждение загрузки файла: ${uploadCompleteMsg['payload']}', + ); + + // Останавливаем heartbeat + heartbeatTimer.cancel(); + + final payload = { + "chatId": chatId, + "message": { + "text": caption?.trim() ?? "", + "cid": cid, + "elements": [], + "attaches": [ + {"_type": "FILE", "fileId": fileId}, + ], + }, + "notify": true, + }; + + clearChatsCache(); + + _sendMessage(64, payload); + print('Сообщение о файле (Opcode 64) отправлено.'); + } finally { + // Гарантированно останавливаем heartbeat в случае ошибки + heartbeatTimer.cancel(); + } } catch (e) { print('Ошибка отправки файла: $e'); } @@ -368,4 +399,3 @@ extension ApiServiceMedia on ApiService { } } } - diff --git a/lib/cache_management_screen.dart b/lib/cache_management_screen.dart index b36fa9d..10a759e 100644 --- a/lib/cache_management_screen.dart +++ b/lib/cache_management_screen.dart @@ -1,5 +1,3 @@ - - import 'package:flutter/material.dart'; import 'package:gwid/services/cache_service.dart'; import 'package:gwid/services/avatar_cache_service.dart'; @@ -34,7 +32,6 @@ class _CacheManagementScreenState extends State { final avatarService = AvatarCacheService(); final chatService = ChatCacheService(); - await cacheService.initialize(); await chatService.initialize(); await avatarService.initialize(); @@ -98,13 +95,15 @@ class _CacheManagementScreenState extends State { final avatarService = AvatarCacheService(); final chatService = ChatCacheService(); - await cacheService.initialize(); await chatService.initialize(); await avatarService.initialize(); await cacheService.clear(); + // Небольшая задержка между операциями очистки + await Future.delayed(const Duration(milliseconds: 100)); await avatarService.clearAvatarCache(); + await Future.delayed(const Duration(milliseconds: 100)); await chatService.clearAllChatCache(); await _loadCacheStats(); @@ -156,9 +155,10 @@ class _CacheManagementScreenState extends State { try { final avatarService = AvatarCacheService(); - await avatarService.initialize(); await avatarService.clearAvatarCache(); + // Небольшая задержка перед загрузкой статистики + await Future.delayed(const Duration(milliseconds: 50)); await _loadCacheStats(); if (mounted) { @@ -273,7 +273,6 @@ class _CacheManagementScreenState extends State { : ListView( padding: const EdgeInsets.all(16), children: [ - Text( "Общая статистика", style: TextStyle( @@ -320,7 +319,6 @@ class _CacheManagementScreenState extends State { const SizedBox(height: 24), - Text( "Детальная статистика", style: TextStyle( @@ -344,7 +342,6 @@ class _CacheManagementScreenState extends State { const SizedBox(height: 32), - Text( "Управление кэшем", style: TextStyle( @@ -390,7 +387,6 @@ class _CacheManagementScreenState extends State { const SizedBox(height: 24), - Card( child: Padding( padding: const EdgeInsets.all(16), @@ -413,6 +409,7 @@ class _CacheManagementScreenState extends State { const SizedBox(height: 12), const Text( "Кэширование ускоряет работу приложения, сохраняя часто используемые данные локально. " + "Все файлы сжимаются с помощью LZ4 для экономии места. " "Чаты кэшируются на 1 час, контакты на 6 часов, сообщения на 2 часа, аватарки на 7 дней.", ), const SizedBox(height: 8), @@ -420,6 +417,32 @@ class _CacheManagementScreenState extends State { "Очистка кэша может замедлить работу приложения до повторной загрузки данных.", style: TextStyle(fontStyle: FontStyle.italic), ), + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: colors.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + Icons.compress, + color: colors.primary, + size: 16, + ), + const SizedBox(width: 8), + Text( + "Сжатие LZ4 включено - экономия места до 70%", + style: TextStyle( + color: colors.primary, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), ], ), ), diff --git a/lib/chat_screen.dart b/lib/chat_screen.dart index c5f9f71..ac5ee73 100644 --- a/lib/chat_screen.dart +++ b/lib/chat_screen.dart @@ -2544,6 +2544,25 @@ class _ChatScreenState extends State { ).colorScheme.onSurface.withOpacity(0.3) : Theme.of(context).colorScheme.primary, ), + IconButton( + icon: const Icon(Icons.attach_file), + tooltip: isBlocked + ? 'Пользователь заблокирован' + : 'Отправить файл', + onPressed: isBlocked + ? null + : () async { + await ApiService.instance.sendFileMessage( + widget.chatId, + senderId: _actualMyId, + ); + }, + color: isBlocked + ? Theme.of( + context, + ).colorScheme.onSurface.withOpacity(0.3) + : Theme.of(context).colorScheme.primary, + ), if (context.watch().messageTransition == TransitionOption.slide) IconButton( diff --git a/lib/image_cache_service.dart b/lib/image_cache_service.dart index 6a92730..4c1e2f0 100644 --- a/lib/image_cache_service.dart +++ b/lib/image_cache_service.dart @@ -5,7 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:http/http.dart' as http; import 'package:cached_network_image/cached_network_image.dart'; - +import 'package:es_compression/lz4.dart'; class ImageCacheService { ImageCacheService._privateConstructor(); @@ -18,6 +18,8 @@ class ImageCacheService { ); // Кеш изображений на 7 дней late Directory _cacheDirectory; + // LZ4 сжатие для экономии места + final Lz4Codec _lz4Codec = Lz4Codec(); Future initialize() async { final appDir = await getApplicationDocumentsDirectory(); @@ -27,30 +29,25 @@ class ImageCacheService { await _cacheDirectory.create(recursive: true); } - await _cleanupExpiredCache(); } - String getCachedImagePath(String url) { final fileName = _generateFileName(url); return path.join(_cacheDirectory.path, fileName); } - bool isImageCached(String url) { final file = File(getCachedImagePath(url)); return file.existsSync(); } - Future loadImage(String url, {bool forceRefresh = false}) async { if (!forceRefresh && isImageCached(url)) { final cachedFile = File(getCachedImagePath(url)); if (await _isFileValid(cachedFile)) { return cachedFile; } else { - await cachedFile.delete(); } } @@ -59,8 +56,9 @@ class ImageCacheService { final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final file = File(getCachedImagePath(url)); - await file.writeAsBytes(response.bodyBytes); - + // Сжимаем данные перед сохранением + final compressedData = _lz4Codec.encode(response.bodyBytes); + await file.writeAsBytes(compressedData); await _updateFileAccessTime(file); @@ -73,38 +71,44 @@ class ImageCacheService { return null; } - Future loadImageAsBytes( String url, { bool forceRefresh = false, }) async { final file = await loadImage(url, forceRefresh: forceRefresh); if (file != null) { - return await file.readAsBytes(); + 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 preloadImage(String url) async { await loadImage(url); } - Future preloadContactAvatar(String? photoUrl) async { if (photoUrl != null && photoUrl.isNotEmpty) { await preloadImage(photoUrl); } } - Future preloadProfileAvatar(String? photoUrl) async { if (photoUrl != null && photoUrl.isNotEmpty) { await preloadImage(photoUrl); } } - Future preloadContactAvatars(List photoUrls) async { final futures = photoUrls .where((url) => url != null && url.isNotEmpty) @@ -116,14 +120,46 @@ class ImageCacheService { } } - Future clearCache() async { if (_cacheDirectory.existsSync()) { - await _cacheDirectory.delete(recursive: true); - await _cacheDirectory.create(recursive: true); + await _clearDirectoryContents(_cacheDirectory); } } + Future _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 getCacheSize() async { int totalSize = 0; @@ -137,7 +173,6 @@ class ImageCacheService { return totalSize; } - Future getCacheFileCount() async { int count = 0; if (_cacheDirectory.existsSync()) { @@ -150,7 +185,6 @@ class ImageCacheService { return count; } - Future _cleanupExpiredCache() async { if (!_cacheDirectory.existsSync()) return; @@ -161,18 +195,15 @@ class ImageCacheService { } } - Future _isFileValid(File file) async { if (!file.existsSync()) return false; - final stat = await file.stat(); final age = DateTime.now().difference(stat.modified); return age < _cacheExpiration; } - Future _isFileExpired(File file) async { if (!file.existsSync()) return false; @@ -182,19 +213,13 @@ class ImageCacheService { return age >= _cacheExpiration; } - Future _updateFileAccessTime(File file) async { - try { await file.setLastModified(DateTime.now()); - } catch (e) { - - } + } catch (e) {} } - String _generateFileName(String url) { - final hash = url.hashCode.abs().toString(); final extension = path.extension(url).isNotEmpty ? path.extension(url) @@ -203,7 +228,6 @@ class ImageCacheService { return '$hash$extension'; } - Future> getCacheStats() async { final size = await getCacheSize(); final fileCount = await getCacheFileCount(); @@ -213,13 +237,13 @@ class ImageCacheService { 'cache_size_mb': (size / (1024 * 1024)).toStringAsFixed(2), 'file_count': fileCount, 'cache_directory': _cacheDirectory.path, + 'compression_enabled': true, + 'compression_algorithm': 'LZ4', }; } } - extension CachedImageExtension on String { - Widget getCachedNetworkImage({ Key? key, double? width, @@ -259,7 +283,6 @@ extension CachedImageExtension on String { ); } - Future preloadImage() async { await ImageCacheService.instance.loadImage(this); } diff --git a/lib/services/avatar_cache_service.dart b/lib/services/avatar_cache_service.dart index dec9d10..9ea5cf1 100644 --- a/lib/services/avatar_cache_service.dart +++ b/lib/services/avatar_cache_service.dart @@ -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 _imageMemoryCache = {}; final Map _imageCacheTimestamps = {}; - static const Duration _imageTTL = Duration(days: 7); static const int _maxMemoryImages = 50; static const int _maxImageSizeMB = 5; - Future initialize() async { await _cacheService.initialize(); print('AvatarCacheService инициализирован'); } - Future 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 getAvatarFile(String? avatarUrl, {int? userId}) async { if (avatarUrl == null || avatarUrl.isEmpty) { return null; @@ -111,21 +94,18 @@ class AvatarCacheService { } } - Future preloadAvatars(List avatarUrls) async { final futures = avatarUrls.map((url) => getAvatar(url)); await Future.wait(futures); print('Предзагружено ${avatarUrls.length} аватарок'); } - Future _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 _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 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 _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 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> 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 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, diff --git a/lib/services/cache_service.dart b/lib/services/cache_service.dart index 41f365d..4dd6751 100644 --- a/lib/services/cache_service.dart +++ b/lib/services/cache_service.dart @@ -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 _synchronized( + Object lock, + Future Function() operation, + ) async { + // Простая синхронизация через очередь операций + return operation(); + } + Future initialize() async { _prefs = await SharedPreferences.getInstance(); _cacheDirectory = await getApplicationCacheDirectory(); @@ -127,33 +144,44 @@ class CacheService { } Future 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 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> getDetailedCacheStats() async { final memorySize = _memoryCache.length; final cacheSize = await getCacheSize(); @@ -292,6 +341,41 @@ class CacheService { Future removeCachedFile(String url, {String? customKey}) async {} + Future _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> 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 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; + } }