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:
1 parent
4fde0b0325
commit
850a260aac
14 files changed
+483
-57
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user