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.
 
 
 
 
 
 

441 lines
20 KiB

#include <QCoreApplication>
// 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 "avatars/avatarmanager.h"
#include "avatars/avatarcrop.h"
#include "ptt_hotkey_manager.h"
#include "connmonitorwindow.h"
#include "../transport/gui_bridge.h"
#include "../../lib/mem.h"
#include "../db/db_manager.h"
#include "../transport/utun_node.h"
#include "../transport/node_config.h"
#include "../transport/config_updater.h"
#include "../../lib/debug_config.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>
#include <QMessageBox>
#include <QFileDialog>
SettingsDialog::SettingsDialog(const QString& configPath, DbManager* db, UtunNode* node,
AudioRecorder* recorder, QWidget* parent)
: QDialog(parent)
, m_configPath(configPath)
{
setWindowTitle(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "Profile"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Storage"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Network"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Connections"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Database"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Status"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Nodes"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Sound"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Audio"));
m_categoryList->addItem(QCoreApplication::translate("ChatGui", "Radio"));
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(QCoreApplication::translate("ChatGui", "Display name:"), profilePage);
m_nicknameEdit = new QLineEdit(profilePage);
m_nicknameEdit->setPlaceholderText(QCoreApplication::translate("ChatGui", "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);
if (auto* avatars = AvatarManager::instance()) {
auto* controls = new QWidget(profilePage); auto* box = new QVBoxLayout(controls); box->setContentsMargins(0, 0, 0, 0);
auto* preview = new QLabel(controls); preview->setFixedSize(64, 64); preview->setAlignment(Qt::AlignCenter);
auto* server = new QComboBox(controls); server->setObjectName(QStringLiteral("avatarServer"));
auto* buttons = new QHBoxLayout;
auto* upload = new QPushButton(QCoreApplication::translate("ChatGui", "Upload avatar"), controls);
auto* clear = new QPushButton(QCoreApplication::translate("ChatGui", "Remove avatar"), controls);
upload->setObjectName(QStringLiteral("uploadAvatar")); clear->setObjectName(QStringLiteral("removeAvatar"));
auto* status = new QLabel(controls); status->setWordWrap(true); status->setObjectName(QStringLiteral("avatarStatus"));
auto* note = new QLabel(QCoreApplication::translate("ChatGui", "Avatar changes apply immediately in channels with the selected storage server."), controls);
note->setWordWrap(true); box->addWidget(preview); box->addWidget(server);
buttons->addWidget(upload); buttons->addWidget(clear); box->addLayout(buttons); box->addWidget(status); box->addWidget(note);
profLayout->addRow(QCoreApplication::translate("ChatGui", "Avatar:"), controls);
auto update = [avatars, server, preview, upload, clear] {
const quint64 selected = server->count() ? server->currentData().toULongLong() : avatars->defaultServer(); server->clear();
const auto servers = avatars->servers();
for (auto it = servers.begin(); it != servers.end(); ++it)
server->addItem(QString("%1 · %2").arg(it.value(), QString::number(it.key(), 16)), QVariant::fromValue(it.key()));
int index = server->findData(QVariant::fromValue(selected)); if (index >= 0) server->setCurrentIndex(index);
upload->setEnabled(avatars->ready() && server->count() && gui_bridge_is_uasync_ready());
clear->setEnabled(avatars->ready() && gui_bridge_is_uasync_ready());
const QImage image = avatars->defaultImage();
preview->setPixmap(image.isNull() ? QPixmap() : QPixmap::fromImage(image).scaled(64, 64, Qt::KeepAspectRatio, Qt::SmoothTransformation));
if (image.isNull()) preview->setText(QCoreApplication::translate("ChatGui", "No avatar"));
};
connect(avatars, &AvatarManager::serversChanged, controls, update);
connect(avatars, &AvatarManager::statusChanged, status, &QLabel::setText);
connect(upload, &QPushButton::clicked, controls, [avatars, server, controls] {
const QString path = QFileDialog::getOpenFileName(controls, QCoreApplication::translate("ChatGui", "Choose avatar"), {},
QCoreApplication::translate("ChatGui", "Images (*.png *.jpg *.jpeg *.webp)"));
QRectF crop;
if (!path.isEmpty() && selectAvatarCrop(path, crop, controls))
avatars->uploadDefault(path, server->currentData().toULongLong(), crop);
});
connect(clear, &QPushButton::clicked, avatars, &AvatarManager::clearDefault); update();
}
m_languageCombo = new QComboBox(profilePage);
m_languageCombo->setObjectName(QStringLiteral("languageCombo"));
m_languageCombo->addItem(QCoreApplication::translate("ChatGui", "System language"), QStringLiteral("system"));
m_languageCombo->addItem(QStringLiteral("English"), QStringLiteral("en"));
m_languageCombo->addItem(QStringLiteral("Русский"), QStringLiteral("ru"));
NodeConfig languageConfig(m_configPath);
if (!languageConfig.load()) {
DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "GUI language: cannot read preferences from %s", qPrintable(m_configPath));
}
m_languageCombo->setCurrentIndex(m_languageCombo->findData(languageConfig.language()));
profLayout->addRow(QCoreApplication::translate("ChatGui", "Interface language:"), m_languageCombo);
auto* languageNote = new QLabel(QCoreApplication::translate("ChatGui", "Language changes take effect after restart."), profilePage);
languageNote->setWordWrap(true);
profLayout->addRow(languageNote);
auto* sortLabel = new QLabel(QCoreApplication::translate("ChatGui", "Channel sorting:"), profilePage);
m_sortCombo = new QComboBox(profilePage);
m_sortCombo->addItem(QCoreApplication::translate("ChatGui", "Alphabetical (A-Z)"));
m_sortCombo->addItem(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "Join requests:"), profilePage);
m_joinPolicyCombo = new QComboBox(profilePage);
m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "Auto-join"));
m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "Ask before joining"));
m_joinPolicyCombo->addItem(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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(QCoreApplication::translate("ChatGui", "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_audioDevicePage->loadFromDb();
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(QCoreApplication::translate("ChatGui", "Cancel"), rightPanel);
cancelBtn->setMinimumWidth(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(QCoreApplication::translate("ChatGui", "Save"), rightPanel);
m_saveBtn->setMinimumWidth(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();
if (row == 9) m_radioPage->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();
if (!m_radioPage->apply()) {
QMessageBox::warning(this, QCoreApplication::translate("ChatGui",
"Radio settings"), QCoreApplication::translate("ChatGui",
"Failed to apply radio settings. Check that the core and configuration file are available; "
"see the radio log for details."));
return;
}
/* Физические сочетания сохраняем в БД; Wayland-хоткеями владеет системный портал. */
if (!PttHotkeyManager::usesPortal()) {
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);
}
}
const QString language = m_languageCombo->currentData().toString();
if (!config_update_option(m_configPath, "gui", "language", language)) {
DEBUG_ERROR(DEBUG_CATEGORY_CONFIG, "GUI language: failed to save preference=%s path=%s",
qPrintable(language), qPrintable(m_configPath));
QMessageBox::warning(this, QCoreApplication::translate("ChatGui", "Settings"),
QCoreApplication::translate("ChatGui", "Failed to save the interface language. Check the configuration file."));
return;
}
DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "GUI language: saved preference=%s; applies after restart", qPrintable(language));
accept();
}
void SettingsDialog::onOpenConnMonitor() {
if (m_connMonitorWindow) {
m_connMonitorWindow->raise();
m_connMonitorWindow->activateWindow();
return;
}
m_connMonitorWindow = new ConnMonitorWindow(nullptr);
m_connMonitorWindow->show();
}