Browse Source

chatgui: per-event sound settings with DB persistence

- Sound files loaded from filesystem (sound_dir in INI config, absolute or relative)
- Per-event enable/disable + volume stored in DB (ui_state table)
- SoundManager: setEvent(), effective volume = master * per-event
- SoundSettingsPage UI: 4 event rows (checkbox + slider)
- chat_core_save_ui_state() for async DB writes via uasync thread
- DbManager::getUiStateInt() for reading settings from GUI thread
- Default sounds copied to sounds/ dir next to DB, config entries written
topo_upd
Evgeny 2 months ago
parent
commit
98bfb9dcd2
  1. 8
      tools/chatgui/db/db_manager.cpp
  2. 1
      tools/chatgui/db/db_manager.h
  3. 1
      tools/chatgui/resources/chatgui.qrc
  4. 57
      tools/chatgui/src/mainwindow.cpp
  5. 8
      tools/chatgui/src/settingsdialog.cpp
  6. 56
      tools/chatgui/src/sound_manager.cpp
  7. 11
      tools/chatgui/src/sound_manager.h
  8. 191
      tools/chatgui/src/soundsettingspage.cpp
  9. 26
      tools/chatgui/src/soundsettingspage.h
  10. 21
      tools/chatgui/transport/chat_core.c
  11. 4
      tools/chatgui/transport/chat_core.h
  12. 3
      tools/chatgui/transport/node_config.cpp

8
tools/chatgui/db/db_manager.cpp

