mirror of
https://github.com/logos-blockchain/lez-explorer-ui.git
synced 2026-04-08 04:23:18 +00:00
feat: first working version
This commit is contained in:
parent
e005abeee2
commit
871765de2d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
result
|
||||
@ -26,6 +26,26 @@ set(SOURCES
|
||||
src/ExplorerPlugin.h
|
||||
src/ExplorerWidget.cpp
|
||||
src/ExplorerWidget.h
|
||||
src/Style.h
|
||||
src/services/MockIndexerService.cpp
|
||||
src/services/MockIndexerService.h
|
||||
src/services/IndexerService.h
|
||||
src/models/Block.h
|
||||
src/models/Transaction.h
|
||||
src/models/Account.h
|
||||
src/widgets/SearchBar.cpp
|
||||
src/widgets/SearchBar.h
|
||||
src/widgets/NavigationBar.cpp
|
||||
src/widgets/NavigationBar.h
|
||||
src/widgets/ClickableFrame.h
|
||||
src/pages/MainPage.cpp
|
||||
src/pages/MainPage.h
|
||||
src/pages/BlockPage.cpp
|
||||
src/pages/BlockPage.h
|
||||
src/pages/TransactionPage.cpp
|
||||
src/pages/TransactionPage.h
|
||||
src/pages/AccountPage.cpp
|
||||
src/pages/AccountPage.h
|
||||
)
|
||||
|
||||
# Create the plugin library
|
||||
|
||||
@ -1,38 +1,197 @@
|
||||
#include "ExplorerWidget.h"
|
||||
#include "services/MockIndexerService.h"
|
||||
#include "widgets/NavigationBar.h"
|
||||
#include "widgets/SearchBar.h"
|
||||
#include "pages/MainPage.h"
|
||||
#include "pages/BlockPage.h"
|
||||
#include "pages/TransactionPage.h"
|
||||
#include "pages/AccountPage.h"
|
||||
|
||||
#include <QPainter>
|
||||
#include <QVBoxLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QLabel>
|
||||
|
||||
ExplorerWidget::ExplorerWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(std::make_unique<MockIndexerService>())
|
||||
{
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
layout->setSpacing(4);
|
||||
|
||||
auto* label = new QLabel("LEZ Explorer", this);
|
||||
label->setAlignment(Qt::AlignCenter);
|
||||
QFont font = label->font();
|
||||
font.setPointSize(24);
|
||||
font.setBold(true);
|
||||
label->setFont(font);
|
||||
label->setStyleSheet("color: white;");
|
||||
// Navigation bar
|
||||
m_navBar = new NavigationBar(this);
|
||||
layout->addWidget(m_navBar);
|
||||
|
||||
layout->addWidget(label);
|
||||
// Search bar
|
||||
m_searchBar = new SearchBar(this);
|
||||
layout->addWidget(m_searchBar);
|
||||
|
||||
// Page stack
|
||||
m_stack = new QStackedWidget(this);
|
||||
layout->addWidget(m_stack, 1);
|
||||
|
||||
// Create main page
|
||||
m_mainPage = new MainPage(m_indexer.get(), this);
|
||||
m_stack->addWidget(m_mainPage);
|
||||
connectPageSignals(m_mainPage);
|
||||
|
||||
m_currentTarget = NavHome{};
|
||||
|
||||
// Connect navigation
|
||||
connect(m_navBar, &NavigationBar::backClicked, this, &ExplorerWidget::navigateBack);
|
||||
connect(m_navBar, &NavigationBar::forwardClicked, this, &ExplorerWidget::navigateForward);
|
||||
connect(m_navBar, &NavigationBar::homeClicked, this, &ExplorerWidget::navigateHome);
|
||||
connect(m_searchBar, &SearchBar::searchRequested, this, &ExplorerWidget::onSearch);
|
||||
|
||||
updateNavButtons();
|
||||
}
|
||||
|
||||
void ExplorerWidget::paintEvent(QPaintEvent* /*event*/)
|
||||
ExplorerWidget::~ExplorerWidget() = default;
|
||||
|
||||
void ExplorerWidget::connectPageSignals(QWidget* page)
|
||||
{
|
||||
QPainter painter(this);
|
||||
painter.setRenderHint(QPainter::Antialiasing);
|
||||
|
||||
// Draw a blue rectangle filling the widget
|
||||
painter.setBrush(QColor(30, 60, 120));
|
||||
painter.setPen(Qt::NoPen);
|
||||
painter.drawRect(rect());
|
||||
|
||||
// Draw a lighter inner rectangle
|
||||
int margin = 40;
|
||||
QRect inner = rect().adjusted(margin, margin, -margin, -margin);
|
||||
painter.setBrush(QColor(40, 80, 160));
|
||||
painter.drawRoundedRect(inner, 16, 16);
|
||||
if (auto* mainPage = qobject_cast<MainPage*>(page)) {
|
||||
connect(mainPage, &MainPage::blockClicked, this, [this](quint64 id) {
|
||||
navigateTo(NavBlock{id});
|
||||
});
|
||||
connect(mainPage, &MainPage::transactionClicked, this, [this](const QString& hash) {
|
||||
navigateTo(NavTransaction{hash});
|
||||
});
|
||||
connect(mainPage, &MainPage::accountClicked, this, [this](const QString& id) {
|
||||
navigateTo(NavAccount{id});
|
||||
});
|
||||
} else if (auto* blockPage = qobject_cast<BlockPage*>(page)) {
|
||||
connect(blockPage, &BlockPage::blockClicked, this, [this](quint64 id) {
|
||||
navigateTo(NavBlock{id});
|
||||
});
|
||||
connect(blockPage, &BlockPage::transactionClicked, this, [this](const QString& hash) {
|
||||
navigateTo(NavTransaction{hash});
|
||||
});
|
||||
} else if (auto* txPage = qobject_cast<TransactionPage*>(page)) {
|
||||
connect(txPage, &TransactionPage::accountClicked, this, [this](const QString& id) {
|
||||
navigateTo(NavAccount{id});
|
||||
});
|
||||
} else if (auto* accPage = qobject_cast<AccountPage*>(page)) {
|
||||
connect(accPage, &AccountPage::transactionClicked, this, [this](const QString& hash) {
|
||||
navigateTo(NavTransaction{hash});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ExplorerWidget::showPage(QWidget* page)
|
||||
{
|
||||
// Remove all pages except main page
|
||||
while (m_stack->count() > 1) {
|
||||
auto* w = m_stack->widget(1);
|
||||
m_stack->removeWidget(w);
|
||||
w->deleteLater();
|
||||
}
|
||||
|
||||
if (page == m_mainPage) {
|
||||
m_stack->setCurrentWidget(m_mainPage);
|
||||
} else {
|
||||
m_stack->addWidget(page);
|
||||
m_stack->setCurrentWidget(page);
|
||||
}
|
||||
}
|
||||
|
||||
void ExplorerWidget::navigateTo(const NavTarget& target, bool addToHistory)
|
||||
{
|
||||
if (addToHistory) {
|
||||
m_backHistory.push(m_currentTarget);
|
||||
m_forwardHistory.clear();
|
||||
}
|
||||
|
||||
m_currentTarget = target;
|
||||
|
||||
std::visit([this](auto&& t) {
|
||||
using T = std::decay_t<decltype(t)>;
|
||||
|
||||
if constexpr (std::is_same_v<T, NavHome>) {
|
||||
m_mainPage->clearSearchResults();
|
||||
m_mainPage->refresh();
|
||||
showPage(m_mainPage);
|
||||
} else if constexpr (std::is_same_v<T, NavBlock>) {
|
||||
auto block = m_indexer->getBlockById(t.blockId);
|
||||
if (block) {
|
||||
auto* page = new BlockPage(*block);
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, NavTransaction>) {
|
||||
auto tx = m_indexer->getTransaction(t.hash);
|
||||
if (tx) {
|
||||
auto* page = new TransactionPage(*tx);
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, NavAccount>) {
|
||||
auto account = m_indexer->getAccount(t.accountId);
|
||||
if (account) {
|
||||
auto* page = new AccountPage(*account, m_indexer.get());
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
}
|
||||
}
|
||||
}, target);
|
||||
|
||||
updateNavButtons();
|
||||
}
|
||||
|
||||
void ExplorerWidget::navigateBack()
|
||||
{
|
||||
if (m_backHistory.isEmpty()) return;
|
||||
|
||||
m_forwardHistory.push(m_currentTarget);
|
||||
NavTarget target = m_backHistory.pop();
|
||||
navigateTo(target, false);
|
||||
}
|
||||
|
||||
void ExplorerWidget::navigateForward()
|
||||
{
|
||||
if (m_forwardHistory.isEmpty()) return;
|
||||
|
||||
m_backHistory.push(m_currentTarget);
|
||||
NavTarget target = m_forwardHistory.pop();
|
||||
navigateTo(target, false);
|
||||
}
|
||||
|
||||
void ExplorerWidget::navigateHome()
|
||||
{
|
||||
navigateTo(NavHome{});
|
||||
}
|
||||
|
||||
void ExplorerWidget::onSearch(const QString& query)
|
||||
{
|
||||
if (query.trimmed().isEmpty()) return;
|
||||
|
||||
auto results = m_indexer->search(query);
|
||||
|
||||
// If exactly one result, navigate directly to it
|
||||
int totalResults = results.blocks.size() + results.transactions.size() + results.accounts.size();
|
||||
if (totalResults == 1) {
|
||||
if (!results.blocks.isEmpty()) {
|
||||
navigateTo(NavBlock{results.blocks.first().blockId});
|
||||
return;
|
||||
}
|
||||
if (!results.transactions.isEmpty()) {
|
||||
navigateTo(NavTransaction{results.transactions.first().hash});
|
||||
return;
|
||||
}
|
||||
if (!results.accounts.isEmpty()) {
|
||||
navigateTo(NavAccount{results.accounts.first().accountId});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Show results on main page
|
||||
navigateTo(NavHome{});
|
||||
m_mainPage->showSearchResults(results);
|
||||
}
|
||||
|
||||
void ExplorerWidget::updateNavButtons()
|
||||
{
|
||||
m_navBar->setBackEnabled(!m_backHistory.isEmpty());
|
||||
m_navBar->setForwardEnabled(!m_forwardHistory.isEmpty());
|
||||
}
|
||||
|
||||
@ -1,13 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStack>
|
||||
#include <memory>
|
||||
#include <variant>
|
||||
|
||||
class QStackedWidget;
|
||||
class NavigationBar;
|
||||
class SearchBar;
|
||||
class MainPage;
|
||||
|
||||
struct NavHome {};
|
||||
struct NavBlock { quint64 blockId; };
|
||||
struct NavTransaction { QString hash; };
|
||||
struct NavAccount { QString accountId; };
|
||||
|
||||
using NavTarget = std::variant<NavHome, NavBlock, NavTransaction, NavAccount>;
|
||||
|
||||
class ExplorerWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ExplorerWidget(QWidget* parent = nullptr);
|
||||
~ExplorerWidget() override;
|
||||
|
||||
protected:
|
||||
void paintEvent(QPaintEvent* event) override;
|
||||
private:
|
||||
void navigateTo(const NavTarget& target, bool addToHistory = true);
|
||||
void navigateBack();
|
||||
void navigateForward();
|
||||
void navigateHome();
|
||||
void onSearch(const QString& query);
|
||||
void updateNavButtons();
|
||||
|
||||
void showPage(QWidget* page);
|
||||
void connectPageSignals(QWidget* page);
|
||||
|
||||
std::unique_ptr<IndexerService> m_indexer;
|
||||
NavigationBar* m_navBar = nullptr;
|
||||
SearchBar* m_searchBar = nullptr;
|
||||
QStackedWidget* m_stack = nullptr;
|
||||
MainPage* m_mainPage = nullptr;
|
||||
|
||||
QStack<NavTarget> m_backHistory;
|
||||
QStack<NavTarget> m_forwardHistory;
|
||||
NavTarget m_currentTarget;
|
||||
};
|
||||
|
||||
44
src/Style.h
Normal file
44
src/Style.h
Normal file
@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
namespace Style {
|
||||
|
||||
inline QString cardFrame()
|
||||
{
|
||||
return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px;";
|
||||
}
|
||||
|
||||
inline QString cardFrameWithLabels()
|
||||
{
|
||||
return "QFrame { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; }"
|
||||
" QFrame QLabel { color: #212529; }";
|
||||
}
|
||||
|
||||
inline QString clickableRow()
|
||||
{
|
||||
return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0;";
|
||||
}
|
||||
|
||||
inline QString clickableRowWithLabels(const QString& selector)
|
||||
{
|
||||
return QString("%1 { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0; }"
|
||||
" %1 QLabel { color: #212529; }").arg(selector);
|
||||
}
|
||||
|
||||
inline QString monoText()
|
||||
{
|
||||
return "font-family: 'Menlo', 'Courier New', 'DejaVu Sans Mono'; font-size: 11px;";
|
||||
}
|
||||
|
||||
inline QString mutedText()
|
||||
{
|
||||
return "color: #6c757d;";
|
||||
}
|
||||
|
||||
inline QString badge(const QString& bgColor)
|
||||
{
|
||||
return QString("color: white; background: %1; border-radius: 4px; padding: 2px 8px;").arg(bgColor);
|
||||
}
|
||||
|
||||
} // namespace Style
|
||||
11
src/models/Account.h
Normal file
11
src/models/Account.h
Normal file
@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
struct Account {
|
||||
QString accountId;
|
||||
QString programOwner;
|
||||
QString balance;
|
||||
QString nonce;
|
||||
int dataSizeBytes = 0;
|
||||
};
|
||||
34
src/models/Block.h
Normal file
34
src/models/Block.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
#include <QDateTime>
|
||||
|
||||
#include "Transaction.h"
|
||||
|
||||
enum class BedrockStatus {
|
||||
Pending,
|
||||
Safe,
|
||||
Finalized
|
||||
};
|
||||
|
||||
inline QString bedrockStatusToString(BedrockStatus status)
|
||||
{
|
||||
switch (status) {
|
||||
case BedrockStatus::Pending: return "Pending";
|
||||
case BedrockStatus::Safe: return "Safe";
|
||||
case BedrockStatus::Finalized: return "Finalized";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
struct Block {
|
||||
quint64 blockId = 0;
|
||||
QString hash;
|
||||
QString prevBlockHash;
|
||||
QDateTime timestamp;
|
||||
QString signature;
|
||||
QVector<Transaction> transactions;
|
||||
BedrockStatus bedrockStatus = BedrockStatus::Pending;
|
||||
QString bedrockParentId;
|
||||
};
|
||||
47
src/models/Transaction.h
Normal file
47
src/models/Transaction.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
enum class TransactionType {
|
||||
Public,
|
||||
PrivacyPreserving,
|
||||
ProgramDeployment
|
||||
};
|
||||
|
||||
inline QString transactionTypeToString(TransactionType type)
|
||||
{
|
||||
switch (type) {
|
||||
case TransactionType::Public: return "Public";
|
||||
case TransactionType::PrivacyPreserving: return "Privacy-Preserving";
|
||||
case TransactionType::ProgramDeployment: return "Program Deployment";
|
||||
}
|
||||
return "Unknown";
|
||||
}
|
||||
|
||||
struct AccountRef {
|
||||
QString accountId;
|
||||
QString nonce;
|
||||
};
|
||||
|
||||
struct Transaction {
|
||||
QString hash;
|
||||
TransactionType type = TransactionType::Public;
|
||||
|
||||
// Public transaction fields
|
||||
QString programId;
|
||||
QVector<AccountRef> accounts;
|
||||
QVector<quint32> instructionData;
|
||||
int signatureCount = 0;
|
||||
int proofSizeBytes = 0;
|
||||
|
||||
// Privacy-preserving transaction fields
|
||||
int newCommitmentsCount = 0;
|
||||
int nullifiersCount = 0;
|
||||
int encryptedStatesCount = 0;
|
||||
quint64 validityWindowStart = 0;
|
||||
quint64 validityWindowEnd = 0;
|
||||
|
||||
// Program deployment fields
|
||||
int bytecodeSizeBytes = 0;
|
||||
};
|
||||
134
src/pages/AccountPage.cpp
Normal file
134
src/pages/AccountPage.cpp
Normal file
@ -0,0 +1,134 @@
|
||||
#include "AccountPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QLabel* makeFieldLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
|
||||
return label;
|
||||
}
|
||||
|
||||
QLabel* makeValueLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Title
|
||||
auto* title = new QLabel("Account Details");
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
|
||||
// Account info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
int row = 0;
|
||||
|
||||
grid->addWidget(makeFieldLabel("Account ID"), row, 0);
|
||||
auto* idVal = makeValueLabel(account.accountId);
|
||||
idVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(idVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Balance"), row, 0);
|
||||
grid->addWidget(makeValueLabel(account.balance), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Program Owner"), row, 0);
|
||||
auto* ownerVal = makeValueLabel(account.programOwner);
|
||||
ownerVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(ownerVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Nonce"), row, 0);
|
||||
grid->addWidget(makeValueLabel(account.nonce), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Data Size"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(account.dataSizeBytes)), row++, 1);
|
||||
|
||||
layout->addWidget(infoFrame);
|
||||
|
||||
// Transaction history
|
||||
auto transactions = indexer->getTransactionsByAccount(account.accountId, 0, 10);
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
auto* txHeader = new QLabel("Transaction History");
|
||||
QFont headerFont = txHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(txHeader);
|
||||
|
||||
for (const auto& tx : transactions) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
QFont boldFont = hashLabel->font();
|
||||
boldFont.setBold(true);
|
||||
hashLabel->setFont(boldFont);
|
||||
|
||||
QString typeColor;
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public: typeColor = "#007bff"; break;
|
||||
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
|
||||
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
|
||||
}
|
||||
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
|
||||
typeLabel->setStyleSheet(Style::badge(typeColor));
|
||||
|
||||
txRow->addWidget(hashLabel);
|
||||
txRow->addWidget(typeLabel);
|
||||
txRow->addStretch();
|
||||
|
||||
QString txHash = tx.hash;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
|
||||
emit transactionClicked(txHash);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
} else {
|
||||
auto* noTx = new QLabel("No transactions found for this account.");
|
||||
noTx->setStyleSheet(Style::mutedText() + " margin-top: 16px;");
|
||||
layout->addWidget(noTx);
|
||||
}
|
||||
|
||||
scrollArea->setWidget(scrollContent);
|
||||
outerLayout->addWidget(scrollArea);
|
||||
}
|
||||
16
src/pages/AccountPage.h
Normal file
16
src/pages/AccountPage.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/Account.h"
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class AccountPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit AccountPage(const Account& account, IndexerService* indexer, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void transactionClicked(const QString& hash);
|
||||
};
|
||||
162
src/pages/BlockPage.cpp
Normal file
162
src/pages/BlockPage.cpp
Normal file
@ -0,0 +1,162 @@
|
||||
#include "BlockPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QLabel* makeFieldLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
|
||||
return label;
|
||||
}
|
||||
|
||||
QLabel* makeValueLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
BlockPage::BlockPage(const Block& block, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Title
|
||||
auto* title = new QLabel(QString("Block #%1").arg(block.blockId));
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
|
||||
// Block info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
int row = 0;
|
||||
|
||||
grid->addWidget(makeFieldLabel("Block ID"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(block.blockId)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Hash"), row, 0);
|
||||
grid->addWidget(makeValueLabel(block.hash), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Previous Hash"), row, 0);
|
||||
auto* prevHashLabel = new QLabel(QString("<a href='#' style='color: #007bff;'>%1</a>").arg(block.prevBlockHash));
|
||||
prevHashLabel->setTextInteractionFlags(Qt::TextBrowserInteraction);
|
||||
if (block.blockId > 1) {
|
||||
quint64 prevBlockId = block.blockId - 1;
|
||||
connect(prevHashLabel, &QLabel::linkActivated, this, [this, prevBlockId]() {
|
||||
emit blockClicked(prevBlockId);
|
||||
});
|
||||
}
|
||||
grid->addWidget(prevHashLabel, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Timestamp"), row, 0);
|
||||
grid->addWidget(makeValueLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC")), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Status"), row, 0);
|
||||
QString statusColor;
|
||||
switch (block.bedrockStatus) {
|
||||
case BedrockStatus::Finalized: statusColor = "#28a745"; break;
|
||||
case BedrockStatus::Safe: statusColor = "#ffc107"; break;
|
||||
case BedrockStatus::Pending: statusColor = "#6c757d"; break;
|
||||
}
|
||||
auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus));
|
||||
statusLabel->setStyleSheet(Style::badge(statusColor) + " max-width: 100px;");
|
||||
grid->addWidget(statusLabel, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Signature"), row, 0);
|
||||
auto* sigLabel = makeValueLabel(block.signature);
|
||||
sigLabel->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(sigLabel, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Transactions"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(block.transactions.size())), row++, 1);
|
||||
|
||||
layout->addWidget(infoFrame);
|
||||
|
||||
// Transactions section
|
||||
if (!block.transactions.isEmpty()) {
|
||||
auto* txHeader = new QLabel("Transactions");
|
||||
QFont headerFont = txHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(txHeader);
|
||||
|
||||
for (const auto& tx : block.transactions) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
QFont boldFont = hashLabel->font();
|
||||
boldFont.setBold(true);
|
||||
hashLabel->setFont(boldFont);
|
||||
|
||||
QString typeColor;
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public: typeColor = "#007bff"; break;
|
||||
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
|
||||
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
|
||||
}
|
||||
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
|
||||
typeLabel->setStyleSheet(Style::badge(typeColor));
|
||||
|
||||
auto* metaLabel = new QLabel();
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public:
|
||||
metaLabel->setText(QString("%1 accounts").arg(tx.accounts.size()));
|
||||
break;
|
||||
case TransactionType::PrivacyPreserving:
|
||||
metaLabel->setText(QString("%1 accounts, %2 commitments").arg(tx.accounts.size()).arg(tx.newCommitmentsCount));
|
||||
break;
|
||||
case TransactionType::ProgramDeployment:
|
||||
metaLabel->setText(QString("%1 bytes").arg(tx.bytecodeSizeBytes));
|
||||
break;
|
||||
}
|
||||
metaLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
txRow->addWidget(hashLabel);
|
||||
txRow->addWidget(typeLabel);
|
||||
txRow->addWidget(metaLabel, 1);
|
||||
|
||||
QString txHash = tx.hash;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
|
||||
emit transactionClicked(txHash);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
}
|
||||
|
||||
scrollArea->setWidget(scrollContent);
|
||||
outerLayout->addWidget(scrollArea);
|
||||
}
|
||||
16
src/pages/BlockPage.h
Normal file
16
src/pages/BlockPage.h
Normal file
@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/Block.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class BlockPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BlockPage(const Block& block, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void blockClicked(quint64 blockId);
|
||||
void transactionClicked(const QString& hash);
|
||||
};
|
||||
256
src/pages/MainPage.cpp
Normal file
256
src/pages/MainPage.cpp
Normal file
@ -0,0 +1,256 @@
|
||||
#include "MainPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
|
||||
MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(indexer)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
|
||||
// Health indicator
|
||||
auto* healthRow = new QHBoxLayout();
|
||||
m_healthLabel = new QLabel(this);
|
||||
healthRow->addWidget(m_healthLabel);
|
||||
healthRow->addStretch();
|
||||
outerLayout->addLayout(healthRow);
|
||||
|
||||
// Scroll area for content
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
m_contentLayout = new QVBoxLayout(scrollContent);
|
||||
m_contentLayout->setAlignment(Qt::AlignTop);
|
||||
|
||||
scrollArea->setWidget(scrollContent);
|
||||
outerLayout->addWidget(scrollArea);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
QWidget* MainPage::createSectionHeader(const QString& title)
|
||||
{
|
||||
auto* label = new QLabel(title);
|
||||
QFont font = label->font();
|
||||
font.setPointSize(16);
|
||||
font.setBold(true);
|
||||
label->setFont(font);
|
||||
label->setStyleSheet("margin-top: 12px; margin-bottom: 4px;");
|
||||
return label;
|
||||
}
|
||||
|
||||
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
|
||||
auto* idLabel = new QLabel(QString("Block #%1").arg(block.blockId));
|
||||
QFont boldFont = idLabel->font();
|
||||
boldFont.setBold(true);
|
||||
idLabel->setFont(boldFont);
|
||||
|
||||
QString statusColor;
|
||||
switch (block.bedrockStatus) {
|
||||
case BedrockStatus::Finalized: statusColor = "#28a745"; break;
|
||||
case BedrockStatus::Safe: statusColor = "#ffc107"; break;
|
||||
case BedrockStatus::Pending: statusColor = "#6c757d"; break;
|
||||
}
|
||||
auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus));
|
||||
statusLabel->setStyleSheet(Style::badge(statusColor));
|
||||
|
||||
auto* hashLabel = new QLabel(block.hash.left(16) + "...");
|
||||
hashLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
auto* txCount = new QLabel(QString("%1 tx").arg(block.transactions.size()));
|
||||
|
||||
auto* timeLabel = new QLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC"));
|
||||
timeLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
row->addWidget(idLabel);
|
||||
row->addWidget(statusLabel);
|
||||
row->addWidget(hashLabel, 1);
|
||||
row->addWidget(txCount);
|
||||
row->addWidget(timeLabel);
|
||||
|
||||
quint64 blockId = block.blockId;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, blockId]() {
|
||||
emit blockClicked(blockId);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
|
||||
void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
QFont boldFont = hashLabel->font();
|
||||
boldFont.setBold(true);
|
||||
hashLabel->setFont(boldFont);
|
||||
|
||||
QString typeColor;
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public: typeColor = "#007bff"; break;
|
||||
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
|
||||
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
|
||||
}
|
||||
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
|
||||
typeLabel->setStyleSheet(Style::badge(typeColor));
|
||||
|
||||
auto* metaLabel = new QLabel();
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public:
|
||||
metaLabel->setText(QString("%1 accounts").arg(tx.accounts.size()));
|
||||
break;
|
||||
case TransactionType::PrivacyPreserving:
|
||||
metaLabel->setText(QString("%1 accounts, %2 commitments").arg(tx.accounts.size()).arg(tx.newCommitmentsCount));
|
||||
break;
|
||||
case TransactionType::ProgramDeployment:
|
||||
metaLabel->setText(QString("%1 bytes").arg(tx.bytecodeSizeBytes));
|
||||
break;
|
||||
}
|
||||
metaLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
row->addWidget(hashLabel);
|
||||
row->addWidget(typeLabel);
|
||||
row->addWidget(metaLabel, 1);
|
||||
|
||||
QString txHash = tx.hash;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, txHash]() {
|
||||
emit transactionClicked(txHash);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
|
||||
void MainPage::addAccountRow(QVBoxLayout* layout, const Account& account)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
|
||||
auto* idLabel = new QLabel(account.accountId.left(16) + "...");
|
||||
QFont boldFont = idLabel->font();
|
||||
boldFont.setBold(true);
|
||||
idLabel->setFont(boldFont);
|
||||
|
||||
auto* balanceLabel = new QLabel(QString("Balance: %1").arg(account.balance));
|
||||
auto* nonceLabel = new QLabel(QString("Nonce: %1").arg(account.nonce));
|
||||
nonceLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
row->addWidget(idLabel);
|
||||
row->addWidget(balanceLabel, 1);
|
||||
row->addWidget(nonceLabel);
|
||||
|
||||
QString accId = account.accountId;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, accId]() {
|
||||
emit accountClicked(accId);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
|
||||
void MainPage::refresh()
|
||||
{
|
||||
clearSearchResults();
|
||||
|
||||
quint64 latestId = m_indexer->getLatestBlockId();
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(latestId));
|
||||
m_healthLabel->setStyleSheet("color: #28a745; font-weight: bold; font-size: 14px;");
|
||||
|
||||
if (m_recentBlocksWidget) {
|
||||
m_contentLayout->removeWidget(m_recentBlocksWidget);
|
||||
delete m_recentBlocksWidget;
|
||||
}
|
||||
|
||||
m_recentBlocksWidget = new QWidget();
|
||||
auto* blocksLayout = new QVBoxLayout(m_recentBlocksWidget);
|
||||
blocksLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
blocksLayout->addWidget(createSectionHeader("Recent Blocks"));
|
||||
|
||||
auto blocks = m_indexer->getBlocks(std::nullopt, 10);
|
||||
for (const auto& block : blocks) {
|
||||
addBlockRow(blocksLayout, block);
|
||||
}
|
||||
|
||||
m_contentLayout->addWidget(m_recentBlocksWidget);
|
||||
}
|
||||
|
||||
void MainPage::showSearchResults(const SearchResults& results)
|
||||
{
|
||||
clearSearchResults();
|
||||
|
||||
if (m_recentBlocksWidget) {
|
||||
m_recentBlocksWidget->hide();
|
||||
}
|
||||
|
||||
m_searchResultsWidget = new QWidget();
|
||||
auto* layout = new QVBoxLayout(m_searchResultsWidget);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
bool hasResults = false;
|
||||
|
||||
if (!results.blocks.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Blocks"));
|
||||
for (const auto& block : results.blocks) {
|
||||
addBlockRow(layout, block);
|
||||
}
|
||||
hasResults = true;
|
||||
}
|
||||
|
||||
if (!results.transactions.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Transactions"));
|
||||
for (const auto& tx : results.transactions) {
|
||||
addTransactionRow(layout, tx);
|
||||
}
|
||||
hasResults = true;
|
||||
}
|
||||
|
||||
if (!results.accounts.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Accounts"));
|
||||
for (const auto& acc : results.accounts) {
|
||||
addAccountRow(layout, acc);
|
||||
}
|
||||
hasResults = true;
|
||||
}
|
||||
|
||||
if (!hasResults) {
|
||||
auto* noResults = new QLabel("No results found.");
|
||||
noResults->setAlignment(Qt::AlignCenter);
|
||||
noResults->setStyleSheet(Style::mutedText() + " padding: 20px; font-size: 14px;");
|
||||
layout->addWidget(noResults);
|
||||
}
|
||||
|
||||
m_contentLayout->insertWidget(0, m_searchResultsWidget);
|
||||
}
|
||||
|
||||
void MainPage::clearSearchResults()
|
||||
{
|
||||
if (m_searchResultsWidget) {
|
||||
m_contentLayout->removeWidget(m_searchResultsWidget);
|
||||
delete m_searchResultsWidget;
|
||||
m_searchResultsWidget = nullptr;
|
||||
}
|
||||
if (m_recentBlocksWidget) {
|
||||
m_recentBlocksWidget->show();
|
||||
}
|
||||
}
|
||||
36
src/pages/MainPage.h
Normal file
36
src/pages/MainPage.h
Normal file
@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QVBoxLayout;
|
||||
class QLabel;
|
||||
|
||||
class MainPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainPage(IndexerService* indexer, QWidget* parent = nullptr);
|
||||
|
||||
void refresh();
|
||||
void showSearchResults(const SearchResults& results);
|
||||
void clearSearchResults();
|
||||
|
||||
signals:
|
||||
void blockClicked(quint64 blockId);
|
||||
void transactionClicked(const QString& hash);
|
||||
void accountClicked(const QString& accountId);
|
||||
|
||||
private:
|
||||
void addBlockRow(QVBoxLayout* layout, const Block& block);
|
||||
void addTransactionRow(QVBoxLayout* layout, const Transaction& tx);
|
||||
void addAccountRow(QVBoxLayout* layout, const Account& account);
|
||||
QWidget* createSectionHeader(const QString& title);
|
||||
|
||||
IndexerService* m_indexer = nullptr;
|
||||
QVBoxLayout* m_contentLayout = nullptr;
|
||||
QWidget* m_searchResultsWidget = nullptr;
|
||||
QWidget* m_recentBlocksWidget = nullptr;
|
||||
QLabel* m_healthLabel = nullptr;
|
||||
};
|
||||
160
src/pages/TransactionPage.cpp
Normal file
160
src/pages/TransactionPage.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include "TransactionPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
|
||||
namespace {
|
||||
|
||||
QLabel* makeFieldLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setStyleSheet(Style::mutedText() + " font-weight: bold;");
|
||||
return label;
|
||||
}
|
||||
|
||||
QLabel* makeValueLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
label->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
label->setWordWrap(true);
|
||||
return label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
|
||||
// Title
|
||||
auto* title = new QLabel("Transaction Details");
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
|
||||
// Transaction info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
int row = 0;
|
||||
|
||||
grid->addWidget(makeFieldLabel("Hash"), row, 0);
|
||||
auto* hashVal = makeValueLabel(tx.hash);
|
||||
hashVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(hashVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Type"), row, 0);
|
||||
QString typeColor;
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public: typeColor = "#007bff"; break;
|
||||
case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break;
|
||||
case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break;
|
||||
}
|
||||
auto* typeLabel = new QLabel(transactionTypeToString(tx.type));
|
||||
typeLabel->setStyleSheet(Style::badge(typeColor) + " max-width: 160px;");
|
||||
grid->addWidget(typeLabel, row++, 1);
|
||||
|
||||
// Type-specific fields
|
||||
switch (tx.type) {
|
||||
case TransactionType::Public:
|
||||
grid->addWidget(makeFieldLabel("Program ID"), row, 0);
|
||||
grid->addWidget(makeValueLabel(tx.programId), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Instruction Data"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 items").arg(tx.instructionData.size())), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Proof Size"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.proofSizeBytes)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Signatures"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(tx.signatureCount)), row++, 1);
|
||||
break;
|
||||
|
||||
case TransactionType::PrivacyPreserving:
|
||||
grid->addWidget(makeFieldLabel("Public Accounts"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(tx.accounts.size())), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("New Commitments"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(tx.newCommitmentsCount)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Nullifiers"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(tx.nullifiersCount)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Encrypted States"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString::number(tx.encryptedStatesCount)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Proof Size"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.proofSizeBytes)), row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Validity Window"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("[%1, %2)").arg(tx.validityWindowStart).arg(tx.validityWindowEnd)), row++, 1);
|
||||
break;
|
||||
|
||||
case TransactionType::ProgramDeployment:
|
||||
grid->addWidget(makeFieldLabel("Bytecode Size"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 bytes").arg(tx.bytecodeSizeBytes)), row++, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
layout->addWidget(infoFrame);
|
||||
|
||||
// Accounts section
|
||||
if (!tx.accounts.isEmpty()) {
|
||||
auto* accHeader = new QLabel("Accounts");
|
||||
QFont headerFont = accHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setBold(true);
|
||||
accHeader->setFont(headerFont);
|
||||
accHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(accHeader);
|
||||
|
||||
for (const auto& accRef : tx.accounts) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* accRow = new QHBoxLayout(frame);
|
||||
|
||||
auto* idLabel = new QLabel(accRef.accountId.left(20) + "...");
|
||||
QFont boldFont = idLabel->font();
|
||||
boldFont.setBold(true);
|
||||
idLabel->setFont(boldFont);
|
||||
|
||||
auto* nonceLabel = new QLabel(QString("Nonce: %1").arg(accRef.nonce));
|
||||
nonceLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
accRow->addWidget(idLabel, 1);
|
||||
accRow->addWidget(nonceLabel);
|
||||
|
||||
QString accId = accRef.accountId;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, accId]() {
|
||||
emit accountClicked(accId);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
}
|
||||
|
||||
scrollArea->setWidget(scrollContent);
|
||||
outerLayout->addWidget(scrollArea);
|
||||
}
|
||||
15
src/pages/TransactionPage.h
Normal file
15
src/pages/TransactionPage.h
Normal file
@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/Transaction.h"
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class TransactionPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit TransactionPage(const Transaction& tx, QWidget* parent = nullptr);
|
||||
|
||||
signals:
|
||||
void accountClicked(const QString& accountId);
|
||||
};
|
||||
28
src/services/IndexerService.h
Normal file
28
src/services/IndexerService.h
Normal file
@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/Block.h"
|
||||
#include "models/Transaction.h"
|
||||
#include "models/Account.h"
|
||||
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
|
||||
struct SearchResults {
|
||||
QVector<Block> blocks;
|
||||
QVector<Transaction> transactions;
|
||||
QVector<Account> accounts;
|
||||
};
|
||||
|
||||
class IndexerService {
|
||||
public:
|
||||
virtual ~IndexerService() = default;
|
||||
|
||||
virtual std::optional<Account> getAccount(const QString& accountId) = 0;
|
||||
virtual std::optional<Block> getBlockById(quint64 blockId) = 0;
|
||||
virtual std::optional<Block> getBlockByHash(const QString& hash) = 0;
|
||||
virtual std::optional<Transaction> getTransaction(const QString& hash) = 0;
|
||||
virtual QVector<Block> getBlocks(std::optional<quint64> before, int limit) = 0;
|
||||
virtual quint64 getLatestBlockId() = 0;
|
||||
virtual QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) = 0;
|
||||
virtual SearchResults search(const QString& query) = 0;
|
||||
};
|
||||
308
src/services/MockIndexerService.cpp
Normal file
308
src/services/MockIndexerService.cpp
Normal file
@ -0,0 +1,308 @@
|
||||
#include "MockIndexerService.h"
|
||||
|
||||
#include <QRandomGenerator>
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
|
||||
QString randomHexString(int bytes)
|
||||
{
|
||||
QString result;
|
||||
result.reserve(bytes * 2);
|
||||
auto* rng = QRandomGenerator::global();
|
||||
for (int i = 0; i < bytes; ++i) {
|
||||
result += QString("%1").arg(rng->bounded(256), 2, 16, QChar('0'));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Generate a base58-like string (simplified — uses alphanumeric chars without 0/O/I/l)
|
||||
QString randomBase58String(int length)
|
||||
{
|
||||
static const char chars[] = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
||||
QString result;
|
||||
result.reserve(length);
|
||||
auto* rng = QRandomGenerator::global();
|
||||
for (int i = 0; i < length; ++i) {
|
||||
result += chars[rng->bounded(static_cast<int>(sizeof(chars) - 1))];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
MockIndexerService::MockIndexerService()
|
||||
{
|
||||
generateData();
|
||||
}
|
||||
|
||||
QString MockIndexerService::randomHash()
|
||||
{
|
||||
return randomHexString(32);
|
||||
}
|
||||
|
||||
QString MockIndexerService::randomAccountId()
|
||||
{
|
||||
return randomBase58String(44);
|
||||
}
|
||||
|
||||
Transaction MockIndexerService::generatePublicTransaction()
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
|
||||
Transaction tx;
|
||||
tx.hash = randomHash();
|
||||
tx.type = TransactionType::Public;
|
||||
tx.programId = randomBase58String(44);
|
||||
tx.signatureCount = rng->bounded(1, 4);
|
||||
tx.proofSizeBytes = rng->bounded(0, 2048);
|
||||
|
||||
int accountCount = rng->bounded(1, 5);
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
// Reuse existing accounts sometimes
|
||||
QString accId;
|
||||
if (!m_accounts.isEmpty() && rng->bounded(100) < 60) {
|
||||
auto keys = m_accounts.keys();
|
||||
accId = keys[rng->bounded(keys.size())];
|
||||
} else {
|
||||
accId = randomAccountId();
|
||||
}
|
||||
tx.accounts.append({accId, QString::number(rng->bounded(1, 100))});
|
||||
}
|
||||
|
||||
int instrCount = rng->bounded(1, 8);
|
||||
for (int i = 0; i < instrCount; ++i) {
|
||||
tx.instructionData.append(rng->generate());
|
||||
}
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
Transaction MockIndexerService::generatePrivacyPreservingTransaction()
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
|
||||
Transaction tx;
|
||||
tx.hash = randomHash();
|
||||
tx.type = TransactionType::PrivacyPreserving;
|
||||
tx.proofSizeBytes = rng->bounded(512, 4096);
|
||||
tx.newCommitmentsCount = rng->bounded(1, 6);
|
||||
tx.nullifiersCount = rng->bounded(1, 4);
|
||||
tx.encryptedStatesCount = rng->bounded(1, 3);
|
||||
tx.validityWindowStart = rng->bounded(1, 50);
|
||||
tx.validityWindowEnd = tx.validityWindowStart + rng->bounded(50, 200);
|
||||
|
||||
int accountCount = rng->bounded(1, 4);
|
||||
for (int i = 0; i < accountCount; ++i) {
|
||||
QString accId;
|
||||
if (!m_accounts.isEmpty() && rng->bounded(100) < 60) {
|
||||
auto keys = m_accounts.keys();
|
||||
accId = keys[rng->bounded(keys.size())];
|
||||
} else {
|
||||
accId = randomAccountId();
|
||||
}
|
||||
tx.accounts.append({accId, QString::number(rng->bounded(1, 100))});
|
||||
}
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
Transaction MockIndexerService::generateProgramDeploymentTransaction()
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
|
||||
Transaction tx;
|
||||
tx.hash = randomHash();
|
||||
tx.type = TransactionType::ProgramDeployment;
|
||||
tx.bytecodeSizeBytes = rng->bounded(1024, 65536);
|
||||
tx.signatureCount = 1;
|
||||
|
||||
return tx;
|
||||
}
|
||||
|
||||
void MockIndexerService::generateData()
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
|
||||
// Generate accounts
|
||||
for (int i = 0; i < 15; ++i) {
|
||||
Account acc;
|
||||
acc.accountId = randomAccountId();
|
||||
acc.programOwner = randomBase58String(44);
|
||||
acc.balance = QString::number(rng->bounded(0, 1000000));
|
||||
acc.nonce = QString::number(rng->bounded(0, 500));
|
||||
acc.dataSizeBytes = rng->bounded(0, 4096);
|
||||
m_accounts[acc.accountId] = acc;
|
||||
}
|
||||
|
||||
// Generate blocks
|
||||
QString prevHash = QString(64, '0');
|
||||
QDateTime timestamp = QDateTime::currentDateTimeUtc().addSecs(-25 * 12); // ~5 min ago
|
||||
|
||||
for (quint64 id = 1; id <= 25; ++id) {
|
||||
Block block;
|
||||
block.blockId = id;
|
||||
block.prevBlockHash = prevHash;
|
||||
block.hash = randomHash();
|
||||
block.timestamp = timestamp;
|
||||
block.signature = randomHexString(64);
|
||||
block.bedrockParentId = randomHexString(32);
|
||||
|
||||
// Older blocks are finalized, middle ones safe, recent ones pending
|
||||
if (id <= 15) {
|
||||
block.bedrockStatus = BedrockStatus::Finalized;
|
||||
} else if (id <= 22) {
|
||||
block.bedrockStatus = BedrockStatus::Safe;
|
||||
} else {
|
||||
block.bedrockStatus = BedrockStatus::Pending;
|
||||
}
|
||||
|
||||
// Generate 1-5 transactions per block
|
||||
int txCount = rng->bounded(1, 6);
|
||||
for (int t = 0; t < txCount; ++t) {
|
||||
Transaction tx;
|
||||
int typeRoll = rng->bounded(100);
|
||||
if (typeRoll < 60) {
|
||||
tx = generatePublicTransaction();
|
||||
} else if (typeRoll < 85) {
|
||||
tx = generatePrivacyPreservingTransaction();
|
||||
} else {
|
||||
tx = generateProgramDeploymentTransaction();
|
||||
}
|
||||
block.transactions.append(tx);
|
||||
m_transactionsByHash[tx.hash] = tx;
|
||||
|
||||
// Ensure accounts referenced in transactions exist
|
||||
for (const auto& accRef : tx.accounts) {
|
||||
if (!m_accounts.contains(accRef.accountId)) {
|
||||
Account acc;
|
||||
acc.accountId = accRef.accountId;
|
||||
acc.programOwner = randomBase58String(44);
|
||||
acc.balance = QString::number(rng->bounded(0, 1000000));
|
||||
acc.nonce = accRef.nonce;
|
||||
acc.dataSizeBytes = rng->bounded(0, 4096);
|
||||
m_accounts[acc.accountId] = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_blocks.append(block);
|
||||
m_blocksByHash[block.hash] = block;
|
||||
prevHash = block.hash;
|
||||
timestamp = timestamp.addSecs(12);
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Account> MockIndexerService::getAccount(const QString& accountId)
|
||||
{
|
||||
auto it = m_accounts.find(accountId);
|
||||
if (it != m_accounts.end()) {
|
||||
return *it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Block> MockIndexerService::getBlockById(quint64 blockId)
|
||||
{
|
||||
if (blockId >= 1 && blockId <= static_cast<quint64>(m_blocks.size())) {
|
||||
return m_blocks[static_cast<int>(blockId - 1)];
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Block> MockIndexerService::getBlockByHash(const QString& hash)
|
||||
{
|
||||
auto it = m_blocksByHash.find(hash);
|
||||
if (it != m_blocksByHash.end()) {
|
||||
return *it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Transaction> MockIndexerService::getTransaction(const QString& hash)
|
||||
{
|
||||
auto it = m_transactionsByHash.find(hash);
|
||||
if (it != m_transactionsByHash.end()) {
|
||||
return *it;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QVector<Block> MockIndexerService::getBlocks(std::optional<quint64> before, int limit)
|
||||
{
|
||||
QVector<Block> result;
|
||||
int startIdx = m_blocks.size() - 1;
|
||||
|
||||
if (before.has_value()) {
|
||||
startIdx = static_cast<int>(*before) - 2; // -1 for 0-index, -1 for "before"
|
||||
}
|
||||
|
||||
for (int i = startIdx; i >= 0 && result.size() < limit; --i) {
|
||||
result.append(m_blocks[i]);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
quint64 MockIndexerService::getLatestBlockId()
|
||||
{
|
||||
if (m_blocks.isEmpty()) return 0;
|
||||
return m_blocks.last().blockId;
|
||||
}
|
||||
|
||||
QVector<Transaction> MockIndexerService::getTransactionsByAccount(const QString& accountId, int offset, int limit)
|
||||
{
|
||||
QVector<Transaction> result;
|
||||
|
||||
// Search all transactions for ones referencing this account
|
||||
for (auto it = m_transactionsByHash.begin(); it != m_transactionsByHash.end(); ++it) {
|
||||
for (const auto& accRef : it->accounts) {
|
||||
if (accRef.accountId == accountId) {
|
||||
result.append(*it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply offset and limit
|
||||
if (offset >= result.size()) return {};
|
||||
return result.mid(offset, limit);
|
||||
}
|
||||
|
||||
SearchResults MockIndexerService::search(const QString& query)
|
||||
{
|
||||
SearchResults results;
|
||||
QString trimmed = query.trimmed();
|
||||
|
||||
if (trimmed.isEmpty()) return results;
|
||||
|
||||
// Try as block ID (numeric)
|
||||
bool ok = false;
|
||||
quint64 blockId = trimmed.toULongLong(&ok);
|
||||
if (ok) {
|
||||
auto block = getBlockById(blockId);
|
||||
if (block) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
}
|
||||
|
||||
// Try as hash (hex string, 64 chars)
|
||||
if (trimmed.size() == 64) {
|
||||
auto block = getBlockByHash(trimmed);
|
||||
if (block) {
|
||||
results.blocks.append(*block);
|
||||
}
|
||||
auto tx = getTransaction(trimmed);
|
||||
if (tx) {
|
||||
results.transactions.append(*tx);
|
||||
}
|
||||
}
|
||||
|
||||
// Try as account ID
|
||||
auto account = getAccount(trimmed);
|
||||
if (account) {
|
||||
results.accounts.append(*account);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
32
src/services/MockIndexerService.h
Normal file
32
src/services/MockIndexerService.h
Normal file
@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "IndexerService.h"
|
||||
|
||||
#include <QMap>
|
||||
|
||||
class MockIndexerService : public IndexerService {
|
||||
public:
|
||||
MockIndexerService();
|
||||
|
||||
std::optional<Account> getAccount(const QString& accountId) override;
|
||||
std::optional<Block> getBlockById(quint64 blockId) override;
|
||||
std::optional<Block> getBlockByHash(const QString& hash) override;
|
||||
std::optional<Transaction> getTransaction(const QString& hash) override;
|
||||
QVector<Block> getBlocks(std::optional<quint64> before, int limit) override;
|
||||
quint64 getLatestBlockId() override;
|
||||
QVector<Transaction> getTransactionsByAccount(const QString& accountId, int offset, int limit) override;
|
||||
SearchResults search(const QString& query) override;
|
||||
|
||||
private:
|
||||
void generateData();
|
||||
QString randomHash();
|
||||
QString randomAccountId();
|
||||
Transaction generatePublicTransaction();
|
||||
Transaction generatePrivacyPreservingTransaction();
|
||||
Transaction generateProgramDeploymentTransaction();
|
||||
|
||||
QVector<Block> m_blocks;
|
||||
QMap<QString, Block> m_blocksByHash;
|
||||
QMap<QString, Transaction> m_transactionsByHash;
|
||||
QMap<QString, Account> m_accounts;
|
||||
};
|
||||
27
src/widgets/ClickableFrame.h
Normal file
27
src/widgets/ClickableFrame.h
Normal file
@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFrame>
|
||||
#include <QMouseEvent>
|
||||
|
||||
class ClickableFrame : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ClickableFrame(QWidget* parent = nullptr)
|
||||
: QFrame(parent)
|
||||
{
|
||||
setCursor(Qt::PointingHandCursor);
|
||||
}
|
||||
|
||||
signals:
|
||||
void clicked();
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent* event) override
|
||||
{
|
||||
if (event->button() == Qt::LeftButton) {
|
||||
emit clicked();
|
||||
}
|
||||
QFrame::mousePressEvent(event);
|
||||
}
|
||||
};
|
||||
45
src/widgets/NavigationBar.cpp
Normal file
45
src/widgets/NavigationBar.cpp
Normal file
@ -0,0 +1,45 @@
|
||||
#include "NavigationBar.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
|
||||
NavigationBar::NavigationBar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_backBtn = new QPushButton("<", this);
|
||||
m_backBtn->setFixedWidth(40);
|
||||
m_backBtn->setMinimumHeight(32);
|
||||
m_backBtn->setEnabled(false);
|
||||
m_backBtn->setToolTip("Back");
|
||||
|
||||
m_forwardBtn = new QPushButton(">", this);
|
||||
m_forwardBtn->setFixedWidth(40);
|
||||
m_forwardBtn->setMinimumHeight(32);
|
||||
m_forwardBtn->setEnabled(false);
|
||||
m_forwardBtn->setToolTip("Forward");
|
||||
|
||||
auto* homeBtn = new QPushButton("Home", this);
|
||||
homeBtn->setMinimumHeight(32);
|
||||
|
||||
layout->addWidget(m_backBtn);
|
||||
layout->addWidget(m_forwardBtn);
|
||||
layout->addWidget(homeBtn);
|
||||
layout->addStretch();
|
||||
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &NavigationBar::backClicked);
|
||||
connect(m_forwardBtn, &QPushButton::clicked, this, &NavigationBar::forwardClicked);
|
||||
connect(homeBtn, &QPushButton::clicked, this, &NavigationBar::homeClicked);
|
||||
}
|
||||
|
||||
void NavigationBar::setBackEnabled(bool enabled)
|
||||
{
|
||||
m_backBtn->setEnabled(enabled);
|
||||
}
|
||||
|
||||
void NavigationBar::setForwardEnabled(bool enabled)
|
||||
{
|
||||
m_forwardBtn->setEnabled(enabled);
|
||||
}
|
||||
24
src/widgets/NavigationBar.h
Normal file
24
src/widgets/NavigationBar.h
Normal file
@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QPushButton;
|
||||
|
||||
class NavigationBar : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NavigationBar(QWidget* parent = nullptr);
|
||||
|
||||
void setBackEnabled(bool enabled);
|
||||
void setForwardEnabled(bool enabled);
|
||||
|
||||
signals:
|
||||
void backClicked();
|
||||
void forwardClicked();
|
||||
void homeClicked();
|
||||
|
||||
private:
|
||||
QPushButton* m_backBtn = nullptr;
|
||||
QPushButton* m_forwardBtn = nullptr;
|
||||
};
|
||||
34
src/widgets/SearchBar.cpp
Normal file
34
src/widgets/SearchBar.cpp
Normal file
@ -0,0 +1,34 @@
|
||||
#include "SearchBar.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
|
||||
SearchBar::SearchBar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_input = new QLineEdit(this);
|
||||
m_input->setPlaceholderText("Search by block ID / block hash / tx hash / account ID...");
|
||||
m_input->setMinimumHeight(32);
|
||||
|
||||
auto* searchBtn = new QPushButton("Search", this);
|
||||
searchBtn->setMinimumHeight(32);
|
||||
|
||||
layout->addWidget(m_input, 1);
|
||||
layout->addWidget(searchBtn);
|
||||
|
||||
connect(m_input, &QLineEdit::returnPressed, this, [this]() {
|
||||
emit searchRequested(m_input->text());
|
||||
});
|
||||
connect(searchBtn, &QPushButton::clicked, this, [this]() {
|
||||
emit searchRequested(m_input->text());
|
||||
});
|
||||
}
|
||||
|
||||
void SearchBar::clear()
|
||||
{
|
||||
m_input->clear();
|
||||
}
|
||||
20
src/widgets/SearchBar.h
Normal file
20
src/widgets/SearchBar.h
Normal file
@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QLineEdit;
|
||||
|
||||
class SearchBar : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SearchBar(QWidget* parent = nullptr);
|
||||
|
||||
void clear();
|
||||
|
||||
signals:
|
||||
void searchRequested(const QString& query);
|
||||
|
||||
private:
|
||||
QLineEdit* m_input = nullptr;
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user