fix: voice message display, playback, recording UI, audio settings

- messagelist: parse content_type from JSON when DB column is empty
- messagelist: recording indicator (red dot + timer + cancel button)
- messagedelegate: editorEvent for play/pause toggle + progress bar
- messagedelegate: VoicePlayState tracking with QTimer
- InputBar: proper recordingContext (mediaDir + channelId)
- DbManager: dbPath() for media directory resolution
- AudioRecorder: peakLevel() for mic level meter
- VoiceEncoder: codec quality presets (Low/Standard/High)
- audiodevicesettingspage: Default device option first
- audiodevicesettingspage: mic level meter + test microphone
- audiodevicesettingspage: test speaker button
- audiodevicesettingspage: codec preset combo (Low/Standard/High)
- mainwindow: load opus_codec_preset from ui_state on startup
This commit is contained in:
Evgeny committed 2026-07-26 21:19:29 +03:00
1 parent 4fde0b0325
commit 850a260aac
14 files changed
+483 -57

No files matched your search

+143 -12
View File
@@ -4,6 +4,7 @@
#include "emoji.h"
#include "animtimer.h"
#include "lottieicon.h"
#include "voiceplayback.h"
#include <cmath>
#include <QPainter>
#include <QPainterPath>
@@ -11,10 +12,88 @@
#include <QAbstractItemView>
#include <QFontMetrics>
#include <QTextLayout>
#include <QMouseEvent>
#include <QMap>
#include <QDateTime>
#include <QTimer>
MessageDelegate::MessageDelegate(QObject *parent)
: QStyledItemDelegate(parent) {}
struct VoicePlayState {
bool playing = false;
qint64 startMs = 0;
float durationSec = 0.0f;
};
static QMap<QString, VoicePlayState> s_voiceStates;
static QTimer* s_voiceTimer = nullptr;
static void ensureVoiceTimer() {
if (!s_voiceTimer) {
s_voiceTimer = new QTimer();
s_voiceTimer->setInterval(100);
QObject::connect(s_voiceTimer, &QTimer::timeout, []() {
bool anyPlaying = false;
qint64 now = QDateTime::currentMSecsSinceEpoch();
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ) {
if (!it->playing) { ++it; continue; }
float elapsed = (now - it->startMs) / 1000.0f;
if (elapsed >= it->durationSec && it->durationSec > 0) {
it->playing = false;
++it;
} else {
anyPlaying = true;
++it;
}
}
if (!anyPlaying) s_voiceTimer->stop();
// Trigger repaint through any visible view
if (auto* w = QApplication::activeWindow()) w->update();
});
}
}
bool MessageDelegate::editorEvent(QEvent *event, QAbstractItemModel *model,
const QStyleOptionViewItem &option,
const QModelIndex &index) {
if (event->type() != QEvent::MouseButtonRelease) return false;
Layout L = calcLayout(option, index);
if (!L.isVoice) return false;
QMouseEvent* me = static_cast<QMouseEvent*>(event);
if (!L.voicePlayBtnRect.contains(me->pos())) return false;
QString filePath = index.data(MsgVoiceFileRole).toString();
if (filePath.isEmpty()) return false;
ensureVoiceTimer();
VoicePlayState& st = s_voiceStates[filePath];
if (st.playing) {
st.playing = false;
model->setData(index, 0, MsgVoicePlayStateRole);
} else {
// Stop any other playing voice
for (auto it = s_voiceStates.begin(); it != s_voiceStates.end(); ++it)
it->playing = false;
// Parse duration
QString durStr = index.data(MsgVoiceDurationRole).toString();
durStr.remove('s');
float dur = durStr.toFloat();
if (dur <= 0) dur = 1.0f;
st.playing = true;
st.startMs = QDateTime::currentMSecsSinceEpoch();
st.durationSec = dur;
VoicePlayer::playOpusFile(filePath);
s_voiceTimer->start();
model->setData(index, 1, MsgVoicePlayStateRole);
}
return true;
}
QSize MessageDelegate::sizeHint(const QStyleOptionViewItem &option,
const QModelIndex &index) const {
Layout L = calcLayout(option, index);
@@ -482,19 +561,42 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
drawBubble(painter, L, option);
if (!L.narrow) drawAvatar(painter, L, index);
/* Play button */
QString filePath = index.data(MsgVoiceFileRole).toString();
VoicePlayState st;
if (!filePath.isEmpty() && s_voiceStates.contains(filePath)) {
st = s_voiceStates[filePath];
} else {
int stateVal = index.data(MsgVoicePlayStateRole).toInt();
st.playing = (stateVal == 1);
}
bool isPlaying = st.playing;
QRectF playR = L.voicePlayBtnRect;
/* Play/Pause button */
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0x33, 0x90, 0xEC));
QPainterPath playPath;
QRectF playR = L.voicePlayBtnRect;
qreal cx = playR.center().x() - playR.width() / 6.0;
qreal cy = playR.center().y();
qreal hs = playR.height() / 2.5;
playPath.moveTo(cx, cy - hs);
playPath.lineTo(cx + hs * 1.5, cy);
playPath.lineTo(cx, cy + hs);
playPath.closeSubpath();
painter->drawPath(playPath);
if (isPlaying) {
/* Pause icon (two vertical bars) */
qreal bw = playR.width() / 5.0;
qreal gap = playR.width() / 8.0;
qreal top = playR.center().y() - playR.height() / 3.0;
qreal bot = playR.center().y() + playR.height() / 3.0;
painter->drawRoundedRect(QRectF(playR.center().x() - gap - bw, top, bw, bot - top), 1, 1);
painter->drawRoundedRect(QRectF(playR.center().x() + gap, top, bw, bot - top), 1, 1);
} else {
/* Play icon (triangle) */
QPainterPath playPath;
qreal cx = playR.center().x() - playR.width() / 6.0;
qreal cy = playR.center().y();
qreal hs = playR.height() / 2.5;
playPath.moveTo(cx, cy - hs);
playPath.lineTo(cx + hs * 1.5, cy);
playPath.lineTo(cx, cy + hs);
playPath.closeSubpath();
painter->drawPath(playPath);
}
/* Duration text */
painter->setPen(option.palette.text().color());
@@ -502,13 +604,42 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
if (!L.voiceDuration.isEmpty())
painter->drawText(L.voiceDurationRect, Qt::AlignLeft | Qt::AlignVCenter, L.voiceDuration);
/* Progress bar */
int pbLeft = L.bubbleRect.left() + kPadH;
int pbWidth = L.bubbleWidth - 2 * kPadH;
int pbTop = L.voicePlayBtnRect.bottom() + 6;
int pbHeight = 3;
if (isPlaying && st.durationSec > 0) {
qint64 now = QDateTime::currentMSecsSinceEpoch();
float elapsed = (now - st.startMs) / 1000.0f;
if (elapsed > st.durationSec) elapsed = st.durationSec;
float frac = elapsed / st.durationSec;
painter->setPen(Qt::NoPen);
painter->setBrush(QColor(0x33, 0x90, 0xEC, 60));
painter->drawRoundedRect(QRect(pbLeft, pbTop, pbWidth, pbHeight), 2, 2);
painter->setBrush(QColor(0x33, 0x90, 0xEC));
painter->drawRoundedRect(QRect(pbLeft, pbTop, (int)(pbWidth * frac), pbHeight), 2, 2);
/* Position text */
QFont timeFont = option.font;
timeFont.setPointSize(timeFont.pointSize() - 2);
painter->setFont(timeFont);
painter->setPen(option.palette.text().color().lighter(150));
int secs = (int)elapsed;
QString posStr = QString("%1:%2").arg(secs / 60).arg(secs % 60, 2, 10, QChar('0'));
QRect posR(pbLeft, pbTop + 4, pbWidth / 2, 14);
painter->drawText(posR, Qt::AlignLeft | Qt::AlignVCenter, posStr);
}
/* Time */
QFont timeFont = option.font;
timeFont.setPointSize(timeFont.pointSize() - 1);
painter->setFont(timeFont);
painter->setPen(option.palette.text().color().lighter(150));
QString timeStr = index.data(MsgTimeRole).toString();
QRect timeR = QRect(L.bubbleRect.right() - kPadH - 50, L.voicePlayBtnRect.bottom() + 4, 50, 16);
QRect timeR = QRect(L.bubbleRect.right() - kPadH - 50, pbTop + 4, 50, 16);
painter->drawText(timeR, Qt::AlignRight | Qt::AlignVCenter, timeStr);
painter->restore();