@ -363,6 +363,14 @@ QString DbManager::getUiState(const QString& key) const {
return val; return val;
} }
int DbManager::getUiStateInt(const QString& key, int defaultVal) const {
QString val = getUiState(key);
if (val.isEmpty()) return defaultVal;
bool ok = false;
int v = val.toInt(&ok);
return ok ? v : defaultVal;
}
/* ── sync operations (read-only, for gui_bridge) ── */ /* ── sync operations (read-only, for gui_bridge) ── */
QByteArray DbManager::syncListChannels() const { QByteArray DbManager::syncListChannels() const {

1
tools/chatgui/db/db_manager.h

@ -104,6 +104,7 @@ public:
/* ── UI state (read-only используем QSettings, оставлен для совместимости) ── */ /* ── UI state (read-only используем QSettings, оставлен для совместимости) ── */
QString getUiState(const QString& key) const; QString getUiState(const QString& key) const;
int getUiStateInt(const QString& key, int defaultVal) const;
/* ── Sync-операции (read-only, для gui_bridge/inline) ── */ /* ── Sync-операции (read-only, для gui_bridge/inline) ── */
QByteArray syncListChannels() const; QByteArray syncListChannels() const;

1
tools/chatgui/resources/chatgui.qrc

@ -5,6 +5,5 @@
<file>animations/greeting.tgs</file> <file>animations/greeting.tgs</file>
<file>animations/stop.tgs</file> <file>animations/stop.tgs</file>
<file>animations/robot.tgs</file> <file>animations/robot.tgs</file>
<file>sounds/msg_incoming.mp3</file>
</qresource> </qresource>
</RCC> </RCC>

57
tools/chatgui/src/mainwindow.cpp

@ -21,6 +21,7 @@
#include <QSettings> #include <QSettings>
#include <QFile> #include <QFile>
#include <QTextStream> #include <QTextStream>
#include <QDir>
extern "C" { extern "C" {
#include "../transport/chat_core.h" #include "../transport/chat_core.h"
@ -85,25 +86,65 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath,
SoundManager* sm = SoundManager::instance(); SoundManager* sm = SoundManager::instance();
if (sm->init()) { if (sm->init()) {
QString soundDir;
QString msgInFile, msgOutFile, connectFile, disconnectFile;
QFile cfgFile(m_cfgPath); QFile cfgFile(m_cfgPath);
if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) { if (cfgFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
bool inGui = false; bool enabled = true; int volume = 80; bool inGui = false;
QTextStream in(&cfgFile); QTextStream in(&cfgFile);
while (!in.atEnd()) { while (!in.atEnd()) {
QString line = in.readLine().trimmed(); QString line = in.readLine().trimmed();
if (line == "[gui]") { inGui = true; continue; } if (line == "[gui]") { inGui = true; continue; }
if (line.startsWith("[") && line.endsWith("]")) inGui = false; if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue; if (!inGui) continue;
if (line.startsWith("sound_enabled=")) if (line.startsWith("sound_dir="))
enabled = line.mid(15).trimmed() != "0"; soundDir = line.mid(10).trimmed();
else if (line.startsWith("sound_volume=")) else if (line.startsWith("sound_msg_incoming="))
volume = line.mid(13).trimmed().toInt(); msgInFile = line.mid(19).trimmed();
else if (line.startsWith("sound_msg_outgoing="))
msgOutFile = line.mid(19).trimmed();
else if (line.startsWith("sound_connect="))
connectFile = line.mid(14).trimmed();
else if (line.startsWith("sound_disconnect="))
disconnectFile = line.mid(17).trimmed();
} }
cfgFile.close(); cfgFile.close();
sm->setEnabled(enabled);
sm->setVolume(volume / 100.0f);
} }
sm->loadSound("msg_incoming", ":/sounds/msg_incoming.mp3");
if (soundDir.isEmpty()) soundDir = "sounds";
QDir binDir(QApplication::applicationDirPath());
QString baseDir = QDir::isAbsolutePath(soundDir) ? soundDir
: binDir.absoluteFilePath(soundDir);
auto resolvePath = [&baseDir](const QString& file) -> QString {
if (file.isEmpty()) return {};
return QDir::cleanPath(baseDir + "/" + file);
};
struct { const char* name; QString file; } events[] = {
{"msg_incoming", msgInFile},
{"msg_outgoing", msgOutFile},
{"connect", connectFile},
{"disconnect", disconnectFile},
};
for (auto& e : events) {
if (!e.file.isEmpty()) {
QString path = resolvePath(e.file);
sm->loadSound(e.name, path);
}
}
if (m_db && m_db->isOpen()) {
sm->setEnabled(m_db->getUiStateInt("sound_enabled", 1));
sm->setVolume(m_db->getUiStateInt("sound_volume", 80) / 100.0f);
QString soundEvents[] = {"msg_incoming", "msg_outgoing", "connect", "disconnect"};
for (int i = 0; i < 4; i++) {
const QString& ek = soundEvents[i];
bool on = m_db->getUiStateInt(QStringLiteral("sound_%1_on").arg(ek), i == 0 ? 1 : 0);
float ev = m_db->getUiStateInt(QStringLiteral("sound_%1_vol").arg(ek), 80) / 100.0f;
sm->setEvent(ek, on, ev);
}
}
} }
if (m_db) { if (m_db) {

8
tools/chatgui/src/settingsdialog.cpp

@ -86,7 +86,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
m_nodesPage = new NodesPage(db, m_pages); m_nodesPage = new NodesPage(db, m_pages);
m_pages->addWidget(m_nodesPage); m_pages->addWidget(m_nodesPage);
m_soundPage = new SoundSettingsPage(m_pages); m_soundPage = new SoundSettingsPage(db, m_pages);
m_pages->addWidget(m_soundPage); m_pages->addWidget(m_soundPage);
rightLayout->addWidget(m_pages, 1); rightLayout->addWidget(m_pages, 1);
@ -124,7 +124,7 @@ SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, QWidget
// Load current values // Load current values
loadProfileFromConfig(m_configPath); loadProfileFromConfig(m_configPath);
m_networkPage->loadFromConfig(m_configPath); m_networkPage->loadFromConfig(m_configPath);
m_soundPage->loadFromConfig(m_configPath); m_soundPage->loadFromDb();
m_categoryList->setCurrentRow(0); m_categoryList->setCurrentRow(0);
} }
@ -148,7 +148,7 @@ void SettingsDialog::onCategoryChanged(int row) {
if (row == 2) m_databasePage->refreshStatus(); if (row == 2) m_databasePage->refreshStatus();
if (row == 3) m_statusPage->refreshStatus(); if (row == 3) m_statusPage->refreshStatus();
if (row == 4) m_nodesPage->refreshNodes(); if (row == 4) m_nodesPage->refreshNodes();
if (row == 5) m_soundPage->loadFromConfig(m_configPath); if (row == 5) m_soundPage->loadFromDb();
} }
void SettingsDialog::onSave() { void SettingsDialog::onSave() {
@ -188,6 +188,6 @@ void SettingsDialog::onSave() {
f.close(); f.close();
m_networkPage->saveToConfig(m_configPath); m_networkPage->saveToConfig(m_configPath);
m_soundPage->saveToConfig(m_configPath); m_soundPage->applyAndSave();
accept(); accept();
} }

56
tools/chatgui/src/sound_manager.cpp

@ -31,30 +31,17 @@ void SoundManager::shutdown() {
if (!m_initialized) return; if (!m_initialized) return;
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) { for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
if (it->sound) { if (it->sound) { ma_sound_uninit(it->sound); delete it->sound; }
ma_sound_uninit(it->sound); if (it->decoder) { ma_decoder_uninit(it->decoder); delete it->decoder; }
delete it->sound;
}
if (it->decoder) {
ma_decoder_uninit(it->decoder);
delete it->decoder;
}
} }
m_sounds.clear(); m_sounds.clear();
if (m_engine) { if (m_engine) { ma_engine_uninit(m_engine); delete m_engine; m_engine = nullptr; }
ma_engine_uninit(m_engine);
delete m_engine;
m_engine = nullptr;
}
m_initialized = false; m_initialized = false;
qDebug("SoundManager: shutdown"); qDebug("SoundManager: shutdown");
} }
void SoundManager::setEnabled(bool enabled) { void SoundManager::setEnabled(bool enabled) { m_enabled = enabled; }
m_enabled = enabled;
qDebug("SoundManager: enabled=%d", enabled);
}
bool SoundManager::isEnabled() const { return m_enabled; } bool SoundManager::isEnabled() const { return m_enabled; }
@ -62,15 +49,25 @@ void SoundManager::setVolume(float volume) {
if (volume < 0.0f) volume = 0.0f; if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f; if (volume > 1.0f) volume = 1.0f;
m_volume = volume; m_volume = volume;
for (auto it = m_sounds.begin(); it != m_sounds.end(); ++it) {
if (it->sound)
ma_sound_set_volume(it->sound, m_volume);
}
} }
float SoundManager::volume() const { return m_volume; } float SoundManager::volume() const { return m_volume; }
bool SoundManager::loadSound(const QString& name, const QString& qrcPath) { void SoundManager::setEvent(const QString& name, bool enabled, float volume) {
if (volume < 0.0f) volume = 0.0f;
if (volume > 1.0f) volume = 1.0f;
EventSettings es;
es.enabled = enabled;
es.volume = volume;
m_eventSettings[name] = es;
qDebug("SoundManager: event '%s' enabled=%d vol=%.2f", qPrintable(name), enabled, volume);
}
SoundManager::EventSettings SoundManager::eventSettings(const QString& name) const {
return m_eventSettings.value(name);
}
bool SoundManager::loadSound(const QString& name, const QString& filePath) {
if (!m_initialized) { if (!m_initialized) {
qWarning("SoundManager: not initialized, cannot load %s", qPrintable(name)); qWarning("SoundManager: not initialized, cannot load %s", qPrintable(name));
return false; return false;
@ -80,9 +77,9 @@ bool SoundManager::loadSound(const QString& name, const QString& qrcPath) {
return false; return false;
} }
QFile f(qrcPath); QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) { if (!f.open(QIODevice::ReadOnly)) {
qWarning("SoundManager: cannot open %s", qPrintable(qrcPath)); qWarning("SoundManager: cannot open %s", qPrintable(filePath));
return false; return false;
} }
@ -105,26 +102,29 @@ bool SoundManager::loadSound(const QString& name, const QString& qrcPath) {
0, NULL, ls.sound); 0, NULL, ls.sound);
if (result != MA_SUCCESS) { if (result != MA_SUCCESS) {
qWarning("SoundManager: sound init failed for %s (%d)", qPrintable(name), result); qWarning("SoundManager: sound init failed for %s (%d)", qPrintable(name), result);
ma_decoder_uninit(ls.decoder); ma_decoder_uninit(ls.decoder); delete ls.decoder;
delete ls.decoder;
delete ls.sound; delete ls.sound;
return false; return false;
} }
ma_sound_set_volume(ls.sound, m_volume);
m_sounds.insert(name, ls); m_sounds.insert(name, ls);
qDebug("SoundManager: loaded '%s' from %s (%lld bytes)", qDebug("SoundManager: loaded '%s' from %s (%lld bytes)",
qPrintable(name), qPrintable(qrcPath), (long long)ls.mp3Data.size()); qPrintable(name), qPrintable(filePath), (long long)ls.mp3Data.size());
return true; return true;
} }
void SoundManager::play(const QString& name) { void SoundManager::play(const QString& name) {
if (!m_enabled || !m_initialized) return; if (!m_enabled || !m_initialized) return;
EventSettings es = m_eventSettings.value(name);
if (!es.enabled) return;
auto it = m_sounds.find(name); auto it = m_sounds.find(name);
if (it == m_sounds.end()) return; if (it == m_sounds.end()) return;
ma_sound* s = it->sound; ma_sound* s = it->sound;
float effectiveVolume = m_volume * es.volume;
ma_sound_set_volume(s, effectiveVolume);
ma_sound_stop(s); ma_sound_stop(s);
ma_sound_seek_to_pcm_frame(s, 0); ma_sound_seek_to_pcm_frame(s, 0);
ma_sound_start(s); ma_sound_start(s);

11
tools/chatgui/src/sound_manager.h

@ -12,6 +12,11 @@ struct ma_sound;
class SoundManager : public QObject { class SoundManager : public QObject {
Q_OBJECT Q_OBJECT
public: public:
struct EventSettings {
bool enabled = true;
float volume = 0.8f;
};
static SoundManager* instance(); static SoundManager* instance();
bool init(); bool init();
@ -22,7 +27,10 @@ public:
void setVolume(float volume); void setVolume(float volume);
float volume() const; float volume() const;
bool loadSound(const QString& name, const QString& qrcPath); void setEvent(const QString& name, bool enabled, float volume);
EventSettings eventSettings(const QString& name) const;
bool loadSound(const QString& name, const QString& filePath);
void play(const QString& name); void play(const QString& name);
private: private:
@ -39,5 +47,6 @@ private:
bool m_initialized = false; bool m_initialized = false;
bool m_enabled = true; bool m_enabled = true;
float m_volume = 0.8f; float m_volume = 0.8f;
QHash<QString, EventSettings> m_eventSettings;
QHash<QString, LoadedSound> m_sounds; QHash<QString, LoadedSound> m_sounds;
}; };

191
tools/chatgui/src/soundsettingspage.cpp

@ -1,16 +1,22 @@
#include "soundsettingspage.h" #include "soundsettingspage.h"
#include "sound_manager.h" #include "sound_manager.h"
#include "../db/db_manager.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
extern "C" {
#include "../transport/chat_core.h"
}
#include <QVBoxLayout> #include <QVBoxLayout>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QCheckBox> #include <QCheckBox>
#include <QSlider> #include <QSlider>
#include <QLabel> #include <QLabel>
#include <QFile> #include <QFrame>
#include <QTextStream>
#include <QDebug> #include <QDebug>
SoundSettingsPage::SoundSettingsPage(QWidget* parent) SoundSettingsPage::SoundSettingsPage(DbManager* db, QWidget* parent)
: QWidget(parent) : QWidget(parent)
, m_db(db)
{ {
auto* layout = new QVBoxLayout(this); auto* layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 12, 0, 0); layout->setContentsMargins(0, 12, 0, 0);
@ -28,8 +34,9 @@ SoundSettingsPage::SoundSettingsPage(QWidget* parent)
m_volumeSlider = new QSlider(Qt::Horizontal, this); m_volumeSlider = new QSlider(Qt::Horizontal, this);
m_volumeSlider->setRange(0, 100); m_volumeSlider->setRange(0, 100);
m_volumeSlider->setFixedWidth(200); m_volumeSlider->setFixedWidth(200);
m_volumeSlider->setStyleSheet("QSlider::groove:horizontal { height: 6px; border-radius: 3px; background: palette(mid); }" m_volumeSlider->setStyleSheet(
"QSlider::handle:horizontal { width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; background: #3390EC; }"); "QSlider::groove:horizontal { height: 6px; border-radius: 3px; background: palette(mid); }"
"QSlider::handle:horizontal { width: 14px; height: 14px; margin: -4px 0; border-radius: 7px; background: #3390EC; }");
volLayout->addWidget(m_volumeSlider); volLayout->addWidget(m_volumeSlider);
m_volumeLabel = new QLabel("80%", this); m_volumeLabel = new QLabel("80%", this);
@ -39,82 +46,136 @@ SoundSettingsPage::SoundSettingsPage(QWidget* parent)
volLayout->addStretch(); volLayout->addStretch();
layout->addLayout(volLayout); layout->addLayout(volLayout);
auto* sep = new QFrame(this);
sep->setFrameShape(QFrame::HLine);
sep->setStyleSheet("QFrame { color: palette(mid); }");
layout->addWidget(sep);
auto* header = new QLabel("Events", this);
header->setStyleSheet("font-size: 13px; font-weight: bold;");
layout->addWidget(header);
m_events[0] = {"msg_incoming", "Incoming message", nullptr, nullptr, nullptr};
m_events[1] = {"msg_outgoing", "Outgoing message", nullptr, nullptr, nullptr};
m_events[2] = {"connect", "Peer connected", nullptr, nullptr, nullptr};
m_events[3] = {"disconnect", "Peer disconnected", nullptr, nullptr, nullptr};
for (int i = 0; i < 4; i++) {
auto& e = m_events[i];
auto* row = new QHBoxLayout();
row->setSpacing(8);
auto* lbl = new QLabel(e.label, this);
lbl->setFixedWidth(140);
lbl->setStyleSheet("font-size: 13px;");
row->addWidget(lbl);
e.check = new QCheckBox(this);
row->addWidget(e.check);
e.slider = new QSlider(Qt::Horizontal, this);
e.slider->setRange(0, 100);
e.slider->setFixedWidth(100);
e.slider->setStyleSheet(
"QSlider::groove:horizontal { height: 4px; border-radius: 2px; background: palette(mid); }"
"QSlider::handle:horizontal { width: 10px; height: 10px; margin: -3px 0; border-radius: 5px; background: #3390EC; }");
row->addWidget(e.slider);
e.volLabel = new QLabel("80%", this);
e.volLabel->setFixedWidth(30);
e.volLabel->setStyleSheet("font-size: 12px;");
row->addWidget(e.volLabel);
row->addStretch();
layout->addLayout(row);
connect(e.slider, &QSlider::valueChanged, this, [this, i](int val) {
m_events[i].volLabel->setText(QString::number(val) + "%");
});
}
layout->addStretch(); layout->addStretch();
connect(m_enabledCheck, &QCheckBox::stateChanged, this, &SoundSettingsPage::onEnabledChanged); connect(m_enabledCheck, &QCheckBox::stateChanged, this, [this](int state) {
bool on = (state == Qt::Checked);
m_volumeSlider->setEnabled(on);
m_volumeLabel->setEnabled(on);
});
connect(m_volumeSlider, &QSlider::valueChanged, this, [this](int val) { connect(m_volumeSlider, &QSlider::valueChanged, this, [this](int val) {
m_volumeLabel->setText(QString::number(val) + "%"); m_volumeLabel->setText(QString::number(val) + "%");
}); });
} }
void SoundSettingsPage::onEnabledChanged(int state) { void SoundSettingsPage::loadFromDb() {
m_volumeSlider->setEnabled(state == Qt::Checked); if (!m_db) return;
m_volumeLabel->setEnabled(state == Qt::Checked);
}
void SoundSettingsPage::loadFromConfig(const QString& path) {
QFile f(path);
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
bool enabled = true;
int volume = 80;
bool inGui = false;
QTextStream in(&f);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
if (line == "[gui]") { inGui = true; continue; }
if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue;
if (line.startsWith("sound_enabled="))
enabled = line.mid(QString("sound_enabled=").size()).trimmed() != "0";
else if (line.startsWith("sound_volume="))
volume = line.mid(QString("sound_volume=").size()).trimmed().toInt();
}
f.close();
bool enabled = m_db->getUiStateInt("sound_enabled", 1);
m_enabledCheck->setChecked(enabled); m_enabledCheck->setChecked(enabled);
m_volumeSlider->setValue(volume);
int vol = m_db->getUiStateInt("sound_volume", 80);
m_volumeSlider->setValue(vol);
m_volumeSlider->setEnabled(enabled); m_volumeSlider->setEnabled(enabled);
m_volumeLabel->setEnabled(enabled); m_volumeLabel->setEnabled(enabled);
m_volumeLabel->setText(QString::number(volume) + "%"); m_volumeLabel->setText(QString::number(vol) + "%");
for (int i = 0; i < 4; i++) {
auto& e = m_events[i];
QString onKey = QStringLiteral("sound_%1_on").arg(e.key);
QString volKey = QStringLiteral("sound_%1_vol").arg(e.key);
bool on = m_db->getUiStateInt(onKey, i == 0 ? 1 : 0); // only msg_incoming on by default
int ev = m_db->getUiStateInt(volKey, 80);
e.check->setChecked(on);
e.slider->setValue(ev);
e.volLabel->setText(QString::number(ev) + "%");
}
} }
void SoundSettingsPage::saveToConfig(const QString& path) const { void SoundSettingsPage::applyAndSave() {
QFile f(path); SoundManager* sm = SoundManager::instance();
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return;
QString content = f.readAll(); bool enabled = m_enabledCheck->isChecked();
f.close(); float masterVol = m_volumeSlider->value() / 100.0f;
sm->setEnabled(enabled);
QStringList lines = content.split('\n'); sm->setVolume(masterVol);
bool inGui = false; for (int i = 0; i < 4; i++) {
bool foundEnabled = false, foundVolume = false; auto& e = m_events[i];
for (int i = 0; i < lines.size(); i++) { bool on = e.check->isChecked();
QString line = lines[i].trimmed(); float ev = e.slider->value() / 100.0f;
if (line == "[gui]") { inGui = true; continue; } sm->setEvent(e.key, on, ev);
if (line.startsWith("[") && line.endsWith("]")) inGui = false;
if (!inGui) continue; /* async DB persist — key\0value\0 */
QString onKey = QStringLiteral("sound_%1_on").arg(e.key);
if (line.startsWith("sound_enabled=")) { QString volKey = QStringLiteral("sound_%1_vol").arg(e.key);
lines[i] = QString("sound_enabled=%1").arg(m_enabledCheck->isChecked() ? 1 : 0); QByteArray onVal = on ? QByteArray("1") : QByteArray("0");
foundEnabled = true; QByteArray volVal = QByteArray::number(e.slider->value());
} else if (line.startsWith("sound_volume=")) {
lines[i] = QString("sound_volume=%1").arg(m_volumeSlider->value()); for (int j = 0; j < 2; j++) {
foundVolume = true; const char* k = j == 0 ? onKey.toUtf8().constData() : volKey.toUtf8().constData();
const char* v = j == 0 ? onVal.constData() : volVal.constData();
size_t klen = strlen(k), vlen = strlen(v);
size_t total = klen + 1 + vlen + 1;
if (total > 256) continue;
void* arg = u_malloc(total);
memcpy(arg, k, klen + 1);
memcpy((char*)arg + klen + 1, v, vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
} }
} }
if (!foundEnabled || !foundVolume) { /* save global settings */
for (int i = 0; i < lines.size(); i++) { {
if (lines[i].trimmed() == "[gui]") { const char* keys[] = {"sound_enabled", "sound_volume"};
if (!foundEnabled) lines.insert(i + 1, QString("sound_enabled=%1").arg(m_enabledCheck->isChecked() ? 1 : 0)); QByteArray vals[] = {enabled ? QByteArray("1") : QByteArray("0"),
if (!foundVolume) lines.insert(i + 1, QString("sound_volume=%1").arg(m_volumeSlider->value())); QByteArray::number(m_volumeSlider->value())};
break; for (int j = 0; j < 2; j++) {
} size_t klen = strlen(keys[j]), vlen = vals[j].size();
size_t total = klen + 1 + vlen + 1;
if (total > 256) continue;
void* arg = u_malloc(total);
memcpy(arg, keys[j], klen + 1);
memcpy((char*)arg + klen + 1, vals[j].constData(), vlen + 1);
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg);
} }
} }
f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text);
f.write(lines.join('\n').toUtf8());
f.close();
} }

