догрузка сообщений в чатах при прокрутке вверх
This commit is contained in:
@@ -726,6 +726,53 @@ extension ApiServiceChats on ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Загружает старые сообщения начиная с указанного timestamp
|
||||||
|
/// [fromTimestamp] - timestamp в миллисекундах самого старого загруженного сообщения
|
||||||
|
/// [backward] - количество сообщений для загрузки (по умолчанию 30)
|
||||||
|
Future<List<Message>> loadOlderMessagesByTimestamp(
|
||||||
|
int chatId,
|
||||||
|
int fromTimestamp, {
|
||||||
|
int backward = 30,
|
||||||
|
}) async {
|
||||||
|
await waitUntilOnline();
|
||||||
|
|
||||||
|
print(
|
||||||
|
"📜 Запрашиваем старые сообщения для чата $chatId начиная с timestamp $fromTimestamp (backward: $backward)",
|
||||||
|
);
|
||||||
|
|
||||||
|
final payload = {
|
||||||
|
"chatId": chatId,
|
||||||
|
"from": fromTimestamp,
|
||||||
|
"forward": 0,
|
||||||
|
"backward": backward,
|
||||||
|
"getMessages": true,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
final int seq = _sendMessage(49, payload);
|
||||||
|
final response = await messages
|
||||||
|
.firstWhere((msg) => msg['seq'] == seq)
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
|
|
||||||
|
if (response['cmd'] == 3) {
|
||||||
|
final error = response['payload'];
|
||||||
|
print('❌ Ошибка получения старых сообщений: $error');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<dynamic> messagesJson = response['payload']?['messages'] ?? [];
|
||||||
|
final messagesList =
|
||||||
|
messagesJson.map((json) => Message.fromJson(json)).toList()
|
||||||
|
..sort((a, b) => a.time.compareTo(b.time));
|
||||||
|
|
||||||
|
print('✅ Получено ${messagesList.length} старых сообщений');
|
||||||
|
return messagesList;
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка при получении старых сообщений: $e');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void sendNavEvent(String event, {int? screenTo, int? screenFrom}) {
|
void sendNavEvent(String event, {int? screenTo, int? screenFrom}) {
|
||||||
if (_userId == null) return;
|
if (_userId == null) return;
|
||||||
|
|
||||||
|
|||||||
@@ -248,23 +248,17 @@ extension ApiServiceContacts on ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<int?> getChatIdByUserId(int userId) async {
|
Future<int?> getChatIdByUserId(int userId) async {
|
||||||
// Используем формулу: chatId = userId1 ^ userId2
|
// ПИДОРИСТИЧЕСКАЯ ФОРМУЛА ОТ ДЕДА chatId = userId1 ^ userId2
|
||||||
// где userId1 - наш ID, userId2 - ID собеседника
|
|
||||||
if (_userId == null) {
|
if (_userId == null) {
|
||||||
print('⚠️ Не удалось вычислить chatId: наш userId не установлен');
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final chatId = _userId! ^ userId;
|
final chatId = _userId! ^ userId;
|
||||||
print('✅ Вычислен chatId для диалога: наш userId=$_userId, собеседник userId=$userId, chatId=$chatId');
|
|
||||||
return chatId;
|
return chatId;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Contact>> fetchContactsByIds(List<int> contactIds) async {
|
Future<List<Contact>> fetchContactsByIds(List<int> contactIds) async {
|
||||||
if (contactIds.isEmpty) {
|
if (contactIds.isEmpty) {
|
||||||
print(
|
|
||||||
'⚠️ [fetchContactsByIds] Пустой список contactIds - пропускаем запрос',
|
|
||||||
);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -670,6 +670,26 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final isAtBottom = positions.first.index == 0;
|
final isAtBottom = positions.first.index == 0;
|
||||||
_isUserAtBottom = isAtBottom;
|
_isUserAtBottom = isAtBottom;
|
||||||
_showScrollToBottomNotifier.value = !isAtBottom;
|
_showScrollToBottomNotifier.value = !isAtBottom;
|
||||||
|
|
||||||
|
// Проверяем, доскроллил ли пользователь до самого старого сообщения (вверх)
|
||||||
|
// При reverse: true, последний визуальный элемент (самый большой index) = самое старое сообщение
|
||||||
|
if (positions.isNotEmpty && _chatItems.isNotEmpty) {
|
||||||
|
final maxIndex = positions.map((p) => p.index).reduce((a, b) => a > b ? a : b);
|
||||||
|
// При reverse: true, когда maxIndex близок к _chatItems.length - 1, мы вверху (старые сообщения)
|
||||||
|
final threshold = _chatItems.length > 5 ? 3 : 1; // Загружаем когда осталось 3 элемента до верха
|
||||||
|
final isNearTop = maxIndex >= _chatItems.length - threshold;
|
||||||
|
|
||||||
|
// Если доскроллили близко к верху и есть еще сообщения, загружаем
|
||||||
|
if (isNearTop && _hasMore && !_isLoadingMore && _messages.isNotEmpty && _oldestLoadedTime != null) {
|
||||||
|
print('📜 Пользователь доскроллил близко к верху (maxIndex: $maxIndex, total: ${_chatItems.length}), загружаем старые сообщения...');
|
||||||
|
// Вызываем после build фазы, чтобы избежать setState() во время build
|
||||||
|
Future.microtask(() {
|
||||||
|
if (mounted && _hasMore && !_isLoadingMore) {
|
||||||
|
_loadMore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -835,6 +855,14 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_messages.clear();
|
_messages.clear();
|
||||||
_messages.addAll(cachedMessages);
|
_messages.addAll(cachedMessages);
|
||||||
|
|
||||||
|
// Устанавливаем _oldestLoadedTime и _hasMore для кэшированных сообщений
|
||||||
|
if (_messages.isNotEmpty) {
|
||||||
|
_oldestLoadedTime = _messages.first.time;
|
||||||
|
// Предполагаем, что могут быть еще сообщения (будет обновлено после загрузки с сервера)
|
||||||
|
_hasMore = true;
|
||||||
|
print('📜 Загружено из кэша: ${_messages.length} сообщений, _oldestLoadedTime=$_oldestLoadedTime');
|
||||||
|
}
|
||||||
|
|
||||||
if (widget.isGroupChat) {
|
if (widget.isGroupChat) {
|
||||||
await _loadGroupParticipants();
|
await _loadGroupParticipants();
|
||||||
}
|
}
|
||||||
@@ -921,7 +949,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
_messages.clear();
|
_messages.clear();
|
||||||
_messages.addAll(slice);
|
_messages.addAll(slice);
|
||||||
_oldestLoadedTime = _messages.isNotEmpty ? _messages.first.time : null;
|
_oldestLoadedTime = _messages.isNotEmpty ? _messages.first.time : null;
|
||||||
_hasMore = allMessages.length > _messages.length;
|
// Если получили максимальное количество сообщений (1000), возможно есть еще
|
||||||
|
// Также проверяем, есть ли сообщения старше самого старого загруженного
|
||||||
|
_hasMore = allMessages.length >= 1000 || allMessages.length > _messages.length;
|
||||||
|
print('📜 Первая загрузка: загружено ${allMessages.length} сообщений, показано ${_messages.length}, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime');
|
||||||
_buildChatItems();
|
_buildChatItems();
|
||||||
_isLoadingHistory = false;
|
_isLoadingHistory = false;
|
||||||
});
|
});
|
||||||
@@ -954,42 +985,80 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadMore() async {
|
Future<void> _loadMore() async {
|
||||||
if (_isLoadingMore || !_hasMore) return;
|
print('📜 _loadMore() вызвана: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore, _oldestLoadedTime=$_oldestLoadedTime');
|
||||||
|
|
||||||
|
if (_isLoadingMore || !_hasMore) {
|
||||||
|
print('📜 _loadMore() пропущена: _isLoadingMore=$_isLoadingMore, _hasMore=$_hasMore');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_messages.isEmpty || _oldestLoadedTime == null) {
|
||||||
|
print('📜 _loadMore() пропущена: _messages.isEmpty=${_messages.isEmpty}, _oldestLoadedTime=$_oldestLoadedTime');
|
||||||
|
_hasMore = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isLoadingMore = true;
|
_isLoadingMore = true;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
|
||||||
final all = await ApiService.instance.getMessageHistory(
|
try {
|
||||||
|
print('📜 Загружаем старые сообщения для chatId=${widget.chatId}, fromTimestamp=$_oldestLoadedTime');
|
||||||
|
// Загружаем старые сообщения начиная с timestamp самого старого загруженного сообщения
|
||||||
|
final olderMessages = await ApiService.instance.loadOlderMessagesByTimestamp(
|
||||||
widget.chatId,
|
widget.chatId,
|
||||||
force: false,
|
_oldestLoadedTime!,
|
||||||
|
backward: 30,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
print('📜 Получено ${olderMessages.length} старых сообщений');
|
||||||
|
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final page = _anyOptimize ? _optPage : _pageSize;
|
if (olderMessages.isEmpty) {
|
||||||
|
// Больше нет старых сообщений
|
||||||
final older = all
|
|
||||||
.where((m) => m.time < (_oldestLoadedTime ?? 1 << 62))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (older.isEmpty) {
|
|
||||||
_hasMore = false;
|
_hasMore = false;
|
||||||
_isLoadingMore = false;
|
_isLoadingMore = false;
|
||||||
setState(() {});
|
setState(() {});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
older.sort((a, b) => a.time.compareTo(b.time));
|
// Фильтруем дубликаты - оставляем только те сообщения, которых еще нет в списке
|
||||||
final take = older.length > page
|
final existingMessageIds = _messages.map((m) => m.id).toSet();
|
||||||
? older.sublist(older.length - page)
|
final newMessages = olderMessages.where((m) => !existingMessageIds.contains(m.id)).toList();
|
||||||
: older;
|
|
||||||
|
|
||||||
_messages.insertAll(0, take);
|
if (newMessages.isEmpty) {
|
||||||
|
// Все сообщения уже есть в списке
|
||||||
|
_hasMore = false;
|
||||||
|
_isLoadingMore = false;
|
||||||
|
setState(() {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
print('📜 Добавляем ${newMessages.length} новых старых сообщений (отфильтровано ${olderMessages.length - newMessages.length} дубликатов)');
|
||||||
|
|
||||||
|
// Добавляем старые сообщения в начало списка
|
||||||
|
_messages.insertAll(0, newMessages);
|
||||||
_oldestLoadedTime = _messages.first.time;
|
_oldestLoadedTime = _messages.first.time;
|
||||||
_hasMore = all.length > _messages.length;
|
|
||||||
|
// Проверяем, есть ли еще сообщения (если получили меньше 30, значит это последние)
|
||||||
|
_hasMore = olderMessages.length >= 30;
|
||||||
|
|
||||||
_buildChatItems();
|
_buildChatItems();
|
||||||
_isLoadingMore = false;
|
_isLoadingMore = false;
|
||||||
|
|
||||||
|
if (mounted) {
|
||||||
setState(() {});
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
_updatePinnedMessage();
|
_updatePinnedMessage();
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Ошибка при загрузке старых сообщений: $e');
|
||||||
|
if (mounted) {
|
||||||
|
_isLoadingMore = false;
|
||||||
|
_hasMore = false;
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _isSameDay(DateTime date1, DateTime date2) {
|
bool _isSameDay(DateTime date1, DateTime date2) {
|
||||||
@@ -1067,6 +1136,16 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_chatItems = items;
|
_chatItems = items;
|
||||||
|
|
||||||
|
// Очищаем ключи для сообщений, которых больше нет в списке
|
||||||
|
final currentMessageIds = _messages.map((m) => m.id).toSet();
|
||||||
|
final keysToRemove = _messageKeys.keys.where((id) => !currentMessageIds.contains(id)).toList();
|
||||||
|
for (final id in keysToRemove) {
|
||||||
|
_messageKeys.remove(id);
|
||||||
|
}
|
||||||
|
if (keysToRemove.isNotEmpty) {
|
||||||
|
print('📜 Очищено ${keysToRemove.length} ключей для удаленных сообщений');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updatePinnedMessage() {
|
void _updatePinnedMessage() {
|
||||||
@@ -2447,11 +2526,8 @@ class _ChatScreenState extends State<ChatScreen> {
|
|||||||
final isLastVisual =
|
final isLastVisual =
|
||||||
index == _chatItems.length - 1;
|
index == _chatItems.length - 1;
|
||||||
|
|
||||||
if (isLastVisual &&
|
// Убрали вызов _loadMore() отсюда - он вызывается из _itemPositionsListener
|
||||||
_hasMore &&
|
// чтобы избежать setState() во время build фазы
|
||||||
!_isLoadingMore) {
|
|
||||||
_loadMore();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item is MessageItem) {
|
if (item is MessageItem) {
|
||||||
final message = item.message;
|
final message = item.message;
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ class CacheService {
|
|||||||
|
|
||||||
Directory? _cacheDirectory;
|
Directory? _cacheDirectory;
|
||||||
|
|
||||||
// LZ4 сжатие для экономии места
|
// LZ4 сжатие для экономии места (может быть null если библиотека недоступна)
|
||||||
final Lz4Codec _lz4Codec = Lz4Codec();
|
Lz4Codec? _lz4Codec;
|
||||||
|
bool _lz4Available = false;
|
||||||
|
|
||||||
// Синхронизация операций очистки кэша
|
// Синхронизация операций очистки кэша
|
||||||
static final _clearLock = Object();
|
static final _clearLock = Object();
|
||||||
@@ -43,6 +44,17 @@ class CacheService {
|
|||||||
|
|
||||||
await _createCacheDirectories();
|
await _createCacheDirectories();
|
||||||
|
|
||||||
|
// Пытаемся инициализировать LZ4, если не получится - используем обычное кэширование
|
||||||
|
try {
|
||||||
|
_lz4Codec = Lz4Codec();
|
||||||
|
_lz4Available = true;
|
||||||
|
print('✅ CacheService: LZ4 compression доступна');
|
||||||
|
} catch (e) {
|
||||||
|
_lz4Codec = null;
|
||||||
|
_lz4Available = false;
|
||||||
|
print('⚠️ CacheService: LZ4 compression недоступна, используется обычное кэширование: $e');
|
||||||
|
}
|
||||||
|
|
||||||
print('CacheService инициализирован');
|
print('CacheService инициализирован');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,13 +252,26 @@ 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) {
|
||||||
// Сжимаем данные перед сохранением
|
// Сжимаем данные перед сохранением, если LZ4 доступна
|
||||||
final compressedData = _lz4Codec.encode(response.bodyBytes);
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
|
try {
|
||||||
|
final compressedData = _lz4Codec!.encode(response.bodyBytes);
|
||||||
await existingFile.writeAsBytes(compressedData);
|
await existingFile.writeAsBytes(compressedData);
|
||||||
|
} catch (e) {
|
||||||
|
// Если сжатие не удалось, сохраняем без сжатия
|
||||||
|
print('⚠️ Ошибка сжатия файла $url, сохраняем без сжатия: $e');
|
||||||
|
await existingFile.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, сохраняем без сжатия
|
||||||
|
await existingFile.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Ошибка кэширования файла $url: $e');
|
print('Ошибка кэширования файла $url: $e');
|
||||||
|
// Не вызываем запрос повторно при ошибке
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -309,17 +334,22 @@ class CacheService {
|
|||||||
Future<Uint8List?> getCachedFileBytes(String url, {String? customKey}) async {
|
Future<Uint8List?> getCachedFileBytes(String url, {String? customKey}) async {
|
||||||
final file = await getCachedFile(url, customKey: customKey);
|
final file = await getCachedFile(url, customKey: customKey);
|
||||||
if (file != null && await file.exists()) {
|
if (file != null && await file.exists()) {
|
||||||
final compressedData = await file.readAsBytes();
|
final fileData = await file.readAsBytes();
|
||||||
|
// Пытаемся декомпрессировать, если LZ4 доступна
|
||||||
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
try {
|
try {
|
||||||
// Декомпрессируем данные
|
final decompressedData = _lz4Codec!.decode(fileData);
|
||||||
final decompressedData = _lz4Codec.decode(compressedData);
|
|
||||||
return Uint8List.fromList(decompressedData);
|
return Uint8List.fromList(decompressedData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
|
// Если декомпрессия не удалась, возможно файл не сжат (старый формат или LZ4 недоступна)
|
||||||
print(
|
print(
|
||||||
'Ошибка декомпрессии файла $url, пробуем прочитать как обычный файл: $e',
|
'⚠️ Ошибка декомпрессии файла $url, пробуем прочитать как обычный файл: $e',
|
||||||
);
|
);
|
||||||
return compressedData;
|
return fileData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, возвращаем данные как есть
|
||||||
|
return fileData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -388,8 +418,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_enabled': _lz4Available,
|
||||||
'compression_algorithm': 'LZ4',
|
'compression_algorithm': _lz4Available ? 'LZ4' : 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -447,9 +477,20 @@ class CacheService {
|
|||||||
await audioDir.create(recursive: true);
|
await audioDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сжимаем аудио данные перед сохранением
|
// Сжимаем аудио данные перед сохранением, если LZ4 доступна
|
||||||
final compressedData = _lz4Codec.encode(response.bodyBytes);
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
|
try {
|
||||||
|
final compressedData = _lz4Codec!.encode(response.bodyBytes);
|
||||||
await existingFile.writeAsBytes(compressedData);
|
await existingFile.writeAsBytes(compressedData);
|
||||||
|
} catch (e) {
|
||||||
|
// Если сжатие не удалось, сохраняем без сжатия
|
||||||
|
print('⚠️ Ошибка сжатия аудио файла $url, сохраняем без сжатия: $e');
|
||||||
|
await existingFile.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, сохраняем без сжатия
|
||||||
|
await existingFile.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
final fileSize = await existingFile.length();
|
final fileSize = await existingFile.length();
|
||||||
print(
|
print(
|
||||||
'CacheService: Audio cached successfully: $filePath (compressed size: $fileSize bytes)',
|
'CacheService: Audio cached successfully: $filePath (compressed size: $fileSize bytes)',
|
||||||
@@ -507,17 +548,22 @@ class CacheService {
|
|||||||
}) async {
|
}) async {
|
||||||
final file = await getCachedAudioFile(url, customKey: customKey);
|
final file = await getCachedAudioFile(url, customKey: customKey);
|
||||||
if (file != null && await file.exists()) {
|
if (file != null && await file.exists()) {
|
||||||
final compressedData = await file.readAsBytes();
|
final fileData = await file.readAsBytes();
|
||||||
|
// Пытаемся декомпрессировать, если LZ4 доступна
|
||||||
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
try {
|
try {
|
||||||
// Декомпрессируем данные
|
final decompressedData = _lz4Codec!.decode(fileData);
|
||||||
final decompressedData = _lz4Codec.decode(compressedData);
|
|
||||||
return Uint8List.fromList(decompressedData);
|
return Uint8List.fromList(decompressedData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
|
// Если декомпрессия не удалась, возможно файл не сжат (старый формат или LZ4 недоступна)
|
||||||
print(
|
print(
|
||||||
'Ошибка декомпрессии аудио файла $url, пробуем прочитать как обычный файл: $e',
|
'⚠️ Ошибка декомпрессии аудио файла $url, пробуем прочитать как обычный файл: $e',
|
||||||
);
|
);
|
||||||
return compressedData;
|
return fileData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, возвращаем данные как есть
|
||||||
|
return fileData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ class ImageCacheService {
|
|||||||
); // Кеш изображений на 7 дней
|
); // Кеш изображений на 7 дней
|
||||||
late Directory _cacheDirectory;
|
late Directory _cacheDirectory;
|
||||||
|
|
||||||
// LZ4 сжатие для экономии места
|
// LZ4 сжатие для экономии места (может быть null если библиотека недоступна)
|
||||||
final Lz4Codec _lz4Codec = Lz4Codec();
|
Lz4Codec? _lz4Codec;
|
||||||
|
bool _lz4Available = false;
|
||||||
|
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
final appDir = await getApplicationDocumentsDirectory();
|
final appDir = await getApplicationDocumentsDirectory();
|
||||||
@@ -29,6 +30,17 @@ class ImageCacheService {
|
|||||||
await _cacheDirectory.create(recursive: true);
|
await _cacheDirectory.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Пытаемся инициализировать LZ4, если не получится - используем обычное кэширование
|
||||||
|
try {
|
||||||
|
_lz4Codec = Lz4Codec();
|
||||||
|
_lz4Available = true;
|
||||||
|
print('✅ LZ4 compression доступна');
|
||||||
|
} catch (e) {
|
||||||
|
_lz4Codec = null;
|
||||||
|
_lz4Available = false;
|
||||||
|
print('⚠️ LZ4 compression недоступна, используется обычное кэширование: $e');
|
||||||
|
}
|
||||||
|
|
||||||
await _cleanupExpiredCache();
|
await _cleanupExpiredCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +68,20 @@ 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));
|
||||||
// Сжимаем данные перед сохранением
|
// Сжимаем данные перед сохранением, если LZ4 доступна
|
||||||
final compressedData = _lz4Codec.encode(response.bodyBytes);
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
|
try {
|
||||||
|
final compressedData = _lz4Codec!.encode(response.bodyBytes);
|
||||||
await file.writeAsBytes(compressedData);
|
await file.writeAsBytes(compressedData);
|
||||||
|
} catch (e) {
|
||||||
|
// Если сжатие не удалось, сохраняем без сжатия
|
||||||
|
print('⚠️ Ошибка сжатия изображения $url, сохраняем без сжатия: $e');
|
||||||
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, сохраняем без сжатия
|
||||||
|
await file.writeAsBytes(response.bodyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
await _updateFileAccessTime(file);
|
await _updateFileAccessTime(file);
|
||||||
|
|
||||||
@@ -77,17 +100,22 @@ class ImageCacheService {
|
|||||||
}) async {
|
}) async {
|
||||||
final file = await loadImage(url, forceRefresh: forceRefresh);
|
final file = await loadImage(url, forceRefresh: forceRefresh);
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
final compressedData = await file.readAsBytes();
|
final fileData = await file.readAsBytes();
|
||||||
|
// Пытаемся декомпрессировать, если LZ4 доступна
|
||||||
|
if (_lz4Available && _lz4Codec != null) {
|
||||||
try {
|
try {
|
||||||
// Декомпрессируем данные
|
final decompressedData = _lz4Codec!.decode(fileData);
|
||||||
final decompressedData = _lz4Codec.decode(compressedData);
|
|
||||||
return Uint8List.fromList(decompressedData);
|
return Uint8List.fromList(decompressedData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Если декомпрессия не удалась, возможно файл не сжат (старый формат)
|
// Если декомпрессия не удалась, возможно файл не сжат (старый формат или LZ4 недоступна)
|
||||||
print(
|
print(
|
||||||
'Ошибка декомпрессии изображения $url, пробуем прочитать как обычный файл: $e',
|
'⚠️ Ошибка декомпрессии изображения $url, пробуем прочитать как обычный файл: $e',
|
||||||
);
|
);
|
||||||
return compressedData;
|
return fileData;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// LZ4 недоступна, возвращаем данные как есть
|
||||||
|
return fileData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -237,8 +265,8 @@ 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_enabled': _lz4Available,
|
||||||
'compression_algorithm': 'LZ4',
|
'compression_algorithm': _lz4Available ? 'LZ4' : 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user