chatgui/chatgui-android: просмотр изображений с зумом + инфо о медиа

- chatgui: просмотрщик изображений (ImageViewerWindow) с зумом/поворотом
- chatgui: отправка картинок как image с определением разрешения и типа
- chatgui: у видео-пузыря строка info (res·size·bitrate) и бейдж только длительность
- chatgui: контекстное меню медиа (сохранить в файл/скачать)
- chatgui-android: большие emoji-only сообщения, битрейт/разрешение у видео
- chatgui-android: сохранение неактивных интерфейсов как закомментированные секции
- chatgui-android: отправка картинок по расширению (jpg/png/gif/bmp/webp)
- photo_sender: content-type и расширение по типу файла
This commit is contained in:
evgeny committed 2026-09-09 16:18:26 +03:00
1 parent ca16b46404
commit c474c37284
17 files changed
+854 -66

No files matched your search

+137
View File
@@ -23,6 +23,9 @@
#include <QDebug>
#include <QFileInfo>
#include <QFile>
#include <QImageReader>
#include <QMenu>
#include <QFileDialog>
#include <vector>
#include <memory>
#include <thread>
@@ -220,10 +223,46 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
QString d = QString("%1:%2").arg(s / 60).arg(s % 60, 2, 10, QChar('0'));
item->setData(d, MsgVoiceDurationRole);
}
item->setData((qlonglong)durMs, MsgVideoDurationMsRole);
} else if (p.startsWith("w=")) {
item->setData(p.mid(2).toInt(), MsgVideoWidthRole);
} else if (p.startsWith("h=")) {
item->setData(p.mid(2).toInt(), MsgVideoHeightRole);
}
}
}
QJsonObject la = QJsonDocument::fromJson(localAttrs).object();
QString st = la.value("st").toString();
QString fp = la.value("fp").toString();
int dlState = 0;
if (st == "fl" && !fp.isEmpty()) {
dlState = 2;
QString fullPath = mediaDirBase + "/" + channelId + "/" + fp;
item->setData(fullPath, MsgVoiceFileRole);
}
item->setData(dlState, MsgFileDownloadStateRole);
}
if (ct.startsWith("image/")) {
QString raw = QString::fromUtf8(body);
int pipePos = raw.indexOf('|');
QString fname = (pipePos > 0) ? raw.left(pipePos) : raw;
item->setData(fname, MsgFileDisplayNameRole);
if (pipePos > 0) {
QString meta = raw.mid(pipePos + 1);
QStringList parts = meta.split('|');
if (parts.size() >= 1)
item->setData(parts[0].toULongLong(), MsgFileSizeRole);
if (parts.size() >= 3)
item->setData(parts[2].toUInt(), MsgMediaNumBlocksRole);
/* хвост: |w=<w>|h=<h> */
for (const QString& p : parts) {
if (p.startsWith("w=")) item->setData(p.mid(2).toInt(), MsgImageWidthRole);
else if (p.startsWith("h=")) item->setData(p.mid(2).toInt(), MsgImageHeightRole);
}
}
QJsonObject la = QJsonDocument::fromJson(localAttrs).object();
QString st = la.value("st").toString();
QString fp = la.value("fp").toString();
@@ -274,9 +313,15 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
msgDelegate->setVideoOpenCallback([this](const QString& path) {
emit openVideoRequested(path);
});
msgDelegate->setImageOpenCallback([this](const QString& path) {
emit openImageRequested(path);
});
m_view->setItemDelegate(msgDelegate);
m_view->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_view->setSelectionMode(QAbstractItemView::NoSelection);
m_view->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_view, &QWidget::customContextMenuRequested,
this, &MessageList::onMessageContextMenu);
m_view->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_view->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
m_view->setFrameShape(QFrame::NoFrame);
@@ -399,6 +444,48 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(m_inputBar, &InputBar::sendImageAttachment, this, [this](const QString& mediaSrc, const QString& mediaDest,
const QString& displayName) {
if (m_currentChannelId.isEmpty()) return;
QByteArray textData = displayName.toUtf8();
auto ts = QDateTime::currentMSecsSinceEpoch();
QFileInfo fi(displayName);
QString suf = fi.suffix().toLower();
QString ct = "image/jpeg";
if (suf == "png") ct = "image/png";
else if (suf == "gif") ct = "image/gif";
else if (suf == "bmp") ct = "image/bmp";
else if (suf == "webp") ct = "image/webp";
int iw = 0, ih = 0;
QImageReader reader(mediaSrc);
reader.setAutoTransform(true);
if (reader.canRead()) {
QSize sz = reader.size();
if (sz.width() > 0 && sz.height() > 0) { iw = sz.width(); ih = sz.height(); }
}
struct chat_msg_submit* req = (struct chat_msg_submit*)
u_malloc(sizeof(struct chat_msg_submit) + textData.size());
memset(req, 0, sizeof(*req));
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
m_currentChannelId.toUtf8().constData());
snprintf(req->content_type, sizeof(req->content_type), "%s", ct.toUtf8().constData());
snprintf(req->media_src, sizeof(req->media_src), "%s", mediaSrc.toUtf8().constData());
snprintf(req->media_dest, sizeof(req->media_dest), "%s", mediaDest.toUtf8().constData());
req->media_copy = (mediaSrc != mediaDest) ? 1 : 0;
req->media_video = 0;
req->width = iw;
req->height = ih;
req->data = (uint8_t*)(req + 1);
req->data_len = (uint32_t)textData.size();
memcpy(req->data, textData.constData(), textData.size());
req->timestamp = (uint64_t)ts;
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
});
connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() {
m_view->viewport()->update();
});
@@ -765,3 +852,53 @@ void MessageList::onAttachmentDownloaded(const QString& chId, int64_t msgId) {
}
}
}
void MessageList::onMessageContextMenu(const QPoint& pos) {
QModelIndex idx = m_view->indexAt(pos);
if (!idx.isValid()) return;
QString ct = idx.data(MsgContentTypeRole).toString();
bool isVideo = (ct == "video/mp4");
bool isImage = ct.startsWith("image/");
if (!isVideo && !isImage) return;
QString filePath = idx.data(MsgVoiceFileRole).toString();
QString displayName = idx.data(MsgFileDisplayNameRole).toString();
bool downloaded = !filePath.isEmpty() && QFile::exists(filePath);
QMenu menu(m_view);
QAction* saveAct = menu.addAction(QString::fromUtf8("Сохранить в файл"));
saveAct->setEnabled(downloaded);
QAction* dlAct = nullptr;
if (!downloaded) dlAct = menu.addAction(QString::fromUtf8("Скачать"));
QAction* chosen = menu.exec(m_view->viewport()->mapToGlobal(pos));
if (chosen == saveAct) {
QString base = displayName;
if (base.isEmpty()) base = QFileInfo(filePath).fileName();
QString dest = QFileDialog::getSaveFileName(this, QString::fromUtf8("Сохранить файл"), base);
if (dest.isEmpty()) return;
if (!QFile::copy(filePath, dest)) {
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "MessageList: save failed src=%s dst=%s",
qPrintable(filePath), qPrintable(dest));
} else {
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "MessageList: saved %s -> %s",
qPrintable(filePath), qPrintable(dest));
}
return;
}
if (chosen == dlAct) {
qint64 msgId = idx.data(MsgIdRole).toLongLong();
QString chId = idx.data(MsgChannelIdRole).toString();
if (chId.isEmpty() || msgId <= 0) return;
struct attachment_dl_req* req = (struct attachment_dl_req*)
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return;
memset(req, 0, sizeof(*req));
strncpy(req->channel_id, chId.toUtf8().constData(), sizeof(req->channel_id) - 1);
req->msg_id = msgId;
gui_bridge_post_uasync_fn(chat_core_attachment_download_trampoline, req);
}
}