dm: маршрутизация через общую группу (group_id), JSON-эскейпинг; звонки: иконки/UI в desktop и android

This commit is contained in:
evgeny committed 2026-09-16 15:05:48 +03:00
1 parent b483335c06
commit e4557b0473
14 files changed
+925 -139

No files matched your search

+156 -19
View File
@@ -11,6 +11,8 @@
#include <QTimer>
#include <QDateTime>
#include <QCloseEvent>
#include <QPainter>
#include <QPainterPath>
#include <cstring>
extern "C" {
@@ -19,29 +21,146 @@ extern "C" {
#include "../../lib/debug_config.h"
}
/* ── рисование иконок звонка (QPainter, без внешних ассетов) ── */
enum class Glyph { Phone, PhoneDown, Mic, MicOff };
static QColor colorForName(const QString& name) {
static const QColor palette[] = {
QColor("#5B9BD5"), QColor("#70AD47"), QColor("#ED7D31"),
QColor("#9B59B6"), QColor("#F4B400"), QColor("#E67E22"),
QColor("#16A085"), QColor("#D35400"),
};
quint32 h = 0;
for (const QChar c : name) h = h * 31u + c.unicode();
return palette[h % 8];
}
static QPixmap makeAvatar(const QString& name, int size) {
QPixmap pm(size, size);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
p.setBrush(colorForName(name));
p.setPen(Qt::NoPen);
p.drawEllipse(0, 0, size, size);
QString initial = name.trimmed().isEmpty() ? QStringLiteral("?") : name.trimmed().left(1).toUpper();
p.setPen(Qt::white);
QFont f = p.font();
f.setBold(true);
f.setPixelSize(size / 2);
p.setFont(f);
p.drawText(QRect(0, 0, size, size), Qt::AlignCenter, initial);
p.end();
return pm;
}
static void drawPhoneGlyph(QPainter* p, const QRectF& r, bool down) {
const qreal s = r.width();
p->save();
p->translate(r.center());
p->rotate(down ? 135.0 : -45.0);
const qreal w = s * 0.36, h = s * 0.64;
QPainterPath path;
path.addRoundedRect(QRectF(-w / 2, -h / 2, w, h), w * 0.5, w * 0.5);
p->setPen(Qt::NoPen);
p->setBrush(Qt::white);
p->drawPath(path);
p->restore();
}
static void drawMicGlyph(QPainter* p, const QRectF& r, bool off) {
const qreal s = r.width();
const QPointF c = r.center();
p->save();
QPen pen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
p->setPen(pen);
p->setBrush(Qt::NoBrush);
const qreal cw = s * 0.30;
p->drawRoundedRect(QRectF(c.x() - cw / 2, r.top() + s * 0.10, cw, s * 0.40), cw / 2, cw / 2);
p->drawArc(QRectF(c.x() - s * 0.20, r.top() + s * 0.42, s * 0.40, s * 0.26), 180 * 16, 180 * 16);
p->drawLine(QPointF(c.x(), r.top() + s * 0.60), QPointF(c.x(), r.top() + s * 0.68));
p->drawLine(QPointF(c.x() - s * 0.17, r.top() + s * 0.68), QPointF(c.x() + s * 0.17, r.top() + s * 0.68));
if (off) {
p->setPen(QPen(Qt::white, s * 0.09, Qt::SolidLine, Qt::RoundCap));
p->drawLine(QPointF(c.x() - s * 0.26, c.y() - s * 0.26), QPointF(c.x() + s * 0.26, c.y() + s * 0.26));
}
p->restore();
}
static QIcon makeRoundIcon(const QColor& bg, Glyph glyph, int size) {
QPixmap pm(size, size);
pm.fill(Qt::transparent);
QPainter p(&pm);
p.setRenderHint(QPainter::Antialiasing);
p.setPen(Qt::NoPen);
p.setBrush(bg);
p.drawEllipse(0, 0, size, size);
const QRectF glyphRect(size * 0.24, size * 0.24, size * 0.52, size * 0.52);
switch (glyph) {
case Glyph::Phone: drawPhoneGlyph(&p, glyphRect, false); break;
case Glyph::PhoneDown: drawPhoneGlyph(&p, glyphRect, true); break;
case Glyph::Mic: drawMicGlyph(&p, glyphRect, false); break;
case Glyph::MicOff: drawMicGlyph(&p, glyphRect, true); break;
}
p.end();
return QIcon(pm);
}
static QPushButton* makeRoundBtn(const QIcon& icon, const QString& tooltip, QWidget* parent) {
auto* btn = new QPushButton(parent);
btn->setIcon(icon);
btn->setIconSize(QSize(52, 52));
btn->setFixedSize(64, 64);
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(tooltip);
btn->setStyleSheet(
"QPushButton { border: none; border-radius: 32px; background: transparent; }"
"QPushButton:pressed { background: rgba(255,255,255,0.15); }");
return btn;
}
static QWidget* wrapWithCaption(QWidget* btn, const QString& caption, QLabel** captionOut = nullptr) {
auto* w = new QWidget(btn->parentWidget());
auto* l = new QVBoxLayout(w);
l->setContentsMargins(0, 0, 0, 0);
l->setSpacing(6);
l->addWidget(btn, 0, Qt::AlignHCenter);
auto* cap = new QLabel(caption, w);
cap->setAlignment(Qt::AlignCenter);
cap->setStyleSheet("color: palette(text); font-size: 11px; background: transparent;");
l->addWidget(cap, 0, Qt::AlignHCenter);
if (captionOut) *captionOut = cap;
return w;
}
CallWindow::CallWindow(QWidget* parent)
: QWidget(parent, Qt::Window) {
setWindowTitle(QStringLiteral("Звонок"));
setFixedSize(320, 300);
setFixedSize(340, 400);
setAttribute(Qt::WA_DeleteOnClose, false);
m_avatarLabel = new QLabel(this);
m_avatarLabel->setFixedSize(80, 80);
m_avatarLabel->setAlignment(Qt::AlignCenter);
m_peerLabel = new QLabel(this);
m_peerLabel->setAlignment(Qt::AlignCenter);
m_peerLabel->setStyleSheet("font-size: 17px; font-weight: bold; color: palette(text);");
m_statusLabel = new QLabel(this);
m_statusLabel->setAlignment(Qt::AlignCenter);
m_statusLabel->setStyleSheet("color: gray; font-size: 13px;");
m_durationLabel = new QLabel(QStringLiteral("00:00"), this);
m_durationLabel->setAlignment(Qt::AlignCenter);
m_durationLabel->setStyleSheet("font-size: 15px; color: palette(text); font-family: monospace;");
m_acceptBtn = new QPushButton(QStringLiteral("Принять"), this);
m_declineBtn = new QPushButton(QStringLiteral("Отклонить"), this);
m_hangupBtn = new QPushButton(QStringLiteral("Завершить"), this);
m_muteBtn = new QPushButton(QStringLiteral("Mute"), this);
m_acceptBtn = makeRoundBtn(makeRoundIcon(QColor("#27ae60"), Glyph::Phone, 96), QStringLiteral("Принять"), this);
m_declineBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Отклонить"), this);
m_hangupBtn = makeRoundBtn(makeRoundIcon(QColor("#e74c3c"), Glyph::PhoneDown, 96), QStringLiteral("Завершить"), this);
m_muteBtn = makeRoundBtn(makeRoundIcon(QColor("#7f8c8d"), Glyph::Mic, 96), QStringLiteral("Mute"), this);
m_muteBtn->setCheckable(true);
m_muteBtn->setStyleSheet(
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;"
" padding: 4px 12px; font-size: 12px; background: palette(button); }"
"QPushButton:checked { background: #c0392b; color: white; }");
m_deviceCombo = new QComboBox(this);
m_deviceCombo->setStyleSheet(
@@ -49,15 +168,29 @@ CallWindow::CallWindow(QWidget* parent)
" padding: 6px 10px; font-size: 13px; background: palette(base); }");
QVBoxLayout* lay = new QVBoxLayout(this);
lay->setContentsMargins(16, 20, 16, 16);
lay->setSpacing(8);
lay->addWidget(m_avatarLabel, 0, Qt::AlignHCenter);
lay->addWidget(m_peerLabel);
lay->addWidget(m_statusLabel);
lay->addWidget(m_durationLabel);
lay->addStretch(1);
m_acceptCell = wrapWithCaption(m_acceptBtn, QStringLiteral("Принять"));
m_declineCell = wrapWithCaption(m_declineBtn, QStringLiteral("Отклонить"));
m_hangupCell = wrapWithCaption(m_hangupBtn, QStringLiteral("Завершить"));
m_muteCell = wrapWithCaption(m_muteBtn, QStringLiteral("Mute"), &m_muteCaption);
QHBoxLayout* btn = new QHBoxLayout();
btn->addWidget(m_acceptBtn);
btn->addWidget(m_declineBtn);
btn->addWidget(m_hangupBtn);
btn->setSpacing(10);
btn->addStretch(1);
btn->addWidget(m_acceptCell);
btn->addWidget(m_declineCell);
btn->addWidget(m_hangupCell);
btn->addWidget(m_muteCell);
btn->addStretch(1);
lay->addLayout(btn);
lay->addWidget(m_muteBtn);
lay->addStretch(1);
lay->addWidget(m_deviceCombo);
connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept);
@@ -77,6 +210,7 @@ CallWindow::~CallWindow() = default;
void CallWindow::setPeerName(const QString& name) {
m_peerLabel->setText(name);
m_avatarLabel->setPixmap(makeAvatar(name, 80));
}
void CallWindow::setState(State s) {
@@ -125,10 +259,10 @@ void CallWindow::populateDeviceCombo() {
}
void CallWindow::updateButtons() {
m_acceptBtn->setVisible(m_state == Incoming);
m_declineBtn->setVisible(m_state == Incoming);
m_hangupBtn->setVisible(m_state == Outgoing || m_state == Active);
m_muteBtn->setVisible(m_state == Active);
m_acceptCell->setVisible(m_state == Incoming);
m_declineCell->setVisible(m_state == Incoming);
m_hangupCell->setVisible(m_state == Outgoing || m_state == Active);
m_muteCell->setVisible(m_state == Active);
m_deviceCombo->setVisible(m_state == Active);
}
@@ -155,7 +289,10 @@ void CallWindow::onHangup() {
void CallWindow::onMuteToggled(bool checked) {
CallAudioEngine::instance()->setMuted(checked);
m_muteBtn->setText(checked ? QStringLiteral("Вкл. микрофон") : QStringLiteral("Mute"));
m_muteBtn->setIcon(makeRoundIcon(checked ? QColor("#e74c3c") : QColor("#7f8c8d"),
checked ? Glyph::MicOff : Glyph::Mic, 96));
if (m_muteCaption)
m_muteCaption->setText(checked ? QStringLiteral("Вкл. микрофон") : QStringLiteral("Mute"));
}
void CallWindow::onDeviceChanged(int index) {