// 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 #include #include #include #include #include #include #include #include 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(); }