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.
 
 
 
 
 
 

568 lines
25 KiB

#include <QCoreApplication>
#include "inputbar.h"
#include "emojipanel.h"
#include "audiorecorder.h"
#include "voicemessageencoder.h"
#include "media_blocks.h"
#include "debug_ui.h"
#include <QDebug>
#include <QTextEdit>
#include <QPushButton>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QFileDialog>
#include <QMessageBox>
#include <QKeyEvent>
#include <QFontMetrics>
#include <QScrollBar>
#include <QApplication>
#include <QLabel>
#include <QResizeEvent>
#include <QDir>
#include <QProgressBar>
#include <QIcon>
#include <QTimer>
#include <QFileInfo>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QUrl>
#include <QToolTip>
#include <cmath>
extern "C" {
#include "video/video.h"
#include "media_async/attachment_send.h"
#include "mem.h"
}
static bool isImageFile(const QString& suffix) {
QString s = suffix.toLower();
return s == "jpg" || s == "jpeg" || s == "png" || s == "gif" ||
s == "bmp" || s == "webp";
}
static bool isVideoFile(const QString& suffix) {
return QStringList{"mp4", "mov", "mkv", "avi", "webm", "m4v"}.contains(suffix.toLower());
}
static QPushButton *makeIconBtn(const QIcon& icon, const QString &tooltip, QWidget *parent) {
auto *btn = new QPushButton(parent);
btn->setIcon(icon);
btn->setIconSize(QSize(20, 20));
btn->setFixedSize(34, 34);
btn->setCursor(Qt::PointingHandCursor);
btn->setToolTip(tooltip);
btn->setStyleSheet(
"QPushButton { border: none; border-radius: 4px; background: transparent; }"
"QPushButton:hover { background: palette(light); }"
);
return btn;
}
static QWidget *wrapBottomBtn(QPushButton *btn, int pad) {
auto *w = new QWidget(btn->parentWidget());
auto *l = new QVBoxLayout(w);
l->setContentsMargins(0, 0, 0, 0);
l->setSpacing(0);
l->addWidget(btn);
l->addSpacing(pad);
return w;
}
QString InputBar::formatFileSize(qint64 bytes) {
if (bytes < 1024) return QCoreApplication::translate("ChatGui", "%1B").arg(bytes);
double kb = bytes / 1024.0;
if (kb < 1024.0) return QCoreApplication::translate("ChatGui", "%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0);
double mb = kb / 1024.0;
if (mb < 1024.0) return QCoreApplication::translate("ChatGui", "%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0);
double gb = mb / 1024.0;
return QCoreApplication::translate("ChatGui", "%1GB").arg(gb, 0, 'f', 1);
}
InputBar::InputBar(EmojiPanel *emojiPanel, AudioRecorder *recorder, QWidget *parent)
: QWidget(parent)
, m_input(new QTextEdit(this))
, m_emojiBtn(nullptr)
, m_pttBtn(nullptr)
, m_emojiPanel(emojiPanel)
, m_recorder(recorder)
{
auto *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(8, 4, 8, 0);
mainLayout->setSpacing(2);
/* Attachment preview bar (hidden until file selected) */
m_attachPreview = new QWidget(this);
auto *prevLay = new QHBoxLayout(m_attachPreview);
prevLay->setContentsMargins(0, 2, 0, 2);
prevLay->setSpacing(4);
m_attachPreviewLabel = new QLabel(m_attachPreview);
m_attachPreviewLabel->setStyleSheet("background: palette(light); border-radius: 6px; padding: 2px 8px;");
prevLay->addWidget(m_attachPreviewLabel, 1);
m_attachClearBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_attachPreview);
m_attachClearBtn->setFixedSize(20, 20);
m_attachClearBtn->setCursor(Qt::PointingHandCursor);
m_attachClearBtn->setStyleSheet("QPushButton { border: none; background: transparent; color: palette(text); }"
"QPushButton:hover { color: #e74c3c; }");
connect(m_attachClearBtn, &QPushButton::clicked, this, &InputBar::clearPendingAttachment);
prevLay->addWidget(m_attachClearBtn);
m_attachPreview->hide();
mainLayout->addWidget(m_attachPreview);
m_preparationLabel = new QLabel(this);
m_preparationLabel->hide();
mainLayout->addWidget(m_preparationLabel);
auto *layout = new QHBoxLayout();
layout->setSpacing(4);
m_attachBtn = makeIconBtn(QIcon(":/icons/attach.png"),
QCoreApplication::translate("ChatGui", "Attach"), this);
connect(m_attachBtn, &QPushButton::clicked, this, &InputBar::onAttachClicked);
layout->addWidget(wrapBottomBtn(m_attachBtn, 3), 0, Qt::AlignBottom);
m_input->setAcceptRichText(false);
m_input->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_input->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
m_input->setTabChangesFocus(true);
m_input->setStyleSheet(
"QTextEdit { border: 1px solid palette(mid); border-radius: 6px;"
" padding: 4px 8px; background: palette(base); }"
);
m_input->installEventFilter(this);
QFontMetrics fm(m_input->font());
m_input->setFixedHeight(fm.lineSpacing() + 12);
layout->addWidget(m_input, 1);
m_emojiBtn = makeIconBtn(QIcon(":/icons/emoji.png"),
QCoreApplication::translate("ChatGui", "Emoji"), this);
layout->addWidget(wrapBottomBtn(m_emojiBtn, 3), 0, Qt::AlignBottom);
m_pttBtn = makeIconBtn(QIcon(":/icons/mic.png"),
QCoreApplication::translate("ChatGui", "Voice message (hold)"), this);
layout->addWidget(wrapBottomBtn(m_pttBtn, 3), 0, Qt::AlignBottom);
auto *sendBtn = makeIconBtn(QIcon(":/icons/send.png"),
QCoreApplication::translate("ChatGui", "Send"), this);
sendBtn->setStyleSheet(sendBtn->styleSheet() +
"QPushButton { border: 1px solid palette(highlight); }");
layout->addWidget(sendBtn, 0, Qt::AlignBottom);
connect(m_input, &QTextEdit::textChanged, this, &InputBar::adjustInputHeight);
connect(sendBtn, &QPushButton::clicked, this, &InputBar::onSendClicked);
/* PTT button: press to record, release to stop */
connect(m_pttBtn, &QPushButton::pressed, this, &InputBar::onPttPressed);
connect(m_pttBtn, &QPushButton::released, this, &InputBar::onPttReleased);
if (m_emojiPanel) {
connect(m_emojiBtn, &QPushButton::clicked, this, [this]() {
m_emojiPanel->setVisible(!m_emojiPanel->isVisible());
});
connect(m_emojiPanel, &EmojiPanel::emojiSelected, this, [this](const QString &emoji) {
m_input->insertPlainText(emoji);
m_input->setFocus();
});
}
/* Recording overlay — appears over the text input during recording */
m_recordOverlay = new QWidget(this);
m_recordOverlay->setStyleSheet("background: rgba(231, 76, 60, 0.88); border-radius: 6px;");
m_recordOverlay->hide();
auto* roLayout = new QHBoxLayout(m_recordOverlay);
roLayout->setContentsMargins(12, 0, 8, 0);
roLayout->setSpacing(6);
m_recordLabel = new QLabel(m_recordOverlay);
m_recordLabel->setStyleSheet("color: white; font-size: 14px; font-weight: bold; background: transparent;");
roLayout->addWidget(m_recordLabel);
m_levelBar = new QProgressBar(m_recordOverlay);
m_levelBar->setRange(0, 100);
m_levelBar->setValue(0);
m_levelBar->setTextVisible(false);
m_levelBar->setFixedSize(80, 14);
m_levelBar->setStyleSheet(
"QProgressBar { border: 1px solid rgba(255,255,255,0.2); border-radius: 3px; background: rgba(0,0,0,0.3); }"
"QProgressBar::chunk { background: qlineargradient(x1:0, y1:0, x2:1, y2:0,"
" stop:0 #27ae60, stop:0.6 #f1c40f, stop:1 #e74c3c); border-radius: 2px; }");
roLayout->addWidget(m_levelBar);
roLayout->addStretch();
m_cancelRecBtn = new QPushButton(QString::fromUtf8("\xE2\x9C\x95"), m_recordOverlay);
m_cancelRecBtn->setFixedSize(24, 24);
m_cancelRecBtn->setStyleSheet(
"QPushButton { border: none; color: white; font-size: 16px; background: transparent; }"
"QPushButton:hover { color: #fdd; }");
connect(m_cancelRecBtn, &QPushButton::clicked, this, [this]() {
m_pttActive = false;
if (m_recorder && m_recorder->isRecording()) m_recorder->stopRecording();
});
roLayout->addWidget(m_cancelRecBtn);
if (recorder) {
connect(recorder, &AudioRecorder::recordingStarted, this, &InputBar::showRecordOverlay);
connect(recorder, &AudioRecorder::recordingStopped, this, [this](int) { hideRecordOverlay(); });
connect(recorder, &AudioRecorder::durationChanged, this, &InputBar::updateRecordingUI);
}
mainLayout->addLayout(layout);
installFileDropTarget(this);
installFileDropTarget(m_input->viewport());
connect(this, &InputBar::attachmentRejected, this, [this](const QString& reason) {
QToolTip::showText(mapToGlobal(QPoint(8, 0)), reason, this);
});
}
/* Qt доставляет drop в viewport, а не в QTextEdit/QListView. Родитель composer принимает drop на кнопках. */
void InputBar::installFileDropTarget(QWidget* target) {
if (!target || m_dropTargets.contains(target)) return;
target->setAcceptDrops(true);
target->installEventFilter(this);
m_dropTargets.insert(target);
if (target != this) connect(target, &QObject::destroyed, this, [this, target] { m_dropTargets.remove(target); });
GUI_DEBUG("InputBar: file drop target installed class=%s", target->metaObject()->className());
}
bool InputBar::canAttachFile() const {
return !m_channelIdForRecord.isEmpty() && m_attachBtn->isEnabled() &&
!m_pttActive && (!m_recorder || !m_recorder->isRecording());
}
void InputBar::rejectAttachment(const QString& reason, const QString& source) {
GUI_WARN("attachment rejected: target=%s source=%s reason=%s", qPrintable(m_channelIdForRecord),
qPrintable(source), qPrintable(reason));
emit attachmentRejected(reason);
}
bool InputBar::eventFilter(QObject *obj, QEvent *event) {
if (m_dropTargets.contains(obj)) {
if (event->type() == QEvent::DragLeave) {
if (m_dropHint) m_dropHint->hide();
} else if (event->type() == QEvent::DragEnter || event->type() == QEvent::DragMove || event->type() == QEvent::Drop) {
auto* drop = static_cast<QDropEvent*>(event);
if (!drop->mimeData()->hasUrls()) return QWidget::eventFilter(obj, event);
if (!canAttachFile() || !(drop->possibleActions() & Qt::CopyAction)) {
if (m_dropHint) m_dropHint->hide();
drop->ignore();
if (event->type() == QEvent::DragEnter)
GUI_DEBUG("attachment drag unavailable: target=%s enabled=%d recording=%d actions=%d",
qPrintable(m_channelIdForRecord), m_attachBtn->isEnabled(), m_pttActive, int(drop->possibleActions()));
return true;
}
const auto urls = drop->mimeData()->urls();
QString error;
if (urls.size() != 1) error = QCoreApplication::translate("ChatGui", "Drop one file at a time.");
else if (!urls.front().isLocalFile()) error = QCoreApplication::translate("ChatGui", "Only local files can be attached.");
if (event->type() != QEvent::Drop) {
auto* target = qobject_cast<QWidget*>(obj);
if (!m_dropHint) {
m_dropHint = new QLabel(target);
m_dropHint->setAttribute(Qt::WA_TransparentForMouseEvents);
m_dropHint->setAlignment(Qt::AlignCenter);
m_dropHint->setWordWrap(true);
m_dropHint->setStyleSheet("QLabel { border: 2px dashed palette(highlight); color: palette(text);"
" background: rgba(40, 110, 160, 80); }");
}
if (m_dropHint->parentWidget() != target) m_dropHint->setParent(target);
m_dropHint->setGeometry(target->rect().adjusted(2, 2, -2, -2));
m_dropHint->setText(error.isEmpty() ? QCoreApplication::translate("ChatGui", "Drop a file to attach") : error);
m_dropHint->show(); m_dropHint->raise();
drop->setDropAction(Qt::CopyAction); drop->accept();
return true;
}
if (m_dropHint) m_dropHint->hide();
if (!error.isEmpty()) { rejectAttachment(error); drop->ignore(); return true; }
const QString source = urls.front().toLocalFile();
const quint64 context = m_attachmentContext;
GUI_INFO("attachment dropped: target=%s source=%s", qPrintable(m_channelIdForRecord), qPrintable(source));
// Завершаем native drag до возможного модального выбора режима видео; контекст проверяется повторно.
QTimer::singleShot(0, this, [this, source, context] { prepareAttachment(source, context); });
drop->setDropAction(Qt::CopyAction); drop->accept();
return true;
}
}
if (obj == m_input && event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
if (ke->modifiers() & Qt::ControlModifier) {
m_input->insertPlainText("\n");
return true;
} else {
onSendClicked();
return true;
}
}
}
return QWidget::eventFilter(obj, event);
}
void InputBar::adjustInputHeight() {
m_input->document()->adjustSize();
int lineSpacing = m_input->fontMetrics().lineSpacing();
int docH = (int)m_input->document()->size().height() + 12;
int maxH = 12 * lineSpacing + 12;
int h = qBound(lineSpacing + 12, docH, maxH);
m_input->setFixedHeight(h);
bool needScroll = docH > maxH;
m_input->setVerticalScrollBarPolicy(needScroll ? Qt::ScrollBarAlwaysOn
: Qt::ScrollBarAlwaysOff);
if (needScroll) {
QTextCursor c = m_input->textCursor();
if (c.atEnd())
m_input->verticalScrollBar()->setValue(m_input->verticalScrollBar()->maximum());
else
m_input->ensureCursorVisible();
}
}
void InputBar::onSendClicked() {
QString text = m_input->toPlainText();
while (!text.isEmpty() && (text.back() == ' ' || text.back() == '\t'))
text.chop(1);
bool hasAttach = !m_pendingAttachSrc.isEmpty();
if (!hasAttach && text.isEmpty()) return;
if (hasAttach) {
if (m_pendingAttachAsImage) {
GUI_INFO("send: image=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
emit sendImageAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName);
m_attachPreview->hide();
} else if (m_pendingAttachAsVideo) {
GUI_INFO("send: video=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
QByteArray source = m_pendingAttachSrc.toUtf8(), name = (QFileInfo(m_pendingAttachName).completeBaseName() + ".mp4").toUtf8();
if (!request || source.size() >= (int)sizeof(request->source) || name.size() >= (int)sizeof(request->info.name)) {
DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: video request allocation/path failed"); u_free(request); return;
}
request->info.kind = ATTACHMENT_VIDEO; request->transcode = 1;
snprintf(request->source, sizeof(request->source), "%s", source.constData());
snprintf(request->info.name, sizeof(request->info.name), "%s", name.constData());
emit submitAttachment(m_channelIdForRecord, request);
m_attachPreview->hide();
} else {
GUI_INFO("send: file=%s name=%s", qPrintable(m_pendingAttachSrc), qPrintable(m_pendingAttachName));
emit sendFileAttachment(m_channelIdForRecord, m_pendingAttachSrc, m_pendingAttachDest, m_pendingAttachName);
m_attachPreview->hide();
}
m_pendingAttachSrc.clear();
m_pendingAttachDest.clear();
m_pendingAttachName.clear();
m_pendingAttachSize = 0;
m_pendingAttachAsVideo = false;
m_pendingAttachAsImage = false;
}
if (!text.isEmpty()) {
emit sendMessage(text);
m_input->clear();
if (m_emojiPanel) m_emojiPanel->hide();
m_input->setFocus();
}
}
void InputBar::setRecordingContext(const QString& mediaDirBase, const QString& channelId) {
if (m_channelIdForRecord != channelId || m_mediaDirBase != mediaDirBase) {
++m_attachmentContext;
clearPendingAttachment();
if (m_dropHint) m_dropHint->hide();
}
m_mediaDirBase = mediaDirBase;
m_channelIdForRecord = channelId;
showPreparation();
}
void InputBar::setDmMode(bool enabled) {
m_dmMode = enabled;
m_pttBtn->setEnabled(true);
m_attachBtn->setEnabled(true);
}
void InputBar::setMediaEnabled(bool enabled) {
if (m_pttBtn) m_pttBtn->setEnabled(enabled);
if (m_attachBtn) m_attachBtn->setEnabled(enabled);
if (!enabled && m_dropHint) m_dropHint->hide();
}
void InputBar::onPttPressed() {
if (!m_recorder || !m_recorder->isInitialized()) return;
m_recorder->startRecording();
m_pttActive = m_recorder->isRecording();
m_recordTarget = m_channelIdForRecord;
m_pttBtn->setStyleSheet(m_pttBtn->styleSheet() +
"QPushButton { background: #e74c3c; border-radius: 4px; }");
}
void InputBar::onPttReleased() {
if (!m_pttActive) return;
m_pttActive = false;
m_recorder->stopRecording();
m_pttBtn->setStyleSheet(
"QPushButton { border: none; border-radius: 4px; background: transparent; }"
"QPushButton:hover { background: palette(light); }");
int durationMs = m_recorder->durationMs();
if (durationMs < 500) {
GUI_INFO("InputBar: voice message too short (%dms), discarded", durationMs);
return;
}
auto* request = (struct attachment_send_req*)u_calloc(1, sizeof(struct attachment_send_req));
if (!request) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "InputBar: voice request allocation failed"); return; }
request->info.kind = ATTACHMENT_VOICE; request->preset = VoiceEncoder::preset();
if (!m_recorder->takeRecording(request)) { u_free(request); return; }
emit submitAttachment(m_recordTarget, request);
}
void InputBar::onAttachClicked() {
if (!canAttachFile()) return;
const quint64 context = m_attachmentContext;
QString srcPath = QFileDialog::getOpenFileName(this, QCoreApplication::translate("ChatGui", "Attach file"));
if (srcPath.isEmpty()) return;
prepareAttachment(srcPath, context);
}
/* Выбор через скрепку и drop одинаково проверяют файл и заменяют прежний preview только после успеха. */
bool InputBar::prepareAttachment(const QString& srcPath, quint64 context) {
if (context != m_attachmentContext || !canAttachFile()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Attachment cancelled: the chat changed or attachments are unavailable."), srcPath);
return false;
}
QFileInfo fi(srcPath);
if (!fi.isFile() || !fi.isReadable()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Select an existing, readable file."), srcPath); return false;
}
if (fi.size() == 0 && (!m_dmMode || isVideoFile(fi.suffix()))) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Cannot attach an empty file."), srcPath); return false;
}
bool asImage = !m_dmMode && isImageFile(fi.suffix());
bool asVideo = false;
if (!asImage && isVideoFile(fi.suffix())) {
QMessageBox box(this);
box.setWindowTitle(QCoreApplication::translate("ChatGui", "Send video"));
box.setText(QCoreApplication::translate("ChatGui", "The file contains video. How would you like to send it?"));
QPushButton *fileBtn = box.addButton(QCoreApplication::translate("ChatGui", "As a file"), QMessageBox::AcceptRole);
QPushButton *videoBtn = box.addButton(QCoreApplication::translate("ChatGui", "As a video"), QMessageBox::DestructiveRole);
box.exec();
asVideo = (box.clickedButton() == videoBtn);
if (!box.clickedButton() || (!asVideo && box.clickedButton() != fileBtn)) {
GUI_DEBUG("attachment selection cancelled: video mode source=%s", qPrintable(srcPath));
return false;
}
GUI_INFO("InputBar: video detected, mode=%s",
asVideo ? "video" : "file");
}
if (context != m_attachmentContext || !canAttachFile()) {
rejectAttachment(QCoreApplication::translate("ChatGui", "Attachment cancelled: the chat changed or attachments are unavailable."), srcPath);
return false;
}
const QString mediaPath = m_mediaDirBase + "/" + m_channelIdForRecord;
if ((!m_dmMode || asVideo) && !QDir().mkpath(mediaPath)) {
GUI_ERROR("attachment media directory unavailable: target=%s path=%s", qPrintable(m_channelIdForRecord), qPrintable(mediaPath));
rejectAttachment(QCoreApplication::translate("ChatGui", "Cannot create the attachment directory."), srcPath); return false;
}
const QString ts = QDateTime::currentDateTime().toString("yyyyMMdd-HHmmss");
const int rnd = rand() & 0xFFFF;
QString destPath;
if (m_dmMode && !asVideo) destPath = srcPath;
else if (asVideo) {
destPath = QString("%1/%2_%3_%4.mp4")
.arg(mediaPath, ts).arg(fi.completeBaseName()).arg(rnd, 4, 16, QChar('0'));
} else {
destPath = QString("%1/%2_%3_%4.%5")
.arg(mediaPath, ts).arg(fi.completeBaseName()).arg(rnd, 4, 16, QChar('0'))
.arg(fi.suffix().isEmpty() ? "bin" : fi.suffix());
}
m_pendingAttachSrc = srcPath;
m_pendingAttachDest = destPath;
m_pendingAttachName = fi.fileName();
m_pendingAttachSize = fi.size();
m_pendingAttachAsVideo = asVideo;
m_pendingAttachAsImage = asImage;
QString labelText = QString::fromUtf8("\xF0\x9F\x93\x8E %1 (%2)")
.arg(fi.fileName(), formatFileSize(fi.size()));
m_attachPreviewLabel->setText(labelText);
m_attachPreview->show();
GUI_INFO("attachment preview ready: target=%s source=%s bytes=%lld video=%d image=%d",
qPrintable(m_channelIdForRecord), qPrintable(srcPath), (long long)fi.size(), asVideo ? 1 : 0, asImage ? 1 : 0);
m_input->setFocus();
return true;
}
void InputBar::clearPendingAttachment() {
m_pendingAttachSrc.clear();
m_pendingAttachDest.clear();
m_pendingAttachName.clear();
m_pendingAttachSize = 0;
m_pendingAttachAsVideo = false;
m_pendingAttachAsImage = false;
m_attachPreview->hide();
}
void InputBar::updatePreparation(const QString& target, const QByteArray& job, int state) {
if (state == 0) { m_preparations[target].insert(job); m_preparationErrors.remove(target); }
else {
m_preparations[target].remove(job);
if (m_preparations[target].isEmpty()) m_preparations.remove(target);
if (state == 2) m_preparationErrors.insert(target);
}
showPreparation();
}
void InputBar::showPreparation() {
QString status;
if (!m_preparations.value(m_channelIdForRecord).isEmpty()) status = QCoreApplication::translate("ChatGui", "Preparing media…");
else if (m_preparationErrors.contains(m_channelIdForRecord)) status = QCoreApplication::translate("ChatGui", "Failed to prepare attachment");
m_preparationLabel->setText(status);
m_preparationLabel->setVisible(!status.isEmpty());
}
void InputBar::resizeEvent(QResizeEvent* event) {
QWidget::resizeEvent(event);
if (m_recordOverlay) {
QRect r = m_input->geometry();
int margin = 4;
m_recordOverlay->setGeometry(r.adjusted(margin, margin, -margin, -margin));
m_recordOverlay->raise();
}
}
void InputBar::showRecordOverlay() {
if (m_recordOverlay) {
m_recordOverlay->setGeometry(m_input->geometry().adjusted(4, 4, -4, -4));
m_recordOverlay->show();
m_recordOverlay->raise();
m_recordLabel->setText(QCoreApplication::translate("ChatGui", "🔴 Recording 0:00"));
if (m_levelBar) m_levelBar->setValue(0);
}
}
void InputBar::hideRecordOverlay() {
if (m_recordOverlay) m_recordOverlay->hide();
}
void InputBar::updateRecordingUI(int ms) {
if (m_recordLabel) {
int secs = ms / 1000;
m_recordLabel->setText(QCoreApplication::translate("ChatGui", "🔴 Recording %1:%2")
.arg(secs / 60).arg(secs % 60, 2, 10, QChar('0')));
}
if (m_levelBar && m_recorder) {
float peak = m_recorder->peakLevel();
float db = peak > 0.0001f ? 20.0f * log10f(peak) : -70.0f;
int pct = (int)((db + 70.0f) / 70.0f * 100.0f);
if (pct < 0) pct = 0; if (pct > 100) pct = 100;
m_levelBar->setValue(pct);
const char* color = pct < 60 ? "#27ae60" : pct < 85 ? "#f1c40f" : "#e74c3c";
m_levelBar->setStyleSheet(QString(
"QProgressBar { border: 1px solid rgba(255,255,255,0.2); border-radius: 3px; background: rgba(0,0,0,0.3); }"
"QProgressBar::chunk { background: %1; border-radius: 2px; }").arg(color));
}
}