26
tools/chatgui/src/soundsettingspage.h

@ -2,6 +2,7 @@
#include <QWidget> #include <QWidget>
class DbManager;
class QCheckBox; class QCheckBox;
class QSlider; class QSlider;
class QLabel; class QLabel;
@ -9,16 +10,23 @@ class QLabel;
class SoundSettingsPage : public QWidget { class SoundSettingsPage : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
explicit SoundSettingsPage(QWidget* parent = nullptr); explicit SoundSettingsPage(DbManager* db, QWidget* parent = nullptr);
void loadFromConfig(const QString& configPath); void loadFromDb();
void saveToConfig(const QString& configPath) const; void applyAndSave();
private slots:
void onEnabledChanged(int state);
private: private:
QCheckBox* m_enabledCheck; struct EventRow {
QSlider* m_volumeSlider; const char* key;
QLabel* m_volumeLabel; const char* label;
QCheckBox* check = nullptr;
QSlider* slider = nullptr;
QLabel* volLabel = nullptr;
};
DbManager* m_db = nullptr;
QCheckBox* m_enabledCheck;
QSlider* m_volumeSlider;
QLabel* m_volumeLabel;
EventRow m_events[4];
}; };

21
tools/chatgui/transport/chat_core.c

@ -394,6 +394,27 @@ void chat_core_update_my_name_trampoline(void* arg) {
u_free(arg); u_free(arg);
} }
void chat_core_save_ui_state(const char* key, const char* value) {
if (!g_cc.initialized || !key || !value) return;
sqlite3_stmt* st = NULL;
sqlite3_prepare_v2(g_cc.db,
"INSERT OR REPLACE INTO ui_state(key, value) VALUES(?, ?)", -1, &st, NULL);
if (st) {
sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(st, 2, value, -1, SQLITE_STATIC);
sqlite3_step(st); sqlite3_finalize(st);
}
}
struct save_ui_state_arg { char data[256]; };
void chat_core_save_ui_state_trampoline(void* arg) {
struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg;
const char* key = a->data;
const char* value = key + strlen(key) + 1;
chat_core_save_ui_state(key, value);
u_free(arg);
}
static void chat_core_collect_status(void); static void chat_core_collect_status(void);
void chat_core_collect_status_trampoline(void* arg) { void chat_core_collect_status_trampoline(void* arg) {

4
tools/chatgui/transport/chat_core.h

@ -98,6 +98,10 @@ void chat_core_update_my_name(const char* name);
void chat_core_update_my_name_trampoline(void* arg); void chat_core_update_my_name_trampoline(void* arg);
void chat_core_sync_my_addresses(void); void chat_core_sync_my_addresses(void);
/* Сохранение key-value в ui_state (вызывается из uasync-потока) */
void chat_core_save_ui_state(const char* key, const char* value);
void chat_core_save_ui_state_trampoline(void* arg);
/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ /* Трамплин для gui_bridge_post_uasync (GUI → uasync) */
void chat_core_submit_trampoline(void* arg); void chat_core_submit_trampoline(void* arg);

3
tools/chatgui/transport/node_config.cpp

@ -37,7 +37,8 @@ static const QSet<QString> CONTROL_KEYS = {
static const QSet<QString> GUI_KEYS = { static const QSet<QString> GUI_KEYS = {
"db_path", "debug_file", "debug_level", "debug_categories", "db_path", "debug_file", "debug_level", "debug_categories",
"sound_enabled", "sound_volume" "sound_dir", "sound_msg_incoming", "sound_msg_outgoing",
"sound_connect", "sound_disconnect"
}; };
static const QSet<QString> ALLOWED_KEYS_KEYS = { static const QSet<QString> ALLOWED_KEYS_KEYS = {

Loading…
Cancel
Save