From: tobtoht Date: Sat, 11 Feb 2023 17:11:21 +0000 (+0100) Subject: settings rework, proxy support X-Git-Url: https://git.nutra.tk/v2?a=commitdiff_plain;h=f3c8e116c012bc7e6f6e365330191720fc30d5a0;p=gamesguru%2Ffeather.git settings rework, proxy support --- diff --git a/monero b/monero index 9a6dad7c..f7705c2c 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit 9a6dad7cf4958f795b41517badf93f1c6c80b313 +Subproject commit f7705c2c6740699a3fa47895473f79c006624559 diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index a513b056..84babb17 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -37,6 +37,8 @@ #include "utils/Updater.h" #include "utils/WebsocketNotifier.h" +//#include "misc_log_ex.h" + MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) @@ -46,6 +48,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa ui->setupUi(this); qDebug() << "Platform tag: " << this->getPlatformTag(); +// MCWARNING("feather", "Platform tag: " << this->getPlatformTag().toStdString()); // Ensure the destructor is called after closeEvent() setAttribute(Qt::WA_DeleteOnClose); @@ -78,7 +81,19 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa connect(m_windowManager, &WindowManager::websocketStatusChanged, this, &MainWindow::onWebsocketStatusChanged); this->onWebsocketStatusChanged(!config()->get(Config::disableWebsocket).toBool()); - connect(m_windowManager, &WindowManager::torSettingsChanged, m_ctx.get(), &AppContext::onTorSettingsChanged); + connect(m_windowManager, &WindowManager::proxySettingsChanged, [this]{ + m_ctx->onProxySettingsChanged(); + this->onProxySettingsChanged(); + }); + connect(m_windowManager, &WindowManager::updateBalance, m_ctx.data(), &AppContext::updateBalance); + connect(m_windowManager, &WindowManager::offlineMode, [this](bool offline){ + if (!m_ctx->wallet) { + return; + } + m_ctx->wallet->setOffline(offline); + this->onConnectionStatusChanged(Wallet::ConnectionStatus_Disconnected); + }); + connect(torManager(), &TorManager::connectionStateChanged, this, &MainWindow::onTorConnectionStateChanged); this->onTorConnectionStateChanged(torManager()->torConnected); @@ -113,10 +128,6 @@ void MainWindow::initStatusBar() { this->statusBar()->setStyleSheet("QStatusBar::item {border: None;}"); #endif -#if defined(Q_OS_MACOS) - this->patchStylesheetMac(); -#endif - this->statusBar()->setFixedHeight(30); m_statusLabelStatus = new QLabel("Idle", this); @@ -143,9 +154,10 @@ void MainWindow::initStatusBar() { m_statusBtnConnectionStatusIndicator = new StatusBarButton(icons()->icon("status_disconnected.svg"), "Connection status", this); connect(m_statusBtnConnectionStatusIndicator, &StatusBarButton::clicked, [this](){ - this->onShowSettingsPage(2); + this->onShowSettingsPage(SettingsNew::Pages::NETWORK); }); this->statusBar()->addPermanentWidget(m_statusBtnConnectionStatusIndicator); + this->onConnectionStatusChanged(Wallet::ConnectionStatus_Disconnected); m_statusAccountSwitcher = new StatusBarButton(icons()->icon("change_account.png"), "Account switcher", this); connect(m_statusAccountSwitcher, &StatusBarButton::clicked, this, &MainWindow::showAccountSwitcherDialog); @@ -163,9 +175,10 @@ void MainWindow::initStatusBar() { connect(m_statusBtnSeed, &StatusBarButton::clicked, this, &MainWindow::showSeedDialog); this->statusBar()->addPermanentWidget(m_statusBtnSeed); - m_statusBtnTor = new StatusBarButton(icons()->icon("tor_logo_disabled.png"), "Tor settings", this); - connect(m_statusBtnTor, &StatusBarButton::clicked, this, &MainWindow::menuTorClicked); - this->statusBar()->addPermanentWidget(m_statusBtnTor); + m_statusBtnProxySettings = new StatusBarButton(icons()->icon("tor_logo_disabled.png"), "Proxy settings", this); + connect(m_statusBtnProxySettings, &StatusBarButton::clicked, this, &MainWindow::menuProxySettingsClicked); + this->statusBar()->addPermanentWidget(m_statusBtnProxySettings); + this->onProxySettingsChanged(); m_statusBtnHwDevice = new StatusBarButton(this->hardwareDevicePairedIcon(), this->getHardwareDevice(), this); connect(m_statusBtnHwDevice, &StatusBarButton::clicked, this, &MainWindow::menuHwDeviceClicked); @@ -417,12 +430,12 @@ void MainWindow::initWalletContext() { connect(m_ctx.get(), &AppContext::keysCorrupted, this, &MainWindow::onKeysCorrupted); connect(m_ctx.get(), &AppContext::selectedInputsChanged, this, &MainWindow::onSelectedInputsChanged); - // Nodes - connect(m_ctx->nodes, &Nodes::nodeExhausted, this, &MainWindow::showNodeExhaustedMessage); - connect(m_ctx->nodes, &Nodes::WSNodeExhausted, this, &MainWindow::showWSNodeExhaustedMessage); - // Wallet - connect(m_ctx->wallet, &Wallet::connectionStatusChanged, this, &MainWindow::onConnectionStatusChanged); + connect(m_ctx->wallet, &Wallet::connectionStatusChanged, [this](int status){ + // Order is important, first inform UI about a potential disconnect, then reconnect + this->onConnectionStatusChanged(status); + this->m_ctx->nodes->autoConnect(); + }); connect(m_ctx->wallet, &Wallet::currentSubaddressAccountChanged, this, &MainWindow::updateTitle); connect(m_ctx->wallet, &Wallet::walletPassphraseNeeded, this, &MainWindow::onWalletPassphraseNeeded); } @@ -521,7 +534,9 @@ void MainWindow::onWalletOpened() { m_ctx->nodes->connectToNode(); m_updateBytes.start(250); - this->addToRecentlyOpened(m_ctx->wallet->cachePath()); + if (config()->get(Config::writeRecentlyOpenedWallets).toBool()) { + this->addToRecentlyOpened(m_ctx->wallet->cachePath()); + } } void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) { @@ -593,6 +608,24 @@ void MainWindow::onWebsocketStatusChanged(bool enabled) { #endif } +void MainWindow::onProxySettingsChanged() { + int proxy = config()->get(Config::proxy).toInt(); + + if (proxy == Config::Proxy::Tor) { + this->onTorConnectionStateChanged(torManager()->torConnected); + m_statusBtnProxySettings->show(); + return; + } + + if (proxy == Config::Proxy::i2p) { + m_statusBtnProxySettings->setIcon(icons()->icon("i2p.png")); + m_statusBtnProxySettings->show(); + return; + } + + m_statusBtnProxySettings->hide(); +} + void MainWindow::onSynchronized() { this->updateNetStats(); this->setStatusText("Synchronized"); @@ -617,28 +650,33 @@ void MainWindow::onConnectionStatusChanged(int status) // Update connection info in status bar. QIcon icon; - switch(status){ - case Wallet::ConnectionStatus_Disconnected: - icon = icons()->icon("status_disconnected.svg"); - this->setStatusText("Disconnected"); - break; - case Wallet::ConnectionStatus_Connecting: - icon = icons()->icon("status_lagging.svg"); - this->setStatusText("Connecting to node"); - break; - case Wallet::ConnectionStatus_WrongVersion: - icon = icons()->icon("status_disconnected.svg"); - this->setStatusText("Incompatible node"); - break; - case Wallet::ConnectionStatus_Synchronizing: - icon = icons()->icon("status_waiting.svg"); - break; - case Wallet::ConnectionStatus_Synchronized: - icon = icons()->icon("status_connected.svg"); - break; - default: - icon = icons()->icon("status_disconnected.svg"); - break; + if (config()->get(Config::offlineMode).toBool()) { + icon = icons()->icon("status_offline.svg"); + this->setStatusText("Offline"); + } else { + switch(status){ + case Wallet::ConnectionStatus_Disconnected: + icon = icons()->icon("status_disconnected.svg"); + this->setStatusText("Disconnected"); + break; + case Wallet::ConnectionStatus_Connecting: + icon = icons()->icon("status_lagging.svg"); + this->setStatusText("Connecting to node"); + break; + case Wallet::ConnectionStatus_WrongVersion: + icon = icons()->icon("status_disconnected.svg"); + this->setStatusText("Incompatible node"); + break; + case Wallet::ConnectionStatus_Synchronizing: + icon = icons()->icon("status_waiting.svg"); + break; + case Wallet::ConnectionStatus_Synchronized: + icon = icons()->icon("status_connected.svg"); + break; + default: + icon = icons()->icon("status_disconnected.svg"); + break; + } } m_statusBtnConnectionStatusIndicator->setIcon(icon); @@ -826,13 +864,6 @@ void MainWindow::showViewOnlyDialog() { dialog.exec(); } -void MainWindow::menuTorClicked() { - auto *dialog = new TorInfoDialog(m_ctx, this); - connect(dialog, &TorInfoDialog::torSettingsChanged, m_windowManager, &WindowManager::onTorSettingsChanged); - dialog->exec(); - dialog->deleteLater(); -} - void MainWindow::menuHwDeviceClicked() { QMessageBox::information(this, "Hardware Device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice())); } @@ -854,21 +885,17 @@ void MainWindow::menuWalletCloseClicked() { this->close(); } +void MainWindow::menuProxySettingsClicked() { + this->menuSettingsClicked(true); +} + void MainWindow::menuAboutClicked() { AboutDialog dialog{this}; dialog.exec(); } -void MainWindow::menuSettingsClicked() { - Settings settings{m_ctx, this}; - for (const auto &widget: m_tickerWidgets) { - connect(&settings, &Settings::preferredFiatCurrencyChanged, widget, &TickerWidgetBase::updateDisplay); - } - connect(&settings, &Settings::preferredFiatCurrencyChanged, m_balanceTickerWidget, &BalanceTickerWidget::updateDisplay); - connect(&settings, &Settings::preferredFiatCurrencyChanged, m_sendWidget, QOverload<>::of(&SendWidget::onPreferredFiatCurrencyChanged)); - connect(&settings, &Settings::skinChanged, this, &MainWindow::skinChanged); - connect(&settings, &Settings::websocketStatusChanged, m_windowManager, &WindowManager::onWebsocketStatusChanged); - settings.exec(); +void MainWindow::menuSettingsClicked(bool showProxyTab) { + m_windowManager->showSettings(m_ctx, this, showProxyTab); } void MainWindow::menuSignVerifyClicked() { @@ -887,13 +914,8 @@ void MainWindow::onShowSettingsPage(int page) { } void MainWindow::skinChanged(const QString &skinName) { - m_windowManager->changeSkin(skinName); ColorScheme::updateFromWidget(this); this->updateWidgetIcons(); - -#if defined(Q_OS_MACOS) - this->patchStylesheetMac(); -#endif } void MainWindow::updateWidgetIcons() { @@ -949,6 +971,17 @@ void MainWindow::closeEvent(QCloseEvent *event) { event->accept(); } +void MainWindow::changeEvent(QEvent* event) +{ + if ((event->type() == QEvent::WindowStateChange) && this->isMinimized()) { + if (config()->get(Config::lockOnMinimize).toBool()) { + this->lockWallet(); + } + } else { + QMainWindow::changeEvent(event); + } +} + void MainWindow::donateButtonClicked() { m_sendWidget->fill(constants::donationAddress, "Donation to the Feather development team"); ui->tabWidget->setCurrentIndex(Tabs::SEND); @@ -1075,22 +1108,6 @@ void MainWindow::showAddressChecker() { } } -void MainWindow::showNodeExhaustedMessage() { - // Spawning dialogs inside a lambda can cause system freezes on linux so we have to do it this way ¯\_(ツ)_/¯ - - auto msg = "Feather is in 'custom node connection mode' but could not " - "find an eligible node to connect to. Please go to Settings->Node " - "and enter a node manually."; - QMessageBox::warning(this, "Could not connect to a node", msg); -} - -void MainWindow::showWSNodeExhaustedMessage() { - auto msg = "Feather is in 'automatic node connection mode' but the " - "websocket server returned no available nodes. Please go to Settings->Node " - "and enter a node manually."; - QMessageBox::warning(this, "Could not connect to a node", msg); -} - void MainWindow::exportKeyImages() { QString fn = QFileDialog::getSaveFileName(this, "Save key images to file", QString("%1/%2_%3").arg(QDir::homePath(), this->walletName(), QString::number(QDateTime::currentSecsSinceEpoch())), "Key Images (*_keyImages)"); if (fn.isEmpty()) return; @@ -1343,14 +1360,39 @@ void MainWindow::bringToFront() { activateWindow(); } +void MainWindow::onPreferredFiatCurrencyChanged() { + for (const auto &widget : m_tickerWidgets) { + widget->updateDisplay(); + } + m_balanceTickerWidget->updateDisplay(); + m_sendWidget->onPreferredFiatCurrencyChanged(); +} + +void MainWindow::onHideUpdateNotifications(bool hidden) { + if (hidden) { + m_statusUpdateAvailable->hide(); + } + else if (m_updater->state == Updater::State::UPDATE_AVAILABLE) { + m_statusUpdateAvailable->show(); + } +} + void MainWindow::onTorConnectionStateChanged(bool connected) { + if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + return; + } + if (connected) - m_statusBtnTor->setIcon(icons()->icon("tor_logo.png")); + m_statusBtnProxySettings->setIcon(icons()->icon("tor_logo.png")); else - m_statusBtnTor->setIcon(icons()->icon("tor_logo_disabled.png")); + m_statusBtnProxySettings->setIcon(icons()->icon("tor_logo_disabled.png")); } void MainWindow::showUpdateNotification() { + if (config()->get(Config::hideUpdateNotifications).toBool()) { + return; + } + QString versionDisplay{m_updater->version}; versionDisplay.replace("beta", "Beta"); QString updateText = QString("Update to Feather %1 is available").arg(versionDisplay); @@ -1602,14 +1644,6 @@ bool MainWindow::verifyPassword(bool sensitive) { return true; } -void MainWindow::patchStylesheetMac() { - auto patch = Utils::fileOpenQRC(":assets/macStylesheet.patch"); - auto patch_text = Utils::barrayToString(patch); - - QString styleSheet = qApp->styleSheet() + patch_text; - qApp->setStyleSheet(styleSheet); -} - void MainWindow::userActivity() { m_userLastActive = QDateTime::currentSecsSinceEpoch(); } diff --git a/src/MainWindow.h b/src/MainWindow.h index 34a380e9..709152ab 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -12,6 +12,7 @@ #include "components.h" #include "CalcWindow.h" #include "SettingsDialog.h" +#include "SettingsNewDialog.h" #include "dialog/AboutDialog.h" #include "dialog/AccountSwitcherDialog.h" @@ -102,21 +103,28 @@ public: void showOrHide(); void bringToFront(); +public slots: + void onPreferredFiatCurrencyChanged(); + void onHideUpdateNotifications(bool hidden); + signals: void closed(); +protected: + void changeEvent(QEvent* event) override; + private slots: // TODO: use a consistent naming convention for slots // Menu void menuOpenClicked(); void menuNewRestoreClicked(); void menuQuitClicked(); - void menuSettingsClicked(); + void menuSettingsClicked(bool showProxytab = false); void menuAboutClicked(); void menuSignVerifyClicked(); void menuVerifyTxProof(); void menuWalletCloseClicked(); - void menuTorClicked(); + void menuProxySettingsClicked(); void menuToggleTabVisible(const QString &key); void menuClearHistoryClicked(); void onExportHistoryCSV(bool checked); @@ -184,6 +192,7 @@ private slots: void tryStoreWallet(); void onWebsocketStatusChanged(bool enabled); void showUpdateNotification(); + void onProxySettingsChanged(); private: friend WindowManager; @@ -199,8 +208,6 @@ private: void saveGeo(); void restoreGeo(); void showDebugInfo(); - void showNodeExhaustedMessage(); - void showWSNodeExhaustedMessage(); void createUnsignedTxDialog(UnsignedTransaction *tx); void updatePasswordIcon(); void updateNetStats(); @@ -217,7 +224,6 @@ private: void updateRecentlyOpenedMenu(); void updateWidgetIcons(); bool verifyPassword(bool sensitive = true); - void patchStylesheetMac(); void fillSendTab(const QString &address, const QString &description); void userActivity(); void checkUserActivity(); @@ -264,7 +270,7 @@ private: StatusBarButton *m_statusBtnPassword; StatusBarButton *m_statusBtnPreferences; StatusBarButton *m_statusBtnSeed; - StatusBarButton *m_statusBtnTor; + StatusBarButton *m_statusBtnProxySettings; StatusBarButton *m_statusBtnHwDevice; QSignalMapper *m_tabShowHideSignalMapper; diff --git a/src/SettingsDialog.cpp b/src/SettingsDialog.cpp index 147e69b5..ccd86953 100644 --- a/src/SettingsDialog.cpp +++ b/src/SettingsDialog.cpp @@ -160,7 +160,7 @@ void Settings::setupPrivacyTab() { } void Settings::setupNodeTab() { - ui->nodeWidget->setupUI(m_ctx); +// ui->nodeWidget->setupUI(m_ctx); connect(ui->nodeWidget, &NodeWidget::nodeSourceChanged, m_ctx->nodes, &Nodes::onNodeSourceChanged); connect(ui->nodeWidget, &NodeWidget::connectToNode, m_ctx->nodes, QOverload::of(&Nodes::connectToNode)); } diff --git a/src/SettingsNewDialog.cpp b/src/SettingsNewDialog.cpp new file mode 100644 index 00000000..6f28c282 --- /dev/null +++ b/src/SettingsNewDialog.cpp @@ -0,0 +1,390 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "SettingsNewDialog.h" +#include "ui_SettingsNewDialog.h" + +#include +#include +#include +#include + +#include "utils/Icons.h" +#include "utils/WebsocketNotifier.h" +#include "widgets/NetworkProxyWidget.h" + +SettingsNew::SettingsNew(QSharedPointer ctx, QWidget *parent) + : QDialog(parent) + , ui(new Ui::SettingsNew) + , m_ctx(std::move(ctx)) +{ + ui->setupUi(this); + + this->setupAppearanceTab(); + this->setupNetworkTab(); + this->setupStorageTab(); + this->setupDisplayTab(); + this->setupMemoryTab(); + this->setupTransactionsTab(); + this->setupMiscTab(); + + connect(ui->selector, &QListWidget::currentItemChanged, [this](QListWidgetItem *current, QListWidgetItem *previous){ + ui->stackedWidget->setCurrentIndex(current->type()); + }); + + ui->selector->setSelectionMode(QAbstractItemView::SingleSelection); + ui->selector->setSelectionBehavior(QAbstractItemView::SelectRows); +// ui->selector->setCurrentRow(config()->get(Config::lastSettingsPage).toInt()); + + new QListWidgetItem(icons()->icon("interface_32px.png"), "Appearance", ui->selector, Pages::APPEARANCE); + new QListWidgetItem(icons()->icon("nw_32px.png"), "Network", ui->selector, Pages::NETWORK); + new QListWidgetItem(icons()->icon("hd_32px.png"), "Storage", ui->selector, Pages::STORAGE); + new QListWidgetItem(icons()->icon("vrdp_32px.png"), "Display", ui->selector, Pages::DISPLAY); +// new QListWidgetItem(icons()->icon("chipset_32px.png"), "Memory", ui->selector, Pages::MEMORY); + new QListWidgetItem(icons()->icon("file_manager_32px.png"), "Transactions", ui->selector, Pages::TRANSACTIONS); + new QListWidgetItem(icons()->icon("settings_disabled_32px.png"), "Misc", ui->selector, Pages::MISC); + + ui->selector->setFixedWidth(ui->selector->sizeHintForColumn(0) + ui->selector->frameWidth() + 5); + +// for (int i = 0; i < ui->selector->count(); ++i) { +// QListWidgetItem *item = ui->selector->item(i); +// item->setSizeHint(QSize(item->sizeHint().width(), ui->selector->iconSize().height() + 8)); +// } + + connect(ui->closeButton, &QDialogButtonBox::accepted, [this]{ + // Save Proxy settings + bool isProxySettingChanged = ui->proxyWidget->isProxySettingsChanged(); + ui->proxyWidget->setProxySettings(); + if (isProxySettingChanged) { + emit proxySettingsChanged(); + } + + config()->set(Config::lastSettingsPage, ui->selector->currentRow()); + this->close(); + }); + + this->setSelection(config()->get(Config::lastSettingsPage).toInt()); + + this->adjustSize(); +} + +void SettingsNew::setupAppearanceTab() { + // [Theme] + this->setupThemeComboBox(); + auto settingsTheme = config()->get(Config::skin).toString(); + if (m_themes.contains(settingsTheme)) { + ui->comboBox_theme->setCurrentIndex(m_themes.indexOf(settingsTheme)); + } + connect(ui->comboBox_theme, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ + emit skinChanged(m_themes.at(pos)); + }); + + // [Amount precision] + for (int i = 0; i <= 12; i++) { + ui->comboBox_amountPrecision->addItem(QString::number(i)); + } + ui->comboBox_amountPrecision->setCurrentIndex(config()->get(Config::amountPrecision).toInt()); + + connect(ui->comboBox_amountPrecision, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ + config()->set(Config::amountPrecision, pos); + emit updateBalance(); + }); + + // [Date format] + QDateTime now = QDateTime::currentDateTime(); + for (const auto & format : m_dateFormats) { + ui->comboBox_dateFormat->addItem(now.toString(format)); + } + QString dateFormatSetting = config()->get(Config::dateFormat).toString(); + if (m_dateFormats.contains(dateFormatSetting)) { + ui->comboBox_dateFormat->setCurrentIndex(m_dateFormats.indexOf(dateFormatSetting)); + } + + connect(ui->comboBox_dateFormat, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ + config()->set(Config::dateFormat, m_dateFormats.at(pos)); + }); + + // [Time format] + for (const auto & format : m_timeFormats) { + ui->comboBox_timeFormat->addItem(now.toString(format)); + } + QString timeFormatSetting = config()->get(Config::timeFormat).toString(); + if (m_timeFormats.contains(timeFormatSetting)) { + ui->comboBox_timeFormat->setCurrentIndex(m_timeFormats.indexOf(timeFormatSetting)); + } + + connect(ui->comboBox_timeFormat, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ + config()->set(Config::timeFormat, m_timeFormats.at(pos)); + }); + + + // [Balance display] + ui->comboBox_balanceDisplay->setCurrentIndex(config()->get(Config::balanceDisplay).toInt()); + connect(ui->comboBox_balanceDisplay, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ + config()->set(Config::balanceDisplay, pos); + emit updateBalance(); + }); + + // [Preferred fiat currency] + QStringList availableFiatCurrencies = appData()->prices.rates.keys(); + for (const auto ¤cy : availableFiatCurrencies) { + ui->comboBox_fiatCurrency->addItem(currency); + } + + QStringList fiatCurrencies; + for (int index = 0; index < ui->comboBox_fiatCurrency->count(); index++) { + fiatCurrencies << ui->comboBox_fiatCurrency->itemText(index); + } + + auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + if (!preferredFiatCurrency.isEmpty() && fiatCurrencies.contains(preferredFiatCurrency)) { + ui->comboBox_fiatCurrency->setCurrentText(preferredFiatCurrency); + } + + connect(ui->comboBox_fiatCurrency, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ + QString selection = ui->comboBox_fiatCurrency->itemText(index); + config()->set(Config::preferredFiatCurrency, selection); + emit preferredFiatCurrencyChanged(selection); + }); +} + +void SettingsNew::setupNetworkTab() { + // Node + if (m_ctx) { + ui->nodeWidget->setupUI(m_ctx->nodes); + connect(ui->nodeWidget, &NodeWidget::nodeSourceChanged, m_ctx->nodes, &Nodes::onNodeSourceChanged); + connect(ui->nodeWidget, &NodeWidget::connectToNode, m_ctx->nodes, QOverload::of(&Nodes::connectToNode)); + } else { + m_nodes = new Nodes(this); + ui->nodeWidget->setupUI(m_nodes); + ui->nodeWidget->setCanConnect(false); + } + + // Proxy + connect(ui->proxyWidget, &NetworkProxyWidget::proxySettingsChanged, this, &SettingsNew::onProxySettingsChanged); + + // Websocket + // [Obtain third-party data] + ui->checkBox_enableWebsocket->setChecked(!config()->get(Config::disableWebsocket).toBool()); + connect(ui->checkBox_enableWebsocket, &QCheckBox::toggled, [this](bool checked){ + config()->set(Config::disableWebsocket, !checked); + this->enableWebsocket(checked); + }); + + // Overview + ui->checkBox_offlineMode->setChecked(config()->get(Config::offlineMode).toBool()); + connect(ui->checkBox_offlineMode, &QCheckBox::toggled, [this](bool checked){ + config()->set(Config::offlineMode, checked); + emit offlineMode(checked); + this->enableWebsocket(!checked); + }); +} + +void SettingsNew::setupStorageTab() { + // Paths + ui->lineEdit_defaultWalletDir->setText(config()->get(Config::walletDirectory).toString()); + ui->lineEdit_configDir->setText(Config::defaultConfigDir().path()); + ui->lineEdit_applicationDir->setText(Utils::applicationPath()); + + bool portableMode = Utils::isPortableMode(); + if (portableMode) { + ui->btn_browseDefaultWalletDir->setDisabled(true); + ui->btn_browseDefaultWalletDir->setToolTip("Portable Mode enabled"); + } + + connect(ui->btn_browseDefaultWalletDir, &QPushButton::clicked, [this]{ + QString walletDirOld = config()->get(Config::walletDirectory).toString(); + QString walletDir = QFileDialog::getExistingDirectory(this, "Select wallet directory ", walletDirOld, QFileDialog::ShowDirsOnly); + if (walletDir.isEmpty()) + return; + config()->set(Config::walletDirectory, walletDir); + ui->lineEdit_defaultWalletDir->setText(walletDir); + }); + + connect(ui->btn_openWalletDir, &QPushButton::clicked, []{ + QDesktopServices::openUrl(QUrl::fromLocalFile(config()->get(Config::walletDirectory).toString())); + }); + + connect(ui->btn_openConfigDir, &QPushButton::clicked, []{ + QDesktopServices::openUrl(QUrl::fromLocalFile(Config::defaultConfigDir().path())); + }); + + ui->frame_portableMode->setVisible(portableMode); + + // Logging + // [Write log files to disk] + ui->checkBox_enableLogging->setChecked(!config()->get(Config::disableLogging).toBool()); + connect(ui->checkBox_enableLogging, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::disableLogging, !toggled); + WalletManager::instance()->setLogLevel(toggled ? config()->get(Config::logLevel).toInt() : -1); + }); + + // [Log level] + ui->comboBox_logLevel->setCurrentIndex(config()->get(Config::logLevel).toInt()); + connect(ui->comboBox_logLevel, &QComboBox::currentIndexChanged, [](int index){ + config()->set(Config::logLevel, index); + if (!config()->get(Config::disableLogging).toBool()) { + WalletManager::instance()->setLogLevel(index); + } + }); + + // [Write stack trace to disk on crash] + ui->checkBox_writeStackTraceToDisk->setChecked(config()->get(Config::writeStackTraceToDisk).toBool()); + connect(ui->checkBox_writeStackTraceToDisk, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::writeStackTraceToDisk, toggled); + }); + + // [Open log file] + connect(ui->btn_openLogFile, &QPushButton::clicked, []{ + QDesktopServices::openUrl(QUrl::fromLocalFile(Config::defaultConfigDir().path() + "/libwallet.log")); + }); + + // Misc + // [Save recently opened wallet to config file] + ui->checkBox_writeRecentlyOpened->setChecked(config()->get(Config::writeRecentlyOpenedWallets).toBool()); + connect(ui->checkBox_writeRecentlyOpened, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::writeRecentlyOpenedWallets, toggled); + if (!toggled) { + config()->set(Config::recentlyOpenedWallets, {}); + } + }); +} + +void SettingsNew::setupDisplayTab() { + // [Hide balance] + ui->checkBox_hideBalance->setChecked(config()->get(Config::hideBalance).toBool()); + connect(ui->checkBox_hideBalance, &QCheckBox::toggled, [this](bool toggled){ + config()->set(Config::hideBalance, toggled); + emit updateBalance(); + }); + + // [Hide update notifications] + ui->checkBox_hideUpdateNotifications->setChecked(config()->get(Config::hideUpdateNotifications).toBool()); + connect(ui->checkBox_hideUpdateNotifications, &QCheckBox::toggled, [this](bool toggled){ + config()->set(Config::hideUpdateNotifications, toggled); + emit hideUpdateNotifications(toggled); + }); + + // [Hide transaction notifications] + ui->checkBox_hideTransactionNotifications->setChecked(config()->get(Config::hideNotifications).toBool()); + connect(ui->checkBox_hideTransactionNotifications, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::hideNotifications, toggled); + }); + + // [Warn before opening external link] + ui->checkBox_warnOnExternalLink->setChecked(config()->get(Config::warnOnExternalLink).toBool()); + connect(ui->checkBox_warnOnExternalLink, &QCheckBox::clicked, this, [this]{ + bool state = ui->checkBox_warnOnExternalLink->isChecked(); + config()->set(Config::warnOnExternalLink, state); + }); + + // [Lock wallet on inactivity] + ui->checkBox_lockOnInactivity->setChecked(config()->get(Config::inactivityLockEnabled).toBool()); + ui->spinBox_lockOnInactivity->setValue(config()->get(Config::inactivityLockTimeout).toInt()); + connect(ui->checkBox_lockOnInactivity, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::inactivityLockEnabled, toggled); + }); + connect(ui->spinBox_lockOnInactivity, QOverload::of(&QSpinBox::valueChanged), [](int value){ + config()->set(Config::inactivityLockTimeout, value); + }); + + // [Lock wallet on minimize] + ui->checkBox_lockOnMinimize->setChecked(config()->get(Config::lockOnMinimize).toBool()); + connect(ui->checkBox_lockOnMinimize, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::lockOnMinimize, toggled); + }); +} + +void SettingsNew::setupMemoryTab() { + // Nothing here, yet +} + +void SettingsNew::setupTransactionsTab() { + // [Multibroadcast outgoing transactions] + ui->checkBox_multibroadcast->setChecked(config()->get(Config::multiBroadcast).toBool()); + connect(ui->checkBox_multibroadcast, &QCheckBox::toggled, [](bool toggled){ + config()->set(Config::multiBroadcast, toggled); + }); + connect(ui->btn_multibroadcast, &QPushButton::clicked, [this]{ + QMessageBox::information(this, "Multibroadcasting", "Multibroadcasting relays outgoing transactions to all nodes in your selected node list. This may improve transaction relay speed and reduces the chance of your transaction failing."); + }); + + // Hide unimplemented settings + ui->checkBox_alwaysOpenAdvancedTxDialog->hide(); + ui->checkBox_requirePasswordToSpend->hide(); +} + +void SettingsNew::setupMiscTab() { + // [Block explorer] + ui->comboBox_blockExplorer->setCurrentIndex(ui->comboBox_blockExplorer->findText(config()->get(Config::blockExplorer).toString())); + connect(ui->comboBox_blockExplorer, QOverload::of(&QComboBox::currentIndexChanged), [this]{ + QString blockExplorer = ui->comboBox_blockExplorer->currentText(); + config()->set(Config::blockExplorer, blockExplorer); + emit blockExplorerChanged(blockExplorer); + }); + + // [Reddit frontend] + ui->comboBox_redditFrontend->setCurrentIndex(ui->comboBox_redditFrontend->findText(config()->get(Config::redditFrontend).toString())); + connect(ui->comboBox_redditFrontend, QOverload::of(&QComboBox::currentIndexChanged), [this]{ + QString redditFrontend = ui->comboBox_redditFrontend->currentText(); + config()->set(Config::redditFrontend, redditFrontend); + }); + + // [LocalMonero frontend] + ui->comboBox_localMoneroFrontend->addItem("localmonero.co", "https://localmonero.co"); + ui->comboBox_localMoneroFrontend->addItem("localmonero.co/nojs", "https://localmonero.co/nojs"); + ui->comboBox_localMoneroFrontend->addItem("nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion", + "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion"); + ui->comboBox_localMoneroFrontend->addItem("yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p", + "http://yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p"); + ui->comboBox_localMoneroFrontend->setCurrentIndex(ui->comboBox_localMoneroFrontend->findData(config()->get(Config::localMoneroFrontend).toString())); + connect(ui->comboBox_localMoneroFrontend, QOverload::of(&QComboBox::currentIndexChanged), [this]{ + QString localMoneroFrontend = ui->comboBox_localMoneroFrontend->currentData().toString(); + config()->set(Config::localMoneroFrontend, localMoneroFrontend); + }); +} + +void SettingsNew::onProxySettingsChanged() { + ui->closeButton->addButton(QDialogButtonBox::Apply); + connect(ui->closeButton->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, [this](){ + ui->proxyWidget->setProxySettings(); + emit proxySettingsChanged(); + ui->closeButton->removeButton(ui->closeButton->button(QDialogButtonBox::Apply)); + }); +} + +void SettingsNew::showNetworkProxyTab() { + this->setSelection(SettingsNew::Pages::NETWORK); + ui->tabWidget_network->setCurrentIndex(1); +} + +void SettingsNew::setupThemeComboBox() { +#if defined(Q_OS_WIN) + m_themes.removeOne("Breeze/Dark"); + m_themes.removeOne("Breeze/Light"); +#elif defined(Q_OS_MAC) + m_themes.removeOne("QDarkStyle"); +#endif + + ui->comboBox_theme->insertItems(0, m_themes); +} + +void SettingsNew::setSelection(int index) { + // You'd really think there is a better way + QListWidgetItem *item = ui->selector->item(index); + QModelIndex idx = ui->selector->indexFromItem(item); + ui->selector->setCurrentRow(index); + ui->selector->selectionModel()->select(idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); +} + +void SettingsNew::enableWebsocket(bool enabled) { + if (enabled && !config()->get(Config::offlineMode).toBool() && !config()->get(Config::disableWebsocket).toBool()) { + websocketNotifier()->websocketClient.restart(); + } else { + websocketNotifier()->websocketClient.stop(); + } + ui->nodeWidget->onWebsocketStatusChanged(); + emit websocketStatusChanged(enabled); +} + +SettingsNew::~SettingsNew() = default; \ No newline at end of file diff --git a/src/SettingsNewDialog.h b/src/SettingsNewDialog.h new file mode 100644 index 00000000..f6dc4870 --- /dev/null +++ b/src/SettingsNewDialog.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_SETTINGSNEWDIALOG_H +#define FEATHER_SETTINGSNEWDIALOG_H + + +#include +#include +#include + +#include "appcontext.h" +#include "widgets/NodeWidget.h" + +namespace Ui { + class SettingsNew; +} + +class SettingsNew : public QDialog +{ +Q_OBJECT + +public: + explicit SettingsNew(QSharedPointer ctx, QWidget *parent = nullptr); + ~SettingsNew() override; + + void showNetworkProxyTab(); + + enum Pages { + APPEARANCE = 0, + NETWORK, + STORAGE, + DISPLAY, + MEMORY, + TRANSACTIONS, + MISC + }; + +signals: + void preferredFiatCurrencyChanged(QString currency); + void skinChanged(QString skinName); + void blockExplorerChanged(QString blockExplorer); + void hideUpdateNotifications(bool hidden); + void websocketStatusChanged(bool enabled); + void proxySettingsChanged(); + void updateBalance(); + void offlineMode(bool offline); + +public slots: +// void checkboxExternalLinkWarn(); +// void fiatCurrencySelected(int index); + +private slots: + void onProxySettingsChanged(); + +private: + void setupAppearanceTab(); + void setupNetworkTab(); + void setupStorageTab(); + void setupDisplayTab(); + void setupMemoryTab(); + void setupTransactionsTab(); + void setupMiscTab(); + + void setupThemeComboBox(); + void setSelection(int index); + void enableWebsocket(bool enabled); + + QScopedPointer ui; + QSharedPointer m_ctx; + Nodes *m_nodes = nullptr; + + QStringList m_themes{"Native", "QDarkStyle", "Breeze/Dark", "Breeze/Light"}; + QStringList m_dateFormats{"yyyy-MM-dd", "MM-dd-yyyy", "dd-MM-yyyy"}; + QStringList m_timeFormats{"hh:mm", "hh:mm ap"}; +}; + +#endif //FEATHER_SETTINGSNEWDIALOG_H diff --git a/src/SettingsNewDialog.ui b/src/SettingsNewDialog.ui new file mode 100644 index 00000000..94002e5d --- /dev/null +++ b/src/SettingsNewDialog.ui @@ -0,0 +1,1120 @@ + + + SettingsNew + + + + 0 + 0 + 841 + 454 + + + + Settings + + + + + + + + + 0 + 0 + + + + + + + + + + + 1 + + + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">Appearance</span></p></body></html> + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + Theme: + + + + + + + + + + Amount precision: + + + + + + + + + + Date format: + + + + + + + + + + Time format: + + + + + + + + + + Balance display: + + + + + + + + Total balance + + + + + Spendable balance (+ unconfirmed balance) + + + + + Spendable balance + + + + + + + + Fiat currency: + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">Network</span></p></body></html> + + + + + + + 0 + + + + Node + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + + + Proxy + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + Websocket + + + + + + + 0 + 0 + + + + <html><head/><body><p>Feather can connect to a service hosted by the developers to obtain pricing information, a curated list of remote nodes, Home feeds, the latest version of Feather Wallet and more.</p><p>This service is only used to fetch information. The wallet does not send information about its state or your transactions to the server. It is not used for any telemetry or crash reports.</p><p>If you disable this connection some functionality will be unavailable.</p></body></html> + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Maximum + + + + 0 + 10 + + + + + + + + Enable websocket connection + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + Offline + + + + + + Disable all network connections (offline mode) + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">Storage</span></p></body></html> + + + + + + + 0 + + + + Paths + + + + + + This application uses the following paths: + + + + + + + + + Wallet directory: + + + + + + + + + true + + + + + + + Change + + + + + + + Open + + + + + + + + + Config directory: + + + + + + + + + true + + + + + + + Open + + + + + + + + + Application directory: + + + + + + + true + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Portable mode is enabled. + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Logging + + + + + + Write stack trace to file on crash (Linux-only) + + + + + + + Write log files to disk + + + + + + + + + + 0 + 0 + + + + Log level: + + + + + + + + Warning + + + + + Info + + + + + Debug + + + + + Trace + + + + + + + + + + Qt::Vertical + + + + 10 + 0 + + + + + + + + + + Open log file + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + Misc + + + + + + Write recently opened wallets to config file + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Display</span></p></body></html> + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Hide balance + + + + + + + Hide update notifications + + + + + + + Hide transaction notifications + + + + + + + Warn before opening external link + + + + + + + + + Lock wallet after inactivity of + + + + + + + + 0 + 0 + + + + 1 + + + 120 + + + + + + + minutes + + + + + + + + + Lock wallet after minimizing the window + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Memory</span></p></body></html> + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Encrypt private spendkey on lock + + + + + + + Qt::Vertical + + + + 20 + 263 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + <html><head/><body><p><span style=" font-weight:600;">Transactions</span></p></body></html> + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + Multibroadcast outgoing transactions + + + + + + + + 0 + 0 + + + + ? + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + + + Always open advanced transaction dialog + + + + + + + Require password to spend + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 0 + + + 0 + + + 0 + + + + + <html><head/><body><p><span style=" font-weight:600;">Misc</span></p></body></html> + + + + + + + + 0 + 0 + + + + 0 + + + + Links + + + + + + + 0 + 0 + + + + Block explorer: + + + + + + + + exploremonero.com + + + + + xmrchain.net + + + + + melo.tools + + + + + moneroblocks.info + + + + + blockchair.info + + + + + blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion + + + + + 127.0.0.1:31312 + + + + + + + + + 0 + 0 + + + + Reddit frontend: + + + + + + + + old.reddit.com + + + + + old.reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad.onion + + + + + reddit.com + + + + + reddittorjg6rue252oqsxryoxengawnmo46qy4kyii5wtqnwfj4ooad.onion + + + + + teddit.net + + + + + + + + + 0 + 0 + + + + LocalMonero frontend: + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Ok + + + + + + + + NodeWidget + QWidget +
widgets/NodeWidget.h
+ 1 +
+ + NetworkProxyWidget + QWidget +
widgets/NetworkProxyWidget.h
+ 1 +
+
+ + + + closeButton + accepted() + SettingsNew + accept() + + + 248 + 254 + + + 157 + 274 + + + + + closeButton + rejected() + SettingsNew + reject() + + + 316 + 260 + + + 286 + 274 + + + + +
diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 7ee8f45f..7bc0e285 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -38,6 +38,7 @@ WindowManager::WindowManager(EventFilter *eventFilter) m_tray->show(); this->initSkins(); + this->patchMacStylesheet(); this->showCrashLogs(); @@ -135,6 +136,34 @@ void WindowManager::raise() { } } +// ######################## SETTINGS ######################## + +void WindowManager::showSettings(QSharedPointer ctx, QWidget *parent, bool showProxyTab) { + SettingsNew settings{ctx, parent}; + + connect(&settings, &SettingsNew::preferredFiatCurrencyChanged, [this]{ + for (const auto &window : m_windows) { + window->onPreferredFiatCurrencyChanged(); + } + }); + connect(&settings, &SettingsNew::skinChanged, this, &WindowManager::onChangeTheme); + connect(&settings, &SettingsNew::updateBalance, this, &WindowManager::updateBalance); + connect(&settings, &SettingsNew::proxySettingsChanged, this, &WindowManager::onProxySettingsChanged); + connect(&settings, &SettingsNew::websocketStatusChanged, this, &WindowManager::onWebsocketStatusChanged); + connect(&settings, &SettingsNew::offlineMode, this, &WindowManager::offlineMode); + connect(&settings, &SettingsNew::hideUpdateNotifications, [this](bool hidden){ + for (const auto &window : m_windows) { + window->onHideUpdateNotifications(hidden); + } + }); + + if (showProxyTab) { + settings.showNetworkProxyTab(); + } + + settings.exec(); +} + // ######################## WALLET OPEN ######################## void WindowManager::tryOpenWallet(const QString &path, const QString &password) { @@ -544,60 +573,56 @@ void WindowManager::onInitialNetworkConfigured() { m_initialNetworkConfigured = true; appData(); - this->initTor(); - this->initWS(); + this->onProxySettingsChanged(); } } -void WindowManager::initTor() { +void WindowManager::onProxySettingsChanged() { + if (Utils::isTorsocks()) { + return; + } + + // Will kill the process if necessary torManager()->init(); torManager()->start(); - this->onTorSettingsChanged(); -} + QNetworkProxy proxy{QNetworkProxy::NoProxy}; + if (config()->get(Config::proxy).toInt() != Config::Proxy::None) { + QString host = config()->get(Config::socks5Host).toString(); + quint16 port = config()->get(Config::socks5Port).toString().toUShort(); -void WindowManager::onTorSettingsChanged() { - if (Utils::isTorsocks()) { - return; - } + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { + host = torManager()->featherTorHost; + port = torManager()->featherTorPort; + } - // use local tor -> bundled tor - QString host = config()->get(Config::socks5Host).toString(); - quint16 port = config()->get(Config::socks5Port).toString().toUShort(); - if (!torManager()->isLocalTor()) { - host = torManager()->featherTorHost; - port = torManager()->featherTorPort; + proxy = QNetworkProxy{QNetworkProxy::Socks5Proxy, host, port}; + getNetworkSocks5()->setProxy(proxy); } - QNetworkProxy proxy{QNetworkProxy::Socks5Proxy, host, port}; - getNetworkTor()->setProxy(proxy); + qWarning() << "Proxy: " << proxy.hostName() << " " << proxy.port(); + + // Switch websocket to new proxy and update URL + websocketNotifier()->websocketClient.stop(); websocketNotifier()->websocketClient.webSocket.setProxy(proxy); + websocketNotifier()->websocketClient.nextWebsocketUrl(); + websocketNotifier()->websocketClient.restart(); - emit torSettingsChanged(); + emit proxySettingsChanged(); } void WindowManager::onWebsocketStatusChanged(bool enabled) { emit websocketStatusChanged(enabled); } -void WindowManager::initWS() { - if (config()->get(Config::offlineMode).toBool()) { - return; - } - - if (config()->get(Config::disableWebsocket).toBool()) { - return; - } - - websocketNotifier()->websocketClient.start(); -} - // ######################## WIZARD ######################## -WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) const { +WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) { auto *wizard = new WalletWizard; connect(wizard, &WalletWizard::initialNetworkConfigured, this, &WindowManager::onInitialNetworkConfigured); - connect(wizard, &WalletWizard::skinChanged, this, &WindowManager::changeSkin); + connect(wizard, &WalletWizard::showSettings, [this, wizard]{ + this->showSettings(nullptr, wizard); + }); connect(wizard, &WalletWizard::openWallet, this, &WindowManager::tryOpenWallet); connect(wizard, &WalletWizard::createWallet, this, &WindowManager::tryCreateWallet); connect(wizard, &WalletWizard::createWalletFromKeys, this, &WindowManager::tryCreateWalletFromKeys); @@ -667,13 +692,32 @@ QString WindowManager::loadStylesheet(const QString &resource) { return data; } -void WindowManager::changeSkin(const QString &skinName) { +void WindowManager::onChangeTheme(const QString &skinName) { if (!m_skins.contains(skinName)) { qWarning() << QString("No such skin %1").arg(skinName); return; } config()->set(Config::skin, skinName); + qApp->setStyleSheet(m_skins[skinName]); qDebug() << QString("Skin changed to %1").arg(skinName); + + this->patchMacStylesheet(); + + for (const auto &window : m_windows) { + window->skinChanged(skinName); + } } + +void WindowManager::patchMacStylesheet() { +#if defined(Q_OS_MACOS) + QString styleSheet = qApp->styleSheet(); + + auto patch = Utils::fileOpenQRC(":assets/macStylesheet.patch"); + auto patch_text = Utils::barrayToString(patch); + styleSheet += patch_text; + + qApp->setStyleSheet(styleSheet); +#endif +} \ No newline at end of file diff --git a/src/WindowManager.h b/src/WindowManager.h index 4d3c7c02..9c5fbd25 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -24,18 +24,21 @@ public: void close(); void closeWindow(MainWindow *window); void showWizard(WalletWizard::Page startPage); - void changeSkin(const QString &skinName); void restartApplication(const QString &binaryFilename); void raise(); + void showSettings(QSharedPointer ctx, QWidget *parent, bool showProxyTab = false); + EventFilter *eventFilter; signals: - void torSettingsChanged(); + void proxySettingsChanged(); void websocketStatusChanged(bool enabled); + void updateBalance(); + void offlineMode(bool offline); public slots: - void onTorSettingsChanged(); + void onProxySettingsChanged(); void onWebsocketStatusChanged(bool enabled); void tryOpenWallet(const QString &path, const QString &password); @@ -48,6 +51,7 @@ private slots: void onDeviceButtonPressed(); void onDeviceError(const QString &errorMessage); void onWalletPassphraseNeeded(bool on_device); + void onChangeTheme(const QString &themeName); private: void tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead); @@ -57,15 +61,15 @@ private: bool autoOpenWallet(); void initWizard(); - WalletWizard* createWizard(WalletWizard::Page startPage) const; + WalletWizard* createWizard(WalletWizard::Page startPage); void handleWalletError(const QString &message); void displayWalletErrorMessage(const QString &message); - void initTor(); - void initWS(); void initSkins(); QString loadStylesheet(const QString &resource); + void patchMacStylesheet(); + void buildTrayMenu(); void startupWarning(); void showWarningMessageBox(const QString &title, const QString &message); diff --git a/src/api/LocalMoneroApi.cpp b/src/api/LocalMoneroApi.cpp index ad61b3b7..2d4cac3c 100644 --- a/src/api/LocalMoneroApi.cpp +++ b/src/api/LocalMoneroApi.cpp @@ -3,21 +3,22 @@ #include "LocalMoneroApi.h" -LocalMoneroApi::LocalMoneroApi(QObject *parent, UtilsNetworking *network, const QString &baseUrl) +#include "utils/config.h" + +LocalMoneroApi::LocalMoneroApi(QObject *parent, UtilsNetworking *network) : QObject(parent) , m_network(network) - , m_baseUrl(baseUrl) { } void LocalMoneroApi::countryCodes() { - QString url = QString("%1/countrycodes").arg(m_baseUrl); + QString url = QString("%1/countrycodes").arg(this->getBaseUrl()); QNetworkReply *reply = m_network->getJson(url); connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::COUNTRY_CODES)); } void LocalMoneroApi::currencies() { - QString url = QString("%1/currencies").arg(m_baseUrl); + QString url = QString("%1/currencies").arg(this->getBaseUrl()); QNetworkReply *reply = m_network->getJson(url); connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::CURRENCIES)); } @@ -25,9 +26,9 @@ void LocalMoneroApi::currencies() { void LocalMoneroApi::paymentMethods(const QString &countryCode) { QString url; if (countryCode.isEmpty()) { - url = QString("%1/payment_methods").arg(m_baseUrl); + url = QString("%1/payment_methods").arg(this->getBaseUrl()); } else { - url = QString("%1/payment_methods/%2").arg(m_baseUrl, countryCode); + url = QString("%1/payment_methods/%2").arg(this->getBaseUrl(), countryCode); } QNetworkReply *reply = m_network->getJson(url); connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::PAYMENT_METHODS)); @@ -50,7 +51,7 @@ void LocalMoneroApi::sellMoneroOnline(const QString ¤cyCode, const QString } void LocalMoneroApi::accountInfo(const QString &username) { - QString url = QString("%1/account_info/%2").arg(m_baseUrl, username); + QString url = QString("%1/account_info/%2").arg(this->getBaseUrl(), username); QNetworkReply *reply = m_network->getJson(url); connect(reply, &QNetworkReply::finished, std::bind(&LocalMoneroApi::onResponse, this, reply, Endpoint::ACCOUNT_INFO)); } @@ -88,7 +89,7 @@ void LocalMoneroApi::onResponse(QNetworkReply *reply, LocalMoneroApi::Endpoint e QString LocalMoneroApi::getBuySellUrl(bool buy, const QString ¤cyCode, const QString &countryCode, const QString &paymentMethod, const QString &amount, int page) { - QString url = QString("%1/%2-monero-online/%3").arg(m_baseUrl, buy ? "buy" : "sell", currencyCode); + QString url = QString("%1/%2-monero-online/%3").arg(this->getBaseUrl(), buy ? "buy" : "sell", currencyCode); if (!countryCode.isEmpty() && paymentMethod.isEmpty()) url += QString("/%1").arg(countryCode); else if (countryCode.isEmpty() && !paymentMethod.isEmpty()) @@ -103,4 +104,16 @@ QString LocalMoneroApi::getBuySellUrl(bool buy, const QString ¤cyCode, con query.addQueryItem("page", QString::number(page)); url += "?" + query.toString(); return url; +} + +QString LocalMoneroApi::getBaseUrl() { + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + return "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion/api/v1"; + } + + if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + return "http://yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p/api/v1"; + } + + return "https://agoradesk.com/api/v1"; } \ No newline at end of file diff --git a/src/api/LocalMoneroApi.h b/src/api/LocalMoneroApi.h index 56a8bf0a..2fe6b98b 100644 --- a/src/api/LocalMoneroApi.h +++ b/src/api/LocalMoneroApi.h @@ -27,7 +27,7 @@ public: QJsonObject obj; }; - explicit LocalMoneroApi(QObject *parent, UtilsNetworking *network, const QString &baseUrl = "https://agoradesk.com/api/v1"); + explicit LocalMoneroApi(QObject *parent, UtilsNetworking *network); void countryCodes(); void currencies(); @@ -44,9 +44,9 @@ private slots: private: QString getBuySellUrl(bool buy, const QString ¤cyCode, const QString &countryCode="", const QString &paymentMethod="", const QString &amount = "", int page = 0); + QString getBaseUrl(); UtilsNetworking *m_network; - QString m_baseUrl; }; diff --git a/src/appcontext.cpp b/src/appcontext.cpp index 5959556d..efad1024 100644 --- a/src/appcontext.cpp +++ b/src/appcontext.cpp @@ -22,9 +22,9 @@ AppContext::AppContext(Wallet *wallet) : wallet(wallet) - , nodes(new Nodes(this, this)) + , nodes(new Nodes(this)) , networkType(constants::networkType) - , m_rpc(new DaemonRpc{this, getNetworkTor(), ""}) + , m_rpc(new DaemonRpc{this, ""}) { connect(this->wallet, &Wallet::moneySpent, this, &AppContext::onMoneySpent); connect(this->wallet, &Wallet::moneyReceived, this, &AppContext::onMoneyReceived); @@ -38,15 +38,14 @@ AppContext::AppContext(Wallet *wallet) connect(this->wallet, &Wallet::deviceError, this, &AppContext::onDeviceError); connect(this->wallet, &Wallet::deviceButtonRequest, this, &AppContext::onDeviceButtonRequest); connect(this->wallet, &Wallet::deviceButtonPressed, this, &AppContext::onDeviceButtonPressed); - connect(this->wallet, &Wallet::connectionStatusChanged, [this]{ - this->nodes->autoConnect(); - }); connect(this->wallet, &Wallet::currentSubaddressAccountChanged, [this]{ this->updateBalance(); }); connect(this, &AppContext::createTransactionError, this, &AppContext::onCreateTransactionError); + nodes->setContext(this); + // Store the wallet every 2 minutes m_storeTimer.start(2 * 60 * 1000); connect(&m_storeTimer, &QTimer::timeout, [this](){ @@ -188,15 +187,12 @@ void AppContext::setSelectedInputs(const QStringList &selectedInputs) { emit selectedInputsChanged(selectedInputs); } -void AppContext::onTorSettingsChanged() { +void AppContext::onProxySettingsChanged() { if (Utils::isTorsocks()) { return; } this->nodes->connectToNode(); - - auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); - qDebug() << "Changed privacyLevel to " << privacyLevel; } void AppContext::stopTimers() { diff --git a/src/appcontext.h b/src/appcontext.h index 81a612a1..4b119835 100644 --- a/src/appcontext.h +++ b/src/appcontext.h @@ -62,7 +62,7 @@ public slots: void onDeviceButtonPressed(); void onDeviceError(const QString &message); - void onTorSettingsChanged(); // should not be here + void onProxySettingsChanged(); // should not be here private slots: void onMoneySpent(const QString &txId, quint64 amount); diff --git a/src/assets.qrc b/src/assets.qrc index cf9e9abf..97bcc93a 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -20,6 +20,7 @@ assets/images/camera_dark.png assets/images/camera_white.png assets/images/change_account.png + assets/images/chipset_32px.png assets/images/clock1.png assets/images/clock2.png assets/images/clock3.png @@ -44,10 +45,14 @@ assets/images/eye_blind.png assets/images/feather.png assets/images/file.png + assets/images/file_manager_32px.png assets/images/gnome-calc.png + assets/images/hd_32px.png assets/images/history.png + assets/images/i2p.png assets/images/info.png assets/images/info2.svg + assets/images/interface_32px.png assets/images/key.png assets/images/ledger.png assets/images/ledger_unpaired.png @@ -65,6 +70,7 @@ assets/images/microphone.png assets/images/mining.png assets/images/network.png + assets/images/nw_32px.png assets/images/offline_tx.png assets/images/person.svg assets/images/preferences.png @@ -81,6 +87,7 @@ assets/images/securityLevelSafestWhite.png assets/images/securityLevelStandardWhite.png assets/images/seed.png + assets/images/settings_disabled_32px.png assets/images/speaker.png assets/images/status_connected_fork.png assets/images/status_connected.png @@ -93,6 +100,7 @@ assets/images/status_lagging_fork.png assets/images/status_lagging.png assets/images/status_lagging.svg + assets/images/status_offline.svg assets/images/status_waiting.png assets/images/status_waiting.svg assets/images/tab_addresses.png @@ -117,6 +125,7 @@ assets/images/unpaid.png assets/images/update.png assets/images/warning.png + assets/images/vrdp_32px.png assets/images/xmrig.ico assets/images/xmrig.svg assets/images/zoom.png diff --git a/src/assets/images/chipset_32px.png b/src/assets/images/chipset_32px.png new file mode 100644 index 00000000..79cb521f Binary files /dev/null and b/src/assets/images/chipset_32px.png differ diff --git a/src/assets/images/file_manager_32px.png b/src/assets/images/file_manager_32px.png new file mode 100644 index 00000000..35edf7be Binary files /dev/null and b/src/assets/images/file_manager_32px.png differ diff --git a/src/assets/images/hd_32px.png b/src/assets/images/hd_32px.png new file mode 100644 index 00000000..a9c0c14b Binary files /dev/null and b/src/assets/images/hd_32px.png differ diff --git a/src/assets/images/i2p.png b/src/assets/images/i2p.png new file mode 100644 index 00000000..39cd6b49 Binary files /dev/null and b/src/assets/images/i2p.png differ diff --git a/src/assets/images/interface_32px.png b/src/assets/images/interface_32px.png new file mode 100644 index 00000000..1bc056c2 Binary files /dev/null and b/src/assets/images/interface_32px.png differ diff --git a/src/assets/images/nw_32px.png b/src/assets/images/nw_32px.png new file mode 100644 index 00000000..2d03afcc Binary files /dev/null and b/src/assets/images/nw_32px.png differ diff --git a/src/assets/images/settings_disabled_32px.png b/src/assets/images/settings_disabled_32px.png new file mode 100644 index 00000000..bd8b5c23 Binary files /dev/null and b/src/assets/images/settings_disabled_32px.png differ diff --git a/src/assets/images/status_offline.svg b/src/assets/images/status_offline.svg new file mode 100644 index 00000000..c8332a8d --- /dev/null +++ b/src/assets/images/status_offline.svg @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + Lapo Calamandrei + + + + + + + + record + media + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/vrdp_32px.png b/src/assets/images/vrdp_32px.png new file mode 100644 index 00000000..505a5867 Binary files /dev/null and b/src/assets/images/vrdp_32px.png differ diff --git a/src/assets/nodes.json b/src/assets/nodes.json index e3a16c88..1c7f846d 100644 --- a/src/assets/nodes.json +++ b/src/assets/nodes.json @@ -15,6 +15,10 @@ "rino4muoj3zk223twblr53hwt4ukqemjqw4fbiwdcrqqevcrclpuzyyd.onion:18081", "majesticrepik35vnngouksfl7jiwf6sj7s2doj3bvdffq27tgqoeayd.onion:18089" ], + "i2p": [ + "ynk3hrwte23asonojqeskoulek2g2cd6tqg4neghnenfyljrvhga.b32.i2p:18081", + "t7lce3j7mwxt32h755u3njjp3k6og3defgueo3iecgsexxnkoezouobc.b32.i2p:18089" + ], "clearnet": [ "node.community.rino.io:18081", "node.sethforprivacy.com:18089", diff --git a/src/components.h b/src/components.h index 26b66df7..5d05ab73 100644 --- a/src/components.h +++ b/src/components.h @@ -10,6 +10,7 @@ #include #include #include +#include class DoublePixmapLabel : public QLabel { diff --git a/src/constants.h b/src/constants.h index 548e0dbd..f0dbe108 100644 --- a/src/constants.h +++ b/src/constants.h @@ -24,15 +24,6 @@ namespace constants // donation constants const QString donationAddress = "47ntfT2Z5384zku39pTM6hGcnLnvpRYW2Azm87GiAAH2bcTidtq278TL6HmwyL8yjMeERqGEBs3cqC8vvHPJd1cWQrGC65f"; const int donationBoundary = 25; - - // websocket constants - const QVector websocketUrls = { - QUrl(QStringLiteral("wss://ws.featherwallet.org/ws")), - QUrl(QStringLiteral("wss://ws.featherwallet.net/ws")) - }; - - // website constants - const QString websiteUrl = "https://featherwallet.org"; } #endif //FEATHER_CONSTANTS_H diff --git a/src/dialog/DebugInfoDialog.ui b/src/dialog/DebugInfoDialog.ui index f4ee55e5..f5e030ff 100644 --- a/src/dialog/DebugInfoDialog.ui +++ b/src/dialog/DebugInfoDialog.ui @@ -212,6 +212,9 @@ TextLabel + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + diff --git a/src/dialog/TorInfoDialog.cpp b/src/dialog/TorInfoDialog.cpp index 1f262333..b1f88232 100644 --- a/src/dialog/TorInfoDialog.cpp +++ b/src/dialog/TorInfoDialog.cpp @@ -4,64 +4,23 @@ #include "TorInfoDialog.h" #include "ui_TorInfoDialog.h" -#include -#include -#include -#include -#include - -#include "utils/ColorScheme.h" -#include "utils/Icons.h" -#include "utils/os/tails.h" #include "utils/TorManager.h" -TorInfoDialog::TorInfoDialog(QSharedPointer ctx, QWidget *parent) +TorInfoDialog::TorInfoDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::TorInfoDialog) - , m_ctx(std::move(ctx)) { ui->setupUi(this); - if (!torManager()->torConnected && !torManager()->errorMsg.isEmpty()) { - ui->message->setText(torManager()->errorMsg); - } else { - ui->message->hide(); - } - - if (torManager()->isLocalTor()) { - ui->frame_logs->setHidden(true); - } else { - ui->logs->setPlainText(torManager()->torLogs); - } - - initConnectionSettings(); - initPrivacyLevel(); - onConnectionStatusChanged(torManager()->torConnected); + ui->logs->setPlainText(torManager()->torLogs); - auto *portValidator = new QRegularExpressionValidator{QRegularExpression("[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]")}; - ui->line_port->setValidator(portValidator); + this->onStatusChanged(torManager()->errorMsg); + this->onConnectionStatusChanged(torManager()->torConnected); + connect(torManager(), &TorManager::statusChanged, this, &TorInfoDialog::onStatusChanged); connect(torManager(), &TorManager::connectionStateChanged, this, &TorInfoDialog::onConnectionStatusChanged); connect(torManager(), &TorManager::logsUpdated, this, &TorInfoDialog::onLogsUpdated); - connect(ui->buttonBox->button(QDialogButtonBox::Apply), &QPushButton::clicked, this, &TorInfoDialog::onApplySettings); - - connect(ui->line_host, &QLineEdit::textEdited, this, &TorInfoDialog::onSettingsChanged); - connect(ui->line_port, &QLineEdit::textEdited, this, &TorInfoDialog::onSettingsChanged); - connect(ui->check_useLocalTor, &QCheckBox::stateChanged, this, &TorInfoDialog::onSettingsChanged); - connect(ui->btnGroup_privacyLevel, &QButtonGroup::idToggled, this, &TorInfoDialog::onSettingsChanged); - - ui->label_changes->hide(); - - ui->btn_configureInitSync->setIcon(icons()->icon("preferences.svg")); - connect(ui->btn_configureInitSync, &QPushButton::clicked, this, &TorInfoDialog::onShowInitSyncConfigDialog); - -#if !defined(HAS_TOR_BIN) && !defined(PLATFORM_INSTALLER) - ui->check_useLocalTor->setChecked(true); - ui->check_useLocalTor->setEnabled(false); - ui->check_useLocalTor->setToolTip("Feather was bundled without Tor"); -#endif - this->adjustSize(); } @@ -70,104 +29,25 @@ void TorInfoDialog::onLogsUpdated() { } void TorInfoDialog::onConnectionStatusChanged(bool connected) { - if (connected) { + if (!torManager()->isStarted()) { + ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_offline.svg").scaledToWidth(16, Qt::SmoothTransformation)); + ui->label_testConnectionStatus->setText("Not running"); + } + else if (connected) { ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_connected.png").scaledToWidth(16, Qt::SmoothTransformation)); ui->label_testConnectionStatus->setText("Connected"); - } else { - ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_disconnected.png").scaledToWidth(16, Qt::SmoothTransformation)); - ui->label_testConnectionStatus->setText("Disconnected"); - } -} - -void TorInfoDialog::onApplySettings() { - config()->set(Config::socks5Host, ui->line_host->text()); - config()->set(Config::socks5Port, ui->line_port->text()); - - int id = ui->btnGroup_privacyLevel->checkedId(); - config()->set(Config::torPrivacyLevel, id); - - ui->label_changes->hide(); - - bool useLocalTor = ui->check_useLocalTor->isChecked(); - if (config()->get(Config::useLocalTor).toBool() && useLocalTor && torManager()->isStarted()) { - QMessageBox::warning(this, "Warning", "Feather is running the bundled Tor daemon, " - "but the option to never start a bundled Tor daemon was selected. " - "A restart is required to apply the setting."); - } - config()->set(Config::useLocalTor, useLocalTor); - - ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_lagging.png").scaledToWidth(16, Qt::SmoothTransformation)); - ui->label_testConnectionStatus->setText("Connecting"); - - emit torSettingsChanged(); -} - -void TorInfoDialog::onSettingsChanged() { - ui->label_changes->show(); -} - -void TorInfoDialog::initConnectionSettings() { - bool localTor = torManager()->isLocalTor(); - ui->label_connectionSettingsMessage->setVisible(!localTor); - ui->frame_connectionSettings->setVisible(localTor); - - ui->line_host->setText(config()->get(Config::socks5Host).toString()); - ui->line_port->setText(config()->get(Config::socks5Port).toString()); - - ui->check_useLocalTor->setChecked(config()->get(Config::useLocalTor).toBool()); -} - -void TorInfoDialog::initPrivacyLevel() { - ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptNode, Config::allTorExceptNode); - ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptInitSync, Config::allTorExceptInitSync); - ui->btnGroup_privacyLevel->setId(ui->radio_allTor, Config::allTor); - - int privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); - auto button = ui->btnGroup_privacyLevel->button(privacyLevel); - if (button) { - button->setChecked(true); - } - - if (m_ctx->nodes->connection().isLocal()) { - ui->label_notice->setText("You are connected to a local node. Traffic to node is not routed over Tor."); - } - else if (Utils::isTorsocks()) { - ui->label_notice->setText("Feather was started with torsocks, all traffic is routed over Tor"); - } - else if (WhonixOS::detect()) { - ui->label_notice->setText("Feather is running on Whonix, all traffic is routed over Tor"); - } - else if (TailsOS::detect()) { - ui->label_notice->setText("Feather is running on Tails, all traffic is routed over Tor"); } else { - ui->frame_notice->hide(); + ui->icon_connectionStatus->setPixmap(QPixmap(":/assets/images/status_disconnected.png").scaledToWidth(16, Qt::SmoothTransformation)); + ui->label_testConnectionStatus->setText("Disconnected"); } - - bool dark = ColorScheme::darkScheme; - QPixmap iconNoTor(dark ? ":/assets/images/securityLevelStandardWhite.png" : ":/assets/images/securityLevelStandard.png"); - QPixmap iconNoSync(dark ? ":/assets/images/securityLevelSaferWhite.png" : ":/assets/images/securityLevelSafer.png"); - QPixmap iconAllTor(dark ? ":/assets/images/securityLevelSafestWhite.png" : ":/assets/images/securityLevelSafest.png"); - ui->icon_noTor->setPixmap(iconNoTor.scaledToHeight(16, Qt::SmoothTransformation)); - ui->icon_noSync->setPixmap(iconNoSync.scaledToHeight(16, Qt::SmoothTransformation)); - ui->icon_allTor->setPixmap(iconAllTor.scaledToHeight(16, Qt::SmoothTransformation)); } -void TorInfoDialog::onStopTor() { - torManager()->stop(); -} - -void TorInfoDialog::onShowInitSyncConfigDialog() { - - int threshold = config()->get(Config::initSyncThreshold).toInt(); +void TorInfoDialog::onStatusChanged(const QString &msg) { + ui->message->setText(msg); - bool ok; - int newThreshold = QInputDialog::getInt(this, "Sync threshold", - "Synchronize over clearnet if wallet is behind more than x blocks: ", - threshold, 0, 10000, 10, &ok); - - if (ok) { - config()->set(Config::initSyncThreshold, newThreshold); + if (msg.isEmpty()) { + ui->message->hide(); } } diff --git a/src/dialog/TorInfoDialog.h b/src/dialog/TorInfoDialog.h index 617ba3d6..5e070584 100644 --- a/src/dialog/TorInfoDialog.h +++ b/src/dialog/TorInfoDialog.h @@ -6,8 +6,6 @@ #include -#include "appcontext.h" - namespace Ui { class TorInfoDialog; } @@ -17,7 +15,7 @@ class TorInfoDialog : public QDialog Q_OBJECT public: - explicit TorInfoDialog(QSharedPointer ctx, QWidget *parent = nullptr); + explicit TorInfoDialog(QWidget *parent = nullptr); ~TorInfoDialog() override; public slots: @@ -25,20 +23,10 @@ public slots: private slots: void onConnectionStatusChanged(bool connected); - void onApplySettings(); - void onSettingsChanged(); - void onStopTor(); - void onShowInitSyncConfigDialog(); - -signals: - void torSettingsChanged(); + void onStatusChanged(const QString &msg = ""); private: - void initConnectionSettings(); - void initPrivacyLevel(); - QScopedPointer ui; - QSharedPointer m_ctx; }; diff --git a/src/dialog/TorInfoDialog.ui b/src/dialog/TorInfoDialog.ui index ffce23df..6cfb9590 100644 --- a/src/dialog/TorInfoDialog.ui +++ b/src/dialog/TorInfoDialog.ui @@ -6,224 +6,14 @@ 0 0 - 703 - 804 + 708 + 496 Tor settings - - - - Connection settings - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - Host - - - - - - - 127.0.0.1 - - - 127.0.0.1 - - - - - - - Port - - - - - - - 9050 - - - 9050 - - - - - - - - - - - - Tor daemon is managed by Feather. - - - - - - - Never start bundled Tor (requires local Tor daemon) - - - - - - - - - - Privacy Level - - - - - - - - - 0 - 0 - - - - icon - - - - - - - Route all traffic over Tor, except traffic to node - - - btnGroup_privacyLevel - - - - - - - - - - - - 0 - 0 - - - - icon - - - - - - - Route all traffic over Tor, except initial wallet synchronization - - - btnGroup_privacyLevel - - - - - - - - 0 - 0 - - - - - - - - - - - - - - - - 0 - 0 - - - - icon - - - - - - - Route all traffic over Tor - - - btnGroup_privacyLevel - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - notice - - - true - - - - - - - - - @@ -252,13 +42,6 @@ - - - - (changes not applied) - - - @@ -310,6 +93,12 @@ + + + 0 + 0 + + Logs @@ -318,7 +107,7 @@ 0 - 0 + 7 0 @@ -345,26 +134,13 @@ - - - - Qt::Vertical - - - - 20 - 0 - - - - Qt::Horizontal - QDialogButtonBox::Apply|QDialogButtonBox::Close + QDialogButtonBox::Close diff --git a/src/dialog/TxBroadcastDialog.cpp b/src/dialog/TxBroadcastDialog.cpp index b37daa8e..9710449d 100644 --- a/src/dialog/TxBroadcastDialog.cpp +++ b/src/dialog/TxBroadcastDialog.cpp @@ -16,7 +16,7 @@ TxBroadcastDialog::TxBroadcastDialog(QWidget *parent, QSharedPointer ui->setupUi(this); auto node = m_ctx->nodes->connection(); - m_rpc = new DaemonRpc(this, getNetworkTor(), node.toAddress()); + m_rpc = new DaemonRpc(this, node.toAddress()); connect(ui->btn_Broadcast, &QPushButton::clicked, this, &TxBroadcastDialog::broadcastTx); connect(ui->btn_Close, &QPushButton::clicked, this, &TxBroadcastDialog::reject); @@ -35,12 +35,6 @@ void TxBroadcastDialog::broadcastTx() { FeatherNode node = ui->radio_useCustom->isChecked() ? FeatherNode(ui->customNode->text()) : m_ctx->nodes->connection(); - if (node.isLocal()) { - m_rpc->setNetwork(getNetworkClearnet()); - } else { - m_rpc->setNetwork(getNetworkTor()); - } - m_rpc->setDaemonAddress(node.toURL()); m_rpc->sendRawTransaction(tx); } @@ -56,6 +50,7 @@ void TxBroadcastDialog::onApiResponse(const DaemonRpc::DaemonResponse &resp) { } this->accept(); + QMessageBox::information(this, "Transaction broadcast", "Transaction submitted successfully.\n\n" "If the transaction belongs to this wallet it may take several minutes before it shows up in the history tab."); } diff --git a/src/dialog/UpdateDialog.cpp b/src/dialog/UpdateDialog.cpp index 3fd9ecbb..e35dad7b 100644 --- a/src/dialog/UpdateDialog.cpp +++ b/src/dialog/UpdateDialog.cpp @@ -51,16 +51,22 @@ UpdateDialog::UpdateDialog(QWidget *parent, QSharedPointer updater) } void UpdateDialog::checkForUpdates() { - ui->label_header->setText("Checking for updates..."); - ui->label_body->setText("..."); + ui->label_header->setText("Checking for updates"); + ui->label_body->setText(".."); + connect(&m_waitingTimer, &QTimer::timeout, [this]{ + ui->label_body->setText(ui->label_body->text() + "."); + }); + m_waitingTimer.start(500); m_updater->checkForUpdates(); } void UpdateDialog::noUpdateAvailable() { + m_waitingTimer.stop(); this->setStatus("Feather is up-to-date.", true); } void UpdateDialog::updateAvailable() { + m_waitingTimer.stop(); ui->frame->show(); ui->btn_installUpdate->hide(); ui->btn_restart->hide(); @@ -70,6 +76,7 @@ void UpdateDialog::updateAvailable() { } void UpdateDialog::onUpdateCheckFailed(const QString &errorMsg) { + m_waitingTimer.stop(); this->setStatus(QString("Failed to check for updates: %1").arg(errorMsg), false); } @@ -78,7 +85,7 @@ void UpdateDialog::onDownloadClicked() { ui->btn_download->hide(); ui->progressBar->show(); - UtilsNetworking network{getNetworkTor()}; + UtilsNetworking network{this}; m_reply = network.get(m_updater->downloadUrl); connect(m_reply, &QNetworkReply::downloadProgress, this, &UpdateDialog::onDownloadProgress); diff --git a/src/dialog/UpdateDialog.h b/src/dialog/UpdateDialog.h index 53cb07b7..37791313 100644 --- a/src/dialog/UpdateDialog.h +++ b/src/dialog/UpdateDialog.h @@ -6,6 +6,7 @@ #include #include +#include #include "utils/Updater.h" @@ -45,11 +46,12 @@ private: QSharedPointer m_updater; QString m_downloadUrl; - QString m_updatePath; std::string m_updateZipArchive; + QTimer m_waitingTimer; + QNetworkReply *m_reply = nullptr; }; diff --git a/src/main.cpp b/src/main.cpp index 367b3f53..8f39957a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -11,6 +11,7 @@ #include "constants.h" #include "MainWindow.h" #include "utils/EventFilter.h" +#include "utils/os/Prestium.h" #include "WindowManager.h" #include "config.h" @@ -40,10 +41,12 @@ void signal_handler(int signum) { std::cout << keyStream.str(); // Write stack trace to disk - QString crashLogPath{Config::defaultConfigDir().path() + "/crash_report.txt"}; - std::ofstream out(crashLogPath.toStdString()); - out << keyStream.str(); - out.close(); + if (config()->get(Config::writeStackTraceToDisk).toBool()) { + QString crashLogPath{Config::defaultConfigDir().path() + "/crash_report.txt"}; + std::ofstream out(crashLogPath.toStdString()); + out << keyStream.str(); + out.close(); + } // Make a last ditch attempt to restart the application QProcess::startDetached(qApp->arguments()[0], qApp->arguments()); @@ -91,12 +94,6 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { QCommandLineOption useLocalTorOption(QStringList() << "use-local-tor", "Use system wide installed Tor instead of the bundled."); parser.addOption(useLocalTorOption); - QCommandLineOption torHostOption(QStringList() << "tor-host", "Address of running Tor instance.", "torHost"); - parser.addOption(torHostOption); - - QCommandLineOption torPortOption(QStringList() << "tor-port", "Port of running Tor instance.", "torPort"); - parser.addOption(torPortOption); - QCommandLineOption quietModeOption(QStringList() << "quiet", "Limit console output"); parser.addOption(quietModeOption); @@ -164,16 +161,17 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { bool logLevelFromEnv; int logLevel = qEnvironmentVariableIntValue("MONERO_LOG_LEVEL", &logLevelFromEnv); - if (!logLevelFromEnv) { - logLevel = 0; + if (logLevelFromEnv) { + config()->set(Config::logLevel, logLevel); + } else { + logLevel = config()->get(Config::logLevel).toInt(); } - config()->set(Config::logLevel, logLevel); if (parser.isSet("quiet") || config()->get(Config::disableLogging).toBool()) { qWarning() << "Logging is disabled"; WalletManager::instance()->setLogLevel(-1); } - else if (logLevelFromEnv && logLevel >= 0 && logLevel <= Monero::WalletManagerFactory::LogLevel_Max) { + else if (logLevel >= 0 && logLevel <= Monero::WalletManagerFactory::LogLevel_Max) { Monero::WalletManagerFactory::setLogLevel(logLevel); } @@ -186,11 +184,12 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { if (!QDir().mkpath(walletDir)) qCritical() << "Unable to create dir: " << walletDir; - // Setup Tor config - if (parser.isSet("tor-host")) - config()->set(Config::socks5Host, parser.value("tor-host")); - if (parser.isSet("tor-port")) - config()->set(Config::socks5Port, parser.value("tor-port")); + // Prestium initial config + if (config()->get(Config::firstRun).toBool() && Prestium::detect()) { + config()->set(Config::proxy, Config::Proxy::i2p); + config()->set(Config::socks5Port, 4448); + } + if (parser.isSet("use-local-tor")) config()->set(Config::useLocalTor, true); diff --git a/src/model/BountiesModel.cpp b/src/model/BountiesModel.cpp index f0cb5ae5..7d084a6a 100644 --- a/src/model/BountiesModel.cpp +++ b/src/model/BountiesModel.cpp @@ -90,13 +90,13 @@ QVariant BountiesModel::headerData(int section, Qt::Orientation orientation, int { switch(section) { case Votes: - return QString("🡅"); + return QString(" 🡅 "); case Title: return QString("Title"); case Status: - return QString("Status"); + return QString(" Status "); case Bounty: - return QString("Bounty"); + return QString(" Bounty "); default: return QVariant(); } diff --git a/src/model/NodeModel.cpp b/src/model/NodeModel.cpp index 6428cacd..1526444f 100644 --- a/src/model/NodeModel.cpp +++ b/src/model/NodeModel.cpp @@ -9,8 +9,8 @@ NodeModel::NodeModel(int nodeSource, QObject *parent) : QAbstractTableModel(parent) , m_nodeSource(nodeSource) - , m_offline(icons()->icon("expired_icon.png")) - , m_online(icons()->icon("confirmed_icon.png")) + , m_offline(icons()->icon("status_offline.svg")) + , m_online(icons()->icon("status_connected.svg")) { } @@ -60,8 +60,9 @@ QVariant NodeModel::data(const QModelIndex &index, int role) const { else if (role == Qt::DecorationRole) { switch (index.column()) { case NodeModel::URL: { - if(m_nodeSource == NodeSource::websocket) + if (m_nodeSource == NodeSource::websocket && !config()->get(Config::offlineMode).toBool()) { return QVariant(node.online ? m_online : m_offline); + } return QVariant(); } default: { @@ -72,6 +73,8 @@ QVariant NodeModel::data(const QModelIndex &index, int role) const { else if (role == Qt::ToolTipRole) { switch (index.column()) { case NodeModel::URL: { + if (node.isConnecting) + return QString("Feather is connecting to this node."); if (node.isActive) return QString("Feather is connected to this node."); } @@ -95,7 +98,7 @@ QVariant NodeModel::headerData(int section, Qt::Orientation orientation, int rol case NodeModel::URL: return QString("Node"); case NodeModel::Height: - return QString("Height"); + return QString("Height "); default: return QVariant(); } diff --git a/src/model/RedditModel.cpp b/src/model/RedditModel.cpp index 5326c568..03654775 100644 --- a/src/model/RedditModel.cpp +++ b/src/model/RedditModel.cpp @@ -86,7 +86,7 @@ QVariant RedditModel::headerData(int section, Qt::Orientation orientation, int r case Author: return QString("Author"); case Comments: - return QString("Comments"); + return QString(" Comments "); default: return QVariant(); } diff --git a/src/utils/NetworkManager.cpp b/src/utils/NetworkManager.cpp index 10187271..26315b10 100644 --- a/src/utils/NetworkManager.cpp +++ b/src/utils/NetworkManager.cpp @@ -5,21 +5,26 @@ #include #include +#include +#include -QNetworkAccessManager *g_networkManagerTor = nullptr; +#include "utils/config.h" +#include "utils/Utils.h" + +QNetworkAccessManager *g_networkManagerSocks5 = nullptr; QNetworkAccessManager *g_networkManagerClearnet = nullptr; -QNetworkAccessManager* getNetworkTor() +QNetworkAccessManager* getNetworkSocks5() { - if (!g_networkManagerTor) { - g_networkManagerTor = new QNetworkAccessManager(QCoreApplication::instance()); + if (!g_networkManagerSocks5) { + g_networkManagerSocks5 = new QNetworkAccessManager(QCoreApplication::instance()); QNetworkProxy proxy; proxy.setType(QNetworkProxy::Socks5Proxy); proxy.setHostName("127.0.0.1"); proxy.setPort(9050); - g_networkManagerTor->setProxy(proxy); + g_networkManagerSocks5->setProxy(proxy); } - return g_networkManagerTor; + return g_networkManagerSocks5; } QNetworkAccessManager* getNetworkClearnet() @@ -30,8 +35,17 @@ QNetworkAccessManager* getNetworkClearnet() return g_networkManagerClearnet; } -//void setTorProxy(const QNetworkProxy &proxy) -//{ -// QNetworkAccessManager *network = getNetworkTor(); -// network->setProxy(proxy); -//} \ No newline at end of file + +QNetworkAccessManager* getNetwork(const QString &address) +{ + if (config()->get(Config::proxy).toInt() == Config::Proxy::None) { + return getNetworkClearnet(); + } + + // Ignore proxy rules for local addresses + if (!address.isEmpty() && Utils::isLocalUrl(QUrl(address))) { + return getNetworkClearnet(); + } + + return getNetworkSocks5(); +} \ No newline at end of file diff --git a/src/utils/NetworkManager.h b/src/utils/NetworkManager.h index 7bed7fae..be285abc 100644 --- a/src/utils/NetworkManager.h +++ b/src/utils/NetworkManager.h @@ -6,9 +6,9 @@ #include -QNetworkAccessManager* getNetworkTor(); +QNetworkAccessManager* getNetworkSocks5(); QNetworkAccessManager* getNetworkClearnet(); -//void setTorProxy(const QNetworkProxy &proxy); +QNetworkAccessManager* getNetwork(const QString &address = ""); #endif //FEATHER_NETWORKMANAGER_H diff --git a/src/utils/TorManager.cpp b/src/utils/TorManager.cpp index 9ebbef59..830dbd6b 100644 --- a/src/utils/TorManager.cpp +++ b/src/utils/TorManager.cpp @@ -42,6 +42,8 @@ void TorManager::init() { if (m_localTor && (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting)) { m_process.kill(); } + + featherTorPort = config()->get(Config::torManagedPort).toString().toUShort(); } void TorManager::stop() { @@ -58,12 +60,12 @@ void TorManager::start() { auto state = m_process.state(); if (state == QProcess::ProcessState::Running || state == QProcess::ProcessState::Starting) { - this->errorMsg = "Can't start Tor, already running or starting"; + this->setErrorMessage("Can't start Tor, already running or starting"); return; } if (Utils::portOpen(featherTorHost, featherTorPort)) { - this->errorMsg = QString("Unable to start Tor on %1:%2. Port already in use.").arg(featherTorHost, QString::number(featherTorPort)); + this->setErrorMessage(QString("Unable to start Tor on %1:%2. Port already in use.").arg(featherTorHost, QString::number(featherTorPort))); return; } @@ -71,7 +73,7 @@ void TorManager::start() { m_restarts += 1; if (m_restarts > 4) { - this->errorMsg = "Tor failed to start: maximum retries exceeded"; + this->setErrorMessage("Tor failed to start: maximum retries exceeded"); return; } @@ -107,6 +109,10 @@ void TorManager::checkConnection() { this->setConnectionState(code == 0); } + else if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + this->setConnectionState(false); + } + else if (m_localTor) { QString host = config()->get(Config::socks5Host).toString(); quint16 port = config()->get(Config::socks5Port).toString().toUShort(); @@ -125,6 +131,7 @@ void TorManager::setConnectionState(bool connected) { void TorManager::stateChanged(QProcess::ProcessState state) { if (state == QProcess::ProcessState::Running) { + this->setErrorMessage(""); qWarning() << "Tor started, awaiting bootstrap"; } else if (state == QProcess::ProcessState::NotRunning) { @@ -155,7 +162,7 @@ void TorManager::handleProcessError(QProcess::ProcessError error) { if (error == QProcess::ProcessError::Crashed) qWarning() << "Tor crashed or killed"; else if (error == QProcess::ProcessError::FailedToStart) { - this->errorMsg = "Tor binary failed to start: " + this->torPath; + this->setErrorMessage("Tor binary failed to start: " + this->torPath); this->m_stopRetries = true; } } @@ -236,6 +243,11 @@ bool TorManager::shouldStartTorDaemon() { return false; #endif + // Don't start a Tor daemon if our proxy config isn't set to Tor + if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + return false; + } + // Don't start a Tor daemon if --use-local-tor is specified if (config()->get(Config::useLocalTor).toBool()) { return false; @@ -250,7 +262,7 @@ bool TorManager::shouldStartTorDaemon() { if (!unpacked) { // Don't try to start a Tor daemon if unpacking failed qWarning() << "Error unpacking embedded Tor. Assuming --use-local-tor"; - this->errorMsg = "Error unpacking embedded Tor. Assuming --use-local-tor"; + this->setErrorMessage("Error unpacking embedded Tor. Assuming --use-local-tor"); return false; } @@ -280,6 +292,11 @@ SemanticVersion TorManager::getVersion(const QString &fileName) { return SemanticVersion::fromString(output); } +void TorManager::setErrorMessage(const QString &msg) { + this->errorMsg = msg; + emit statusChanged(msg); +} + TorManager* TorManager::instance() { if (!m_instance) { diff --git a/src/utils/TorManager.h b/src/utils/TorManager.h index 038ba9fc..16bf76c0 100644 --- a/src/utils/TorManager.h +++ b/src/utils/TorManager.h @@ -43,7 +43,7 @@ public: signals: void connectionStateChanged(bool connected); - void startupFailure(QString reason); + void statusChanged(QString reason); void logsUpdated(); private slots: @@ -55,6 +55,7 @@ private slots: private: bool shouldStartTorDaemon(); void setConnectionState(bool connected); + void setErrorMessage(const QString &msg); static QPointer m_instance; diff --git a/src/utils/Updater.cpp b/src/utils/Updater.cpp index 4e12c0d3..faf37327 100644 --- a/src/utils/Updater.cpp +++ b/src/utils/Updater.cpp @@ -4,10 +4,11 @@ #include "Updater.h" #include +#undef config #include +#include "utils/config.h" #include "config-feather.h" -#include "constants.h" #include "Utils.h" #include "utils/AsyncTask.h" #include "utils/networking.h" @@ -22,8 +23,12 @@ Updater::Updater(QObject *parent) : } void Updater::checkForUpdates() { - UtilsNetworking network{getNetworkTor()}; - QNetworkReply *reply = network.getJson("https://featherwallet.org/updates.json"); + UtilsNetworking network{this}; + QNetworkReply *reply = network.getJson(QString("%1/updates.json").arg(this->getWebsiteUrl())); + if (!reply) { + emit updateCheckFailed("Can't check for websites: offline mode enabled"); + return; + } connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onUpdateCheckResponse, this, reply)); } @@ -77,9 +82,10 @@ void Updater::wsUpdatesReceived(const QJsonObject &updates) { // Hooray! New update available - QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(constants::websiteUrl, newVersion); + QString hashesUrl = QString("%1/files/releases/hashes-%2-plain.txt").arg(this->getWebsiteUrl(), newVersion); + qDebug() << hashesUrl; - UtilsNetworking network{getNetworkTor()}; + UtilsNetworking network{this}; QNetworkReply *reply = network.get(hashesUrl); connect(reply, &QNetworkReply::finished, this, std::bind(&Updater::onSignedHashesReceived, this, reply, platformTag, newVersion)); @@ -115,7 +121,7 @@ void Updater::onSignedHashesReceived(QNetworkReply *reply, const QString &platfo this->state = Updater::State::UPDATE_AVAILABLE; this->version = version; this->binaryFilename = binaryFilename; - this->downloadUrl = QString("https://featherwallet.org/files/releases/%1/%2").arg(platformTag, binaryFilename); + this->downloadUrl = QString("%1/files/releases/%2/%3").arg(this->getWebsiteUrl(), platformTag, binaryFilename); this->hash = hash; this->signer = signer; this->platformTag = platformTag; @@ -154,6 +160,18 @@ QString Updater::getPlatformTag() { return ""; } +QString Updater::getWebsiteUrl() { + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + return "http://featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion"; + } + else if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + return "http://rwzulgcql2y3n6os2jhmhg6un2m33rylazfnzhf56likav47aylq.b32.i2p"; + } + else { + return "https://featherwallet.org"; + } +} + QByteArray Updater::verifyParseSignedHashes( const QByteArray &armoredSignedHashes, const QString &binaryFilename, diff --git a/src/utils/Updater.h b/src/utils/Updater.h index d280520f..634ccc6c 100644 --- a/src/utils/Updater.h +++ b/src/utils/Updater.h @@ -50,6 +50,7 @@ private: QString verifySignature(const epee::span data, const openpgp::signature_rsa &signature) const; void onSignedHashesReceived(QNetworkReply *reply, const QString &platformTag, const QString &version); QString getPlatformTag(); + QString getWebsiteUrl(); private: std::vector m_maintainers; diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index 6555d121..b7f454ae 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -464,9 +464,6 @@ void externalLinkWarning(QWidget *parent, const QString &url){ QString body = QString("You are about to open the following link:\n\n%1").arg(url); - if (!(TailsOS::detect() || WhonixOS::detect())) - body += "\n\nYou will NOT be using Tor."; - QMessageBox linkWarning(parent); linkWarning.setWindowTitle("External link warning"); linkWarning.setText(body); @@ -574,4 +571,9 @@ QFont relativeFont(int delta) { font.setPointSize(font.pointSize() + delta); return font; } + +bool isLocalUrl(const QUrl &url) { + QRegularExpression localNetwork(R"((^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.))"); + return (localNetwork.match(url.host()).hasMatch() || url.host() == "localhost"); +} } diff --git a/src/utils/Utils.h b/src/utils/Utils.h index 0739bf4e..dfbb50e0 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -66,6 +66,8 @@ namespace Utils QString getAccountName(); QFont relativeFont(int delta); + bool isLocalUrl(const QUrl &url); + template QString QtEnumToString (QEnum value) { return QString::fromStdString(std::string(QMetaEnum::fromType().valueToKey(value))); diff --git a/src/utils/WebsocketClient.cpp b/src/utils/WebsocketClient.cpp index aadf34a7..39142ed7 100644 --- a/src/utils/WebsocketClient.cpp +++ b/src/utils/WebsocketClient.cpp @@ -6,6 +6,8 @@ #include #include "utils/Utils.h" +#include "utils/config.h" + WebsocketClient::WebsocketClient(QObject *parent) : QObject(parent) { @@ -27,7 +29,7 @@ WebsocketClient::WebsocketClient(QObject *parent) connect(&m_connectionTimeout, &QTimer::timeout, this, &WebsocketClient::onConnectionTimeout); - m_websocketUrlIndex = QRandomGenerator::global()->bounded(constants::websocketUrls.length()); + m_websocketUrlIndex = QRandomGenerator::global()->bounded(m_websocketUrls[this->networkType()].length()); this->nextWebsocketUrl(); } @@ -42,10 +44,18 @@ void WebsocketClient::start() { return; } + if (config()->get(Config::offlineMode).toBool()) { + return; + } + + if (config()->get(Config::disableWebsocket).toBool()) { + return; + } + // connect & reconnect on errors/close - qDebug() << "WebSocket connect:" << m_url.url(); auto state = webSocket.state(); if (state != QAbstractSocket::ConnectedState && state != QAbstractSocket::ConnectingState) { + qDebug() << "WebSocket connect:" << m_url.url(); webSocket.open(m_url); } } @@ -90,8 +100,22 @@ void WebsocketClient::onError(QAbstractSocket::SocketError error) { } void WebsocketClient::nextWebsocketUrl() { - m_url = constants::websocketUrls[m_websocketUrlIndex]; - m_websocketUrlIndex = (m_websocketUrlIndex+1)%constants::websocketUrls.length(); + Config::Proxy networkType = this->networkType(); + m_websocketUrlIndex = (m_websocketUrlIndex+1)%m_websocketUrls[networkType].length(); + m_url = m_websocketUrls[networkType][m_websocketUrlIndex]; +} + +Config::Proxy WebsocketClient::networkType() { + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + // Websocket performance with onion services is abysmal, connect to clearnet server unless instructed otherwise + return Config::Proxy::Tor; + } + else if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + return Config::Proxy::i2p; + } + else { + return Config::Proxy::None; + } } void WebsocketClient::onConnectionTimeout() { diff --git a/src/utils/WebsocketClient.h b/src/utils/WebsocketClient.h index 1b88151c..2368253c 100644 --- a/src/utils/WebsocketClient.h +++ b/src/utils/WebsocketClient.h @@ -9,6 +9,7 @@ #include #include #include "constants.h" +#include "utils/config.h" class WebsocketClient : public QObject { Q_OBJECT @@ -20,6 +21,7 @@ public: void restart(); void stop(); void sendMsg(const QByteArray &data); + void nextWebsocketUrl(); QWebSocket webSocket; @@ -33,10 +35,25 @@ private slots: void onStateChanged(QAbstractSocket::SocketState state); void onbinaryMessageReceived(const QByteArray &message); void onError(QAbstractSocket::SocketError error); - void nextWebsocketUrl(); void onConnectionTimeout(); private: + Config::Proxy networkType(); + const QHash> m_websocketUrls = { + {Config::Proxy::None, { + QUrl(QStringLiteral("wss://ws.featherwallet.org/ws")), + QUrl(QStringLiteral("wss://ws.featherwallet.net/ws")) + }}, + {Config::Proxy::Tor, { + QUrl(QStringLiteral("ws://7e6egbawekbkxzkv4244pqeqgoo4axko2imgjbedwnn6s5yb6b7oliqd.onion/ws")), + QUrl(QStringLiteral("ws://an5ecwgzyujqe7jverkp42d22zhvjes2mrhvol6tpqcgfkzwseqrafqd.onion/ws")) + }}, + {Config::Proxy::i2p, { + QUrl(QStringLiteral("ws://hk5smvnkifjcm5346bs6cmnczwbiupr4jyiw3gz5z7ybaigp72fa.b32.i2p/ws")), + QUrl(QStringLiteral("ws://tr7iahturgfii64txw7cjhrfunmpg35w2lmmqmsa6i4jxwi7vplq.b32.i2p/ws")) + }} + }; + QUrl m_url; QTimer m_pingTimer; QTimer m_connectionTimeout; diff --git a/src/utils/config.cpp b/src/utils/config.cpp index 80095742..47c5a1c3 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -68,14 +68,18 @@ static const QHash configStrings = { {Config::balanceDisplay, {QS("balanceDisplay"), Config::BalanceDisplay::spendablePlusUnconfirmed}}, {Config::inactivityLockEnabled, {QS("inactivityLockEnabled"), false}}, {Config::inactivityLockTimeout, {QS("inactivityLockTimeout"), 10}}, + {Config::lockOnMinimize, {QS("lockOnMinimize"), false}}, {Config::disableWebsocket, {QS("disableWebsocket"), false}}, {Config::offlineMode, {QS("offlineMode"), false}}, {Config::multiBroadcast, {QS("multiBroadcast"), true}}, {Config::warnOnExternalLink,{QS("warnOnExternalLink"), true}}, {Config::hideBalance, {QS("hideBalance"), false}}, - {Config::hideNotifications, {QS("hideNotifications"), false}}, - {Config::disableLogging, {QS("disableLogging"), false}}, + {Config::hideNotifications, {QS("hideNotifications"), false}}, + {Config::hideUpdateNotifications, {QS("hideUpdateNotifications"), false}}, + {Config::disableLogging, {QS("disableLogging"), true}}, + {Config::writeStackTraceToDisk, {QS("writeStackTraceToDisk"), true}}, + {Config::writeRecentlyOpenedWallets, {QS("writeRecentlyOpenedWallets"), true}}, {Config::blockExplorer,{QS("blockExplorer"), "exploremonero.com"}}, {Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}}, @@ -86,11 +90,14 @@ static const QHash configStrings = { {Config::cryptoSymbols, {QS("cryptoSymbols"), QStringList{"BTC", "ETH", "LTC", "XMR", "ZEC"}}}, // Tor + {Config::proxy, {QS("proxy"), Config::Proxy::Tor}}, {Config::torPrivacyLevel, {QS("torPrivacyLevel"), 1}}, + {Config::torOnlyAllowOnion, {QS("torOnlyAllowOnion"), false}}, {Config::socks5Host, {QS("socks5Host"), "127.0.0.1"}}, {Config::socks5Port, {QS("socks5Port"), "9050"}}, {Config::socks5User, {QS("socks5User"), ""}}, // Unused {Config::socks5Pass, {QS("socks5Pass"), ""}}, // Unused + {Config::torManagedPort, {QS("torManagedPort"), "19450"}}, {Config::useLocalTor, {QS("useLocalTor"), false}}, {Config::initSyncThreshold, {QS("initSyncThreshold"), 360}} }; diff --git a/src/utils/config.h b/src/utils/config.h index baa7f9d2..ff7d208f 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -24,7 +24,6 @@ public: firstRun, warnOnStagenet, warnOnTestnet, - logLevel, homeWidget, donateBeg, @@ -64,39 +63,61 @@ public: // Settings lastSettingsPage, - preferredFiatCurrency, + + // Appearance skin, amountPrecision, dateFormat, timeFormat, balanceDisplay, - inactivityLockEnabled, - inactivityLockTimeout, + preferredFiatCurrency, + + // Network -> Proxy + proxy, + socks5Host, + socks5Port, + socks5User, + socks5Pass, + useLocalTor, // Prevents Feather from starting bundled Tor daemon + torOnlyAllowOnion, + torPrivacyLevel, // Tor node network traffic strategy + torManagedPort, // Port for managed Tor daemon + initSyncThreshold, // Switch to Tor after initial sync threshold blocks + + // Network -> Websocket disableWebsocket, + + // Network -> Offline offlineMode, - multiBroadcast, - warnOnExternalLink, - hideBalance, + // Storage -> Logging + writeStackTraceToDisk, disableLogging, + logLevel, + + // Storage -> Misc + writeRecentlyOpenedWallets, + + // Display + hideBalance, + hideUpdateNotifications, hideNotifications, + warnOnExternalLink, + inactivityLockEnabled, + inactivityLockTimeout, + lockOnMinimize, + + // Transactions + multiBroadcast, + // Misc blockExplorer, redditFrontend, localMoneroFrontend, - bountiesFrontend, + bountiesFrontend, // unused fiatSymbols, cryptoSymbols, - - // Tor - torPrivacyLevel, - socks5Host, - socks5Port, - socks5User, - socks5Pass, - useLocalTor, // Prevents Feather from starting bundled Tor daemon - initSyncThreshold }; enum PrivacyLevel { @@ -116,6 +137,13 @@ public: Solo }; + enum Proxy { + None = 0, + Tor, + i2p, + socks5 + }; + ~Config() override; QVariant get(ConfigKey key); QString getFileName(); diff --git a/src/utils/daemonrpc.cpp b/src/utils/daemonrpc.cpp index b80aed9a..84145634 100644 --- a/src/utils/daemonrpc.cpp +++ b/src/utils/daemonrpc.cpp @@ -3,9 +3,9 @@ #include "daemonrpc.h" -DaemonRpc::DaemonRpc(QObject *parent, QNetworkAccessManager *network, QString daemonAddress) +DaemonRpc::DaemonRpc(QObject *parent, QString daemonAddress) : QObject(parent) - , m_network(new UtilsNetworking(network, this)) + , m_network(new UtilsNetworking(this)) , m_daemonAddress(std::move(daemonAddress)) { } @@ -18,7 +18,9 @@ void DaemonRpc::sendRawTransaction(const QString &tx_as_hex, bool do_not_relay, QString url = QString("%1/send_raw_transaction").arg(m_daemonAddress); QNetworkReply *reply = m_network->postJson(url, req); - connect(reply, &QNetworkReply::finished, std::bind(&DaemonRpc::onResponse, this, reply, Endpoint::SEND_RAW_TRANSACTION)); + connect(reply, &QNetworkReply::finished, [this, reply]{ + onResponse(reply, Endpoint::SEND_RAW_TRANSACTION); + }); } void DaemonRpc::getTransactions(const QStringList &txs_hashes, bool decode_as_json, bool prune) { @@ -29,12 +31,19 @@ void DaemonRpc::getTransactions(const QStringList &txs_hashes, bool decode_as_js QString url = QString("%1/get_transactions").arg(m_daemonAddress); QNetworkReply *reply = m_network->postJson(url, req); - connect(reply, &QNetworkReply::finished, std::bind(&DaemonRpc::onResponse, this, reply, Endpoint::GET_TRANSACTIONS)); + connect(reply, &QNetworkReply::finished, [this, reply]{ + onResponse(reply, Endpoint::GET_TRANSACTIONS); + }); } void DaemonRpc::onResponse(QNetworkReply *reply, Endpoint endpoint) { - const auto ok = reply->error() == QNetworkReply::NoError; - const auto err = reply->errorString(); + if (!reply) { + emit ApiResponse(DaemonResponse(false, endpoint, "Offline mode")); + return; + } + + bool ok = reply->error() == QNetworkReply::NoError; + QString err = reply->errorString(); QByteArray data = reply->readAll(); reply->deleteLater(); @@ -93,7 +102,3 @@ QString DaemonRpc::onSendRawTransactionFailed(const QJsonObject &obj) { void DaemonRpc::setDaemonAddress(const QString &daemonAddress) { m_daemonAddress = daemonAddress; } - -void DaemonRpc::setNetwork(QNetworkAccessManager *network) { - m_network = new UtilsNetworking(network, this); -} \ No newline at end of file diff --git a/src/utils/daemonrpc.h b/src/utils/daemonrpc.h index 01f1a62c..496ce68a 100644 --- a/src/utils/daemonrpc.h +++ b/src/utils/daemonrpc.h @@ -27,13 +27,12 @@ public: QJsonObject obj; }; - explicit DaemonRpc(QObject *parent, QNetworkAccessManager *network, QString daemonAddress); + explicit DaemonRpc(QObject *parent, QString daemonAddress); void sendRawTransaction(const QString &tx_as_hex, bool do_not_relay = false, bool do_sanity_checks = true); void getTransactions(const QStringList &txs_hashes, bool decode_as_json = false, bool prune = false); void setDaemonAddress(const QString &daemonAddress); - void setNetwork(QNetworkAccessManager *network); signals: void ApiResponse(DaemonResponse resp); diff --git a/src/utils/networking.cpp b/src/utils/networking.cpp index e2a5cda2..4c7293bf 100644 --- a/src/utils/networking.cpp +++ b/src/utils/networking.cpp @@ -6,11 +6,11 @@ #include "utils/Utils.h" #include "utils/networking.h" +#include "utils/NetworkManager.h" #include "config.h" -UtilsNetworking::UtilsNetworking(QNetworkAccessManager *networkAccessManager, QObject *parent) - : QObject(parent) - , m_networkAccessManager(networkAccessManager) {} +UtilsNetworking::UtilsNetworking(QObject *parent) + : QObject(parent) {} void UtilsNetworking::setUserAgent(const QString &userAgent) { this->m_userAgent = userAgent; @@ -21,6 +21,8 @@ QNetworkReply* UtilsNetworking::get(const QString &url) { return nullptr; } + m_networkAccessManager = getNetwork(url); + QNetworkRequest request; request.setUrl(QUrl(url)); request.setRawHeader("User-Agent", m_userAgent.toUtf8()); @@ -33,6 +35,8 @@ QNetworkReply* UtilsNetworking::getJson(const QString &url) { return nullptr; } + m_networkAccessManager = getNetwork(url); + QNetworkRequest request; request.setUrl(QUrl(url)); request.setRawHeader("User-Agent", m_userAgent.toUtf8()); @@ -46,6 +50,8 @@ QNetworkReply* UtilsNetworking::postJson(const QString &url, const QJsonObject & return nullptr; } + m_networkAccessManager = getNetwork(url); + QNetworkRequest request; request.setUrl(QUrl(url)); request.setRawHeader("User-Agent", m_userAgent.toUtf8()); diff --git a/src/utils/networking.h b/src/utils/networking.h index c140cd0b..bc33a0cb 100644 --- a/src/utils/networking.h +++ b/src/utils/networking.h @@ -16,7 +16,7 @@ class UtilsNetworking : public QObject Q_OBJECT public: - explicit UtilsNetworking(QNetworkAccessManager *networkAccessManager, QObject *parent = nullptr); + explicit UtilsNetworking(QObject *parent = nullptr); QNetworkReply* get(const QString &url); QNetworkReply* getJson(const QString &url); @@ -24,7 +24,7 @@ public: void setUserAgent(const QString &userAgent); private: - QString m_userAgent = "Mozilla/5.0 (Windows NT 10.0; rv:68.0) Gecko/20100101 Firefox/68.0"; + QString m_userAgent = "Mozilla/5.0 (Windows NT 10.0; rv:102.0) Gecko/20100101 Firefox/102.0"; QNetworkAccessManager *m_networkAccessManager; }; diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index a31fb856..b1f77278 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -94,18 +94,23 @@ void NodeList::ensureStructure(QJsonObject &obj, NetworkType::Type networkType) obj[networkTypeStr] = netTypeObj; } -Nodes::Nodes(AppContext *ctx, QObject *parent) +Nodes::Nodes(QObject *parent) : QObject(parent) , modelWebsocket(new NodeModel(NodeSource::websocket, this)) , modelCustom(new NodeModel(NodeSource::custom, this)) - , m_ctx(ctx) , m_connection(FeatherNode()) { + // TODO: This class is in desperate need of refactoring + this->loadConfig(); - connect(m_ctx, &AppContext::walletRefreshed, this, &Nodes::onWalletRefreshed); connect(websocketNotifier(), &WebsocketNotifier::NodesReceived, this, &Nodes::onWSNodesReceived); } +void Nodes::setContext(AppContext *ctx) { + m_ctx = ctx; + connect(m_ctx, &AppContext::walletRefreshed, this, &Nodes::onWalletRefreshed); +} + void Nodes::loadConfig() { QStringList customNodes = m_nodes.getNodes(constants::networkType, NodeList::custom); for (const auto &node : customNodes) { @@ -165,6 +170,9 @@ void Nodes::loadConfig() { for (const auto &node : nodes_json[netKey].toObject()["clearnet"].toArray()) { nodes_list.append(node); } + for (const auto &node : nodes_json[netKey].toObject()["i2p"].toArray()) { + nodes_list.append(node); + } for (auto node: nodes_list) { FeatherNode wsNode(node.toString()); @@ -183,30 +191,40 @@ void Nodes::loadConfig() { void Nodes::connectToNode() { // auto connect - m_wsExhaustedWarningEmitted = false; - m_customExhaustedWarningEmitted = false; this->autoConnect(true); } void Nodes::connectToNode(const FeatherNode &node) { - if (!node.isValid()) + if (!m_ctx) { return; + } + + if (!node.isValid()) { + return; + } if (config()->get(Config::offlineMode).toBool()) { return; } - qInfo() << QString("Attempting to connect to %1 (%2)").arg(node.toAddress()).arg(node.custom ? "custom" : "ws"); + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + if (!node.isOnion()) { + return; + } + } + + qInfo() << QString("Attempting to connect to %1 (%2)").arg(node.toAddress(), node.custom ? "custom" : "ws"); - if (!node.url.userName().isEmpty() && !node.url.password().isEmpty()) + if (!node.url.userName().isEmpty() && !node.url.password().isEmpty()) { m_ctx->wallet->setDaemonLogin(node.url.userName(), node.url.password()); + } - // Don't use SSL over Tor - m_ctx->wallet->setUseSSL(!node.isOnion()); + // Don't use SSL over Tor/i2p + m_ctx->wallet->setUseSSL(!node.isAnonymityNetwork()); QString proxyAddress; - if (useTorProxy(node)) { - if (!torManager()->isLocalTor()) { + if (useSocks5Proxy(node)) { + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { proxyAddress = QString("%1:%2").arg(torManager()->featherTorHost, QString::number(torManager()->featherTorPort)); } else { proxyAddress = QString("%1:%2").arg(config()->get(Config::socks5Host).toString(), @@ -225,6 +243,10 @@ void Nodes::connectToNode(const FeatherNode &node) { } void Nodes::autoConnect(bool forceReconnect) { + if (!m_ctx) { + return; + } + // this function is responsible for automatically connecting to a daemon. if (m_ctx->wallet == nullptr || !m_enableAutoconnect) { return; @@ -234,7 +256,7 @@ void Nodes::autoConnect(bool forceReconnect) { bool wsMode = (this->source() == NodeSource::websocket); if (wsMode && !m_wsNodesReceived && websocketNodes().count() == 0) { - // this situation should rarely occur due to the usage of the websocket node cache on startup. + // this situation should rarely onneccur due to the usage of the websocket node cache on startup. qInfo() << "Feather is in websocket connection mode but was not able to receive any nodes (yet)."; return; } @@ -244,7 +266,7 @@ void Nodes::autoConnect(bool forceReconnect) { m_recentFailures << m_connection.toAddress(); } - // try a connect + // try connect FeatherNode node = this->pickEligibleNode(); this->connectToNode(node); return; @@ -257,8 +279,6 @@ void Nodes::autoConnect(bool forceReconnect) { m_connection.isActive = true; // reset node exhaustion state - m_wsExhaustedWarningEmitted = false; - m_customExhaustedWarningEmitted = false; m_recentFailures.clear(); } @@ -374,7 +394,7 @@ void Nodes::setCustomNodes(const QList &nodes) { } void Nodes::onWalletRefreshed() { - if (config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptInitSync) { + if (config()->get(Config::proxy) == Config::Proxy::Tor && config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptInitSync) { // Don't reconnect if we're connected to a local node (traffic will not be routed through Tor) if (m_connection.isLocal()) return; @@ -388,20 +408,29 @@ void Nodes::onWalletRefreshed() { } bool Nodes::useOnionNodes() { + if (config()->get(Config::proxy) != Config::Proxy::Tor) { + return false; + } + + if (config()->get(Config::torOnlyAllowOnion).toBool()) { + return true; + } + auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); if (privacyLevel == Config::allTor) { return true; } if (privacyLevel == Config::allTorExceptInitSync) { - if (m_ctx->refreshed) + if (m_ctx && m_ctx->refreshed) { return true; + } if (appData()->heights.contains(constants::networkType)) { int initSyncThreshold = config()->get(Config::initSyncThreshold).toInt(); int networkHeight = appData()->heights[constants::networkType]; - if (m_ctx->wallet->blockChainHeight() > (networkHeight - initSyncThreshold)) { + if (m_ctx && m_ctx->wallet->blockChainHeight() > (networkHeight - initSyncThreshold)) { return true; } } @@ -410,7 +439,15 @@ bool Nodes::useOnionNodes() { return false; } -bool Nodes::useTorProxy(const FeatherNode &node) { +bool Nodes::useI2PNodes() { + if (config()->get(Config::proxy) == Config::Proxy::i2p) { + return true; + } + + return false; +} + +bool Nodes::useSocks5Proxy(const FeatherNode &node) { if (node.isLocal()) { return false; } @@ -423,7 +460,15 @@ bool Nodes::useTorProxy(const FeatherNode &node) { return true; } - return this->useOnionNodes(); + if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor) { + return this->useOnionNodes(); + } + + if (config()->get(Config::proxy).toInt() != Config::Proxy::None) { + return true; + } + + return false; } void Nodes::updateModels() { @@ -450,28 +495,7 @@ void Nodes::resetLocalState() { } void Nodes::exhausted() { - if (this->source() == NodeSource::websocket) - this->WSNodeExhaustedWarning(); - else - this->nodeExhaustedWarning(); -} - -void Nodes::nodeExhaustedWarning(){ - if (m_customExhaustedWarningEmitted) - return; - - emit nodeExhausted(); - qWarning() << "Could not find an eligible custom node to connect to."; - m_customExhaustedWarningEmitted = true; -} - -void Nodes::WSNodeExhaustedWarning() { - if (m_wsExhaustedWarningEmitted) - return; - - emit WSNodeExhausted(); - qWarning() << "Could not find an eligible websocket node to connect to."; - m_wsExhaustedWarningEmitted = true; + // Do nothing } QList Nodes::nodes() { @@ -485,6 +509,7 @@ QList Nodes::customNodes() { QList Nodes::websocketNodes() { bool onionNode = this->useOnionNodes(); + bool i2pNode = this->useI2PNodes(); QList nodes; for (const auto &node : m_websocketNodes) { @@ -492,20 +517,24 @@ QList Nodes::websocketNodes() { continue; } + if (i2pNode && !node.isI2P()) { + continue; + } + if (!onionNode && node.isOnion()) { continue; } + if (!i2pNode && node.isI2P()) { + continue; + } + nodes.push_back(node); } return nodes; } -void Nodes::onTorSettingsChanged() { - this->autoConnect(true); -} - FeatherNode Nodes::connection() { return m_connection; } diff --git a/src/utils/nodes.h b/src/utils/nodes.h index ebad101b..ed261232 100644 --- a/src/utils/nodes.h +++ b/src/utils/nodes.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include "model/NodeModel.h" @@ -83,6 +82,14 @@ struct FeatherNode { return url.host().endsWith(".onion"); } + bool isI2P() const { + return url.host().endsWith(".i2p"); + } + + bool isAnonymityNetwork() const { + return isOnion() || isI2P(); + }; + QString toAddress() const { return QString("%1:%2").arg(url.host(), QString::number(url.port())); } @@ -111,7 +118,8 @@ class Nodes : public QObject { Q_OBJECT public: - explicit Nodes(AppContext *ctx, QObject *parent = nullptr); + explicit Nodes(QObject *parent = nullptr); + void setContext(AppContext *ctx); void loadConfig(); NodeSource source(); @@ -132,17 +140,11 @@ public slots: void setCustomNodes(const QList& nodes); void autoConnect(bool forceReconnect = false); - void onTorSettingsChanged(); - -signals: - void WSNodeExhausted(); - void nodeExhausted(); - private slots: void onWalletRefreshed(); private: - AppContext *m_ctx; + AppContext *m_ctx = nullptr; QJsonObject m_configJson; NodeList m_nodes; @@ -155,20 +157,17 @@ private: FeatherNode m_connection; // current active connection, if any bool m_wsNodesReceived = false; - bool m_wsExhaustedWarningEmitted = true; - bool m_customExhaustedWarningEmitted = true; bool m_enableAutoconnect = true; FeatherNode pickEligibleNode(); bool useOnionNodes(); - bool useTorProxy(const FeatherNode &node); + bool useI2PNodes(); + bool useSocks5Proxy(const FeatherNode &node); void updateModels(); void resetLocalState(); void exhausted(); - void WSNodeExhaustedWarning(); - void nodeExhaustedWarning(); int modeHeight(const QList &nodes); }; diff --git a/src/utils/os/Prestium.cpp b/src/utils/os/Prestium.cpp new file mode 100644 index 00000000..f1ef7919 --- /dev/null +++ b/src/utils/os/Prestium.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "Prestium.h" + +#include +#include + +#include "utils/Utils.h" + +bool Prestium::detect() +{ + // Temporary detect + if (Utils::fileExists("/etc/hostname")) { + QByteArray data = Utils::fileOpen("/etc/hostname"); + if (data == "prestium\n") { + return true; + } + } + + return QSysInfo::prettyProductName().contains("Prestium"); +} \ No newline at end of file diff --git a/src/utils/os/Prestium.h b/src/utils/os/Prestium.h new file mode 100644 index 00000000..879c9a3d --- /dev/null +++ b/src/utils/os/Prestium.h @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_PRESTIUM_H +#define FEATHER_PRESTIUM_H + + +class Prestium { +public: + static bool detect(); +}; + +#endif //FEATHER_PRESTIUM_H diff --git a/src/widgets/LocalMoneroWidget.cpp b/src/widgets/LocalMoneroWidget.cpp index d5938795..6865b13b 100644 --- a/src/widgets/LocalMoneroWidget.cpp +++ b/src/widgets/LocalMoneroWidget.cpp @@ -25,7 +25,7 @@ LocalMoneroWidget::LocalMoneroWidget(QWidget *parent, QSharedPointer ui->combo_currency->addItem(config()->get(Config::preferredFiatCurrency).toString()); - m_network = new UtilsNetworking(getNetworkTor(), this); + m_network = new UtilsNetworking(this); m_api = new LocalMoneroApi(this, m_network); m_model = new LocalMoneroModel(this); diff --git a/src/widgets/NetworkProxyWidget.cpp b/src/widgets/NetworkProxyWidget.cpp new file mode 100644 index 00000000..9a354e83 --- /dev/null +++ b/src/widgets/NetworkProxyWidget.cpp @@ -0,0 +1,126 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "NetworkProxyWidget.h" +#include "ui_NetworkProxyWidget.h" + +#include +#include +#include + +#include "utils/config.h" +#include "utils/os/Prestium.h" + +NetworkProxyWidget::NetworkProxyWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::NetworkProxyWidget) + , m_torInfoDialog(new TorInfoDialog(this)) +{ + ui->setupUi(this); + + ui->comboBox_proxy->setCurrentIndex(config()->get(Config::proxy).toInt()); + connect(ui->comboBox_proxy, &QComboBox::currentIndexChanged, [this](int index){ + this->onProxySettingsChanged(); + ui->frame_proxy->setVisible(index != Config::Proxy::None); + ui->groupBox_proxySettings->setTitle(QString("%1 settings").arg(ui->comboBox_proxy->currentText())); + ui->frame_tor->setVisible(index == Config::Proxy::Tor); + this->updatePort(); + }); + + int proxy = config()->get(Config::proxy).toInt(); + ui->frame_proxy->setVisible(proxy != Config::Proxy::None); + ui->frame_tor->setVisible(proxy == Config::Proxy::Tor); + ui->groupBox_proxySettings->setTitle(QString("%1 settings").arg(ui->comboBox_proxy->currentText())); + + // [Host] + connect(ui->line_host, &QLineEdit::textChanged, this, &NetworkProxyWidget::onProxySettingsChanged); + + // [Port] + auto *portValidator = new QRegularExpressionValidator{QRegularExpression("[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]")}; + ui->line_port->setValidator(portValidator); + ui->line_port->setText(config()->get(Config::socks5Port).toString()); + connect(ui->line_port, &QLineEdit::textChanged, this, &NetworkProxyWidget::onProxySettingsChanged); + + // [Tor settings] + // [Let Feather start and manage a Tor daemon] +#if !defined(HAS_TOR_BIN) && !defined(PLATFORM_INSTALLER) + ui->checkBox_torManaged->setChecked(false); + ui->checkBox_torManaged->setEnabled(false); + ui->checkBox_torManaged->setToolTip("Feather was bundled without Tor"); +#else + ui->checkBox_torManaged->setChecked(!config()->get(Config::useLocalTor).toBool()); + connect(ui->checkBox_torManaged, &QCheckBox::toggled, [this](bool toggled){ + this->updatePort(); + this->onProxySettingsChanged(); + if (!m_disableTorLogs) { + ui->frame_torShowLogs->setVisible(toggled); + } + }); +#endif + + // [Only allow connections to onion services] + ui->checkBox_torOnlyAllowOnion->setChecked(config()->get(Config::torOnlyAllowOnion).toBool()); + connect(ui->checkBox_torOnlyAllowOnion, &QCheckBox::toggled, this, &NetworkProxyWidget::onProxySettingsChanged); + + // [Node traffic] + ui->comboBox_torNodeTraffic->setCurrentIndex(config()->get(Config::torPrivacyLevel).toInt()); + connect(ui->comboBox_torNodeTraffic, &QComboBox::currentIndexChanged, this, &NetworkProxyWidget::onProxySettingsChanged); + + // [Show Tor logs] + ui->frame_torShowLogs->setVisible(!config()->get(Config::useLocalTor).toBool()); +#if !defined(HAS_TOR_BIN) && !defined(PLATFORM_INSTALLER) + ui->frame_torShowLogs->setVisible(false); +#endif + connect(ui->btn_torShowLogs, &QPushButton::clicked, [this]{ + m_torInfoDialog->show(); + }); + + ui->frame_notice->hide(); +} + +void NetworkProxyWidget::onProxySettingsChanged() { + if (!m_proxySettingsChanged) { + emit proxySettingsChanged(); + } + + m_proxySettingsChanged = true; +} + +void NetworkProxyWidget::updatePort() { + int socks5port; + switch (ui->comboBox_proxy->currentIndex()) { + case Config::Proxy::Tor: { + socks5port = 9050; + break; + } + case Config::Proxy::i2p: { + if (Prestium::detect()) { + socks5port = 4448; + } else { + socks5port = 4447; + } + break; + } + default: { + socks5port = 9050; + } + } + ui->line_port->setText(QString::number(socks5port)); +} + +void NetworkProxyWidget::setProxySettings() { + config()->set(Config::proxy, ui->comboBox_proxy->currentIndex()); + config()->set(Config::socks5Host, ui->line_host->text()); + config()->set(Config::socks5Port, ui->line_port->text()); + config()->set(Config::useLocalTor, !ui->checkBox_torManaged->isChecked()); + config()->set(Config::torOnlyAllowOnion, ui->checkBox_torOnlyAllowOnion->isChecked()); + config()->set(Config::torPrivacyLevel, ui->comboBox_torNodeTraffic->currentIndex()); + m_proxySettingsChanged = false; +} + +void NetworkProxyWidget::setDisableTorLogs() { + m_disableTorLogs = true; + ui->frame_torShowLogs->hide(); +} + +NetworkProxyWidget::~NetworkProxyWidget() = default; diff --git a/src/widgets/NetworkProxyWidget.h b/src/widgets/NetworkProxyWidget.h new file mode 100644 index 00000000..4b1b4796 --- /dev/null +++ b/src/widgets/NetworkProxyWidget.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_NETWORKPROXYWIDGET_H +#define FEATHER_NETWORKPROXYWIDGET_H + +#include +#include + +#include "dialog/TorInfoDialog.h" + +namespace Ui { + class NetworkProxyWidget; +} + +class NetworkProxyWidget : public QWidget +{ +Q_OBJECT + +public: + explicit NetworkProxyWidget(QWidget *parent = nullptr); + ~NetworkProxyWidget() override; + + void setProxySettings(); + bool isProxySettingsChanged() { + return m_proxySettingsChanged; + }; + void setDisableTorLogs(); + +signals: + void proxySettingsChanged(); + +private: + void onProxySettingsChanged(); + void updatePort(); + + QScopedPointer ui; + TorInfoDialog *m_torInfoDialog; + + bool m_disableTorLogs = false; + bool m_proxySettingsChanged = false; +}; + + +#endif //FEATHER_NETWORKPROXYWIDGET_H diff --git a/src/widgets/NetworkProxyWidget.ui b/src/widgets/NetworkProxyWidget.ui new file mode 100644 index 00000000..def52dae --- /dev/null +++ b/src/widgets/NetworkProxyWidget.ui @@ -0,0 +1,293 @@ + + + NetworkProxyWidget + + + + 0 + 0 + 649 + 382 + + + + Form + + + + + + + + + 0 + 0 + + + + Proxy: + + + + + + + + None + + + + + Tor + + + + + i2p + + + + + socks5 + + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Tor settings + + + + + + + + Socks5 Host: + + + + + + + 127.0.0.1 + + + 127.0.0.1 + + + + + + + Socks5 Port: + + + + + + + 9050 + + + 9050 + + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Let Feather start and manage a Tor daemon + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + + 0 + 0 + + + + + + + + Show status + + + + + + + + + + + + Only allow connections to onion services + + + + + + + + + + 0 + 0 + + + + Node traffic: + + + + + + + + Never over Tor + + + + + Switch to Tor after initial synchronization + + + + + Always over Tor + + + + + + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + Feather is connected to a local node. Proxy settings ignored for node traffic. + + + true + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + diff --git a/src/widgets/NodeWidget.cpp b/src/widgets/NodeWidget.cpp index 374353c9..ab4d8c83 100644 --- a/src/widgets/NodeWidget.cpp +++ b/src/widgets/NodeWidget.cpp @@ -8,7 +8,6 @@ #include #include #include -#include #include #include "model/NodeModel.h" @@ -20,39 +19,43 @@ NodeWidget::NodeWidget(QWidget *parent) { ui->setupUi(this); - connect(ui->btn_add_custom, &QPushButton::clicked, this, &NodeWidget::onCustomAddClicked); + connect(ui->btn_addCustomNodes, &QPushButton::clicked, this, &NodeWidget::onCustomAddClicked); - ui->nodeBtnGroup->setId(ui->radioButton_websocket, NodeSource::websocket); - ui->nodeBtnGroup->setId(ui->radioButton_custom, NodeSource::custom); - - connect(ui->nodeBtnGroup, &QButtonGroup::idClicked, [this](int id){ - config()->set(Config::nodeSource, id); - emit nodeSourceChanged(static_cast(id)); + connect(ui->checkBox_websocketList, &QCheckBox::stateChanged, [this](int id){ + bool custom = (id == 0); + ui->stackedWidget->setCurrentIndex(custom); + ui->frame_addCustomNodes->setVisible(custom); + config()->set(Config::nodeSource, custom); + emit nodeSourceChanged(static_cast(custom)); }); m_contextActionRemove = new QAction("Remove", this); - m_contextActionConnect = new QAction(icons()->icon("connect.svg"), "Connect to node", this); - m_contextActionOpenStatusURL = new QAction(icons()->icon("network.png"), "Visit status page", this); - m_contextActionCopy = new QAction(icons()->icon("copy.png"), "Copy", this); + m_contextActionConnect = new QAction("Connect to node", this); + m_contextActionOpenStatusURL = new QAction("Visit status page", this); + m_contextActionCopy = new QAction("Copy", this); connect(m_contextActionConnect, &QAction::triggered, this, &NodeWidget::onContextConnect); connect(m_contextActionRemove, &QAction::triggered, this, &NodeWidget::onContextCustomNodeRemove); connect(m_contextActionOpenStatusURL, &QAction::triggered, this, &NodeWidget::onContextStatusURL); connect(m_contextActionCopy, &QAction::triggered, this, &NodeWidget::onContextNodeCopy); connect(m_contextActionRemove, &QAction::triggered, this, &NodeWidget::onContextCustomNodeRemove); - ui->wsView->setContextMenuPolicy(Qt::CustomContextMenu); - ui->customView->setContextMenuPolicy(Qt::CustomContextMenu); - connect(ui->wsView, &QTreeView::customContextMenuRequested, this, &NodeWidget::onShowWSContextMenu); - connect(ui->customView, &QTreeView::customContextMenuRequested, this, &NodeWidget::onShowCustomContextMenu); + ui->treeView_websocket->setContextMenuPolicy(Qt::CustomContextMenu); + ui->treeView_custom->setContextMenuPolicy(Qt::CustomContextMenu); + connect(ui->treeView_websocket, &QTreeView::customContextMenuRequested, this, &NodeWidget::onShowWSContextMenu); + connect(ui->treeView_custom, &QTreeView::customContextMenuRequested, this, &NodeWidget::onShowCustomContextMenu); + + connect(ui->treeView_websocket, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); + connect(ui->treeView_custom, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); - connect(ui->customView, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); - connect(ui->wsView, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); + int index = config()->get(Config::nodeSource).toInt(); + ui->stackedWidget->setCurrentIndex(config()->get(Config::nodeSource).toInt()); + ui->frame_addCustomNodes->setVisible(index); this->onWebsocketStatusChanged(); } void NodeWidget::onShowWSContextMenu(const QPoint &pos) { - m_activeView = ui->wsView; + m_activeView = ui->treeView_websocket; FeatherNode node = this->selectedNode(); if (node.toAddress().isEmpty()) return; @@ -60,7 +63,7 @@ void NodeWidget::onShowWSContextMenu(const QPoint &pos) { } void NodeWidget::onShowCustomContextMenu(const QPoint &pos) { - m_activeView = ui->customView; + m_activeView = ui->treeView_custom; FeatherNode node = this->selectedNode(); if (node.toAddress().isEmpty()) return; @@ -69,33 +72,32 @@ void NodeWidget::onShowCustomContextMenu(const QPoint &pos) { void NodeWidget::onWebsocketStatusChanged() { bool disabled = config()->get(Config::disableWebsocket).toBool() || config()->get(Config::offlineMode).toBool(); - QString labelText = disabled ? "From cached list" : "From websocket (recommended)"; - ui->radioButton_websocket->setText(labelText); - ui->wsView->setColumnHidden(1, disabled); + ui->treeView_websocket->setColumnHidden(1, disabled); } void NodeWidget::showContextMenu(const QPoint &pos, const FeatherNode &node) { QMenu menu(this); - if (!node.isActive) { + if (m_canConnect && !node.isActive) { menu.addAction(m_contextActionConnect); } menu.addAction(m_contextActionOpenStatusURL); menu.addAction(m_contextActionCopy); - if (m_activeView == ui->customView) + if (m_activeView == ui->treeView_custom) { menu.addAction(m_contextActionRemove); + } menu.exec(m_activeView->viewport()->mapToGlobal(pos)); } void NodeWidget::onContextConnect() { QObject *obj = sender(); - if (obj == ui->customView) - m_activeView = ui->customView; + if (obj == ui->treeView_custom) + m_activeView = ui->treeView_custom; else - m_activeView = ui->wsView; + m_activeView = ui->treeView_websocket; FeatherNode node = this->selectedNode(); if (!node.toAddress().isEmpty()) @@ -118,7 +120,7 @@ FeatherNode NodeWidget::selectedNode() { if (!index.isValid()) return FeatherNode(); FeatherNode node; - if (m_activeView == ui->customView) { + if (m_activeView == ui->treeView_custom) { node = m_customModel->node(index.row()); } else { node = m_wsModel->node(index.row()); @@ -127,21 +129,23 @@ FeatherNode NodeWidget::selectedNode() { } void NodeWidget::onContextCustomNodeRemove() { - QModelIndex index = ui->customView->currentIndex(); - if (!index.isValid()) return; + QModelIndex index = ui->treeView_custom->currentIndex(); + if (!index.isValid()) { + return; + } FeatherNode node = m_customModel->node(index.row()); - auto nodes = m_ctx->nodes->customNodes(); + auto nodes = m_nodes->customNodes(); QMutableListIterator i(nodes); while (i.hasNext()) if (i.next() == node) i.remove(); - m_ctx->nodes->setCustomNodes(nodes); + m_nodes->setCustomNodes(nodes); } void NodeWidget::onCustomAddClicked(){ - auto currentNodes = m_ctx->nodes->customNodes(); + auto currentNodes = m_nodes->customNodes(); QString currentNodesText; for (auto &entry: currentNodes) { @@ -149,51 +153,52 @@ void NodeWidget::onCustomAddClicked(){ } bool ok; - QString text = QInputDialog::getMultiLineText(this, "Add custom node(s).", "E.g: user:password@127.0.0.1:18081", currentNodesText, &ok); - if (!ok || text.isEmpty()) + QString text = QInputDialog::getMultiLineText(this, "Add custom node(s).", "One node per line\nE.g: user:password@127.0.0.1:18081", currentNodesText, &ok); + if (!ok || text.isEmpty()) { return; + } QList nodesList; auto newNodesList = text.split("\n"); for (auto &newNodeText: newNodesList) { newNodeText = newNodeText.replace("\r", "").trimmed(); - if (newNodeText.isEmpty()) + if (newNodeText.isEmpty()) { continue; + } auto node = FeatherNode(newNodeText); node.custom = true; nodesList.append(node); } - m_ctx->nodes->setCustomNodes(nodesList); + m_nodes->setCustomNodes(nodesList); } -void NodeWidget::setupUI(QSharedPointer ctx) { - m_ctx = ctx; - - auto nodeSource = m_ctx->nodes->source(); +void NodeWidget::setupUI(Nodes *nodes) { + m_nodes = nodes; + + auto nodeSource = m_nodes->source(); + ui->checkBox_websocketList->setChecked(nodeSource == NodeSource::websocket); - if(nodeSource == NodeSource::websocket){ - ui->radioButton_websocket->setChecked(true); - } else if(nodeSource == NodeSource::custom) { - ui->radioButton_custom->setChecked(true); - } + this->setWSModel(m_nodes->modelWebsocket); + this->setCustomModel(m_nodes->modelCustom); +} - this->setWSModel(m_ctx->nodes->modelWebsocket); - this->setCustomModel(m_ctx->nodes->modelCustom); +void NodeWidget::setCanConnect(bool canConnect) { + m_canConnect = canConnect; } void NodeWidget::setWSModel(NodeModel *model) { m_wsModel = model; - ui->wsView->setModel(m_wsModel); - ui->wsView->header()->setSectionResizeMode(NodeModel::URL, QHeaderView::Stretch); - ui->wsView->header()->setSectionResizeMode(NodeModel::Height, QHeaderView::ResizeToContents); + ui->treeView_websocket->setModel(m_wsModel); + ui->treeView_websocket->header()->setSectionResizeMode(NodeModel::URL, QHeaderView::Stretch); + ui->treeView_websocket->header()->setSectionResizeMode(NodeModel::Height, QHeaderView::ResizeToContents); } void NodeWidget::setCustomModel(NodeModel *model) { m_customModel = model; - ui->customView->setModel(m_customModel); - ui->customView->header()->setSectionResizeMode(NodeModel::URL, QHeaderView::Stretch); + ui->treeView_custom->setModel(m_customModel); + ui->treeView_custom->header()->setSectionResizeMode(NodeModel::URL, QHeaderView::Stretch); } NodeModel* NodeWidget::model() { diff --git a/src/widgets/NodeWidget.h b/src/widgets/NodeWidget.h index a35efa35..1ef0299b 100644 --- a/src/widgets/NodeWidget.h +++ b/src/widgets/NodeWidget.h @@ -26,7 +26,8 @@ public: ~NodeWidget(); void setWSModel(NodeModel *model); void setCustomModel(NodeModel *model); - void setupUI(QSharedPointer ctx); + void setupUI(Nodes *nodes); + void setCanConnect(bool canConnect); NodeModel* model(); public slots: @@ -50,9 +51,10 @@ private: FeatherNode selectedNode(); QScopedPointer ui; - QSharedPointer m_ctx; + Nodes *m_nodes; NodeModel *m_customModel; NodeModel *m_wsModel; + bool m_canConnect = true; QTreeView *m_activeView; diff --git a/src/widgets/NodeWidget.ui b/src/widgets/NodeWidget.ui index e107d327..f72c8ad4 100644 --- a/src/widgets/NodeWidget.ui +++ b/src/widgets/NodeWidget.ui @@ -6,14 +6,14 @@ 0 0 - 604 - 271 + 724 + 451 Form - + 0 @@ -26,76 +26,117 @@ 0 - - - - 6 + + + + 0 - - + + + + 0 + + + 0 + + + 0 + + + 0 + - - - - - From websocket (recommended) - - - true - - - nodeBtnGroup - - - - - - - false - - - false - - - - + + + false + + + false + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + - - - - - From custom list - - - nodeBtnGroup - - - - - - - false - - - - - - - Add custom node(s) - - - - + + + false + + + + + + + + + + + Let Feather manage this list + + - - - 0 + + + Qt::Horizontal - + + QSizePolicy::MinimumExpanding + + + + 25 + 0 + + + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Add custom node(s) + + + + + @@ -103,7 +144,4 @@ - - - diff --git a/src/wizard/PageMenu.cpp b/src/wizard/PageMenu.cpp index 45c9ee8b..7dd137f2 100644 --- a/src/wizard/PageMenu.cpp +++ b/src/wizard/PageMenu.cpp @@ -7,6 +7,8 @@ #include +#include "SettingsNewDialog.h" + PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget *parent) : QWizardPage(parent) , ui(new Ui::PageMenu) @@ -16,14 +18,9 @@ PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget ui->setupUi(this); this->setButtonText(QWizard::FinishButton, "Open recent wallet"); -#if defined(Q_OS_MAC) - ui->check_darkMode->setVisible(false); -#endif - QString settingsSkin = config()->get(Config::skin).toString(); - ui->check_darkMode->setChecked(settingsSkin == "QDarkStyle"); - connect(ui->check_darkMode, &QCheckBox::toggled, this, &PageMenu::enableDarkMode); + connect(ui->btn_openSettings, &QPushButton::clicked, this, &PageMenu::showSettings); } void PageMenu::initializePage() { diff --git a/src/wizard/PageMenu.h b/src/wizard/PageMenu.h index ba81f34c..6710dd4c 100644 --- a/src/wizard/PageMenu.h +++ b/src/wizard/PageMenu.h @@ -25,7 +25,7 @@ public: int nextId() const override; signals: - void enableDarkMode(bool enable); + void showSettings(); private: Ui::PageMenu *ui; diff --git a/src/wizard/PageMenu.ui b/src/wizard/PageMenu.ui index 9f7b74f1..510e84a5 100644 --- a/src/wizard/PageMenu.ui +++ b/src/wizard/PageMenu.ui @@ -88,14 +88,28 @@ - - - Qt::NoFocus - - - Dark mode - - + + + + + Settings + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + diff --git a/src/wizard/PageNetwork.cpp b/src/wizard/PageNetwork.cpp index 701ada84..23912991 100644 --- a/src/wizard/PageNetwork.cpp +++ b/src/wizard/PageNetwork.cpp @@ -7,6 +7,7 @@ #include #include "constants.h" +#include "utils/os/Prestium.h" #include "Utils.h" #include "WalletWizard.h" @@ -55,7 +56,11 @@ PageNetwork::PageNetwork(QWidget *parent) } int PageNetwork::nextId() const { - return WalletWizard::Page_NetworkTor; + if (Prestium::detect()) { + return WalletWizard::Page_NetworkWebsocket; + } + + return WalletWizard::Page_NetworkProxy; } bool PageNetwork::validatePage() { diff --git a/src/wizard/PageNetwork.ui b/src/wizard/PageNetwork.ui index a151c99d..cc6a1929 100644 --- a/src/wizard/PageNetwork.ui +++ b/src/wizard/PageNetwork.ui @@ -51,7 +51,7 @@ - In most cases you want to let Feather pick one at random. Nodes are hosted by the Feather team and trusted members of the Monero community. + In most cases you want to let Feather pick one at random. Nodes are hosted by the developers and trusted members of the Monero community. true @@ -171,7 +171,7 @@ - Custom node: + Node: diff --git a/src/wizard/PageNetworkProxy.cpp b/src/wizard/PageNetworkProxy.cpp new file mode 100644 index 00000000..5e01b596 --- /dev/null +++ b/src/wizard/PageNetworkProxy.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "PageNetworkProxy.h" +#include "ui_PageNetworkProxy.h" +#include "WalletWizard.h" + +#include + +PageNetworkProxy::PageNetworkProxy(QWidget *parent) + : QWizardPage(parent) + , ui(new Ui::PageNetworkProxy) +{ + ui->setupUi(this); + + connect(ui->radio_configureManually, &QRadioButton::toggled, [this](bool checked){ + ui->frame_privacyLevel->setVisible(checked); + this->adjustSize(); + this->updateGeometry(); + }); + + ui->proxyWidget->setDisableTorLogs(); +} + +void PageNetworkProxy::initializePage() { + // Fuck you Qt. No squish. + QTimer::singleShot(1, [this]{ + ui->frame_privacyLevel->setVisible(false); + }); +} + +int PageNetworkProxy::nextId() const { + return WalletWizard::Page_NetworkWebsocket; +} + +bool PageNetworkProxy::validatePage() { + if (ui->proxyWidget->isProxySettingsChanged()) { + ui->proxyWidget->setProxySettings(); + } + + emit initialNetworkConfigured(); + return true; +} \ No newline at end of file diff --git a/src/wizard/PageNetworkTor.h b/src/wizard/PageNetworkProxy.h similarity index 56% rename from src/wizard/PageNetworkTor.h rename to src/wizard/PageNetworkProxy.h index 63a486ad..ba6ba623 100644 --- a/src/wizard/PageNetworkTor.h +++ b/src/wizard/PageNetworkProxy.h @@ -1,23 +1,23 @@ // SPDX-License-Identifier: BSD-3-Clause // SPDX-FileCopyrightText: 2020-2023 The Monero Project -#ifndef FEATHER_PAGENETWORKTOR_H -#define FEATHER_PAGENETWORKTOR_H +#ifndef FEATHER_PageNetworkProxy_H +#define FEATHER_PageNetworkProxy_H #include #include "appcontext.h" namespace Ui { - class PageNetworkTor; + class PageNetworkProxy; } -class PageNetworkTor : public QWizardPage +class PageNetworkProxy : public QWizardPage { Q_OBJECT public: - explicit PageNetworkTor(QWidget *parent = nullptr); + explicit PageNetworkProxy(QWidget *parent = nullptr); void initializePage() override; bool validatePage() override; int nextId() const override; @@ -26,7 +26,7 @@ signals: void initialNetworkConfigured(); private: - Ui::PageNetworkTor *ui; + Ui::PageNetworkProxy *ui; }; -#endif //FEATHER_PAGENETWORKTOR_H +#endif //FEATHER_PageNetworkProxy_H diff --git a/src/wizard/PageNetworkProxy.ui b/src/wizard/PageNetworkProxy.ui new file mode 100644 index 00000000..68c4cb7d --- /dev/null +++ b/src/wizard/PageNetworkProxy.ui @@ -0,0 +1,132 @@ + + + PageNetworkProxy + + + + 0 + 0 + 671 + 450 + + + + WizardPage + + + + + + + 75 + true + + + + How should Feather route its network traffic? + + + + + + + By default, Feather routes most traffic over Tor. + + + true + + + + + + + An exception is made for the initial wallet synchronization after opening a wallet. Synchronization requires a lot of data transfer and is therefore very slow over Tor. + + + true + + + + + + + Connections to local nodes are never routed over Tor. + + + + + + + Use default settings + + + true + + + + + + + Change proxy settings + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + NetworkProxyWidget + QWidget +
widgets/NetworkProxyWidget.h
+ 1 +
+
+ + +
diff --git a/src/wizard/PageNetworkTor.cpp b/src/wizard/PageNetworkTor.cpp deleted file mode 100644 index cdf014e0..00000000 --- a/src/wizard/PageNetworkTor.cpp +++ /dev/null @@ -1,56 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause -// SPDX-FileCopyrightText: 2020-2023 The Monero Project - -#include "PageNetworkTor.h" -#include "ui_PageNetworkTor.h" -#include "WalletWizard.h" - -PageNetworkTor::PageNetworkTor(QWidget *parent) - : QWizardPage(parent) - , ui(new Ui::PageNetworkTor) -{ - ui->setupUi(this); - - QPixmap iconAllTorExceptNode(":/assets/images/securityLevelStandard.png"); - QPixmap iconAllTorExceptInitSync(":/assets/images/securityLevelSafer.png"); - QPixmap iconAllTor(":/assets/images/securityLevelSafest.png"); - ui->icon_allTorExceptNode->setPixmap(iconAllTorExceptNode.scaledToHeight(16, Qt::SmoothTransformation)); - ui->icon_allTorExceptInitSync->setPixmap(iconAllTorExceptInitSync.scaledToHeight(16, Qt::SmoothTransformation)); - ui->icon_allTor->setPixmap(iconAllTor.scaledToHeight(16, Qt::SmoothTransformation)); - - connect(ui->radio_configureManually, &QRadioButton::toggled, [this](bool checked){ - ui->frame_privacyLevel->setVisible(checked); - this->adjustSize(); - this->updateGeometry(); - }); - - ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptNode, Config::allTorExceptNode); - ui->btnGroup_privacyLevel->setId(ui->radio_allTorExceptInitSync, Config::allTorExceptInitSync); - ui->btnGroup_privacyLevel->setId(ui->radio_allTor, Config::allTor); - - int privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); - auto button = ui->btnGroup_privacyLevel->button(privacyLevel); - if (button) { - button->setChecked(true); - } -} - -void PageNetworkTor::initializePage() { - // Fuck you Qt. No squish. - QTimer::singleShot(1, [this]{ - ui->frame_privacyLevel->setVisible(false); - }); -} - -int PageNetworkTor::nextId() const { - return WalletWizard::Page_NetworkWebsocket; -} - -bool PageNetworkTor::validatePage() { - int id = ui->btnGroup_privacyLevel->checkedId(); - config()->set(Config::torPrivacyLevel, id); - - emit initialNetworkConfigured(); - - return true; -} \ No newline at end of file diff --git a/src/wizard/PageNetworkTor.ui b/src/wizard/PageNetworkTor.ui deleted file mode 100644 index 8cabfc8e..00000000 --- a/src/wizard/PageNetworkTor.ui +++ /dev/null @@ -1,208 +0,0 @@ - - - PageNetworkTor - - - - 0 - 0 - 618 - 438 - - - - WizardPage - - - - - - - 75 - true - - - - How should Feather route its network traffic? - - - - - - - By default, Feather routes most traffic over Tor. - - - true - - - - - - - An exception is made for the initial wallet synchronization after opening a wallet. Synchronization requires a lot of data transfer and is therefore very slow over Tor. - - - true - - - - - - - On Tails, Whonix, or when Feather is started with Torsocks, all traffic is routed through Tor regardless of application configuration. - - - true - - - - - - - Connections to local nodes are never routed over Tor. - - - - - - - Use default settings (recommended) - - - true - - - - - - - Configure manually - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - 0 - 0 - - - - icon - - - - - - - - 0 - 0 - - - - Route all traffic over Tor, except traffic to node - - - btnGroup_privacyLevel - - - - - - - - - - - - 0 - 0 - - - - icon - - - - - - - - 0 - 0 - - - - Route all traffic over Tor, except initial wallet sync - - - btnGroup_privacyLevel - - - - - - - - - - - - 0 - 0 - - - - icon - - - - - - - - 0 - 0 - - - - Route all traffic over Tor - - - btnGroup_privacyLevel - - - - - - - - - - - - - - - - diff --git a/src/wizard/PageNetworkWebsocket.ui b/src/wizard/PageNetworkWebsocket.ui index f4bebb1c..ebf28c44 100644 --- a/src/wizard/PageNetworkWebsocket.ui +++ b/src/wizard/PageNetworkWebsocket.ui @@ -30,7 +30,7 @@ - <html><head/><body><p>Feather can connect to an onion service hosted by the Feather developers to obtain pricing information, a curated list of remote nodes, Home feeds, the latest version of Feather Wallet and more.</p><p>This service is only used to fetch information and can only be reached over Tor. The wallet does not send information about its state or your transactions to the server. It is not used for any telemetry or crash reports.</p><p>If you opt to disable this connection some wallet functionality will be disabled. You can re-enable it at any time.</p></body></html> + <html><head/><body><p>Feather can connect to a service hosted by the developers to obtain pricing information, a curated list of remote nodes, Home feeds, the latest version of Feather Wallet and more.</p><p>This service is only used to fetch information. The wallet does not send information about its state or your transactions to the server. It is not used for any telemetry or crash reports.</p><p>If you disable this connection some wallet functionality will be unavailable. You can re-enable it at any time.</p></body></html> true diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index 91e68935..7d39777b 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -16,9 +16,10 @@ #include "PageSetSeedPassphrase.h" #include "PageSetSubaddressLookahead.h" #include "PageHardwareDevice.h" -#include "PageNetworkTor.h" +#include "PageNetworkProxy.h" #include "PageNetworkWebsocket.h" #include "constants.h" +#include "SettingsNewDialog.h" #include #include @@ -34,7 +35,7 @@ WalletWizard::WalletWizard(QWidget *parent) m_walletKeysFilesModel->refresh(); auto networkPage = new PageNetwork(this); - auto networkTorPage = new PageNetworkTor(this); + auto networkProxyPage = new PageNetworkProxy(this); auto networkWebsocketPage = new PageNetworkWebsocket(this); auto menuPage = new PageMenu(&m_wizardFields, m_walletKeysFilesModel, this); auto openWalletPage = new PageOpenWallet(m_walletKeysFilesModel, this); @@ -49,7 +50,7 @@ WalletWizard::WalletWizard(QWidget *parent) setPage(Page_CreateWalletSeed, createWalletSeed); setPage(Page_SetPasswordPage, walletSetPasswordPage); setPage(Page_Network, networkPage); - setPage(Page_NetworkTor, networkTorPage); + setPage(Page_NetworkProxy, networkProxyPage); setPage(Page_NetworkWebsocket, networkWebsocketPage); setPage(Page_WalletRestoreSeed, new PageWalletRestoreSeed(&m_wizardFields, this)); setPage(Page_WalletRestoreKeys, new PageWalletRestoreKeys(&m_wizardFields, this)); @@ -69,12 +70,7 @@ WalletWizard::WalletWizard(QWidget *parent) emit initialNetworkConfigured(); }); - connect(menuPage, &PageMenu::enableDarkMode, [this](bool enable){ - if (enable) - emit skinChanged("QDarkStyle"); - else - emit skinChanged("Native"); - }); + connect(menuPage, &PageMenu::showSettings, this, &WalletWizard::showSettings); connect(walletSetPasswordPage, &PageSetPassword::createWallet, this, &WalletWizard::onCreateWallet); diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 04d31a6d..b423a6e3 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -82,7 +82,7 @@ public: Page_WalletRestoreKeys, Page_SetRestoreHeight, Page_HardwareDevice, - Page_NetworkTor, + Page_NetworkProxy, Page_NetworkWebsocket }; @@ -91,7 +91,7 @@ public: signals: void initialNetworkConfigured(); - void skinChanged(const QString &skin); + void showSettings(); void openWallet(QString path, QString password); void defaultWalletDirChanged(QString walletDir);