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