Добавил отправку файлов ! ! !
This commit is contained in:
@@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user