You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
14 KiB
362 lines
14 KiB
// settingsdialog.cpp — Settings dialog with category list |
|
#include "settingsdialog.h" |
|
#include "networksettingspage.h" |
|
#include "databasesettingspage.h" |
|
#include "statuspage.h" |
|
#include "nodespage.h" |
|
#include "soundsettingspage.h" |
|
#include "audiodevicesettingspage.h" |
|
#include "storagesettingspage.h" |
|
#include "radiosettingspage.h" |
|
#include "connmonitorwindow.h" |
|
#include "../transport/gui_bridge.h" |
|
#include "../../lib/mem.h" |
|
#include "../db/db_manager.h" |
|
#include "../transport/utun_node.h" |
|
extern "C" { |
|
#include "chat/chat_core.h" |
|
} |
|
#include <QVBoxLayout> |
|
#include <QHBoxLayout> |
|
#include <QPushButton> |
|
#include <QLabel> |
|
#include <QFormLayout> |
|
#include <QLineEdit> |
|
#include <QComboBox> |
|
#include <QFile> |
|
#include <QTextStream> |
|
|
|
SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node, |
|
AudioRecorder* recorder, QWidget* parent) |
|
: QDialog(parent) |
|
, m_configPath(configPath) |
|
{ |
|
setWindowTitle("Settings"); |
|
setMinimumSize(760, 460); |
|
setWindowModality(Qt::WindowModal); |
|
|
|
auto* mainLayout = new QHBoxLayout(this); |
|
mainLayout->setSpacing(0); |
|
mainLayout->setContentsMargins(0, 0, 0, 0); |
|
|
|
// --- Left sidebar: category list --- |
|
m_categoryList = new QListWidget(this); |
|
m_categoryList->setFixedWidth(150); |
|
m_categoryList->setStyleSheet( |
|
"QListWidget { border: none; border-right: 1px solid palette(mid);" |
|
" background: palette(window); font-size: 13px; }" |
|
"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("Connections"); |
|
m_categoryList->addItem("Database"); |
|
m_categoryList->addItem("Status"); |
|
m_categoryList->addItem("Nodes"); |
|
m_categoryList->addItem("Sound"); |
|
m_categoryList->addItem("Audio"); |
|
mainLayout->addWidget(m_categoryList); |
|
|
|
// --- Right panel: stacked pages --- |
|
auto* rightPanel = new QWidget(this); |
|
auto* rightLayout = new QVBoxLayout(rightPanel); |
|
rightLayout->setContentsMargins(20, 16, 20, 16); |
|
rightLayout->setSpacing(0); |
|
|
|
m_pages = new QStackedWidget(rightPanel); |
|
|
|
// Profile page |
|
auto* profilePage = new QWidget(m_pages); |
|
auto* profLayout = new QFormLayout(profilePage); |
|
profLayout->setContentsMargins(0, 12, 0, 0); |
|
auto* nameLabel = new QLabel("Display name:", profilePage); |
|
m_nicknameEdit = new QLineEdit(profilePage); |
|
m_nicknameEdit->setPlaceholderText("Your name (shown to others)"); |
|
m_nicknameEdit->setMaxLength(32); |
|
m_nicknameEdit->setStyleSheet( |
|
"QLineEdit { border: 1px solid palette(mid); border-radius: 4px;" |
|
" padding: 6px 10px; font-size: 13px; background: palette(base); }"); |
|
profLayout->addRow(nameLabel, m_nicknameEdit); |
|
|
|
auto* sortLabel = new QLabel("Channel sorting:", profilePage); |
|
m_sortCombo = new QComboBox(profilePage); |
|
m_sortCombo->addItem("Alphabetical (A-Z)"); |
|
m_sortCombo->addItem("Recent Activity"); |
|
m_sortCombo->setStyleSheet( |
|
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;" |
|
" padding: 6px 10px; font-size: 13px; background: palette(base); }"); |
|
profLayout->addRow(sortLabel, m_sortCombo); |
|
|
|
auto* joinLabel = new QLabel("Join requests:", profilePage); |
|
m_joinPolicyCombo = new QComboBox(profilePage); |
|
m_joinPolicyCombo->addItem("Auto-join"); |
|
m_joinPolicyCombo->addItem("Ask before joining"); |
|
m_joinPolicyCombo->addItem("Deny all"); |
|
m_joinPolicyCombo->setStyleSheet( |
|
"QComboBox { border: 1px solid palette(mid); border-radius: 4px;" |
|
" padding: 6px 10px; font-size: 13px; background: palette(base); }"); |
|
profLayout->addRow(joinLabel, m_joinPolicyCombo); |
|
|
|
m_autoConnectCheck = new QCheckBox("Auto-connect to group nodes", profilePage); |
|
m_autoConnectCheck->setStyleSheet("font-size: 13px;"); |
|
profLayout->addRow(m_autoConnectCheck); |
|
m_pages->addWidget(profilePage); |
|
|
|
// Storage page |
|
m_storagePage = new StorageSettingsPage(db, m_pages); |
|
m_storagePage->setConfigPath(m_configPath); |
|
m_pages->addWidget(m_storagePage); |
|
|
|
// Network page |
|
m_networkPage = new NetworkSettingsPage(m_pages); |
|
m_pages->addWidget(m_networkPage); |
|
|
|
// Connections page — button to open standalone window |
|
{ |
|
auto* connPage = new QWidget(m_pages); |
|
auto* connLayout = new QVBoxLayout(connPage); |
|
connLayout->setAlignment(Qt::AlignCenter); |
|
auto* hintLabel = new QLabel("Live ETCP connection monitor\nwith per-link BBR statistics", connPage); |
|
hintLabel->setAlignment(Qt::AlignCenter); |
|
hintLabel->setStyleSheet("font-size: 12px; color: palette(text); padding: 20px;"); |
|
connLayout->addWidget(hintLabel); |
|
auto* openBtn = new QPushButton("Open Connection Monitor", connPage); |
|
openBtn->setStyleSheet("QPushButton { padding: 10px 20px; font-size: 13px; }"); |
|
connect(openBtn, &QPushButton::clicked, this, &SettingsDialog::onOpenConnMonitor); |
|
connLayout->addWidget(openBtn); |
|
m_pages->addWidget(connPage); |
|
} |
|
|
|
// Database page |
|
m_databasePage = new DatabaseSettingsPage(db, m_pages); |
|
m_pages->addWidget(m_databasePage); |
|
|
|
// Status page |
|
m_statusPage = new StatusPage(node, m_pages); |
|
m_pages->addWidget(m_statusPage); |
|
|
|
// Nodes page |
|
m_nodesPage = new NodesPage(db, m_pages); |
|
m_pages->addWidget(m_nodesPage); |
|
|
|
m_soundPage = new SoundSettingsPage(db, m_pages); |
|
m_pages->addWidget(m_soundPage); |
|
|
|
m_audioDevicePage = new AudioDeviceSettingsPage(db, recorder, m_pages); |
|
m_audioDevicePage->setConfigPath(m_configPath); |
|
m_pages->addWidget(m_audioDevicePage); |
|
|
|
m_radioPage = new RadioSettingsPage(db, m_pages); |
|
m_pages->addWidget(m_radioPage); |
|
rightLayout->addWidget(m_pages, 1); |
|
|
|
// --- Bottom buttons --- |
|
auto* btnLayout = new QHBoxLayout(); |
|
btnLayout->setSpacing(8); |
|
btnLayout->addStretch(); |
|
|
|
auto* cancelBtn = new QPushButton("Cancel", rightPanel); |
|
cancelBtn->setFixedWidth(90); |
|
cancelBtn->setStyleSheet( |
|
"QPushButton { border: 1px solid palette(mid); border-radius: 4px;" |
|
" padding: 6px 16px; font-size: 13px; background: palette(button); }" |
|
"QPushButton:hover { background: palette(light); }"); |
|
connect(cancelBtn, &QPushButton::clicked, this, &QDialog::reject); |
|
btnLayout->addWidget(cancelBtn); |
|
|
|
m_saveBtn = new QPushButton("Save", rightPanel); |
|
m_saveBtn->setFixedWidth(90); |
|
m_saveBtn->setDefault(true); |
|
m_saveBtn->setStyleSheet( |
|
"QPushButton { border: none; border-radius: 4px;" |
|
" padding: 6px 16px; font-size: 13px; font-weight: bold;" |
|
" background: #3390EC; color: white; }" |
|
"QPushButton:hover { background: #2B7ED4; }"); |
|
connect(m_saveBtn, &QPushButton::clicked, this, &SettingsDialog::onSave); |
|
btnLayout->addWidget(m_saveBtn); |
|
|
|
rightLayout->addLayout(btnLayout); |
|
mainLayout->addWidget(rightPanel, 1); |
|
|
|
connect(m_categoryList, &QListWidget::currentRowChanged, |
|
this, &SettingsDialog::onCategoryChanged); |
|
|
|
// Load current values |
|
loadProfileFromConfig(m_configPath); |
|
if (db) { |
|
int sortMode = db->getUiStateInt("channel_sort", 0); |
|
m_sortCombo->setCurrentIndex(sortMode == 1 ? 1 : 0); |
|
int joinPolicy = db->getUiStateInt("join_policy", 1); |
|
m_joinPolicyCombo->setCurrentIndex(joinPolicy >= 0 && joinPolicy <= 2 ? joinPolicy : 1); |
|
m_autoConnectCheck->setChecked(db->getUiStateInt("group_autoconnect", 1) != 0); |
|
} |
|
m_networkPage->loadFromConfig(m_configPath); |
|
m_soundPage->loadFromDb(); |
|
m_categoryList->setCurrentRow(0); |
|
} |
|
|
|
void SettingsDialog::loadProfileFromConfig(const QString& path) { |
|
QFile f(path); |
|
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) return; |
|
QTextStream in(&f); |
|
while (!in.atEnd()) { |
|
QString line = in.readLine().trimmed(); |
|
if (line.startsWith("my_node_name=")) { |
|
QString nm = line.mid(QString("my_node_name=").size()).trimmed(); |
|
m_nicknameEdit->setText(nm); |
|
m_oldNickname = nm; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
void SettingsDialog::onCategoryChanged(int row) { |
|
m_pages->setCurrentIndex(row); |
|
if (row == 1) m_storagePage->loadFromDb(); |
|
if (row == 4) m_databasePage->refreshStatus(); |
|
if (row == 5) m_statusPage->refreshStatus(); |
|
if (row == 6) m_nodesPage->refreshNodes(); |
|
if (row == 7) m_soundPage->loadFromDb(); |
|
if (row == 8) m_audioDevicePage->loadFromDb(); |
|
} |
|
|
|
void SettingsDialog::onSave() { |
|
QString newName = m_nicknameEdit->text().trimmed(); |
|
bool nameChanged = (newName != m_oldNickname); |
|
|
|
/* update name in uTun if changed */ |
|
if (nameChanged) { |
|
struct update_my_name_arg* arg = (struct update_my_name_arg*)u_calloc(1, sizeof(*arg)); |
|
if (arg) { |
|
arg->inst = gui_bridge_get_inst(); |
|
snprintf(arg->name, sizeof(arg->name), "%s", newName.toUtf8().constData()); |
|
gui_bridge_post_uasync_fn(chat_core_update_my_name_trampoline, arg); |
|
} |
|
m_oldNickname = newName; |
|
} |
|
|
|
/* Save nickname to config — только при изменении имени, иначе перезапись плодит дубли */ |
|
if (nameChanged) { |
|
QFile f(m_configPath); |
|
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) { |
|
QString content = f.readAll(); |
|
f.close(); |
|
|
|
QTextStream ts(&content); |
|
QStringList lines; |
|
while (!ts.atEnd()) lines << ts.readLine(); |
|
|
|
bool found = false; |
|
for (int i = 0; i < lines.size(); i++) { |
|
if (lines[i].trimmed().startsWith("my_node_name=")) { |
|
if (!found) { lines[i] = "my_node_name=" + newName; found = true; } |
|
else lines.removeAt(i--); /* дубль — удалить */ |
|
} |
|
} |
|
if (!found) { |
|
for (int i = 0; i < lines.size(); i++) { |
|
if (lines[i].trimmed() == "[global]") { |
|
lines.insert(i + 1, "my_node_name=" + newName); |
|
found = true; |
|
break; |
|
} |
|
} |
|
if (!found) lines << "my_node_name=" + newName; |
|
} |
|
|
|
if (f.open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) { |
|
f.write(lines.join('\n').toUtf8()); |
|
f.close(); |
|
} |
|
} |
|
} |
|
|
|
m_networkPage->saveToConfig(m_configPath); |
|
|
|
/* save channel sort preference to ui_state */ |
|
{ |
|
const char* key = "channel_sort"; |
|
QByteArray val = QByteArray::number(m_sortCombo->currentIndex()); |
|
size_t klen = strlen(key), vlen = (size_t)val.size(); |
|
size_t total = klen + 1 + vlen + 1; |
|
if (total <= 256) { |
|
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); |
|
if (!arg) return; |
|
arg->inst = gui_bridge_get_inst(); |
|
memcpy(arg->data, key, klen + 1); |
|
memcpy(arg->data + klen + 1, val.constData(), vlen + 1); |
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
} |
|
} |
|
|
|
m_soundPage->applyAndSave(); |
|
m_audioDevicePage->applyAndSave(); |
|
m_storagePage->applyAndSave(); |
|
m_radioPage->apply(); |
|
|
|
/* save radio PTT hotkeys to ui_state */ |
|
{ |
|
const char* key = "radio_ptt_hotkeys"; |
|
QByteArray val = m_radioPage->hotkeys().join('|').toUtf8(); |
|
size_t klen = strlen(key), vlen = (size_t)val.size(); |
|
size_t total = klen + 1 + vlen + 1; |
|
if (total <= 256) { |
|
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); |
|
if (!arg) return; |
|
arg->inst = gui_bridge_get_inst(); |
|
memcpy(arg->data, key, klen + 1); |
|
memcpy(arg->data + klen + 1, val.constData(), vlen + 1); |
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
} |
|
} |
|
|
|
/* save join_policy */ |
|
{ |
|
const char* key = "join_policy"; |
|
int v = m_joinPolicyCombo->currentIndex(); |
|
QByteArray val = QByteArray::number(v); |
|
size_t klen = strlen(key), vl = (size_t)val.size(), total = klen + 1 + vl + 1; |
|
if (total <= 256) { |
|
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); |
|
if (!arg) return; |
|
arg->inst = gui_bridge_get_inst(); |
|
memcpy(arg->data, key, klen + 1); |
|
memcpy(arg->data + klen + 1, val.constData(), vl + 1); |
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
} |
|
if (!m_configPath.isEmpty()) { |
|
postChatSetting(m_configPath, "join_policy", v); |
|
} |
|
} |
|
|
|
/* save group_autoconnect */ |
|
{ |
|
const char* key = "group_autoconnect"; |
|
int v = m_autoConnectCheck->isChecked() ? 1 : 0; |
|
QByteArray val = QByteArray::number(v); |
|
size_t klen = strlen(key), vl = (size_t)val.size(), total = klen + 1 + vl + 1; |
|
if (total <= 256) { |
|
struct save_ui_state_arg* arg = (struct save_ui_state_arg*)u_malloc(sizeof(struct save_ui_state_arg)); |
|
if (!arg) return; |
|
arg->inst = gui_bridge_get_inst(); |
|
memcpy(arg->data, key, klen + 1); |
|
memcpy(arg->data + klen + 1, val.constData(), vl + 1); |
|
gui_bridge_post_uasync_fn(chat_core_save_ui_state_trampoline, arg); |
|
} |
|
if (!m_configPath.isEmpty()) { |
|
postChatSetting(m_configPath, "group_autoconnect", v); |
|
} |
|
} |
|
accept(); |
|
} |
|
|
|
void SettingsDialog::onOpenConnMonitor() { |
|
if (m_connMonitorWindow) { |
|
m_connMonitorWindow->raise(); |
|
m_connMonitorWindow->activateWindow(); |
|
return; |
|
} |
|
m_connMonitorWindow = new ConnMonitorWindow(nullptr); |
|
m_connMonitorWindow->show(); |
|
}
|
|
|