Добавил отправку файлов ! ! !

This commit is contained in:
jganenok
2025-11-22 12:08:57 +07:00
parent 4e81b607fa
commit 95b67c7891
6 changed files with 353 additions and 151 deletions

View File

@@ -233,49 +233,8 @@ extension ApiServiceMedia on ApiService {
await waitUntilOnline(); 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 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({ _emitLocal({
'ver': 11, 'ver': 11,
'cmd': 1, 'cmd': 1,
@@ -302,8 +261,80 @@ extension ApiServiceMedia on ApiService {
}, },
}); });
_sendMessage(64, payload); // Запрашиваем URL для загрузки файла
print('Сообщение о файле (Opcode 64) отправлено.'); 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) { } catch (e) {
print('Ошибка отправки файла: $e'); print('Ошибка отправки файла: $e');
} }
@@ -368,4 +399,3 @@ extension ApiServiceMedia on ApiService {
} }
} }
} }

View File

@@ -1,5 +1,3 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:gwid/services/cache_service.dart'; import 'package:gwid/services/cache_service.dart';
import 'package:gwid/services/avatar_cache_service.dart'; import 'package:gwid/services/avatar_cache_service.dart';
@@ -34,7 +32,6 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
final avatarService = AvatarCacheService(); final avatarService = AvatarCacheService();
final chatService = ChatCacheService(); final chatService = ChatCacheService();
await cacheService.initialize(); await cacheService.initialize();
await chatService.initialize(); await chatService.initialize();
await avatarService.initialize(); await avatarService.initialize();
@@ -98,13 +95,15 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
final avatarService = AvatarCacheService(); final avatarService = AvatarCacheService();
final chatService = ChatCacheService(); final chatService = ChatCacheService();
await cacheService.initialize(); await cacheService.initialize();
await chatService.initialize(); await chatService.initialize();
await avatarService.initialize(); await avatarService.initialize();
await cacheService.clear(); await cacheService.clear();
// Небольшая задержка между операциями очистки
await Future.delayed(const Duration(milliseconds: 100));
await avatarService.clearAvatarCache(); await avatarService.clearAvatarCache();
await Future.delayed(const Duration(milliseconds: 100));
await chatService.clearAllChatCache(); await chatService.clearAllChatCache();
await _loadCacheStats(); await _loadCacheStats();
@@ -156,9 +155,10 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
try { try {
final avatarService = AvatarCacheService(); final avatarService = AvatarCacheService();
await avatarService.initialize(); await avatarService.initialize();
await avatarService.clearAvatarCache(); await avatarService.clearAvatarCache();
// Небольшая задержка перед загрузкой статистики
await Future.delayed(const Duration(milliseconds: 50));
await _loadCacheStats(); await _loadCacheStats();
if (mounted) { if (mounted) {
@@ -273,7 +273,6 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
: ListView( : ListView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
children: [ children: [
Text( Text(
"Общая статистика", "Общая статистика",
style: TextStyle( style: TextStyle(
@@ -320,7 +319,6 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Text( Text(
"Детальная статистика", "Детальная статистика",
style: TextStyle( style: TextStyle(
@@ -344,7 +342,6 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
const SizedBox(height: 32), const SizedBox(height: 32),
Text( Text(
"Управление кэшем", "Управление кэшем",
style: TextStyle( style: TextStyle(
@@ -390,7 +387,6 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
const SizedBox(height: 24), const SizedBox(height: 24),
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -413,6 +409,7 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
const SizedBox(height: 12), const SizedBox(height: 12),
const Text( const Text(
"Кэширование ускоряет работу приложения, сохраняя часто используемые данные локально. " "Кэширование ускоряет работу приложения, сохраняя часто используемые данные локально. "
"Все файлы сжимаются с помощью LZ4 для экономии места. "
"Чаты кэшируются на 1 час, контакты на 6 часов, сообщения на 2 часа, аватарки на 7 дней.", "Чаты кэшируются на 1 час, контакты на 6 часов, сообщения на 2 часа, аватарки на 7 дней.",
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -420,6 +417,32 @@ class _CacheManagementScreenState extends State<CacheManagementScreen> {
"Очистка кэша может замедлить работу приложения до повторной загрузки данных.", "Очистка кэша может замедлить работу приложения до повторной загрузки данных.",
style: TextStyle(fontStyle: FontStyle.italic), 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,
),
),
],
),
),
], ],
), ),
), ),

View File

@@ -2544,6 +2544,25 @@ class _ChatScreenState extends State<ChatScreen> {
).colorScheme.onSurface.withOpacity(0.3) ).colorScheme.onSurface.withOpacity(0.3)
: Theme.of(context).colorScheme.primary, : 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<ThemeProvider>().messageTransition == if (context.watch<ThemeProvider>().messageTransition ==
TransitionOption.slide) TransitionOption.slide)
IconButton( IconButton(

View File

@@ -5,7 +5,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:es_compression/lz4.dart';
class ImageCacheService { class ImageCacheService {
ImageCacheService._privateConstructor(); ImageCacheService._privateConstructor();
@@ -18,6 +18,8 @@ class ImageCacheService {
); // Кеш изображений на 7 дней ); // Кеш изображений на 7 дней
late Directory _cacheDirectory; late Directory _cacheDirectory;
// LZ4 сжатие для экономии места
final Lz4Codec _lz4Codec = Lz4Codec();
Future<void> initialize() async { Future<void> initialize() async {
final appDir = await getApplicationDocumentsDirectory(); final appDir = await getApplicationDocumentsDirectory();
@@ -27,30 +29,25 @@ class ImageCacheService {
await _cacheDirectory.create(recursive: true); await _cacheDirectory.create(recursive: true);
} }
await _cleanupExpiredCache(); await _cleanupExpiredCache();
} }
String getCachedImagePath(String url) { String getCachedImagePath(String url) {
final fileName = _generateFileName(url); final fileName = _generateFileName(url);
return path.join(_cacheDirectory.path, fileName); return path.join(_cacheDirectory.path, fileName);
} }
bool isImageCached(String url) { bool isImageCached(String url) {
final file = File(getCachedImagePath(url)); final file = File(getCachedImagePath(url));
return file.existsSync(); return file.existsSync();
} }
Future<File?> loadImage(String url, {bool forceRefresh = false}) async { Future<File?> loadImage(String url, {bool forceRefresh = false}) async {
if (!forceRefresh && isImageCached(url)) { if (!forceRefresh && isImageCached(url)) {
final cachedFile = File(getCachedImagePath(url)); final cachedFile = File(getCachedImagePath(url));
if (await _isFileValid(cachedFile)) { if (await _isFileValid(cachedFile)) {
return cachedFile; return cachedFile;
} else { } else {
await cachedFile.delete(); await cachedFile.delete();
} }
} }
@@ -59,8 +56,9 @@ class ImageCacheService {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final file = File(getCachedImagePath(url)); final file = File(getCachedImagePath(url));
await file.writeAsBytes(response.bodyBytes); // Сжимаем данные перед сохранением
final compressedData = _lz4Codec.encode(response.bodyBytes);
await file.writeAsBytes(compressedData);
await _updateFileAccessTime(file); await _updateFileAccessTime(file);
@@ -73,38 +71,44 @@ class ImageCacheService {
return null; return null;
} }
Future<Uint8List?> loadImageAsBytes( Future<Uint8List?> loadImageAsBytes(
String url, { String url, {
bool forceRefresh = false, bool forceRefresh = false,
}) async { }) async {
final file = await loadImage(url, forceRefresh: forceRefresh); final file = await loadImage(url, forceRefresh: forceRefresh);
if (file != null) { 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; return null;
} }
Future<void> preloadImage(String url) async { Future<void> preloadImage(String url) async {
await loadImage(url); await loadImage(url);
} }
Future<void> preloadContactAvatar(String? photoUrl) async { Future<void> preloadContactAvatar(String? photoUrl) async {
if (photoUrl != null && photoUrl.isNotEmpty) { if (photoUrl != null && photoUrl.isNotEmpty) {
await preloadImage(photoUrl); await preloadImage(photoUrl);
} }
} }
Future<void> preloadProfileAvatar(String? photoUrl) async { Future<void> preloadProfileAvatar(String? photoUrl) async {
if (photoUrl != null && photoUrl.isNotEmpty) { if (photoUrl != null && photoUrl.isNotEmpty) {
await preloadImage(photoUrl); await preloadImage(photoUrl);
} }
} }
Future<void> preloadContactAvatars(List<String?> photoUrls) async { Future<void> preloadContactAvatars(List<String?> photoUrls) async {
final futures = photoUrls final futures = photoUrls
.where((url) => url != null && url.isNotEmpty) .where((url) => url != null && url.isNotEmpty)
@@ -116,14 +120,46 @@ class ImageCacheService {
} }
} }
Future<void> clearCache() async { Future<void> clearCache() async {
if (_cacheDirectory.existsSync()) { if (_cacheDirectory.existsSync()) {
await _cacheDirectory.delete(recursive: true); await _clearDirectoryContents(_cacheDirectory);
await _cacheDirectory.create(recursive: true);
} }
} }
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<int> getCacheSize() async { Future<int> getCacheSize() async {
int totalSize = 0; int totalSize = 0;
@@ -137,7 +173,6 @@ class ImageCacheService {
return totalSize; return totalSize;
} }
Future<int> getCacheFileCount() async { Future<int> getCacheFileCount() async {
int count = 0; int count = 0;
if (_cacheDirectory.existsSync()) { if (_cacheDirectory.existsSync()) {
@@ -150,7 +185,6 @@ class ImageCacheService {
return count; return count;
} }
Future<void> _cleanupExpiredCache() async { Future<void> _cleanupExpiredCache() async {
if (!_cacheDirectory.existsSync()) return; if (!_cacheDirectory.existsSync()) return;
@@ -161,18 +195,15 @@ class ImageCacheService {
} }
} }
Future<bool> _isFileValid(File file) async { Future<bool> _isFileValid(File file) async {
if (!file.existsSync()) return false; if (!file.existsSync()) return false;
final stat = await file.stat(); final stat = await file.stat();
final age = DateTime.now().difference(stat.modified); final age = DateTime.now().difference(stat.modified);
return age < _cacheExpiration; return age < _cacheExpiration;
} }
Future<bool> _isFileExpired(File file) async { Future<bool> _isFileExpired(File file) async {
if (!file.existsSync()) return false; if (!file.existsSync()) return false;
@@ -182,19 +213,13 @@ class ImageCacheService {
return age >= _cacheExpiration; return age >= _cacheExpiration;
} }
Future<void> _updateFileAccessTime(File file) async { Future<void> _updateFileAccessTime(File file) async {
try { try {
await file.setLastModified(DateTime.now()); await file.setLastModified(DateTime.now());
} catch (e) { } catch (e) {}
}
} }
String _generateFileName(String url) { String _generateFileName(String url) {
final hash = url.hashCode.abs().toString(); final hash = url.hashCode.abs().toString();
final extension = path.extension(url).isNotEmpty final extension = path.extension(url).isNotEmpty
? path.extension(url) ? path.extension(url)
@@ -203,7 +228,6 @@ class ImageCacheService {
return '$hash$extension'; return '$hash$extension';
} }
Future<Map<String, dynamic>> getCacheStats() async { Future<Map<String, dynamic>> getCacheStats() async {
final size = await getCacheSize(); final size = await getCacheSize();
final fileCount = await getCacheFileCount(); final fileCount = await getCacheFileCount();
@@ -213,13 +237,13 @@ class ImageCacheService {
'cache_size_mb': (size / (1024 * 1024)).toStringAsFixed(2), 'cache_size_mb': (size / (1024 * 1024)).toStringAsFixed(2),
'file_count': fileCount, 'file_count': fileCount,
'cache_directory': _cacheDirectory.path, 'cache_directory': _cacheDirectory.path,
'compression_enabled': true,
'compression_algorithm': 'LZ4',
}; };
} }
} }
extension CachedImageExtension on String { extension CachedImageExtension on String {
Widget getCachedNetworkImage({ Widget getCachedNetworkImage({
Key? key, Key? key,
double? width, double? width,
@@ -259,7 +283,6 @@ extension CachedImageExtension on String {
); );
} }
Future<void> preloadImage() async { Future<void> preloadImage() async {
await ImageCacheService.instance.loadImage(this); await ImageCacheService.instance.loadImage(this);
} }

View File

@@ -1,5 +1,3 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
@@ -10,7 +8,6 @@ import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:gwid/services/cache_service.dart'; import 'package:gwid/services/cache_service.dart';
import 'package:path/path.dart' as p;
class AvatarCacheService { class AvatarCacheService {
static final AvatarCacheService _instance = AvatarCacheService._internal(); static final AvatarCacheService _instance = AvatarCacheService._internal();
@@ -19,45 +16,37 @@ class AvatarCacheService {
final CacheService _cacheService = CacheService(); final CacheService _cacheService = CacheService();
final Map<String, Uint8List> _imageMemoryCache = {}; final Map<String, Uint8List> _imageMemoryCache = {};
final Map<String, DateTime> _imageCacheTimestamps = {}; final Map<String, DateTime> _imageCacheTimestamps = {};
static const Duration _imageTTL = Duration(days: 7); static const Duration _imageTTL = Duration(days: 7);
static const int _maxMemoryImages = 50; static const int _maxMemoryImages = 50;
static const int _maxImageSizeMB = 5; static const int _maxImageSizeMB = 5;
Future<void> initialize() async { Future<void> initialize() async {
await _cacheService.initialize(); await _cacheService.initialize();
print('AvatarCacheService инициализирован'); print('AvatarCacheService инициализирован');
} }
Future<ImageProvider?> getAvatar(String? avatarUrl, {int? userId}) async { Future<ImageProvider?> getAvatar(String? avatarUrl, {int? userId}) async {
if (avatarUrl == null || avatarUrl.isEmpty) { if (avatarUrl == null || avatarUrl.isEmpty) {
return null; return null;
} }
try { try {
final cacheKey = _generateCacheKey(avatarUrl, userId); final cacheKey = _generateCacheKey(avatarUrl, userId);
if (_imageMemoryCache.containsKey(cacheKey)) { if (_imageMemoryCache.containsKey(cacheKey)) {
final timestamp = _imageCacheTimestamps[cacheKey]; final timestamp = _imageCacheTimestamps[cacheKey];
if (timestamp != null && !_isExpired(timestamp, _imageTTL)) { if (timestamp != null && !_isExpired(timestamp, _imageTTL)) {
final imageData = _imageMemoryCache[cacheKey]!; final imageData = _imageMemoryCache[cacheKey]!;
return MemoryImage(imageData); return MemoryImage(imageData);
} else { } else {
_imageMemoryCache.remove(cacheKey); _imageMemoryCache.remove(cacheKey);
_imageCacheTimestamps.remove(cacheKey); _imageCacheTimestamps.remove(cacheKey);
} }
} }
final cachedFile = await _cacheService.getCachedFile( final cachedFile = await _cacheService.getCachedFile(
avatarUrl, avatarUrl,
customKey: cacheKey, customKey: cacheKey,
@@ -65,11 +54,9 @@ class AvatarCacheService {
if (cachedFile != null && await cachedFile.exists()) { if (cachedFile != null && await cachedFile.exists()) {
final imageData = await cachedFile.readAsBytes(); final imageData = await cachedFile.readAsBytes();
_imageMemoryCache[cacheKey] = imageData; _imageMemoryCache[cacheKey] = imageData;
_imageCacheTimestamps[cacheKey] = DateTime.now(); _imageCacheTimestamps[cacheKey] = DateTime.now();
if (_imageMemoryCache.length > _maxMemoryImages) { if (_imageMemoryCache.length > _maxMemoryImages) {
await _evictOldestImages(); await _evictOldestImages();
} }
@@ -77,13 +64,10 @@ class AvatarCacheService {
return MemoryImage(imageData); return MemoryImage(imageData);
} }
final imageData = await _downloadImage(avatarUrl); final imageData = await _downloadImage(avatarUrl);
if (imageData != null) { if (imageData != null) {
await _cacheService.cacheFile(avatarUrl, customKey: cacheKey); await _cacheService.cacheFile(avatarUrl, customKey: cacheKey);
_imageMemoryCache[cacheKey] = imageData; _imageMemoryCache[cacheKey] = imageData;
_imageCacheTimestamps[cacheKey] = DateTime.now(); _imageCacheTimestamps[cacheKey] = DateTime.now();
@@ -96,7 +80,6 @@ class AvatarCacheService {
return null; return null;
} }
Future<File?> getAvatarFile(String? avatarUrl, {int? userId}) async { Future<File?> getAvatarFile(String? avatarUrl, {int? userId}) async {
if (avatarUrl == null || avatarUrl.isEmpty) { if (avatarUrl == null || avatarUrl.isEmpty) {
return null; return null;
@@ -111,21 +94,18 @@ class AvatarCacheService {
} }
} }
Future<void> preloadAvatars(List<String> avatarUrls) async { Future<void> preloadAvatars(List<String> avatarUrls) async {
final futures = avatarUrls.map((url) => getAvatar(url)); final futures = avatarUrls.map((url) => getAvatar(url));
await Future.wait(futures); await Future.wait(futures);
print('Предзагружено ${avatarUrls.length} аватарок'); print('Предзагружено ${avatarUrls.length} аватарок');
} }
Future<Uint8List?> _downloadImage(String url) async { Future<Uint8List?> _downloadImage(String url) async {
try { try {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
final imageData = response.bodyBytes; final imageData = response.bodyBytes;
if (imageData.length > _maxImageSizeMB * 1024 * 1024) { if (imageData.length > _maxImageSizeMB * 1024 * 1024) {
print('Изображение слишком большое: ${imageData.length} байт'); print('Изображение слишком большое: ${imageData.length} байт');
return null; return null;
@@ -139,7 +119,6 @@ class AvatarCacheService {
return null; return null;
} }
String _generateCacheKey(String url, int? userId) { String _generateCacheKey(String url, int? userId) {
if (userId != null) { if (userId != null) {
return 'avatar_${userId}_${_hashUrl(url)}'; return 'avatar_${userId}_${_hashUrl(url)}';
@@ -147,23 +126,19 @@ class AvatarCacheService {
return 'avatar_${_hashUrl(url)}'; return 'avatar_${_hashUrl(url)}';
} }
String _hashUrl(String url) { String _hashUrl(String url) {
final bytes = utf8.encode(url); final bytes = utf8.encode(url);
final digest = sha256.convert(bytes); final digest = sha256.convert(bytes);
return digest.toString().substring(0, 16); return digest.toString().substring(0, 16);
} }
bool _isExpired(DateTime timestamp, Duration ttl) { bool _isExpired(DateTime timestamp, Duration ttl) {
return DateTime.now().difference(timestamp) > ttl; return DateTime.now().difference(timestamp) > ttl;
} }
Future<void> _evictOldestImages() async { Future<void> _evictOldestImages() async {
if (_imageMemoryCache.isEmpty) return; if (_imageMemoryCache.isEmpty) return;
final sortedEntries = _imageCacheTimestamps.entries.toList() final sortedEntries = _imageCacheTimestamps.entries.toList()
..sort((a, b) => a.value.compareTo(b.value)); ..sort((a, b) => a.value.compareTo(b.value));
@@ -175,18 +150,15 @@ class AvatarCacheService {
} }
} }
Future<void> clearAvatarCache() async { Future<void> clearAvatarCache() async {
_imageMemoryCache.clear(); _imageMemoryCache.clear();
_imageCacheTimestamps.clear(); _imageCacheTimestamps.clear();
try { try {
final cacheDir = await getApplicationCacheDirectory(); final cacheDir = await getApplicationCacheDirectory();
final avatarDir = Directory('${cacheDir.path}/avatars'); final avatarDir = Directory('${cacheDir.path}/avatars');
if (await avatarDir.exists()) { if (await avatarDir.exists()) {
await avatarDir.delete(recursive: true); await _clearDirectoryContents(avatarDir);
await avatarDir.create(recursive: true);
} }
} catch (e) { } catch (e) {
print('Ошибка очистки кэша аватарок: $e'); print('Ошибка очистки кэша аватарок: $e');
@@ -195,23 +167,54 @@ class AvatarCacheService {
print('Кэш аватарок очищен'); 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 { Future<void> removeAvatarFromCache(String avatarUrl, {int? userId}) async {
try { try {
final cacheKey = _generateCacheKey(avatarUrl, userId); final cacheKey = _generateCacheKey(avatarUrl, userId);
_imageMemoryCache.remove(cacheKey); _imageMemoryCache.remove(cacheKey);
_imageCacheTimestamps.remove(cacheKey); _imageCacheTimestamps.remove(cacheKey);
await _cacheService.removeCachedFile(avatarUrl, customKey: cacheKey); await _cacheService.removeCachedFile(avatarUrl, customKey: cacheKey);
} catch (e) { } catch (e) {
print('Ошибка удаления аватарки из кэша: $e'); print('Ошибка удаления аватарки из кэша: $e');
} }
} }
Future<Map<String, dynamic>> getAvatarCacheStats() async { Future<Map<String, dynamic>> getAvatarCacheStats() async {
try { try {
final memoryImages = _imageMemoryCache.length; final memoryImages = _imageMemoryCache.length;
@@ -221,7 +224,6 @@ class AvatarCacheService {
totalMemorySize += imageData.length; totalMemorySize += imageData.length;
} }
int diskSize = 0; int diskSize = 0;
try { try {
final cacheDir = await getApplicationCacheDirectory(); final cacheDir = await getApplicationCacheDirectory();
@@ -250,12 +252,10 @@ class AvatarCacheService {
} }
} }
Future<bool> hasAvatarInCache(String avatarUrl, {int? userId}) async { Future<bool> hasAvatarInCache(String avatarUrl, {int? userId}) async {
try { try {
final cacheKey = _generateCacheKey(avatarUrl, userId); final cacheKey = _generateCacheKey(avatarUrl, userId);
if (_imageMemoryCache.containsKey(cacheKey)) { if (_imageMemoryCache.containsKey(cacheKey)) {
final timestamp = _imageCacheTimestamps[cacheKey]; final timestamp = _imageCacheTimestamps[cacheKey];
if (timestamp != null && !_isExpired(timestamp, _imageTTL)) { if (timestamp != null && !_isExpired(timestamp, _imageTTL)) {
@@ -263,7 +263,6 @@ class AvatarCacheService {
} }
} }
return await _cacheService.hasCachedFile(avatarUrl, customKey: cacheKey); return await _cacheService.hasCachedFile(avatarUrl, customKey: cacheKey);
} catch (e) { } catch (e) {
print('Ошибка проверки существования аватарки в кэше: $e'); print('Ошибка проверки существования аватарки в кэше: $e');
@@ -271,7 +270,6 @@ class AvatarCacheService {
} }
} }
Widget getAvatarWidget( Widget getAvatarWidget(
String? avatarUrl, { String? avatarUrl, {
int? userId, int? userId,
@@ -310,7 +308,6 @@ class AvatarCacheService {
); );
} }
Widget _buildFallbackAvatar( Widget _buildFallbackAvatar(
String? text, String? text,
double size, double size,

View File

@@ -1,9 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:es_compression/lz4.dart';
class CacheService { class CacheService {
static final CacheService _instance = CacheService._internal(); static final CacheService _instance = CacheService._internal();
@@ -20,6 +22,21 @@ class CacheService {
Directory? _cacheDirectory; 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 { Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance(); _prefs = await SharedPreferences.getInstance();
_cacheDirectory = await getApplicationCacheDirectory(); _cacheDirectory = await getApplicationCacheDirectory();
@@ -127,33 +144,44 @@ class CacheService {
} }
Future<void> clear() async { Future<void> clear() async {
_memoryCache.clear(); // Синхронизируем операцию очистки кэша
_cacheTimestamps.clear(); return _synchronized(_clearLock, () async {
_memoryCache.clear();
_cacheTimestamps.clear();
if (_prefs != null) { if (_prefs != null) {
try { try {
final keys = _prefs!.getKeys().where((key) => key.startsWith('cache_')); final keys = _prefs!.getKeys().where(
for (final key in keys) { (key) => key.startsWith('cache_'),
await _prefs!.remove(key); );
} for (final key in keys) {
} catch (e) { await _prefs!.remove(key);
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');
} }
} 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) { bool _isExpired(DateTime timestamp, Duration ttl) {
@@ -212,7 +240,9 @@ class CacheService {
final response = await http.get(Uri.parse(url)); final response = await http.get(Uri.parse(url));
if (response.statusCode == 200) { if (response.statusCode == 200) {
await existingFile.writeAsBytes(response.bodyBytes); // Сжимаем данные перед сохранением
final compressedData = _lz4Codec.encode(response.bodyBytes);
await existingFile.writeAsBytes(compressedData);
return filePath; return filePath;
} }
} catch (e) { } catch (e) {
@@ -276,6 +306,25 @@ class CacheService {
return file != null; 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 { Future<Map<String, dynamic>> getDetailedCacheStats() async {
final memorySize = _memoryCache.length; final memorySize = _memoryCache.length;
final cacheSize = await getCacheSize(); final cacheSize = await getCacheSize();
@@ -292,6 +341,41 @@ class CacheService {
Future<void> removeCachedFile(String url, {String? customKey}) async {} 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 { Future<Map<String, dynamic>> getCacheStats() async {
final sizes = await getCacheSize(); final sizes = await getCacheSize();
final memoryEntries = _memoryCache.length; final memoryEntries = _memoryCache.length;
@@ -304,6 +388,8 @@ class CacheService {
'memorySize': sizes['memory'], 'memorySize': sizes['memory'],
'filesSizeMB': (sizes['files']! / (1024 * 1024)).toStringAsFixed(2), 'filesSizeMB': (sizes['files']! / (1024 * 1024)).toStringAsFixed(2),
'maxMemorySize': _maxMemoryCacheSize, 'maxMemorySize': _maxMemoryCacheSize,
'compression_enabled': true,
'compression_algorithm': 'LZ4',
}; };
} }
@@ -361,10 +447,12 @@ class CacheService {
await audioDir.create(recursive: true); 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(); final fileSize = await existingFile.length();
print( print(
'CacheService: Audio cached successfully: $filePath (size: $fileSize bytes)', 'CacheService: Audio cached successfully: $filePath (compressed size: $fileSize bytes)',
); );
return filePath; return filePath;
} else { } else {
@@ -412,4 +500,26 @@ class CacheService {
final file = await getCachedAudioFile(url, customKey: customKey); final file = await getCachedAudioFile(url, customKey: customKey);
return file != null; 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;
}
} }