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

+187 -9
View File
@@ -22,6 +22,7 @@
#include <QDesktopServices>
#include <QUrl>
#include <QDebug>
#include <QImageReader>
extern "C" {
#include "../transport/gui_bridge.h"
@@ -106,6 +107,32 @@ bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
return true;
}
if (L.isImage) {
int dlState = index.data(MsgFileDownloadStateRole).toInt();
qint64 msgId = index.data(MsgIdRole).toLongLong();
QString chId = index.data(MsgChannelIdRole).toString();
if (dlState == 2) { /* скачано — открыть просмотрщик */
QString fp = index.data(MsgVoiceFileRole).toString();
if (!fp.isEmpty() && QFile::exists(fp) && m_imageOpenCb) {
m_imageOpenCb(fp);
return true;
}
return false;
}
if (dlState == 1) return false; /* скачивается */
if (chId.isEmpty() || msgId <= 0) return false;
struct attachment_dl_req* req = (struct attachment_dl_req*)
u_malloc(sizeof(struct attachment_dl_req));
if (!req) return false;
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);
return true;
}
if (L.isFile) {
int dlState = index.data(MsgFileDownloadStateRole).toInt();
qint64 msgId = index.data(MsgIdRole).toLongLong();
@@ -288,6 +315,13 @@ static QString formatFileSize(qint64 bytes) {
return QString("%1GB").arg(gb, 0, 'f', 1);
}
static QString formatBitrate(qint64 bytes, qint64 durMs) {
if (bytes <= 0 || durMs <= 0) return QString();
qint64 kbps = bytes * 8 / durMs;
if (kbps >= 1000) return QString("%1Mbps").arg(kbps / 1000.0, 0, 'f', 1);
return QString("%1kbps").arg(kbps);
}
static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) {
int maxW = 0;
int pos = 0;
@@ -376,8 +410,9 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
if (L.isVideo) {
int barW = 3, barPad = 4;
int thW = 200, thH = 112; /* 16:9 превью */
int infoH = 16; /* строка под пузырём: res·size·bitrate + время */
L.bubbleWidth = thW;
L.totalHeight = qMax(thH, kAvatar) + kItemGap;
L.totalHeight = qMax(thH, kAvatar) + infoH + kItemGap;
int contentEdge = L.isOutgoing
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
@@ -386,6 +421,7 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
L.bubbleRect = QRect(bx, 0, thW, thH);
L.videoThumbRect = L.bubbleRect;
L.mediaBarRect = QRect(L.isOutgoing ? bx + thW + 1 : bx - barW - 1, 0, barW, thH);
L.statusRect = QRect(bx, thH + 2, thW, infoH - 2);
int dlState = index.data(MsgFileDownloadStateRole).toInt();
int bd = index.data(MsgMediaBlocksDoneRole).toInt();
@@ -395,7 +431,42 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
if (dlState == 1) { L.progressBarRect = QRect(bx, thH - 3, thW, 3); }
L.videoPlayBtnRect = QRect(bx + thW / 2 - 24, thH / 2 - 24, 48, 48);
L.videoDurBadgeRect = QRect(bx + thW - 116, thH - 26, 108, 18);
L.videoDurBadgeRect = QRect(bx + thW - 66, thH - 26, 58, 18);
if (!L.narrow) {
int avatarY = L.bubbleRect.bottom() - kAvatar;
if (!L.isOutgoing)
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
else
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar);
}
return L;
}
/* Image message detection */
L.isImage = contentType.startsWith("image/");
if (L.isImage) {
int barW = 3, barPad = 4;
int thW = 220, thH = 160;
L.bubbleWidth = thW;
L.totalHeight = qMax(thH, kAvatar) + kItemGap;
int contentEdge = L.isOutgoing
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
int bx = L.isOutgoing ? (contentEdge - thW - barW - barPad) : contentEdge;
L.bubbleRect = QRect(bx, 0, thW, thH);
L.imageThumbRect = L.bubbleRect;
L.mediaBarRect = QRect(L.isOutgoing ? bx + thW + 1 : bx - barW - 1, 0, barW, thH);
int dlState = index.data(MsgFileDownloadStateRole).toInt();
int bd = index.data(MsgMediaBlocksDoneRole).toInt();
int nb = index.data(MsgMediaNumBlocksRole).toInt();
L.mediaBarColor = (dlState == 2) ? 2 : (dlState == 1 || (bd > 0 && nb > 0)) ? 1 : 0;
if (dlState == 1 && nb > 0) L.progressFrac = (float)bd / nb;
if (dlState == 1) { L.progressBarRect = QRect(bx, thH - 3, thW, 3); }
L.imageBadgeRect = QRect(bx + thW - 150, thH - 26, 142, 18);
if (!L.narrow) {
int avatarY = L.bubbleRect.bottom() - kAvatar;
@@ -790,15 +861,19 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
while (pos < trimmed.length()) {
if (trimmed.at(pos).isSpace()) { ++pos; continue; }
QString emojiStr;
for (const auto &cat : builtinEmojiSet())
bool found = false;
for (const auto &cat : builtinEmojiSet()) {
for (const auto &e : cat.items) {
if (e.unicode.length() > trimmed.length() - pos) continue;
if (QStringView(trimmed).mid(pos, e.unicode.length()) == e.unicode) {
emojiStr = e.unicode;
pos += e.unicode.length();
found = true;
break;
}
}
if (found) break;
}
if (emojiStr.isEmpty()) { ++pos; continue; }
bool animated = g_animatedEmojiMap.contains(emojiStr);
@@ -1010,12 +1085,9 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
painter->drawPath(tri);
}
/* бейдж: размер · длительность (m:ss) */
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
QString sizeStr = formatFileSize(fsz);
/* бейдж: длительность (m:ss) */
QString durStr = index.data(MsgVoiceDurationRole).toString();
QString badge = durStr.isEmpty() ? sizeStr : sizeStr + " \xC2\xB7 " + durStr;
if (!badge.isEmpty()) {
if (!durStr.isEmpty()) {
QFont durFont = option.font;
durFont.setPointSize(durFont.pointSize() - 1);
painter->setFont(durFont);
@@ -1023,7 +1095,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
painter->setBrush(QColor(0, 0, 0, 160));
painter->drawRoundedRect(L.videoDurBadgeRect, 5, 5);
painter->setPen(Qt::white);
painter->drawText(L.videoDurBadgeRect, Qt::AlignCenter, badge);
painter->drawText(L.videoDurBadgeRect, Qt::AlignCenter, durStr);
}
painter->restore();
@@ -1049,6 +1121,112 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); }
}
/* строка под пузырём: res · size · bitrate + время */
{
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
qint64 durMs = index.data(MsgVideoDurationMsRole).toLongLong();
int vw = index.data(MsgVideoWidthRole).toInt();
int vh = index.data(MsgVideoHeightRole).toInt();
QStringList info;
if (vw > 0 && vh > 0) info << QString("%1×%2").arg(vw).arg(vh);
QString sz = formatFileSize(fsz);
if (!sz.isEmpty()) info << sz;
QString br = formatBitrate(fsz, durMs);
if (!br.isEmpty()) info << br;
QFont smallFont = option.font;
smallFont.setPointSize(smallFont.pointSize() - 2);
painter->setFont(smallFont);
painter->setPen(QColor(0x80, 0x80, 0x80));
if (!info.isEmpty())
painter->drawText(L.statusRect, Qt::AlignLeft | Qt::AlignVCenter, info.join(" \xC2\xB7 "));
painter->drawText(L.statusRect, Qt::AlignRight | Qt::AlignVCenter,
index.data(MsgTimeRole).toString());
}
painter->restore();
return;
}
if (L.isImage) {
drawBubble(painter, L, option);
if (!L.narrow) drawAvatar(painter, L, index);
int dlState = index.data(MsgFileDownloadStateRole).toInt();
QString filePath = index.data(MsgVoiceFileRole).toString();
painter->save();
painter->setRenderHint(QPainter::Antialiasing);
QPainterPath clip;
clip.addRoundedRect(L.imageThumbRect, kRadius, kRadius);
painter->setClipPath(clip);
QPixmap img;
if (!filePath.isEmpty() && QFile::exists(filePath)) {
QImageReader reader(filePath);
reader.setAutoTransform(true);
reader.setScaledSize(L.imageThumbRect.size() * 2);
QImage im = reader.read();
if (!im.isNull()) img = QPixmap::fromImage(im);
}
if (!img.isNull()) {
painter->fillRect(L.imageThumbRect, QColor(20, 20, 24));
QPixmap scaled = img.scaled(L.imageThumbRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
int dx = (L.imageThumbRect.width() - scaled.width()) / 2;
int dy = (L.imageThumbRect.height() - scaled.height()) / 2;
painter->drawPixmap(L.imageThumbRect.left() + dx, L.imageThumbRect.top() + dy, scaled);
} else {
painter->fillRect(L.imageThumbRect, QColor(30, 30, 35));
QFont iconFont = option.font;
iconFont.setPointSize(iconFont.pointSize() + 10);
painter->setFont(iconFont);
painter->setPen(QColor(150, 150, 150));
painter->drawText(L.imageThumbRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x93\xB7"));
}
/* бейдж: разрешение · размер */
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
int iw = index.data(MsgImageWidthRole).toInt();
int ih = index.data(MsgImageHeightRole).toInt();
QString res = (iw > 0 && ih > 0) ? QString("%1×%2").arg(iw).arg(ih) : QString();
QString sizeStr = formatFileSize(fsz);
QString badge = res.isEmpty() ? sizeStr : res + " \xC2\xB7 " + sizeStr;
if (!badge.isEmpty()) {
QFont durFont = option.font;
durFont.setPointSize(durFont.pointSize() - 1);
painter->setFont(durFont);
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0, 0, 0, 160));
painter->drawRoundedRect(L.imageBadgeRect, 5, 5);
painter->setPen(Qt::white);
painter->drawText(L.imageBadgeRect, Qt::AlignCenter, badge);
}
painter->restore();
/* progress overlay (скачивание) */
if (dlState == 1) {
painter->setBrush(QColor(0, 0, 0, 140));
painter->drawRect(L.imageThumbRect);
QFont pfont = option.font;
pfont.setPointSize(pfont.pointSize() + 2);
painter->setFont(pfont);
painter->setPen(Qt::white);
painter->drawText(L.imageThumbRect, Qt::AlignCenter,
QStringLiteral("%1%").arg((int)(L.progressFrac * 100)));
}
/* media bar (левая кромка) */
{ QColor barC = L.mediaBarColor == 2 ? QColor(0x40, 0xC0, 0x40) : L.mediaBarColor == 1 ? QColor(0xF0, 0xC0, 0x40) : QColor(0x80, 0x80, 0x80);
painter->setPen(Qt::NoPen); painter->setBrush(barC); painter->drawRect(L.mediaBarRect); }
if (L.progressBarRect.isValid()) {
painter->setBrush(QColor(0x44, 0x44, 0x44));
painter->drawRoundedRect(L.progressBarRect, 2, 2);
int pw = (int)(L.progressBarRect.width() * L.progressFrac);
if (pw > 0) { painter->setBrush(QColor(0x33, 0x90, 0xEC)); painter->drawRoundedRect(QRect(L.progressBarRect.left(), L.progressBarRect.top(), pw, 3), 2, 2); }
}
painter->restore();
return;
}