}
void MainWindow::onViewOnBlockExplorer(const QString &txid) {
- QString blockExplorerLink = Utils::blockExplorerLink(conf()->get(Config::blockExplorer).toString(), constants::networkType, txid);
+ QString blockExplorerLink = Utils::blockExplorerLink(txid);
+ if (blockExplorerLink.isEmpty()) {
+ Utils::showError(this, "Unable to open block explorer", "No block explorer configured", {"Go to Settings -> Misc -> Block explorer"});
+ return;
+ }
Utils::externalLinkWarning(this, blockExplorerLink);
}
void Settings::setupMiscTab() {
// [Block explorer]
- ui->comboBox_blockExplorer->setCurrentIndex(ui->comboBox_blockExplorer->findText(conf()->get(Config::blockExplorer).toString()));
- connect(ui->comboBox_blockExplorer, QOverload<int>::of(&QComboBox::currentIndexChanged), [this]{
- QString blockExplorer = ui->comboBox_blockExplorer->currentText();
- conf()->set(Config::blockExplorer, blockExplorer);
- emit blockExplorerChanged(blockExplorer);
- });
+ ui->blockExplorerConfigureWidget->setup("Block explorers", Config::blockExplorers, Config::blockExplorer, {"%txid%"});
// [Reddit frontend]
ui->comboBox_redditFrontend->setCurrentIndex(ui->comboBox_redditFrontend->findText(conf()->get(Config::redditFrontend).toString()));
signals:
void preferredFiatCurrencyChanged(QString currency);
void skinChanged(QString skinName);
- void blockExplorerChanged(QString blockExplorer);
void hideUpdateNotifications(bool hidden);
void websocketStatusChanged(bool enabled);
void proxySettingsChanged();
<rect>
<x>0</x>
<y>0</y>
- <width>841</width>
+ <width>908</width>
<height>454</height>
</rect>
</property>
<item>
<widget class="QStackedWidget" name="stackedWidget">
<property name="currentIndex">
- <number>1</number>
+ <number>7</number>
</property>
<widget class="QWidget" name="page_appearance">
<layout class="QVBoxLayout" name="verticalLayout_6">
</widget>
</item>
<item>
- <widget class="QComboBox" name="comboBox_blockExplorer">
- <item>
- <property name="text">
- <string>exploremonero.com</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>xmrchain.net</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>melo.tools</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>moneroblocks.info</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>blockchair.info</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>127.0.0.1:31312</string>
- </property>
- </item>
- </widget>
+ <widget class="UrlListConfigureWidget" name="blockExplorerConfigureWidget" native="true"/>
</item>
<item>
<widget class="QLabel" name="label_18">
<header>widgets/PluginWidget.h</header>
<container>1</container>
</customwidget>
+ <customwidget>
+ <class>UrlListConfigureWidget</class>
+ <extends>QWidget</extends>
+ <header>widgets/UrlListConfigureWidget.h</header>
+ <container>1</container>
+ </customwidget>
</customwidgets>
<resources/>
<connections>
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#include "MultiLineInputDialog.h"
+#include "ui_MultiLineInputDialog.h"
+
+#include <QDialog>
+#include <QFontMetrics>
+
+#include "utils/Utils.h"
+
+MultiLineInputDialog::MultiLineInputDialog(QWidget *parent, const QString &title, const QString &label, const QStringList &defaultList)
+ : WindowModalDialog(parent)
+ , ui(new Ui::MultiLineInputDialog)
+{
+ ui->setupUi(this);
+
+ this->setWindowTitle(title);
+ ui->label->setText(label);
+
+ QFontMetrics metrics(ui->plainTextEdit->font());
+ int maxWidth = 0;
+ for (const QString &line : defaultList) {
+ int width = metrics.boundingRect(line).width();
+ maxWidth = qMax(maxWidth, width);
+ }
+ ui->plainTextEdit->setMinimumWidth(maxWidth + 10);
+
+ ui->plainTextEdit->setWordWrapMode(QTextOption::NoWrap);
+ ui->plainTextEdit->setPlainText(defaultList.join("\n") + "\n");
+
+ this->adjustSize();
+}
+
+QStringList MultiLineInputDialog::getList() {
+ return ui->plainTextEdit->toPlainText().split("\n");
+}
+
+MultiLineInputDialog::~MultiLineInputDialog() = default;
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#ifndef FEATHER_MULTILINEINPUTDIALOG_H
+#define FEATHER_MULTILINEINPUTDIALOG_H
+
+#include <QDialog>
+
+#include "components.h"
+
+namespace Ui {
+ class MultiLineInputDialog;
+}
+
+class MultiLineInputDialog : public WindowModalDialog
+{
+Q_OBJECT
+
+public:
+ explicit MultiLineInputDialog(QWidget *parent, const QString &title, const QString &label, const QStringList &defaultList);
+ ~MultiLineInputDialog() override;
+
+ QStringList getList();
+
+private:
+ QScopedPointer<Ui::MultiLineInputDialog> ui;
+};
+
+
+#endif //FEATHER_MULTILINEINPUTDIALOG_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MultiLineInputDialog</class>
+ <widget class="QDialog" name="MultiLineInputDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>663</width>
+ <height>357</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Dialog</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPlainTextEdit" name="plainTextEdit">
+ <property name="minimumSize">
+ <size>
+ <width>500</width>
+ <height>0</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>MultiLineInputDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>MultiLineInputDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
}
void TxInfoDialog::viewOnBlockExplorer() {
- Utils::externalLinkWarning(this, Utils::blockExplorerLink(conf()->get(Config::blockExplorer).toString(), constants::networkType, m_txid));
+ QString link = Utils::blockExplorerLink(m_txid);
+
+ if (link.isEmpty()) {
+ Utils::showError(this, "Unable to open block explorer", "No block explorer configured", {"Go to Settings -> Misc -> Block explorer"});
+ return;
+ }
+
+ Utils::externalLinkWarning(this, link);
}
TxInfoDialog::~TxInfoDialog() = default;
\ No newline at end of file
return item;
}
-QString blockExplorerLink(const QString &blockExplorer, NetworkType::Type nettype, const QString &txid) {
- if (blockExplorer == "exploremonero.com") {
- if (nettype == NetworkType::MAINNET) {
- return QString("https://exploremonero.com/transaction/%1").arg(txid);
- }
- }
- else if (blockExplorer == "moneroblocks.info") {
- if (nettype == NetworkType::MAINNET) {
- return QString("https://moneroblocks.info/tx/%1").arg(txid);
- }
- }
- else if (blockExplorer == "blockchair.com") {
- if (nettype == NetworkType::MAINNET) {
- return QString("https://blockchair.com/monero/transaction/%1").arg(txid);
- }
- }
- else if (blockExplorer == "melo.tools") {
- switch (nettype) {
- case NetworkType::MAINNET:
- return QString("https://melo.tools/explorer/mainnet/tx/%1").arg(txid);
- case NetworkType::STAGENET:
- return QString("https://melo.tools/explorer/stagenet/tx/%1").arg(txid);
- case NetworkType::TESTNET:
- return QString("https://melo.tools/explorer/testnet/tx/%1").arg(txid);
- }
- }
- else if (blockExplorer == "blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion") {
- if (nettype == NetworkType::MAINNET) {
- return QString("http://blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion/monero/transaction/%1").arg(txid);
- }
- }
- else if (blockExplorer == "127.0.0.1:31312") {
- if (nettype == NetworkType::MAINNET) {
- return QString("http://127.0.0.1:31312/tx?id=%1").arg(txid);
- }
+QString blockExplorerLink(const QString &txid) {
+ QString link = conf()->get(Config::blockExplorer).toString();
+
+ QUrl url(link);
+ if (url.scheme() != "http" && url.scheme() != "https") {
+ return {};
}
-
- switch (nettype) {
- case NetworkType::MAINNET:
- return QString("https://xmrchain.net/tx/%1").arg(txid);
- case NetworkType::STAGENET:
- return QString("https://stagenet.xmrchain.net/tx/%1").arg(txid);
- case NetworkType::TESTNET:
- return QString("https://testnet.xmrchain.net/tx/%1").arg(txid);
+
+ if (!link.contains("%txid%")) {
+ return {};
}
- return {};
+ return link.replace("%txid%", txid);
}
void externalLinkWarning(QWidget *parent, const QString &url){
QStandardItem *qStandardItem(const QString &text);
QStandardItem *qStandardItem(const QString &text, QFont &font);
- QString blockExplorerLink(const QString &blockExplorer, NetworkType::Type nettype, const QString &txid);
+ QString blockExplorerLink(const QString &txid);
void externalLinkWarning(QWidget *parent, const QString &url);
QString displayAddress(const QString& address, int sections = 3, const QString & sep = " ");
{Config::writeStackTraceToDisk, {QS("writeStackTraceToDisk"), true}},
{Config::writeRecentlyOpenedWallets, {QS("writeRecentlyOpenedWallets"), true}},
- {Config::blockExplorer,{QS("blockExplorer"), "exploremonero.com"}},
+ {Config::blockExplorers, {QS("blockExplorers"), QStringList{"https://xmrchain.net/tx/%txid%",
+ "https://melo.tools/explorer/mainnet/tx/%txid%",
+ "https://moneroblocks.info/tx/%txid%",
+ "https://blockchair.com/monero/transaction/%txid%",
+ "http://blkchairbknpn73cfjhevhla7rkp4ed5gg2knctvv7it4lioy22defid.onion/monero/transaction/%txid%",
+ "http://127.0.0.1:31312/tx?id=%txid%"}}},
+ {Config::blockExplorer,{QS("blockExplorer"), "https://xmrchain.net/tx/%txid%"}},
{Config::redditFrontend, {QS("redditFrontend"), "old.reddit.com"}},
{Config::localMoneroFrontend, {QS("localMoneroFrontend"), "https://localmonero.co"}},
{Config::bountiesFrontend, {QS("bountiesFrontend"), "https://bounties.monero.social"}},
offlineTxSigningForceKISync,
// Misc
+ blockExplorers,
blockExplorer,
redditFrontend,
localMoneroFrontend,
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#include "UrlListConfigureWidget.h"
+#include "ui_UrlListConfigureWidget.h"
+
+#include <QComboBox>
+#include <QWidget>
+#include <QInputDialog>
+
+#include "dialog/MultiLineInputDialog.h"
+#include "utils/config.h"
+#include "utils/Utils.h"
+
+UrlListConfigureWidget::UrlListConfigureWidget(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::UrlListConfigureWidget)
+{
+ ui->setupUi(this);
+}
+
+void UrlListConfigureWidget::setup(const QString &what, Config::ConfigKey list, Config::ConfigKey preferred, const QStringList &keys) {
+ m_what = what;
+ m_listKey = list;
+ m_preferredKey = preferred;
+ m_keys = keys;
+
+ this->setupComboBox();
+
+ connect(ui->configure, &QPushButton::clicked, this, &UrlListConfigureWidget::onConfigureClicked);
+ connect(ui->comboBox, &QComboBox::currentIndexChanged, this, &UrlListConfigureWidget::onUrlSelected);
+}
+
+void UrlListConfigureWidget::onConfigureClicked() {
+ QStringList list = conf()->get(m_listKey).toStringList();
+ QStringList newList;
+
+ while (true) {
+ auto input = MultiLineInputDialog(this, m_what, QString("Set %1 (one per line):").arg(m_what.toLower()), list);
+ auto status = input.exec();
+
+ if (status == QDialog::Rejected) {
+ break;
+ }
+
+ newList = input.getList();
+ newList.removeAll("");
+ newList.removeDuplicates();
+
+ bool error = false;
+ for (const auto& item : newList) {
+ auto url = QUrl::fromUserInput(item);
+ qDebug() << url.scheme();
+ if (url.scheme() != "http" && url.scheme() != "https") {
+ Utils::showError(this, QString("Invalid %1 entered").arg(m_what.toLower()), QString("Invalid URL: %1").arg(item));
+ error = true;
+ break;
+ }
+
+ for (const auto& key : m_keys) {
+ if (!item.contains(key)) {
+ Utils::showError(this, QString("Invalid %1 entered").arg(m_what.toLower()), QString("Key %1 missing from URL: %2").arg(key, item));
+ error = true;
+ break;
+ }
+ }
+ }
+ if (error) {
+ list = newList;
+ continue;
+ }
+
+ conf()->set(m_listKey, newList);
+ this->setupComboBox();
+ break;
+ }
+}
+
+void UrlListConfigureWidget::setupComboBox() {
+ m_urls = conf()->get(m_listKey).toStringList();
+
+ QStringList cleanList;
+ for (const auto &item : m_urls) {
+ QUrl url(item);
+ cleanList << url.host();
+ }
+
+ ui->comboBox->clear();
+ ui->comboBox->insertItems(0, cleanList);
+
+ if (m_urls.empty()) {
+ return;
+ }
+
+ QString preferred = conf()->get(m_preferredKey).toString();
+ if (m_urls.contains(preferred)) {
+ ui->comboBox->setCurrentIndex(m_urls.indexOf(preferred));
+ }
+ else {
+ conf()->set(m_preferredKey, m_urls.at(0));
+ }
+}
+
+void UrlListConfigureWidget::onUrlSelected(int index) {
+ if (index < 0 || index >= m_urls.length()) {
+ return;
+ }
+
+ conf()->set(m_preferredKey, m_urls.at(index));
+}
+
+UrlListConfigureWidget::~UrlListConfigureWidget() = default;
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: 2020-2024 The Monero Project
+
+#ifndef FEATHER_URLLISTCONFIGUREWIDGET_H
+#define FEATHER_URLLISTCONFIGUREWIDGET_H
+
+#include <QWidget>
+#include <QTextEdit>
+
+#include "utils/config.h"
+
+namespace Ui {
+ class UrlListConfigureWidget;
+}
+
+class UrlListConfigureWidget : public QWidget
+{
+Q_OBJECT
+
+public:
+ explicit UrlListConfigureWidget(QWidget *parent = nullptr);
+ ~UrlListConfigureWidget() override;
+
+ void setup(const QString &what, Config::ConfigKey list, Config::ConfigKey preferred, const QStringList& keys);
+
+private slots:
+ void onConfigureClicked();
+ void onUrlSelected(int index);
+
+private:
+ void setupComboBox();
+
+ QScopedPointer<Ui::UrlListConfigureWidget> ui;
+
+ QString m_what;
+ Config::ConfigKey m_listKey;
+ Config::ConfigKey m_preferredKey;
+ QStringList m_keys;
+ QStringList m_urls;
+};
+
+#endif //FEATHER_URLLISTCONFIGUREWIDGET_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>UrlListConfigureWidget</class>
+ <widget class="QWidget" name="UrlListConfigureWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>617</width>
+ <height>27</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QComboBox" name="comboBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="configure">
+ <property name="text">
+ <string>Configure</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>