You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

328 lines
12 KiB

#include "callwindow.h"
#include "../transport/gui_bridge.h"
#include "call_audio_engine.h"
#include "sound_manager.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QPushButton>
#include <QComboBox>
#include <QTimer>
#include <QDateTime>
#include <QCloseEvent>
#include <QPainter>
#include <QPainterPath>
#include <cstring>
extern "C" {
#include "call/call.h"
#include "../../lib/mem.h"
#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(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_bgpPathLabel = new QLabel(this);
m_bgpPathLabel->setAlignment(Qt::AlignCenter);
m_bgpPathLabel->setWordWrap(true);
m_bgpPathLabel->setStyleSheet("color: gray; font-size: 11px;");
m_bgpPathLabel->hide();
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_deviceCombo = new QComboBox(this);
m_deviceCombo->setStyleSheet(
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;"
" 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->addWidget(m_bgpPathLabel);
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->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->addStretch(1);
lay->addWidget(m_deviceCombo);
connect(m_acceptBtn, &QPushButton::clicked, this, &CallWindow::onAccept);
connect(m_declineBtn, &QPushButton::clicked, this, &CallWindow::onDecline);
connect(m_hangupBtn, &QPushButton::clicked, this, &CallWindow::onHangup);
connect(m_muteBtn, &QPushButton::toggled, this, &CallWindow::onMuteToggled);
connect(m_deviceCombo, QOverload<int>::of(&QComboBox::activated), this, &CallWindow::onDeviceChanged);
m_durationTimer = new QTimer(this);
m_durationTimer->setInterval(1000);
connect(m_durationTimer, &QTimer::timeout, this, &CallWindow::updateDuration);
setState(Outgoing);
}
CallWindow::~CallWindow() = default;
void CallWindow::setPeerName(const QString& name) {
m_peerLabel->setText(name);
m_avatarLabel->setPixmap(makeAvatar(name, 80));
}
void CallWindow::setBgpPath(const QString& path) {
m_bgpPathLabel->setText(path);
m_bgpPathLabel->setVisible(!path.isEmpty());
}
void CallWindow::setState(State s) {
m_state = s;
switch (s) {
case Outgoing:
m_statusLabel->setText(QStringLiteral("Звоним…"));
m_durationLabel->hide();
break;
case Incoming:
m_statusLabel->setText(QStringLiteral("Входящий звонок…"));
m_durationLabel->hide();
break;
case Active:
m_statusLabel->setText(QStringLiteral("Разговор"));
m_durationLabel->setText(QStringLiteral("00:00"));
m_durationLabel->show();
m_startMs = QDateTime::currentMSecsSinceEpoch();
m_durationTimer->start();
populateDeviceCombo();
m_muteBtn->setChecked(false);
break;
case Ended:
m_statusLabel->setText(QStringLiteral("Завершён"));
m_durationTimer->stop();
break;
}
updateButtons();
}
void CallWindow::populateDeviceCombo() {
m_deviceCombo->blockSignals(true);
m_deviceCombo->clear();
m_deviceCombo->addItem(QStringLiteral("Default (auto)"), -1);
const auto devs = SoundManager::enumeratePlaybackDevices();
for (int i = 0; i < devs.size(); i++) {
QString label = devs[i].name;
if (devs[i].isDefault) label += QStringLiteral(" (default)");
m_deviceCombo->addItem(label, i);
}
int cur = CallAudioEngine::instance()->playbackDeviceIndex();
for (int i = 0; i < m_deviceCombo->count(); i++) {
if (m_deviceCombo->itemData(i).toInt() == cur) { m_deviceCombo->setCurrentIndex(i); break; }
}
m_deviceCombo->blockSignals(false);
}
void CallWindow::updateButtons() {
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);
}
void CallWindow::postCallFn(void (*fn)(void*), uint64_t callId) {
struct call_id_arg* a = (struct call_id_arg*)u_malloc(sizeof(*a));
if (!a) return;
a->inst = gui_bridge_get_inst();
a->call_id = callId;
gui_bridge_post_uasync_fn(fn, a);
}
void CallWindow::onAccept() {
setState(Active);
postCallFn(call_accept_trampoline, m_callId);
}
void CallWindow::onDecline() {
postCallFn(call_decline_trampoline, m_callId);
}
void CallWindow::onHangup() {
postCallFn(call_hangup_trampoline, m_callId);
}
void CallWindow::onMuteToggled(bool checked) {
CallAudioEngine::instance()->setMuted(checked);
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) {
int devIdx = m_deviceCombo->itemData(index).toInt();
CallAudioEngine::instance()->setPlaybackDeviceIndex(devIdx);
}
void CallWindow::closeEvent(QCloseEvent* event) {
if (m_state == Active) postCallFn(call_hangup_trampoline, m_callId);
emit closed(m_callId);
event->accept();
}
void CallWindow::updateDuration() {
if (m_state != Active) return;
qint64 ms = QDateTime::currentMSecsSinceEpoch() - m_startMs;
qint64 s = ms / 1000;
m_durationLabel->setText(QStringLiteral("%1:%2")
.arg(s / 60, 2, 10, QChar('0'))
.arg(s % 60, 2, 10, QChar('0')));
}