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 _memoryCache = {}; final Map _cacheTimestamps = {}; static const Duration _defaultTTL = Duration(hours: 24); static const int _maxMemoryCacheSize = 1000; SharedPreferences? _prefs; Directory? _cacheDirectory; Future initialize() async { _prefs = await SharedPreferences.getInstance(); _cacheDirectory = await getApplicationCacheDirectory(); await _createCacheDirectories(); print('CacheService инициализирован'); } Future _createCacheDirectories() async { if (_cacheDirectory == null) return; final directories = [ 'avatars', 'images', 'files', 'chats', 'contacts', 'audio', ]; for (final dir in directories) { final directory = Directory('${_cacheDirectory!.path}/$dir'); if (!await directory.exists()) { await directory.create(recursive: true); } } } Future get(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 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 set(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 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 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 _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> 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 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 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 hashString = key.hashCode.abs().toString(); final hash = hashString.length >= 16 ? hashString.substring(0, 16) : hashString.padRight(16, '0'); 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('.')); if (extension.isNotEmpty && extension.length < 10) { return extension; } if (url.contains('audio') || url.contains('voice') || url.contains('.mp3') || url.contains('.ogg') || url.contains('.m4a')) { return '.mp3'; } return '.jpg'; } catch (e) { return '.jpg'; } } Future hasCachedFile(String url, {String? customKey}) async { final file = await getCachedFile(url, customKey: customKey); return file != null; } Future> 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 removeCachedFile(String url, {String? customKey}) async {} Future> 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, }; } Future cacheAudioFile(String url, {String? customKey}) async { if (_cacheDirectory == null) { print('CacheService: _cacheDirectory is null, initializing...'); await initialize(); if (_cacheDirectory == null) { print('CacheService: Failed to initialize cache directory'); return null; } } try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/audio/$fileName'; final existingFile = File(filePath); if (await existingFile.exists()) { print('CacheService: Audio file already cached: $filePath'); return filePath; } print('CacheService: Downloading audio from: $url'); print('CacheService: Target file path: $filePath'); final response = await http .get( Uri.parse(url), headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', }, ) .timeout( const Duration(seconds: 30), onTimeout: () { print('CacheService: Request timeout'); throw TimeoutException('Request timeout'); }, ); print( 'CacheService: Response status: ${response.statusCode}, content-length: ${response.contentLength}', ); if (response.statusCode == 200) { if (response.bodyBytes.isEmpty) { print('CacheService: Response body is empty'); return null; } final audioDir = Directory('${_cacheDirectory!.path}/audio'); if (!await audioDir.exists()) { await audioDir.create(recursive: true); } await existingFile.writeAsBytes(response.bodyBytes); final fileSize = await existingFile.length(); print( 'CacheService: Audio cached successfully: $filePath (size: $fileSize bytes)', ); return filePath; } else { print( 'CacheService: Failed to download audio, status code: ${response.statusCode}', ); print( 'CacheService: Response body: ${response.body.substring(0, response.body.length > 200 ? 200 : response.body.length)}', ); } } catch (e, stackTrace) { print('Ошибка кэширования аудио файла $url: $e'); print('Stack trace: $stackTrace'); if (e is TimeoutException) { print('CacheService: Request timed out'); } else if (e is SocketException) { print('CacheService: Network error - ${e.message}'); } else if (e is HttpException) { print('CacheService: HTTP error - ${e.message}'); } } return null; } Future getCachedAudioFile(String url, {String? customKey}) async { if (_cacheDirectory == null) return null; try { final fileName = _generateFileName(url, customKey); final filePath = '${_cacheDirectory!.path}/audio/$fileName'; final file = File(filePath); if (await file.exists()) { return file; } } catch (e) { print('Ошибка получения кэшированного аудио файла: $e'); } return null; } Future hasCachedAudioFile(String url, {String? customKey}) async { final file = await getCachedAudioFile(url, customKey: customKey); return file != null; } }