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:
evgeny committed 2026-07-29 14:15:10 +03:00
1 parent ce5a84da24
commit 65dbfa6813
43 files changed
+1983 -391

No files matched your search

+1
View File
@@ -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
+2
View File
@@ -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
+17 -48
View File
@@ -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");
}
+2 -11
View File
@@ -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;
};
+15 -20
View File
@@ -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) {
+2 -6
View File
@@ -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:
+7 -5
View File
@@ -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);
}
}
+79
View File
@@ -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;
+2
View File
@@ -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;
+20 -19
View File
@@ -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());
+13 -5
View File
@@ -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();
}
+2
View File
@@ -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;
};
+90
View File
@@ -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());
}
+22
View File
@@ -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;
};