feat: media_async + media_index modules, media_files table, tests
- media_async: thread-per-task async engine with primitives (sha256, sign, copy, uuid, file_size) - media_index: media_files DB table with register_async for indexing chunks - chat_msg: async media submission via register_async, simplified chat_msg_submit fields - voice_recorder/attachment_sender: write single file (no chunk splitting on caller side) - inputbar/messagelist: updated to new simplified submit interface - test_media_async: 4-thread stress test (200K ops / 2 sec, 0 errors) - test_media_index: 9 cases covering commit edge cases + async flow - build: Makefile.am, CMakeLists.txt, utun_sources.cmake updated
This commit is contained in:
1 parent
ce5a84da24
commit
65dbfa6813
43 files changed
+1983
-391
No files matched your search
@@ -119,6 +119,7 @@ add_executable(chatgui
|
||||
src/voiceplayback.cpp
|
||||
src/media_blocks.cpp
|
||||
src/audiodevicesettingspage.cpp
|
||||
src/storagesettingspage.cpp
|
||||
transport/utun_node.cpp
|
||||
transport/node_config.cpp
|
||||
transport/config_updater.cpp
|
||||
|
||||
@@ -67,6 +67,8 @@ target_include_directories(utun PUBLIC
|
||||
${SRC_DIR}
|
||||
${SRC_DIR}/transport_layer
|
||||
${SRC_DIR}/routing_layer
|
||||
${SRC_DIR}/media_delivery
|
||||
${SRC_DIR}/media_async
|
||||
${LIB_DIR}
|
||||
${SRC_DIR}/uip
|
||||
${CMAKE_SOURCE_DIR}/db
|
||||
|
||||
@@ -158,11 +158,8 @@ void AudioDeviceSettingsPage::loadFromDb() {
|
||||
m_savedInputIdx = m_db->getUiStateInt("audio_input_device", -1);
|
||||
m_savedCodecPreset = m_db->getUiStateInt("opus_codec_preset", 1);
|
||||
m_savedCompressorEnabled = m_db->getUiStateInt("compressor_enabled", 0);
|
||||
m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 30);
|
||||
m_savedCompressorLookbackMs = m_db->getUiStateInt("compressor_lookback_ms", 200);
|
||||
m_savedCompressorLookaheadMs = m_db->getUiStateInt("compressor_lookahead_ms", 100);
|
||||
m_savedCompressorRiseRateTenths = m_db->getUiStateInt("compressor_rise_rate_tenths", 20);
|
||||
m_savedCompressorTargetLevelDb = m_db->getUiStateInt("compressor_target_level_db", -12);
|
||||
m_savedCompressorMaxGainDb = m_db->getUiStateInt("compressor_max_gain_db", 25);
|
||||
m_savedCompressorRiseRate = m_db->getUiStateInt("compressor_rise_rate", 10);
|
||||
}
|
||||
|
||||
/* Select saved indices */
|
||||
@@ -181,10 +178,7 @@ void AudioDeviceSettingsPage::loadFromDb() {
|
||||
|
||||
m_compressorEnabledCb->setChecked(m_savedCompressorEnabled != 0);
|
||||
m_compressorMaxGainSlider->setValue(m_savedCompressorMaxGainDb);
|
||||
m_compressorLookbackSlider->setValue(m_savedCompressorLookbackMs);
|
||||
m_compressorLookaheadSlider->setValue(m_savedCompressorLookaheadMs);
|
||||
m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRateTenths);
|
||||
m_compressorTargetLevelSlider->setValue(m_savedCompressorTargetLevelDb);
|
||||
m_compressorRiseRateSlider->setValue(m_savedCompressorRiseRate);
|
||||
updateCompressorLabels();
|
||||
}
|
||||
|
||||
@@ -211,30 +205,20 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
|
||||
int compEnabled = m_compressorEnabledCb->isChecked() ? 1 : 0;
|
||||
int compMaxGain = m_compressorMaxGainSlider->value();
|
||||
int compLookback = m_compressorLookbackSlider->value();
|
||||
int compLookahead = m_compressorLookaheadSlider->value();
|
||||
int compRiseRateTenths = m_compressorRiseRateSlider->value();
|
||||
int compTargetLevel = m_compressorTargetLevelSlider->value();
|
||||
int compRiseRate = m_compressorRiseRateSlider->value();
|
||||
|
||||
saveKey("compressor_enabled", compEnabled);
|
||||
saveKey("compressor_max_gain_db", compMaxGain);
|
||||
saveKey("compressor_lookback_ms", compLookback);
|
||||
saveKey("compressor_lookahead_ms", compLookahead);
|
||||
saveKey("compressor_rise_rate_tenths", compRiseRateTenths);
|
||||
saveKey("compressor_target_level_db", compTargetLevel);
|
||||
saveKey("compressor_rise_rate", compRiseRate);
|
||||
|
||||
m_savedOutputIdx = outIdx;
|
||||
m_savedInputIdx = inIdx;
|
||||
m_savedCodecPreset = codecPreset;
|
||||
m_savedCompressorEnabled = compEnabled;
|
||||
m_savedCompressorMaxGainDb = compMaxGain;
|
||||
m_savedCompressorLookbackMs = compLookback;
|
||||
m_savedCompressorLookaheadMs = compLookahead;
|
||||
m_savedCompressorRiseRateTenths = compRiseRateTenths;
|
||||
m_savedCompressorTargetLevelDb = compTargetLevel;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB lookback=%d lookahead=%d rise=%.1fx target=%ddB",
|
||||
outIdx, inIdx, codecPreset, compEnabled, compMaxGain, compLookback, compLookahead,
|
||||
(float)compRiseRateTenths / 10.0f, compTargetLevel);
|
||||
m_savedCompressorRiseRate = compRiseRate;
|
||||
DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "saved output=%d input=%d codec=%d compressor=%d gain=%ddB rise=%.1fx",
|
||||
outIdx, inIdx, codecPreset, compEnabled, compMaxGain, (float)compRiseRate);
|
||||
|
||||
if (m_recorder) {
|
||||
m_recorder->setCompressorEnabled(compEnabled != 0);
|
||||
@@ -243,10 +227,10 @@ void AudioDeviceSettingsPage::applyAndSave() {
|
||||
compCfg.sample_rate = 48000;
|
||||
compCfg.channels = 1;
|
||||
compCfg.max_gain_db = (float)compMaxGain;
|
||||
compCfg.lookback_ms = compLookback;
|
||||
compCfg.lookahead_ms = compLookahead;
|
||||
compCfg.rise_rate_per_500ms = (float)compRiseRateTenths / 10.0f;
|
||||
compCfg.target_level = powf(10.0f, (float)compTargetLevel / 20.0f);
|
||||
compCfg.lookback_ms = 200;
|
||||
compCfg.lookahead_ms = 100;
|
||||
compCfg.rise_rate_per_sec = (float)compRiseRate;
|
||||
compCfg.target_level = 1.0f;
|
||||
audio_compressor_configure(m_recorder->compressor(), &compCfg);
|
||||
}
|
||||
}
|
||||
@@ -342,37 +326,22 @@ void AudioDeviceSettingsPage::buildCompressorUI(QVBoxLayout* layout) {
|
||||
};
|
||||
|
||||
makeSliderRow("Max gain:", m_compressorMaxGainSlider, m_compressorMaxGainLabel,
|
||||
0, 30, 1);
|
||||
makeSliderRow("Lookback time:", m_compressorLookbackSlider, m_compressorLookbackLabel,
|
||||
50, 500, 10);
|
||||
makeSliderRow("Lookahead time:", m_compressorLookaheadSlider, m_compressorLookaheadLabel,
|
||||
20, 500, 10);
|
||||
makeSliderRow("Rise rate (x/500ms):", m_compressorRiseRateSlider, m_compressorRiseRateLabel,
|
||||
11, 100, 1);
|
||||
makeSliderRow("Target level:", m_compressorTargetLevelSlider, m_compressorTargetLevelLabel,
|
||||
-40, 0, 1);
|
||||
0, 50, 1);
|
||||
makeSliderRow("Rise rate (x/sec):", m_compressorRiseRateSlider, m_compressorRiseRateLabel,
|
||||
2, 10, 1);
|
||||
|
||||
auto connectSlider = [this](QSlider* slider) {
|
||||
connect(slider, &QSlider::valueChanged, this, &AudioDeviceSettingsPage::updateCompressorLabels);
|
||||
};
|
||||
connectSlider(m_compressorMaxGainSlider);
|
||||
connectSlider(m_compressorLookbackSlider);
|
||||
connectSlider(m_compressorLookaheadSlider);
|
||||
connectSlider(m_compressorRiseRateSlider);
|
||||
connectSlider(m_compressorTargetLevelSlider);
|
||||
}
|
||||
|
||||
void AudioDeviceSettingsPage::updateCompressorLabels() {
|
||||
if (m_compressorMaxGainLabel)
|
||||
m_compressorMaxGainLabel->setText(QString::number(m_compressorMaxGainSlider->value()) + " dB");
|
||||
if (m_compressorLookbackLabel)
|
||||
m_compressorLookbackLabel->setText(QString::number(m_compressorLookbackSlider->value()) + " ms");
|
||||
if (m_compressorLookaheadLabel)
|
||||
m_compressorLookaheadLabel->setText(QString::number(m_compressorLookaheadSlider->value()) + " ms");
|
||||
if (m_compressorRiseRateLabel) {
|
||||
float v = (float)m_compressorRiseRateSlider->value() / 10.0f;
|
||||
m_compressorRiseRateLabel->setText(QString::number(v, 'f', 1) + "x");
|
||||
int v = m_compressorRiseRateSlider->value();
|
||||
m_compressorRiseRateLabel->setText(QString::number(v) + "x");
|
||||
}
|
||||
if (m_compressorTargetLevelLabel)
|
||||
m_compressorTargetLevelLabel->setText(QString::number(m_compressorTargetLevelSlider->value()) + " dBFS");
|
||||
}
|
||||
@@ -45,19 +45,10 @@ private:
|
||||
|
||||
QCheckBox* m_compressorEnabledCb;
|
||||
QSlider* m_compressorMaxGainSlider;
|
||||
QSlider* m_compressorLookbackSlider;
|
||||
QSlider* m_compressorLookaheadSlider;
|
||||
QSlider* m_compressorRiseRateSlider;
|
||||
QSlider* m_compressorTargetLevelSlider;
|
||||
QLabel* m_compressorMaxGainLabel;
|
||||
QLabel* m_compressorLookbackLabel;
|
||||
QLabel* m_compressorLookaheadLabel;
|
||||
QLabel* m_compressorRiseRateLabel;
|
||||
QLabel* m_compressorTargetLevelLabel;
|
||||
int m_savedCompressorEnabled = 0;
|
||||
int m_savedCompressorMaxGainDb = 30;
|
||||
int m_savedCompressorLookbackMs = 200;
|
||||
int m_savedCompressorLookaheadMs = 100;
|
||||
int m_savedCompressorRiseRateTenths = 20;
|
||||
int m_savedCompressorTargetLevelDb = -12;
|
||||
int m_savedCompressorMaxGainDb = 25;
|
||||
int m_savedCompressorRiseRate = 10;
|
||||
};
|
||||
@@ -233,21 +233,15 @@ void InputBar::onPttReleased() {
|
||||
qPrintable(mediaPath), QDir(mediaPath).exists(), qPrintable(m_channelIdForRecord));
|
||||
|
||||
QString fileName = VoiceEncoder::generateFileName();
|
||||
QString tempPath = mediaPath + "/" + fileName;
|
||||
QString mediaFile = mediaPath + "/" + fileName;
|
||||
float durationSec = 0;
|
||||
int frames = VoiceEncoder::encodeToFile(pcm, pcmSize, 48000, 1, tempPath, durationSec);
|
||||
int frames = VoiceEncoder::encodeToFile(pcm, pcmSize, 48000, 1, mediaFile, durationSec);
|
||||
if (frames <= 0) {
|
||||
QFile::remove(tempPath);
|
||||
QFile::remove(mediaFile);
|
||||
return;
|
||||
}
|
||||
|
||||
MediaBlockInfo blk = media_split_to_blocks(tempPath, mediaPath, true);
|
||||
if (blk.numBlocks <= 0) {
|
||||
DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar: block split failed");
|
||||
return;
|
||||
}
|
||||
|
||||
m_voiceFilePath = QString("%1/%2_%3_%4.%5").arg(mediaPath, blk.dt, blk.basename, blk.suffix, blk.ext);
|
||||
m_voiceFilePath = mediaFile;
|
||||
|
||||
const auto& wf = m_recorder->waveformLevels();
|
||||
QString wfStr = "wf=";
|
||||
@@ -267,10 +261,9 @@ void InputBar::onPttReleased() {
|
||||
wfStr += b64[idx];
|
||||
}
|
||||
wfStr += QString(";dur=%1;").arg(durationMs);
|
||||
GUI_INFO("InputBar: voice message ready: dt=%s stem=%s blocks=%d dur=%.1fs",
|
||||
qPrintable(blk.dt), qPrintable(blk.basename), blk.numBlocks, durationSec);
|
||||
emit sendVoiceMessage(blk.dt, blk.basename, blk.suffix, blk.ext,
|
||||
blk.numBlocks, blk.blockSize, durationMs, wfStr);
|
||||
GUI_INFO("InputBar: voice message ready: file=%s dur=%.1fs",
|
||||
qPrintable(mediaFile), durationSec);
|
||||
emit sendVoiceMessage(mediaFile, mediaFile, durationMs, wfStr);
|
||||
}
|
||||
|
||||
void InputBar::onAttachClicked() {
|
||||
@@ -285,13 +278,15 @@ void InputBar::onAttachClicked() {
|
||||
VoiceEncoder::ensureMediaDir(mediaPath);
|
||||
QDir().mkpath(mediaPath);
|
||||
|
||||
MediaBlockInfo blk = media_split_to_blocks(srcPath, mediaPath, false);
|
||||
if (blk.numBlocks <= 0) { DEBUG_WARN(DEBUG_CATEGORY_DEBUG, "InputBar::onAttachClicked: split failed"); return; }
|
||||
QDateTime now = QDateTime::currentDateTime();
|
||||
QString ts = now.toString("yyyyMMdd-HHmmss");
|
||||
int rnd = rand() & 0xFFFF;
|
||||
QString 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());
|
||||
|
||||
GUI_INFO("InputBar::onAttachClicked: file=%s dt=%s blocks=%d",
|
||||
qPrintable(fi.fileName()), qPrintable(blk.dt), blk.numBlocks);
|
||||
emit sendFileAttachment(blk.dt, blk.basename, blk.suffix, blk.ext,
|
||||
blk.numBlocks, blk.blockSize, fi.fileName());
|
||||
GUI_INFO("InputBar::onAttachClicked: src=%s dest=%s",
|
||||
qPrintable(srcPath), qPrintable(destPath));
|
||||
emit sendFileAttachment(srcPath, destPath, fi.fileName());
|
||||
}
|
||||
|
||||
void InputBar::resizeEvent(QResizeEvent* event) {
|
||||
|
||||
@@ -19,13 +19,9 @@ public:
|
||||
|
||||
signals:
|
||||
void sendMessage(const QString &text);
|
||||
void sendVoiceMessage(const QString& mediaDt, const QString& mediaBasename,
|
||||
const QString& mediaSuffix, const QString& mediaExt,
|
||||
int numBlocks, qint64 blockSize,
|
||||
void sendVoiceMessage(const QString& mediaSrc, const QString& mediaDest,
|
||||
int durationMs, const QString& waveformStr);
|
||||
void sendFileAttachment(const QString& mediaDt, const QString& mediaBasename,
|
||||
const QString& mediaSuffix, const QString& mediaExt,
|
||||
int numBlocks, qint64 blockSize,
|
||||
void sendFileAttachment(const QString& mediaSrc, const QString& mediaDest,
|
||||
const QString& displayName);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -217,11 +217,13 @@ void MainWindow::setupSoundFromConfig() {
|
||||
m_recorder->setCompressorEnabled(compEnabled != 0);
|
||||
if (m_recorder->compressor()) {
|
||||
audio_compressor_config_t compCfg = {0};
|
||||
compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 30);
|
||||
compCfg.lookback_ms = m_db->getUiStateInt("compressor_lookback_ms", 200);
|
||||
compCfg.lookahead_ms = m_db->getUiStateInt("compressor_lookahead_ms", 100);
|
||||
compCfg.rise_rate_per_500ms = (float)m_db->getUiStateInt("compressor_rise_rate_tenths", 20) / 10.0f;
|
||||
compCfg.target_level = powf(10.0f, (float)m_db->getUiStateInt("compressor_target_level_db", -12) / 20.0f);
|
||||
compCfg.sample_rate = 48000;
|
||||
compCfg.channels = 1;
|
||||
compCfg.max_gain_db = (float)m_db->getUiStateInt("compressor_max_gain_db", 25);
|
||||
compCfg.lookback_ms = 200;
|
||||
compCfg.lookahead_ms = 100;
|
||||
compCfg.rise_rate_per_sec = (float)m_db->getUiStateInt("compressor_rise_rate", 10);
|
||||
compCfg.target_level = 1.0f;
|
||||
audio_compressor_configure(m_recorder->compressor(), &compCfg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,6 +185,16 @@ static int measureTextHeight(const QFont &font, const QString &text, int width)
|
||||
return r.height();
|
||||
}
|
||||
|
||||
static QString formatFileSize(qint64 bytes) {
|
||||
if (bytes < 1024) return QString("%1B").arg(bytes);
|
||||
double kb = bytes / 1024.0;
|
||||
if (kb < 1024.0) return QString("%1KB").arg(kb, 0, 'f', kb < 10.0 ? 1 : 0);
|
||||
double mb = kb / 1024.0;
|
||||
if (mb < 1024.0) return QString("%1MB").arg(mb, 0, 'f', mb < 10.0 ? 1 : 0);
|
||||
double gb = mb / 1024.0;
|
||||
return QString("%1GB").arg(gb, 0, 'f', 1);
|
||||
}
|
||||
|
||||
static int calcMaxLineWidth(const QFont &font, const QString &text, int availW) {
|
||||
int maxW = 0;
|
||||
int pos = 0;
|
||||
@@ -259,6 +269,35 @@ MessageDelegate::Layout MessageDelegate::calcLayout(
|
||||
return L;
|
||||
}
|
||||
|
||||
/* File attachment detection */
|
||||
L.isFile = (contentType == "application/octet-stream");
|
||||
if (L.isFile) {
|
||||
QFontMetrics fm(option.font);
|
||||
int th = fm.height();
|
||||
int ih = 24; /* icon height */
|
||||
L.bubbleWidth = 230;
|
||||
int bubbleH = kPadTop + ih + 2 + th + kPadBot;
|
||||
L.totalHeight = qMax(bubbleH, kAvatar) + kItemGap;
|
||||
|
||||
int contentEdge = L.isOutgoing
|
||||
? L.viewWidth - (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap))
|
||||
: (L.narrow ? kMarginH : (kMarginH + kAvatar + kGap));
|
||||
int bx = L.isOutgoing ? (contentEdge - L.bubbleWidth) : contentEdge;
|
||||
L.bubbleRect = QRect(bx, 0, L.bubbleWidth, bubbleH);
|
||||
|
||||
L.textRect = QRect(bx + kPadH + ih + 8, kPadTop, L.bubbleWidth - 2*kPadH - ih - 8, th);
|
||||
L.statusRect = QRect(bx + kPadH + ih + 8 - 48, kPadTop + th + 2, L.bubbleWidth - 2*kPadH - ih - 8 + 48 - 40, th);
|
||||
|
||||
if (!L.narrow) {
|
||||
int avatarY = L.bubbleRect.bottom() - kAvatar;
|
||||
if (!L.isOutgoing)
|
||||
L.avatarRect = QRect(kMarginH, avatarY, kAvatar, kAvatar);
|
||||
else
|
||||
L.avatarRect = QRect(L.viewWidth - kMarginH - kAvatar, avatarY, kAvatar, kAvatar);
|
||||
}
|
||||
return L;
|
||||
}
|
||||
|
||||
L.emojiCount = detectEmojiOnly(text);
|
||||
L.emojiOnly = (L.emojiCount >= 1);
|
||||
|
||||
@@ -758,6 +797,46 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio
|
||||
return;
|
||||
}
|
||||
|
||||
if (L.isFile) {
|
||||
drawBubble(painter, L, option);
|
||||
if (!L.narrow) drawAvatar(painter, L, index);
|
||||
|
||||
/* File icon */
|
||||
QFont iconFont = option.font;
|
||||
iconFont.setPointSize(iconFont.pointSize() + 6);
|
||||
painter->setFont(iconFont);
|
||||
painter->setPen(option.palette.text().color());
|
||||
QRect iconRect(L.bubbleRect.left() + kPadH, kPadTop, 24, 24);
|
||||
painter->drawText(iconRect, Qt::AlignCenter, QString::fromUtf8("\xF0\x9F\x93\x84"));
|
||||
|
||||
QFont fileFont = option.font;
|
||||
fileFont.setBold(true);
|
||||
painter->setFont(fileFont);
|
||||
painter->setPen(option.palette.text().color());
|
||||
QString displayName = index.data(MsgFileDisplayNameRole).toString();
|
||||
painter->drawText(L.textRect, Qt::AlignLeft | Qt::AlignVCenter, displayName);
|
||||
|
||||
QFont sizeFont = option.font;
|
||||
sizeFont.setPointSize(sizeFont.pointSize() - 2);
|
||||
painter->setFont(sizeFont);
|
||||
painter->setPen(QColor(0x80, 0x80, 0x80));
|
||||
qint64 fsz = index.data(MsgFileSizeRole).toLongLong();
|
||||
QString sizeStr = formatFileSize(fsz);
|
||||
int nameH = QFontMetrics(fileFont).height();
|
||||
QRect sizeRect(L.textRect.left(), L.textRect.top() + nameH, L.textRect.width(), QFontMetrics(sizeFont).height());
|
||||
painter->drawText(sizeRect, Qt::AlignLeft | Qt::AlignVCenter, sizeStr);
|
||||
|
||||
/* Timestamp */
|
||||
QFont tsFont = option.font;
|
||||
tsFont.setPointSize(tsFont.pointSize() - 1);
|
||||
painter->setFont(tsFont);
|
||||
painter->drawText(QRect(L.textRect.right() - 40, L.textRect.top() + nameH, 40, QFontMetrics(sizeFont).height()),
|
||||
Qt::AlignRight | Qt::AlignVCenter, index.data(MsgTimeRole).toString());
|
||||
|
||||
painter->restore();
|
||||
return;
|
||||
}
|
||||
|
||||
auto *cv = qobject_cast<const ChatView *>(option.widget);
|
||||
bool singleTextSel = false;
|
||||
bool multiRowSel = false;
|
||||
|
||||
@@ -26,6 +26,7 @@ enum MessageDataRole {
|
||||
MsgVoiceNumBlocksRole = Qt::UserRole + 21,
|
||||
MsgVoiceSigsRole = Qt::UserRole + 22,
|
||||
MsgFileDisplayNameRole = Qt::UserRole + 23,
|
||||
MsgFileSizeRole = Qt::UserRole + 24,
|
||||
};
|
||||
|
||||
class MessageDelegate : public QStyledItemDelegate {
|
||||
@@ -61,6 +62,7 @@ private:
|
||||
QRect emojiArea;
|
||||
bool isOutgoing = false;
|
||||
bool isVoice = false;
|
||||
bool isFile = false;
|
||||
QRect voicePlayBtnRect;
|
||||
QRect voiceWaveformRect;
|
||||
QRect voiceDurationRect;
|
||||
|
||||
@@ -153,8 +153,17 @@ static void setVoiceMessageRoles(QStandardItem* item, const QByteArray& data,
|
||||
}
|
||||
}
|
||||
if (ct == "application/octet-stream") {
|
||||
QString fname = QString::fromUtf8(data);
|
||||
QString raw = QString::fromUtf8(data);
|
||||
int pipePos = raw.indexOf('|');
|
||||
QString fname = (pipePos > 0) ? raw.left(pipePos) : raw;
|
||||
item->setData(fname, MsgFileDisplayNameRole);
|
||||
|
||||
if (pipePos > 0) {
|
||||
QString meta = raw.mid(pipePos + 1);
|
||||
QStringList parts = meta.split('|');
|
||||
if (parts.size() >= 1)
|
||||
item->setData(parts[0].toULongLong(), MsgFileSizeRole);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,10 +255,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
|
||||
});
|
||||
|
||||
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
int numBlocks, qint64 blockSize,
|
||||
int durationMs, const QString& wfStr) {
|
||||
connect(m_inputBar, &InputBar::sendVoiceMessage, this, [this](const QString& mediaSrc, const QString& mediaDest,
|
||||
int durationMs, const QString& wfStr) {
|
||||
if (m_currentChannelId.isEmpty()) return;
|
||||
|
||||
QByteArray textData = wfStr.toUtf8();
|
||||
@@ -261,11 +268,9 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
|
||||
m_currentChannelId.toUtf8().constData());
|
||||
snprintf(req->content_type, sizeof(req->content_type), "audio/opus");
|
||||
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
|
||||
req->media_num_blocks = (uint32_t)numBlocks;
|
||||
snprintf(req->media_src, sizeof(req->media_src), "%s", mediaSrc.toUtf8().constData());
|
||||
snprintf(req->media_dest, sizeof(req->media_dest), "%s", mediaDest.toUtf8().constData());
|
||||
req->media_copy = 0;
|
||||
req->data = (uint8_t*)(req + 1);
|
||||
req->data_len = (uint32_t)textData.size();
|
||||
memcpy(req->data, textData.constData(), textData.size());
|
||||
@@ -273,10 +278,8 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
gui_bridge_post_uasync_fn(chat_core_submit_trampoline, req);
|
||||
});
|
||||
|
||||
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& dt, const QString& basename,
|
||||
const QString& suffix, const QString& ext,
|
||||
int numBlocks, qint64 blockSize,
|
||||
const QString& displayName) {
|
||||
connect(m_inputBar, &InputBar::sendFileAttachment, this, [this](const QString& mediaSrc, const QString& mediaDest,
|
||||
const QString& displayName) {
|
||||
if (m_currentChannelId.isEmpty()) return;
|
||||
|
||||
QByteArray textData = displayName.toUtf8();
|
||||
@@ -288,11 +291,9 @@ MessageList::MessageList(DbManager* db, AudioRecorder* recorder, QWidget *parent
|
||||
snprintf(req->channel_id, sizeof(req->channel_id), "%s",
|
||||
m_currentChannelId.toUtf8().constData());
|
||||
snprintf(req->content_type, sizeof(req->content_type), "application/octet-stream");
|
||||
snprintf(req->media_dt, sizeof(req->media_dt), "%s", dt.toUtf8().constData());
|
||||
snprintf(req->media_basename, sizeof(req->media_basename), "%s", basename.toUtf8().constData());
|
||||
snprintf(req->media_suffix, sizeof(req->media_suffix), "%s", suffix.toUtf8().constData());
|
||||
snprintf(req->media_ext, sizeof(req->media_ext), "%s", ext.toUtf8().constData());
|
||||
req->media_num_blocks = (uint32_t)numBlocks;
|
||||
snprintf(req->media_src, sizeof(req->media_src), "%s", mediaSrc.toUtf8().constData());
|
||||
snprintf(req->media_dest, sizeof(req->media_dest), "%s", mediaDest.toUtf8().constData());
|
||||
req->media_copy = (mediaSrc != mediaDest) ? 1 : 0;
|
||||
req->data = (uint8_t*)(req + 1);
|
||||
req->data_len = (uint32_t)textData.size();
|
||||
memcpy(req->data, textData.constData(), textData.size());
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "nodespage.h"
|
||||
#include "soundsettingspage.h"
|
||||
#include "audiodevicesettingspage.h"
|
||||
#include "storagesettingspage.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "../../lib/mem.h"
|
||||
#include "../db/db_manager.h"
|
||||
@@ -45,6 +46,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
||||
"QListWidget::item { padding: 10px 14px; }"
|
||||
"QListWidget::item:selected { background: #3390EC; color: white; }");
|
||||
m_categoryList->addItem("Profile");
|
||||
m_categoryList->addItem("Storage");
|
||||
m_categoryList->addItem("Network");
|
||||
m_categoryList->addItem("Database");
|
||||
m_categoryList->addItem("Status");
|
||||
@@ -84,6 +86,10 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNod
|
||||
profLayout->addRow(sortLabel, m_sortCombo);
|
||||
m_pages->addWidget(profilePage);
|
||||
|
||||
// Storage page
|
||||
m_storagePage = new StorageSettingsPage(db, m_pages);
|
||||
m_pages->addWidget(m_storagePage);
|
||||
|
||||
// Network page
|
||||
m_networkPage = new NetworkSettingsPage(m_pages);
|
||||
m_pages->addWidget(m_networkPage);
|
||||
@@ -166,11 +172,12 @@ void SettingsDialog::loadProfileFromConfig(const QString& path) {
|
||||
|
||||
void SettingsDialog::onCategoryChanged(int row) {
|
||||
m_pages->setCurrentIndex(row);
|
||||
if (row == 2) m_databasePage->refreshStatus();
|
||||
if (row == 3) m_statusPage->refreshStatus();
|
||||
if (row == 4) m_nodesPage->refreshNodes();
|
||||
if (row == 5) m_soundPage->loadFromDb();
|
||||
if (row == 6) m_audioDevicePage->loadFromDb();
|
||||
if (row == 1) m_storagePage->loadFromDb();
|
||||
if (row == 3) m_databasePage->refreshStatus();
|
||||
if (row == 4) m_statusPage->refreshStatus();
|
||||
if (row == 5) m_nodesPage->refreshNodes();
|
||||
if (row == 6) m_soundPage->loadFromDb();
|
||||
if (row == 7) m_audioDevicePage->loadFromDb();
|
||||
}
|
||||
|
||||
void SettingsDialog::onSave() {
|
||||
@@ -227,5 +234,6 @@ void SettingsDialog::onSave() {
|
||||
|
||||
m_soundPage->applyAndSave();
|
||||
m_audioDevicePage->applyAndSave();
|
||||
m_storagePage->applyAndSave();
|
||||
accept();
|
||||
}
|
||||
@@ -15,6 +15,7 @@ class StatusPage;
|
||||
class NodesPage;
|
||||
class SoundSettingsPage;
|
||||
class AudioDeviceSettingsPage;
|
||||
class StorageSettingsPage;
|
||||
class DbManager;
|
||||
class UtunNode;
|
||||
class AudioRecorder;
|
||||
@@ -45,4 +46,5 @@ private:
|
||||
NodesPage* m_nodesPage;
|
||||
SoundSettingsPage* m_soundPage;
|
||||
AudioDeviceSettingsPage* m_audioDevicePage;
|
||||
StorageSettingsPage* m_storagePage;
|
||||
};
|
||||
@@ -0,0 +1,90 @@
|
||||
#include "storagesettingspage.h"
|
||||
#include "../db/db_manager.h"
|
||||
#include "../transport/gui_bridge.h"
|
||||
#include "../../lib/mem.h"
|
||||
extern "C" {
|
||||
#include "chat/chat_core.h"
|
||||
}
|
||||
#include <QVBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QCheckBox>
|
||||
#include <QSpinBox>
|
||||
#include <QLabel>
|
||||
#include <QFrame>
|
||||
|
||||
static void saveUiStateInt(const char* key, int val) {
|
||||
QByteArray v = QByteArray::number(val);
|
||||
size_t klen = strlen(key), vlen = (size_t)v.size(), total = klen + 1 + vlen + 1;
|
||||
if (total > 256) return;
|
||||
void* arg = u_malloc(total);
|
||||
memcpy(arg, key, klen + 1);
|
||||
memcpy((char*)arg + klen + 1, v.constData(), vlen + 1);
|
||||
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
|
||||
}
|
||||
|
||||
StorageSettingsPage::StorageSettingsPage(DbManager* db, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_db(db)
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 12, 0, 0);
|
||||
layout->setSpacing(14);
|
||||
|
||||
auto* title = new QLabel("Media Storage", this);
|
||||
title->setStyleSheet("font-weight: bold; font-size: 13px;");
|
||||
layout->addWidget(title);
|
||||
|
||||
auto* form = new QFormLayout();
|
||||
form->setSpacing(10);
|
||||
form->setContentsMargins(0, 8, 0, 0);
|
||||
|
||||
m_autoloadCheck = new QCheckBox("Autoload media", this);
|
||||
m_autoloadCheck->setStyleSheet("font-size: 13px;");
|
||||
form->addRow(m_autoloadCheck);
|
||||
|
||||
m_autoloadMaxSize = new QSpinBox(this);
|
||||
m_autoloadMaxSize->setRange(1, 500);
|
||||
m_autoloadMaxSize->setSuffix(" MB");
|
||||
m_autoloadMaxSize->setFixedWidth(110);
|
||||
m_autoloadMaxSize->setStyleSheet(
|
||||
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;"
|
||||
" padding: 5px 8px; font-size: 13px; background: palette(base); }");
|
||||
form->addRow("Autoload max size:", m_autoloadMaxSize);
|
||||
|
||||
m_maxStorageSize = new QSpinBox(this);
|
||||
m_maxStorageSize->setRange(1, 100);
|
||||
m_maxStorageSize->setSuffix(" GB");
|
||||
m_maxStorageSize->setFixedWidth(110);
|
||||
m_maxStorageSize->setStyleSheet(
|
||||
"QSpinBox { border: 1px solid palette(mid); border-radius: 4px;"
|
||||
" padding: 5px 8px; font-size: 13px; background: palette(base); }");
|
||||
form->addRow("Max storage size:", m_maxStorageSize);
|
||||
|
||||
layout->addLayout(form);
|
||||
|
||||
auto* sep = new QFrame(this);
|
||||
sep->setFrameShape(QFrame::HLine);
|
||||
sep->setStyleSheet("QFrame { color: palette(mid); }");
|
||||
layout->addWidget(sep);
|
||||
|
||||
layout->addStretch();
|
||||
}
|
||||
|
||||
void StorageSettingsPage::loadFromDb() {
|
||||
if (!m_db) return;
|
||||
|
||||
bool autoload = m_db->getUiStateInt("storage_autoload", 1);
|
||||
m_autoloadCheck->setChecked(autoload);
|
||||
|
||||
int maxSizeMb = m_db->getUiStateInt("storage_autoload_maxsize_mb", 10);
|
||||
m_autoloadMaxSize->setValue(maxSizeMb);
|
||||
|
||||
int maxGb = m_db->getUiStateInt("storage_maxsize_gb", 1);
|
||||
m_maxStorageSize->setValue(maxGb);
|
||||
}
|
||||
|
||||
void StorageSettingsPage::applyAndSave() {
|
||||
saveUiStateInt("storage_autoload", m_autoloadCheck->isChecked() ? 1 : 0);
|
||||
saveUiStateInt("storage_autoload_maxsize_mb", m_autoloadMaxSize->value());
|
||||
saveUiStateInt("storage_maxsize_gb", m_maxStorageSize->value());
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class DbManager;
|
||||
class QCheckBox;
|
||||
class QSpinBox;
|
||||
|
||||
class StorageSettingsPage : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit StorageSettingsPage(DbManager* db, QWidget* parent = nullptr);
|
||||
|
||||
void loadFromDb();
|
||||
void applyAndSave();
|
||||
|
||||
private:
|
||||
DbManager* m_db = nullptr;
|
||||
QCheckBox* m_autoloadCheck;
|
||||
QSpinBox* m_autoloadMaxSize;
|
||||
QSpinBox* m_maxStorageSize;
|
||||
};
|
||||
Reference in new issue
Block a user