оптимизация: слегоньца набурмалдил
This commit is contained in:
@@ -512,12 +512,15 @@ class ChatsModule {
|
||||
if (accountId == null) return;
|
||||
|
||||
final dialogRows = await AppDatabase.loadDialogChats(accountId);
|
||||
final byParticipant = <int, List<Map<String, dynamic>>>{};
|
||||
final byParticipant =
|
||||
<int, List<({Map<String, dynamic> row, CachedChat cached})>>{};
|
||||
for (final row in dialogRows) {
|
||||
final cached = CachedChat.fromDbRow(row);
|
||||
for (final pid in cached.participants.keys) {
|
||||
if (pid == accountId) continue;
|
||||
byParticipant.putIfAbsent(pid, () => []).add(row);
|
||||
byParticipant
|
||||
.putIfAbsent(pid, () => [])
|
||||
.add((row: row, cached: cached));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,8 +532,9 @@ class ChatsModule {
|
||||
final options = ContactCache.getOptions(contactId) ?? const <String>{};
|
||||
final affected = byParticipant[contactId];
|
||||
if (affected == null) continue;
|
||||
for (final row in affected) {
|
||||
final cached = CachedChat.fromDbRow(row);
|
||||
for (final entry in affected) {
|
||||
final row = entry.row;
|
||||
final cached = entry.cached;
|
||||
final sameTitle = cached.title == name;
|
||||
final sameAvatar = (cached.iconUrl ?? '') == (avatar ?? '');
|
||||
final sameOptions = cached.options.length == options.length &&
|
||||
|
||||
@@ -44,10 +44,13 @@ class FoldersModule {
|
||||
List<dynamic>? foldersOrder,
|
||||
) {
|
||||
if (foldersOrder == null || foldersOrder.isEmpty) return;
|
||||
final orderedIds = foldersOrder.map((id) => id.toString()).toList();
|
||||
final orderIndex = <String, int>{};
|
||||
for (var i = 0; i < foldersOrder.length; i++) {
|
||||
orderIndex.putIfAbsent(foldersOrder[i].toString(), () => i);
|
||||
}
|
||||
folders.sort((a, b) {
|
||||
final aIndex = orderedIds.indexOf(a.id);
|
||||
final bIndex = orderedIds.indexOf(b.id);
|
||||
final aIndex = orderIndex[a.id] ?? -1;
|
||||
final bIndex = orderIndex[b.id] ?? -1;
|
||||
if (aIndex == -1 && bIndex == -1) return 0;
|
||||
if (aIndex == -1) return 1;
|
||||
if (bIndex == -1) return -1;
|
||||
|
||||
@@ -92,25 +92,31 @@ const int _compressionThreshold = 32;
|
||||
/// `0` — без сжатия, иначе `(rawLen ~/ compLen) + 1` (множитель размера, по
|
||||
/// которому получатель выделяет буфер под распаковку).
|
||||
Uint8List packPacket(int opcode, Map<dynamic, dynamic> payload, {int seq = 0}) {
|
||||
final header = ByteData(headerSize);
|
||||
final Uint8List raw = msgpack.serialize(payload);
|
||||
|
||||
final List<int> body;
|
||||
final int flag;
|
||||
if (raw.length < _compressionThreshold) {
|
||||
body = raw;
|
||||
flag = 0;
|
||||
} else {
|
||||
body = lz4Compress(raw);
|
||||
flag = (raw.length ~/ body.length) + 1;
|
||||
}
|
||||
|
||||
final out = Uint8List(headerSize + body.length);
|
||||
final header = ByteData.view(out.buffer, out.offsetInBytes, headerSize);
|
||||
header.setUint8(0, 10);
|
||||
header.setUint8(1, CmdType.request);
|
||||
header.setUint16(2, seq, Endian.big);
|
||||
header.setUint16(4, opcode, Endian.big);
|
||||
|
||||
final raw = Uint8List.fromList(msgpack.serialize(payload));
|
||||
|
||||
if (raw.length < _compressionThreshold) {
|
||||
header.setUint32(6, raw.length & 0xFFFFFF, Endian.big);
|
||||
return Uint8List.fromList(header.buffer.asUint8List() + raw);
|
||||
}
|
||||
|
||||
final compressed = lz4Compress(raw);
|
||||
final compLen = compressed.length;
|
||||
final flag = (raw.length ~/ compLen) + 1;
|
||||
header.setUint32(6, ((flag & 0xFF) << 24) | (compLen & 0xFFFFFF), Endian.big);
|
||||
|
||||
return Uint8List.fromList(header.buffer.asUint8List() + compressed);
|
||||
header.setUint32(
|
||||
6,
|
||||
((flag & 0xFF) << 24) | (body.length & 0xFFFFFF),
|
||||
Endian.big,
|
||||
);
|
||||
out.setRange(headerSize, out.length, body);
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Распаковка пакета от сервера
|
||||
|
||||
@@ -435,17 +435,20 @@ class AppDatabase {
|
||||
// Chats cache
|
||||
|
||||
static Future<void> saveChats(List<Map<String, dynamic>> rows) async {
|
||||
if (rows.isEmpty) return;
|
||||
try {
|
||||
final db = await _instance;
|
||||
final batch = db.batch();
|
||||
for (final row in rows) {
|
||||
batch.insert(
|
||||
'chats_cache',
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
await db.transaction((txn) async {
|
||||
final batch = txn.batch();
|
||||
for (final row in rows) {
|
||||
batch.insert(
|
||||
'chats_cache',
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
} catch (e) {
|
||||
logger.e("Ошибка при сохранении чата: $e");
|
||||
}
|
||||
|
||||
@@ -7,46 +7,73 @@ import '../utils/logger.dart';
|
||||
/// Копит сырые байты из сокета, нарезает их на байтовые срезы целых пакетов.
|
||||
class PacketReceiver {
|
||||
Uint8List _buffer = Uint8List(0);
|
||||
int _start = 0;
|
||||
int _end = 0;
|
||||
|
||||
static const int _maxBufferSize = 2 * 1024 * 1024; // 2 мегабуйта
|
||||
|
||||
/// Добавляет байты в буфер и возвращает все собранные пакеты как сырые срезы.
|
||||
/// Полностью синхронный — нарезка не блокируется на распаковке, поэтому
|
||||
/// конкурентные вызовы из stream-листенера не могут пересечься на `_buffer`.
|
||||
///
|
||||
/// Накопление идёт без перекопирования всего буфера на каждый чанк: целые
|
||||
/// пакеты отдаются как `sublistView`, а потреблённый префикс отбрасывается
|
||||
/// сдвигом указателя `_start`, а не пересборкой буфера.
|
||||
List<Uint8List> feed(Uint8List data) {
|
||||
final newBuffer = Uint8List(_buffer.length + data.length);
|
||||
newBuffer.setAll(0, _buffer);
|
||||
newBuffer.setAll(_buffer.length, data);
|
||||
_buffer = newBuffer;
|
||||
_append(data);
|
||||
|
||||
if (_buffer.length > _maxBufferSize) {
|
||||
if (_end - _start > _maxBufferSize) {
|
||||
logger.e(
|
||||
'PacketReceiver: переполнение буфера (${_buffer.length} B), сброс',
|
||||
'PacketReceiver: переполнение буфера (${_end - _start} B), сброс',
|
||||
);
|
||||
reset();
|
||||
return const [];
|
||||
}
|
||||
|
||||
final packets = <Uint8List>[];
|
||||
while (_buffer.length >= headerSize) {
|
||||
while (_end - _start >= headerSize) {
|
||||
final bd = ByteData.view(
|
||||
_buffer.buffer,
|
||||
_buffer.offsetInBytes,
|
||||
_buffer.offsetInBytes + _start,
|
||||
headerSize,
|
||||
);
|
||||
final packedLen = bd.getUint32(6, Endian.big);
|
||||
final payloadLength = packedLen & 0xFFFFFF;
|
||||
final totalLength = headerSize + payloadLength;
|
||||
|
||||
if (_buffer.length < totalLength) break;
|
||||
if (_end - _start < totalLength) break;
|
||||
|
||||
packets.add(Uint8List.sublistView(_buffer, 0, totalLength));
|
||||
_buffer = _buffer.sublist(totalLength);
|
||||
packets.add(Uint8List.sublistView(_buffer, _start, _start + totalLength));
|
||||
_start += totalLength;
|
||||
}
|
||||
|
||||
if (_start == _end) {
|
||||
_start = 0;
|
||||
_end = 0;
|
||||
}
|
||||
return packets;
|
||||
}
|
||||
|
||||
void _append(Uint8List data) {
|
||||
final pending = _end - _start;
|
||||
if (pending == 0) {
|
||||
_buffer = Uint8List.fromList(data);
|
||||
_start = 0;
|
||||
_end = data.length;
|
||||
return;
|
||||
}
|
||||
final total = pending + data.length;
|
||||
final newBuffer = Uint8List(total);
|
||||
newBuffer.setRange(0, pending, _buffer, _start);
|
||||
newBuffer.setRange(pending, total, data);
|
||||
_buffer = newBuffer;
|
||||
_start = 0;
|
||||
_end = total;
|
||||
}
|
||||
|
||||
void reset() {
|
||||
_buffer = Uint8List(0);
|
||||
_start = 0;
|
||||
_end = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ class MediaCache {
|
||||
static int get maxBytes => AppMediaCacheLimit.current.value;
|
||||
|
||||
static Directory? _dir;
|
||||
static int? _cachedSize;
|
||||
|
||||
static Future<Directory> _cacheDir() async {
|
||||
final cached = _dir;
|
||||
@@ -81,6 +82,12 @@ class MediaCache {
|
||||
}
|
||||
await sink.close();
|
||||
await part.rename(file.path);
|
||||
final known = _cachedSize;
|
||||
if (known != null) {
|
||||
try {
|
||||
_cachedSize = known + await file.length();
|
||||
} catch (_) {}
|
||||
}
|
||||
await _enforceLimit();
|
||||
return file;
|
||||
} catch (_) {
|
||||
@@ -96,7 +103,18 @@ class MediaCache {
|
||||
}
|
||||
|
||||
/// Суммарный размер кэша в байтах.
|
||||
///
|
||||
/// Результат держится в памяти и поддерживается инкрементально при
|
||||
/// загрузке/очистке/вытеснении — повторные вызовы не пересканируют каталог.
|
||||
static Future<int> currentSize() async {
|
||||
final cached = _cachedSize;
|
||||
if (cached != null) return cached;
|
||||
final total = await _scanSize();
|
||||
_cachedSize = total;
|
||||
return total;
|
||||
}
|
||||
|
||||
static Future<int> _scanSize() async {
|
||||
final dir = await _cacheDir();
|
||||
var total = 0;
|
||||
await for (final entity in dir.list()) {
|
||||
@@ -121,34 +139,43 @@ class MediaCache {
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
_cachedSize = 0;
|
||||
return freed;
|
||||
}
|
||||
|
||||
/// Вытесняет старые файлы (по mtime), пока размер превышает [maxBytes].
|
||||
///
|
||||
/// Под лимитом — ранний выход без сканирования каталога (частый случай).
|
||||
/// Каталог обходится только когда лимит реально превышен.
|
||||
static Future<void> _enforceLimit() async {
|
||||
final limit = maxBytes;
|
||||
if (limit <= 0) return;
|
||||
|
||||
var total = _cachedSize ?? await _scanSize();
|
||||
if (total <= limit) {
|
||||
_cachedSize = total;
|
||||
return;
|
||||
}
|
||||
|
||||
final dir = await _cacheDir();
|
||||
final files = <File>[];
|
||||
var total = 0;
|
||||
await for (final entity in dir.list()) {
|
||||
if (entity is File && !entity.path.endsWith('.part')) {
|
||||
files.add(entity);
|
||||
try {
|
||||
total += await entity.length();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
if (maxBytes <= 0 || total <= maxBytes) return;
|
||||
|
||||
files.sort((a, b) =>
|
||||
a.statSync().modified.compareTo(b.statSync().modified));
|
||||
|
||||
for (final file in files) {
|
||||
if (total <= maxBytes) break;
|
||||
if (total <= limit) break;
|
||||
try {
|
||||
total -= await file.length();
|
||||
await file.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
_cachedSize = total;
|
||||
}
|
||||
|
||||
static String _sanitize(String name) {
|
||||
|
||||
@@ -183,8 +183,12 @@ class _ChatListScreenState extends State<ChatListScreen>
|
||||
|
||||
List<CachedChat> _selectedChatObjects() {
|
||||
if (_selectedChats.isEmpty) return const [];
|
||||
final ids = _selectedChats;
|
||||
return _chats.where((c) => ids.contains(c.id.toString())).toList();
|
||||
final ids = <int>{};
|
||||
for (final s in _selectedChats) {
|
||||
final v = int.tryParse(s);
|
||||
if (v != null) ids.add(v);
|
||||
}
|
||||
return _chats.where((c) => ids.contains(c.id)).toList();
|
||||
}
|
||||
|
||||
_DeleteKind _categorizeChat(CachedChat c, int myId) {
|
||||
|
||||
@@ -136,8 +136,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
Timer? _shimmerStartTimer;
|
||||
bool _historyKickedOff = false;
|
||||
List<CachedMessage> _messages = [];
|
||||
int _messagesRevision = 0;
|
||||
List<Object>? _combinedItemsCache;
|
||||
int? _combinedItemsKey;
|
||||
bool _floatingDateScheduled = false;
|
||||
int _myId = 0;
|
||||
CachedChat? chat;
|
||||
|
||||
@@ -146,7 +148,6 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
late final AnimationController _floatingDateAnimController;
|
||||
late final CurvedAnimation _floatingDateCurved;
|
||||
final Map<int, GlobalKey> _separatorKeys = {};
|
||||
double _lastScrollOffset = 0;
|
||||
String? _lastSentId;
|
||||
|
||||
@override
|
||||
@@ -226,6 +227,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
.toList();
|
||||
setState(() {
|
||||
_messages = first;
|
||||
_messagesRevision++;
|
||||
_isLoading = false;
|
||||
_onLoadingFinished();
|
||||
});
|
||||
@@ -349,7 +351,10 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
final changed = !_listsEquivalent(_messages, merged);
|
||||
if (!changed && !markLoaded) return;
|
||||
setState(() {
|
||||
if (changed) _messages = merged;
|
||||
if (changed) {
|
||||
_messages = merged;
|
||||
_messagesRevision++;
|
||||
}
|
||||
if (markLoaded) {
|
||||
_isLoading = false;
|
||||
_onLoadingFinished();
|
||||
@@ -590,6 +595,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
setState(() {
|
||||
_lastSentId = message.id;
|
||||
_messages.add(message);
|
||||
_messagesRevision++;
|
||||
});
|
||||
_clearTyping(message.senderId);
|
||||
Haptics.tap();
|
||||
@@ -598,11 +604,17 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
case MessageEditedEvent(:final message):
|
||||
final idx = _messages.indexWhere((m) => m.id == message.id);
|
||||
if (idx == -1) return;
|
||||
setState(() => _messages[idx] = message);
|
||||
setState(() {
|
||||
_messages[idx] = message;
|
||||
_messagesRevision++;
|
||||
});
|
||||
case MessageRemovedEvent(:final messageId):
|
||||
final idx = _messages.indexWhere((m) => m.id == messageId);
|
||||
if (idx == -1) return;
|
||||
setState(() => _messages.removeAt(idx));
|
||||
setState(() {
|
||||
_messages.removeAt(idx);
|
||||
_messagesRevision++;
|
||||
});
|
||||
_reactionNotifiers.remove(messageId)?.dispose();
|
||||
case MessageReactionsChangedEvent(:final messageId, :final reactionInfo):
|
||||
_reactionNotifiers[messageId]?.value = reactionInfo;
|
||||
@@ -722,6 +734,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
setState(() {
|
||||
_lastSentId = tempId;
|
||||
_messages.add(tempMessage);
|
||||
_messagesRevision++;
|
||||
_messageController.clear();
|
||||
});
|
||||
unawaited(_persistOutgoing(tempMessage));
|
||||
@@ -748,6 +761,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
setState(() {
|
||||
_messages[index] = sent;
|
||||
_messagesRevision++;
|
||||
});
|
||||
unawaited(_persistOutgoing(sent, removeId: tempId));
|
||||
}
|
||||
@@ -776,6 +790,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
);
|
||||
setState(() {
|
||||
_messages[index] = failed;
|
||||
_messagesRevision++;
|
||||
});
|
||||
unawaited(_persistOutgoing(failed));
|
||||
}
|
||||
@@ -806,48 +821,61 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
if (forwardIds.isEmpty) return;
|
||||
|
||||
final resolved = <int, ({String name, String? avatar})>{};
|
||||
for (final id in forwardIds) {
|
||||
final name = await messagesModule.searchContactById(id);
|
||||
final avatar = ContactCache.getAvatar(id);
|
||||
if (name != null && mounted) {
|
||||
setState(() {
|
||||
for (var i = 0; i < _messages.length; i++) {
|
||||
final msg = _messages[i];
|
||||
if (msg.attachments != null) {
|
||||
final newAttaches = msg.attachments!.map((a) {
|
||||
if (a is ForwardedMessageAttachment &&
|
||||
a.originalSenderId == id &&
|
||||
a.originalSenderName == null) {
|
||||
return ForwardedMessageAttachment(
|
||||
originalSenderId: id,
|
||||
originalSenderName: name,
|
||||
originalSenderAvatar: avatar,
|
||||
originalMessageId: a.originalMessageId,
|
||||
originalTime: a.originalTime,
|
||||
originalText: a.originalText,
|
||||
originalChatId: a.originalChatId,
|
||||
originalAttachments: a.originalAttachments,
|
||||
originalContact: a.originalContact,
|
||||
);
|
||||
}
|
||||
return a;
|
||||
}).toList();
|
||||
_messages[i] = CachedMessage(
|
||||
id: msg.id,
|
||||
accountId: msg.accountId,
|
||||
chatId: msg.chatId,
|
||||
senderId: msg.senderId,
|
||||
text: msg.text,
|
||||
time: msg.time,
|
||||
status: msg.status,
|
||||
payload: msg.payload,
|
||||
attachments: newAttaches,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (name != null) {
|
||||
resolved[id] = (name: name, avatar: ContactCache.getAvatar(id));
|
||||
}
|
||||
}
|
||||
if (resolved.isEmpty || !mounted) return;
|
||||
|
||||
var anyChanged = false;
|
||||
for (var i = 0; i < _messages.length; i++) {
|
||||
final msg = _messages[i];
|
||||
final attaches = msg.attachments;
|
||||
if (attaches == null) continue;
|
||||
|
||||
var msgChanged = false;
|
||||
final newAttaches = attaches.map((a) {
|
||||
if (a is ForwardedMessageAttachment &&
|
||||
a.originalSenderName == null &&
|
||||
resolved.containsKey(a.originalSenderId)) {
|
||||
final r = resolved[a.originalSenderId]!;
|
||||
msgChanged = true;
|
||||
return ForwardedMessageAttachment(
|
||||
originalSenderId: a.originalSenderId,
|
||||
originalSenderName: r.name,
|
||||
originalSenderAvatar: r.avatar,
|
||||
originalMessageId: a.originalMessageId,
|
||||
originalTime: a.originalTime,
|
||||
originalText: a.originalText,
|
||||
originalChatId: a.originalChatId,
|
||||
originalAttachments: a.originalAttachments,
|
||||
originalContact: a.originalContact,
|
||||
);
|
||||
}
|
||||
return a;
|
||||
}).toList();
|
||||
|
||||
if (!msgChanged) continue;
|
||||
anyChanged = true;
|
||||
_messages[i] = CachedMessage(
|
||||
id: msg.id,
|
||||
accountId: msg.accountId,
|
||||
chatId: msg.chatId,
|
||||
senderId: msg.senderId,
|
||||
text: msg.text,
|
||||
time: msg.time,
|
||||
status: msg.status,
|
||||
payload: msg.payload,
|
||||
attachments: newAttaches,
|
||||
);
|
||||
}
|
||||
|
||||
if (anyChanged) {
|
||||
setState(() => _messagesRevision++);
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
@@ -863,7 +891,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
}
|
||||
|
||||
List<Object> _buildCombinedItems() {
|
||||
final key = Object.hashAll(_messages.map(identityHashCode));
|
||||
final key = Object.hash(_messagesRevision, _messages.length);
|
||||
final cached = _combinedItemsCache;
|
||||
if (cached != null && _combinedItemsKey == key) return cached;
|
||||
|
||||
@@ -906,25 +934,22 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
|
||||
void _onScrollForDate() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final currentOffset = _scrollController.position.pixels;
|
||||
final scrollingUp = currentOffset > _lastScrollOffset;
|
||||
_lastScrollOffset = currentOffset;
|
||||
|
||||
_floatingDateTimer?.cancel();
|
||||
|
||||
if (!scrollingUp) {
|
||||
_floatingDateAnimController.reverse();
|
||||
return;
|
||||
}
|
||||
|
||||
_floatingDateTimer = Timer(const Duration(seconds: 2), () {
|
||||
_floatingDateTimer = Timer(const Duration(seconds: 1), () {
|
||||
if (mounted) _floatingDateAnimController.reverse();
|
||||
});
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _updateFloatingDate());
|
||||
|
||||
if (_floatingDateScheduled) return;
|
||||
_floatingDateScheduled = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_floatingDateScheduled = false;
|
||||
_updateFloatingDate();
|
||||
});
|
||||
}
|
||||
|
||||
void _updateFloatingDate() {
|
||||
if (!mounted) return;
|
||||
if (!mounted || _separatorKeys.isEmpty) return;
|
||||
DateTime? result;
|
||||
|
||||
final listRenderBox = _listKey.currentContext?.findRenderObject();
|
||||
@@ -1611,6 +1636,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
setState(() {
|
||||
_lastSentId = tempId;
|
||||
_messages.add(msg);
|
||||
_messagesRevision++;
|
||||
});
|
||||
Haptics.send();
|
||||
_scrollToBottom();
|
||||
@@ -1638,6 +1664,7 @@ class _ChatScreenState extends State<ChatScreen>
|
||||
payload: old.payload,
|
||||
attachments: attachment != null ? [attachment] : old.attachments,
|
||||
);
|
||||
_messagesRevision++;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1924,7 +1924,7 @@ class _VoiceMessageBubble extends StatefulWidget {
|
||||
|
||||
class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
bool _isPlaying = false;
|
||||
double _progress = 0.0;
|
||||
final ValueNotifier<double> _progress = ValueNotifier(0.0);
|
||||
bool _transcriptionVisible = false;
|
||||
String? _transcriptionText;
|
||||
bool _transcriptionLoading = false;
|
||||
@@ -1935,6 +1935,12 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
_transcriptionText = widget.preloadedText;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_progress.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
final min = seconds ~/ 60;
|
||||
final sec = seconds % 60;
|
||||
@@ -2027,18 +2033,14 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
builder: (context, constraints) {
|
||||
return GestureDetector(
|
||||
onTapDown: (details) {
|
||||
setState(() {
|
||||
_progress = (details.localPosition.dx /
|
||||
constraints.maxWidth)
|
||||
.clamp(0.0, 1.0);
|
||||
});
|
||||
_progress.value =
|
||||
(details.localPosition.dx / constraints.maxWidth)
|
||||
.clamp(0.0, 1.0);
|
||||
},
|
||||
onHorizontalDragUpdate: (details) {
|
||||
setState(() {
|
||||
_progress = (details.localPosition.dx /
|
||||
constraints.maxWidth)
|
||||
.clamp(0.0, 1.0);
|
||||
});
|
||||
_progress.value =
|
||||
(details.localPosition.dx / constraints.maxWidth)
|
||||
.clamp(0.0, 1.0);
|
||||
},
|
||||
child: Container(
|
||||
height: 4,
|
||||
@@ -2046,13 +2048,16 @@ class _VoiceMessageBubbleState extends State<_VoiceMessageBubble> {
|
||||
color: waveInactiveColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: _progress.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: waveActiveColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
child: ValueListenableBuilder<double>(
|
||||
valueListenable: _progress,
|
||||
builder: (context, progress, _) => FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: progress.clamp(0.0, 1.0),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: waveActiveColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user