340 lines
8.9 KiB
Dart
340 lines
8.9 KiB
Dart
|
|
|
|
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,
|
|
),
|
|
);
|
|
}
|
|
}
|