diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index b23df101..be452a8f 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -358,6 +358,13 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent , m_recorder(recorder) { auto *layout = new QVBoxLayout(this); + m_imagePreviewTimer.setSingleShot(true); + connect(&m_imagePreviewTimer, &QTimer::timeout, this, &MessageList::prepareImagePreviews); + connect(m_model, &QAbstractItemModel::rowsInserted, this, [this]() { m_imagePreviewTimer.start(0); }); + connect(m_model, &QAbstractItemModel::dataChanged, this, + [this](const QModelIndex&, const QModelIndex&, const QVector& roles) { + if (roles.isEmpty() || roles.contains(MsgVoiceFileRole) || roles.contains(MsgContentTypeRole)) m_imagePreviewTimer.start(0); + }); if (auto* manager = AvatarManager::instance()) connect(manager, &AvatarManager::changed, this, [this, manager](const QString& channel, quint64 owner) { if (channel != m_currentChannelId && !isDmChannel(m_currentChannelId)) return; @@ -1230,6 +1237,43 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) { } /* Общие превью для каналов/PM; не более двух decoder workers, без запуска из paint. */ +void MessageList::prepareImagePreviews() { + for (int row = 0; row < m_model->rowCount(); ++row) { + auto* item = m_model->item(row); + if (!item->data(MsgContentTypeRole).toString().startsWith("image/")) continue; + const QString path = item->data(MsgVoiceFileRole).toString(); + if (path.isEmpty() || item->data(MsgImagePreviewRole).isValid() || item->data(MsgImageErrorRole).toBool()) continue; + if (const auto* image = m_imagePreviews.object(path)) { + item->setData(*image, MsgImagePreviewRole); item->setData(image->isNull(), MsgImageErrorRole); continue; + } + if (m_imageJobs.contains(path)) continue; + m_imageJobs.insert(path); m_thumbnailPool.setMaxThreadCount(2); + QPointer self(this); + QObject* dispatcher = QCoreApplication::instance(); + m_thumbnailPool.start([self, dispatcher, path]() { + QImageReader reader(path); reader.setAutoTransform(true); + const QSize size = reader.size(); + QImage image; + if (size.isValid() && size.width() <= 16384 && size.height() <= 16384) { + reader.setScaledSize(size.scaled(600, 600, Qt::KeepAspectRatio)); image = reader.read(); + } + if (image.isNull()) DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "image preview failed path=%s dimensions=%dx%d error=%s", + qPrintable(path), size.width(), size.height(), qPrintable(reader.errorString())); + else DEBUG_DEBUG(DEBUG_CATEGORY_MEDIA, "image preview ready path=%s dimensions=%dx%d", qPrintable(path), image.width(), image.height()); + QMetaObject::invokeMethod(dispatcher, [self, path, image]() { + if (!self) return; + self->m_imageJobs.remove(path); + self->m_imagePreviews.insert(path, new QImage(image), qMax(1, (int)image.sizeInBytes())); + for (int r = 0; r < self->m_model->rowCount(); ++r) { + auto* current = self->m_model->item(r); + if (current->data(MsgVoiceFileRole).toString() != path) continue; + current->setData(image, MsgImagePreviewRole); current->setData(image.isNull(), MsgImageErrorRole); + } + }, Qt::QueuedConnection); + }); + } +} + void MessageList::prepareVideoThumbnail(const QString& path) { if (path.isEmpty() || m_thumbnailJobs.contains(path) || !QFile::exists(path) || QFile::exists(path + ".jpg")) return; m_thumbnailPool.setMaxThreadCount(2); diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index cd8d6627..6ce6573b 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -14,6 +14,8 @@ #include #include #include +#include +#include #include "stream_sources.h" class ChatView; @@ -138,8 +140,12 @@ private: QList m_streamSources; // Проверенный полный снимок выбранной группы. RadioPanel* m_radioPanel = nullptr; /* верхняя панель устройств, VAD и PTT */ bool m_radioOn = false; // Локальный audio engine успешно включён для текущей группы. - QThreadPool m_thumbnailPool; // До двух FFmpeg workers; деструктор pool дожидается jobs. + QThreadPool m_thumbnailPool; // До двух media workers; деструктор pool дожидается jobs. QSet m_thumbnailJobs; // Пути незавершённых jobs, без дублей. + QCache m_imagePreviews{32 * 1024 * 1024}; // Ограниченный кеш decoded-превью по неизменному пути. + QSet m_imageJobs; // Незавершённые image jobs; GUI изменяет, workers получают копию пути. + QTimer m_imagePreviewTimer; // Объединяет изменения модели до одного обхода видимой истории. + void prepareImagePreviews(); // Асинхронное декодирование локальных изображений и обновление актуальных строк модели. QHash m_feedback; // message_id hex → последний полный снимок реакций/опроса. QPointer m_pollDialog; // Один диалог голосования; уничтожение owner безопасно для exec. QString m_pollDialogMessage, m_pendingPoll; // Открытый message_id и ожидаемый запрос состояния.