feat: better styling
@ -8,7 +8,7 @@ set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets)
|
||||
|
||||
# Try to find the component-interfaces package first
|
||||
find_package(component-interfaces QUIET)
|
||||
@ -46,6 +46,7 @@ set(SOURCES
|
||||
src/pages/TransactionPage.h
|
||||
src/pages/AccountPage.cpp
|
||||
src/pages/AccountPage.h
|
||||
src/explorer_resources.qrc
|
||||
)
|
||||
|
||||
# Create the plugin library
|
||||
@ -68,6 +69,8 @@ target_include_directories(lez_explorer_ui PRIVATE
|
||||
target_link_libraries(lez_explorer_ui PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
Qt6::Svg
|
||||
Qt6::SvgWidgets
|
||||
component-interfaces
|
||||
)
|
||||
|
||||
|
||||
@ -19,7 +19,7 @@ pkgs.stdenv.mkDerivation rec {
|
||||
pkgs.libglvnd
|
||||
]);
|
||||
|
||||
qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins";
|
||||
qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins:${pkgs.qt6.qtsvg}/lib/qt-6/plugins";
|
||||
|
||||
qtWrapperArgs = [
|
||||
"--prefix" "LD_LIBRARY_PATH" ":" qtLibPath
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtsvg
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "ExplorerWidget.h"
|
||||
#include "Style.h"
|
||||
#include "services/MockIndexerService.h"
|
||||
#include "widgets/NavigationBar.h"
|
||||
#include "widgets/SearchBar.h"
|
||||
@ -15,9 +16,11 @@ ExplorerWidget::ExplorerWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(std::make_unique<MockIndexerService>())
|
||||
{
|
||||
setStyleSheet(Style::appBackground());
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(8, 8, 8, 8);
|
||||
layout->setSpacing(4);
|
||||
layout->setContentsMargins(16, 12, 16, 12);
|
||||
layout->setSpacing(8);
|
||||
|
||||
// Navigation bar
|
||||
m_navBar = new NavigationBar(this);
|
||||
|
||||
132
src/Style.h
@ -4,41 +4,139 @@
|
||||
|
||||
namespace Style {
|
||||
|
||||
// --- Color palette ---
|
||||
|
||||
namespace Color {
|
||||
inline const char* bg() { return "#1e1e2e"; }
|
||||
inline const char* surface() { return "#2a2a3c"; }
|
||||
inline const char* surfaceHover() { return "#33334a"; }
|
||||
inline const char* border() { return "#3b3b52"; }
|
||||
inline const char* text() { return "#cdd6f4"; }
|
||||
inline const char* subtext() { return "#7f849c"; }
|
||||
inline const char* accent() { return "#89b4fa"; }
|
||||
inline const char* green() { return "#a6e3a1"; }
|
||||
inline const char* yellow() { return "#f9e2af"; }
|
||||
inline const char* red() { return "#f38ba8"; }
|
||||
inline const char* blue() { return "#89b4fa"; }
|
||||
inline const char* mauve() { return "#cba6f7"; }
|
||||
inline const char* peach() { return "#fab387"; }
|
||||
inline const char* teal() { return "#94e2d5"; }
|
||||
}
|
||||
|
||||
// --- Base font size applied to the entire app ---
|
||||
|
||||
inline int baseFontSize() { return 14; }
|
||||
|
||||
// --- Widget styles ---
|
||||
|
||||
inline QString appBackground()
|
||||
{
|
||||
return QString("background-color: %1; color: %2; font-size: %3px;")
|
||||
.arg(Color::bg(), Color::text())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
inline QString cardFrame()
|
||||
{
|
||||
return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px;";
|
||||
return QString(
|
||||
"background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px;"
|
||||
).arg(Color::surface(), Color::border());
|
||||
}
|
||||
|
||||
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;";
|
||||
return QString(
|
||||
"QFrame { background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px; }"
|
||||
" QFrame QLabel { color: %3; font-size: %4px; }"
|
||||
).arg(Color::surface(), Color::border(), Color::text())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
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;";
|
||||
return QString(
|
||||
"%1 { background: %2; border: 1px solid %3; border-radius: 8px; padding: 12px 16px; margin: 3px 0; }"
|
||||
" %1 QLabel { color: %4; font-size: %5px; }"
|
||||
).arg(selector, Color::surface(), Color::border(), Color::text())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
inline QString mutedText()
|
||||
{
|
||||
return "color: #6c757d;";
|
||||
return QString("color: %1;").arg(Color::subtext());
|
||||
}
|
||||
|
||||
inline QString monoText()
|
||||
{
|
||||
return QString("font-family: 'Menlo', 'Courier New'; font-size: %1px; color: %2;")
|
||||
.arg(baseFontSize() - 1)
|
||||
.arg(Color::accent());
|
||||
}
|
||||
|
||||
inline QString badge(const QString& bgColor)
|
||||
{
|
||||
return QString("color: white; background: %1; border-radius: 4px; padding: 2px 8px;").arg(bgColor);
|
||||
return QString("color: %1; background: %2; border-radius: 4px; padding: 3px 12px; font-weight: bold; font-size: %3px;")
|
||||
.arg(Color::bg(), bgColor)
|
||||
.arg(baseFontSize() - 1);
|
||||
}
|
||||
|
||||
inline QString sectionHeader()
|
||||
{
|
||||
return QString("color: %1;").arg(Color::text());
|
||||
}
|
||||
|
||||
inline QString navButton()
|
||||
{
|
||||
return QString(
|
||||
"QPushButton { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 6px 14px; font-size: %7px; }"
|
||||
" QPushButton:hover { background: %4; border-color: %5; }"
|
||||
" QPushButton:disabled { color: %6; background: %1; border-color: %3; opacity: 0.4; }"
|
||||
).arg(Color::surface(), Color::text(), Color::border(), Color::surfaceHover(), Color::accent(), Color::subtext())
|
||||
.arg(baseFontSize() + 2);
|
||||
}
|
||||
|
||||
inline QString searchInput()
|
||||
{
|
||||
return QString(
|
||||
"QLineEdit { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 10px 14px; font-size: %6px; }"
|
||||
" QLineEdit:focus { border-color: %4; }"
|
||||
" QLineEdit::placeholder { color: %5; }"
|
||||
).arg(Color::surface(), Color::text(), Color::border(), Color::accent(), Color::subtext())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
inline QString searchButton()
|
||||
{
|
||||
return QString(
|
||||
"QPushButton { background: %1; color: %2; border: none; border-radius: 6px; padding: 10px 20px; font-size: %4px; font-weight: bold; }"
|
||||
" QPushButton:hover { background: %3; }"
|
||||
).arg(Color::accent(), Color::bg(), Color::mauve())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
inline QString healthLabel()
|
||||
{
|
||||
return QString("color: %1; font-weight: bold; font-size: %2px;")
|
||||
.arg(Color::green())
|
||||
.arg(baseFontSize());
|
||||
}
|
||||
|
||||
// --- Status colors ---
|
||||
|
||||
inline QString statusColor(const QString& status)
|
||||
{
|
||||
if (status == "Finalized") return Color::green();
|
||||
if (status == "Safe") return Color::yellow();
|
||||
return Color::subtext(); // Pending
|
||||
}
|
||||
|
||||
// --- Transaction type colors ---
|
||||
|
||||
inline QString txTypeColor(const QString& type)
|
||||
{
|
||||
if (type == "Public") return Color::blue();
|
||||
if (type == "Privacy-Preserving") return Color::mauve();
|
||||
return Color::peach(); // Program Deployment
|
||||
}
|
||||
|
||||
} // namespace Style
|
||||
|
||||
12
src/explorer_resources.qrc
Normal file
@ -0,0 +1,12 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>icons/arrow-left.svg</file>
|
||||
<file>icons/arrow-right.svg</file>
|
||||
<file>icons/home.svg</file>
|
||||
<file>icons/search.svg</file>
|
||||
<file>icons/box.svg</file>
|
||||
<file>icons/file-text.svg</file>
|
||||
<file>icons/user.svg</file>
|
||||
<file>icons/activity.svg</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
3
src/icons/activity.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#a6e3a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 216 B |
4
src/icons/arrow-left.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="19" y1="12" x2="5" y2="12"/>
|
||||
<polyline points="12 19 5 12 12 5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 241 B |
4
src/icons/arrow-right.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
<polyline points="12 5 19 12 12 19"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 242 B |
5
src/icons/box.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 395 B |
7
src/icons/file-text.svg
Normal file
@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
<line x1="16" y1="13" x2="8" y2="13"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17"/>
|
||||
<polyline points="10 9 9 9 8 9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 390 B |
4
src/icons/home.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
4
src/icons/search.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 243 B |
4
src/icons/user.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
@ -8,6 +8,7 @@
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
|
||||
namespace {
|
||||
|
||||
@ -32,30 +33,45 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
scrollContent->setObjectName("scrollContent");
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Title
|
||||
// Title with icon
|
||||
auto* titleRow = new QHBoxLayout();
|
||||
titleRow->setSpacing(10);
|
||||
auto* titleIcon = new QLabel();
|
||||
titleIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(30, 30));
|
||||
auto* title = new QLabel("Account Details");
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setPointSize(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
|
||||
titleRow->addWidget(titleIcon);
|
||||
titleRow->addWidget(title);
|
||||
titleRow->addStretch();
|
||||
layout->addLayout(titleRow);
|
||||
layout->addSpacing(8);
|
||||
|
||||
// Account info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setFrameShape(QFrame::NoFrame);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setVerticalSpacing(10);
|
||||
grid->setHorizontalSpacing(20);
|
||||
int row = 0;
|
||||
|
||||
grid->addWidget(makeFieldLabel("Account ID"), row, 0);
|
||||
@ -64,7 +80,9 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge
|
||||
grid->addWidget(idVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Balance"), row, 0);
|
||||
grid->addWidget(makeValueLabel(account.balance), row++, 1);
|
||||
auto* balVal = makeValueLabel(account.balance);
|
||||
balVal->setStyleSheet(QString("color: %1; font-weight: bold;").arg(Style::Color::green()));
|
||||
grid->addWidget(balVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Program Owner"), row, 0);
|
||||
auto* ownerVal = makeValueLabel(account.programOwner);
|
||||
@ -83,34 +101,42 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge
|
||||
auto transactions = indexer->getTransactionsByAccount(account.accountId, 0, 10);
|
||||
|
||||
if (!transactions.isEmpty()) {
|
||||
auto* headerRow = new QHBoxLayout();
|
||||
headerRow->setContentsMargins(0, 16, 0, 6);
|
||||
headerRow->setSpacing(8);
|
||||
auto* headerIcon = new QLabel();
|
||||
headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24));
|
||||
auto* txHeader = new QLabel("Transaction History");
|
||||
QFont headerFont = txHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setPointSize(20);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(txHeader);
|
||||
txHeader->setStyleSheet(Style::sectionHeader());
|
||||
headerRow->addWidget(headerIcon);
|
||||
headerRow->addWidget(txHeader);
|
||||
headerRow->addStretch();
|
||||
layout->addLayout(headerRow);
|
||||
|
||||
for (const auto& tx : transactions) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
txRow->setSpacing(10);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
|
||||
txRow->addWidget(icon);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash);
|
||||
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));
|
||||
QString typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
txRow->addWidget(hashLabel);
|
||||
txRow->addWidget(typeLabel);
|
||||
|
||||
@ -8,9 +8,34 @@
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
#include <QEvent>
|
||||
#include <QMouseEvent>
|
||||
|
||||
namespace {
|
||||
|
||||
class PrevHashClickFilter : public QObject {
|
||||
public:
|
||||
PrevHashClickFilter(quint64 blockId, BlockPage* page, QObject* parent)
|
||||
: QObject(parent), m_blockId(blockId), m_page(page) {}
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject* obj, QEvent* event) override {
|
||||
if (event->type() == QEvent::MouseButtonRelease) {
|
||||
auto* me = static_cast<QMouseEvent*>(event);
|
||||
if (me->button() == Qt::LeftButton) {
|
||||
emit m_page->blockClicked(m_blockId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QObject::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
private:
|
||||
quint64 m_blockId;
|
||||
BlockPage* m_page;
|
||||
};
|
||||
|
||||
QLabel* makeFieldLabel(const QString& text)
|
||||
{
|
||||
auto* label = new QLabel(text);
|
||||
@ -32,66 +57,89 @@ BlockPage::BlockPage(const Block& block, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
scrollContent->setObjectName("scrollContent");
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Title
|
||||
// Title with icon
|
||||
auto* titleRow = new QHBoxLayout();
|
||||
titleRow->setSpacing(10);
|
||||
auto* titleIcon = new QLabel();
|
||||
titleIcon->setPixmap(QIcon(":/icons/box.svg").pixmap(30, 30));
|
||||
auto* title = new QLabel(QString("Block #%1").arg(block.blockId));
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setPointSize(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
|
||||
titleRow->addWidget(titleIcon);
|
||||
titleRow->addWidget(title);
|
||||
titleRow->addStretch();
|
||||
layout->addLayout(titleRow);
|
||||
layout->addSpacing(8);
|
||||
|
||||
// Block info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setFrameShape(QFrame::NoFrame);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setVerticalSpacing(10);
|
||||
grid->setHorizontalSpacing(20);
|
||||
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);
|
||||
auto* hashVal = makeValueLabel(block.hash);
|
||||
hashVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(hashVal, 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) {
|
||||
auto* prevHashLabel = new QLabel(block.prevBlockHash);
|
||||
prevHashLabel->setStyleSheet(QString("color: %1; text-decoration: underline;").arg(Style::Color::accent()));
|
||||
prevHashLabel->setCursor(Qt::PointingHandCursor);
|
||||
prevHashLabel->setWordWrap(true);
|
||||
prevHashLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
quint64 prevBlockId = block.blockId - 1;
|
||||
connect(prevHashLabel, &QLabel::linkActivated, this, [this, prevBlockId]() {
|
||||
emit blockClicked(prevBlockId);
|
||||
});
|
||||
prevHashLabel->installEventFilter(new PrevHashClickFilter(prevBlockId, this, this));
|
||||
grid->addWidget(prevHashLabel, row++, 1);
|
||||
} else {
|
||||
grid->addWidget(makeValueLabel(block.prevBlockHash), row++, 1);
|
||||
}
|
||||
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;");
|
||||
QString statusStr = bedrockStatusToString(block.bedrockStatus);
|
||||
auto* statusLabel = new QLabel(statusStr);
|
||||
statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr)));
|
||||
statusLabel->setMaximumWidth(120);
|
||||
grid->addWidget(statusLabel, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Signature"), row, 0);
|
||||
auto* sigLabel = makeValueLabel(block.signature);
|
||||
// Insert zero-width spaces every 32 chars so QLabel can word-wrap the long hex string
|
||||
QString wrappableSig = block.signature;
|
||||
for (int i = 32; i < wrappableSig.size(); i += 33) {
|
||||
wrappableSig.insert(i, QChar(0x200B));
|
||||
}
|
||||
auto* sigLabel = new QLabel(wrappableSig);
|
||||
sigLabel->setStyleSheet(Style::monoText());
|
||||
sigLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
|
||||
sigLabel->setWordWrap(true);
|
||||
grid->addWidget(sigLabel, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Transactions"), row, 0);
|
||||
@ -101,34 +149,42 @@ BlockPage::BlockPage(const Block& block, QWidget* parent)
|
||||
|
||||
// Transactions section
|
||||
if (!block.transactions.isEmpty()) {
|
||||
auto* headerRow = new QHBoxLayout();
|
||||
headerRow->setContentsMargins(0, 16, 0, 6);
|
||||
headerRow->setSpacing(8);
|
||||
auto* headerIcon = new QLabel();
|
||||
headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24));
|
||||
auto* txHeader = new QLabel("Transactions");
|
||||
QFont headerFont = txHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setPointSize(20);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(txHeader);
|
||||
txHeader->setStyleSheet(Style::sectionHeader());
|
||||
headerRow->addWidget(headerIcon);
|
||||
headerRow->addWidget(txHeader);
|
||||
headerRow->addStretch();
|
||||
layout->addLayout(headerRow);
|
||||
|
||||
for (const auto& tx : block.transactions) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
txRow->setSpacing(10);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
|
||||
txRow->addWidget(icon);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash);
|
||||
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));
|
||||
QString typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
auto* metaLabel = new QLabel();
|
||||
switch (tx.type) {
|
||||
|
||||
@ -7,16 +7,22 @@
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
|
||||
MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(indexer)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Health indicator
|
||||
auto* healthRow = new QHBoxLayout();
|
||||
auto* healthIcon = new QLabel(this);
|
||||
healthIcon->setPixmap(QIcon(":/icons/activity.svg").pixmap(20, 20));
|
||||
m_healthLabel = new QLabel(this);
|
||||
healthRow->addWidget(healthIcon);
|
||||
healthRow->addWidget(m_healthLabel);
|
||||
healthRow->addStretch();
|
||||
outerLayout->addLayout(healthRow);
|
||||
@ -25,10 +31,13 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
scrollContent->setObjectName("scrollContent");
|
||||
m_contentLayout = new QVBoxLayout(scrollContent);
|
||||
m_contentLayout->setAlignment(Qt::AlignTop);
|
||||
m_contentLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
scrollArea->setWidget(scrollContent);
|
||||
outerLayout->addWidget(scrollArea);
|
||||
@ -36,52 +45,77 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent)
|
||||
refresh();
|
||||
}
|
||||
|
||||
QWidget* MainPage::createSectionHeader(const QString& title)
|
||||
QWidget* MainPage::createSectionHeader(const QString& title, const QString& iconPath)
|
||||
{
|
||||
auto* container = new QWidget();
|
||||
auto* layout = new QHBoxLayout(container);
|
||||
layout->setContentsMargins(0, 12, 0, 6);
|
||||
layout->setSpacing(8);
|
||||
layout->setAlignment(Qt::AlignVCenter);
|
||||
|
||||
if (!iconPath.isEmpty()) {
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(iconPath).pixmap(24, 24));
|
||||
icon->setFixedSize(24, 24);
|
||||
layout->addWidget(icon);
|
||||
}
|
||||
|
||||
auto* label = new QLabel(title);
|
||||
QFont font = label->font();
|
||||
font.setPointSize(16);
|
||||
font.setPointSize(20);
|
||||
font.setBold(true);
|
||||
label->setFont(font);
|
||||
label->setStyleSheet("margin-top: 12px; margin-bottom: 4px;");
|
||||
return label;
|
||||
label->setStyleSheet(Style::sectionHeader());
|
||||
layout->addWidget(label);
|
||||
layout->addStretch();
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
row->setSpacing(10);
|
||||
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/box.svg").pixmap(20, 20));
|
||||
row->addWidget(icon);
|
||||
|
||||
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));
|
||||
QString statusStr = bedrockStatusToString(block.bedrockStatus);
|
||||
auto* statusLabel = new QLabel(statusStr);
|
||||
statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr)));
|
||||
|
||||
auto* hashLabel = new QLabel(block.hash.left(16) + "...");
|
||||
hashLabel->setStyleSheet(Style::mutedText());
|
||||
auto* hashLabel = new QLabel(block.hash);
|
||||
hashLabel->setStyleSheet(Style::mutedText() + " font-family: 'Menlo', 'Courier New'; font-size: 11px;");
|
||||
hashLabel->setFixedWidth(470);
|
||||
|
||||
auto* txCount = new QLabel(QString("%1 tx").arg(block.transactions.size()));
|
||||
auto* txIcon = new QLabel();
|
||||
txIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(18, 18));
|
||||
|
||||
auto* txCount = new QLabel(QString::number(block.transactions.size()));
|
||||
|
||||
auto* timeLabel = new QLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC"));
|
||||
timeLabel->setStyleSheet(Style::mutedText());
|
||||
|
||||
statusLabel->setFixedWidth(90);
|
||||
statusLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
row->addWidget(idLabel);
|
||||
row->addWidget(statusLabel);
|
||||
row->addWidget(hashLabel, 1);
|
||||
row->addWidget(hashLabel);
|
||||
row->addStretch(1);
|
||||
row->addWidget(txIcon);
|
||||
row->addWidget(txCount);
|
||||
row->addWidget(timeLabel);
|
||||
row->addWidget(statusLabel);
|
||||
|
||||
quint64 blockId = block.blockId;
|
||||
connect(frame, &ClickableFrame::clicked, this, [this, blockId]() {
|
||||
@ -94,24 +128,24 @@ void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block)
|
||||
void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
row->setSpacing(10);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash.left(16) + "...");
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20));
|
||||
row->addWidget(icon);
|
||||
|
||||
auto* hashLabel = new QLabel(tx.hash);
|
||||
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));
|
||||
QString typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
auto* metaLabel = new QLabel();
|
||||
switch (tx.type) {
|
||||
@ -142,12 +176,17 @@ void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
|
||||
void MainPage::addAccountRow(QVBoxLayout* layout, const Account& account)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
row->setSpacing(10);
|
||||
|
||||
auto* idLabel = new QLabel(account.accountId.left(16) + "...");
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20));
|
||||
row->addWidget(icon);
|
||||
|
||||
auto* idLabel = new QLabel(account.accountId);
|
||||
QFont boldFont = idLabel->font();
|
||||
boldFont.setBold(true);
|
||||
idLabel->setFont(boldFont);
|
||||
@ -174,27 +213,60 @@ void MainPage::refresh()
|
||||
|
||||
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;");
|
||||
m_healthLabel->setStyleSheet(Style::healthLabel());
|
||||
|
||||
if (m_recentBlocksWidget) {
|
||||
m_contentLayout->removeWidget(m_recentBlocksWidget);
|
||||
delete m_recentBlocksWidget;
|
||||
m_recentBlocksWidget = nullptr;
|
||||
m_blocksLayout = nullptr;
|
||||
m_loadMoreBtn = nullptr;
|
||||
}
|
||||
|
||||
m_recentBlocksWidget = new QWidget();
|
||||
auto* blocksLayout = new QVBoxLayout(m_recentBlocksWidget);
|
||||
blocksLayout->setContentsMargins(0, 0, 0, 0);
|
||||
m_oldestLoadedBlockId = std::nullopt;
|
||||
|
||||
blocksLayout->addWidget(createSectionHeader("Recent Blocks"));
|
||||
m_recentBlocksWidget = new QWidget();
|
||||
auto* outerLayout = new QVBoxLayout(m_recentBlocksWidget);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->addWidget(createSectionHeader("Recent Blocks", ":/icons/box.svg"));
|
||||
|
||||
// Dedicated layout for block rows — button is appended after this in outerLayout
|
||||
auto* blockRowsWidget = new QWidget();
|
||||
m_blocksLayout = new QVBoxLayout(blockRowsWidget);
|
||||
m_blocksLayout->setContentsMargins(0, 0, 0, 0);
|
||||
outerLayout->addWidget(blockRowsWidget);
|
||||
|
||||
auto blocks = m_indexer->getBlocks(std::nullopt, 10);
|
||||
for (const auto& block : blocks) {
|
||||
addBlockRow(blocksLayout, block);
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (!m_oldestLoadedBlockId || block.blockId < *m_oldestLoadedBlockId)
|
||||
m_oldestLoadedBlockId = block.blockId;
|
||||
}
|
||||
|
||||
m_loadMoreBtn = new QPushButton("Load more");
|
||||
m_loadMoreBtn->setStyleSheet(Style::searchButton());
|
||||
m_loadMoreBtn->setVisible(m_oldestLoadedBlockId && *m_oldestLoadedBlockId > 1);
|
||||
connect(m_loadMoreBtn, &QPushButton::clicked, this, &MainPage::loadMoreBlocks);
|
||||
outerLayout->addWidget(m_loadMoreBtn, 0, Qt::AlignHCenter);
|
||||
|
||||
m_contentLayout->addWidget(m_recentBlocksWidget);
|
||||
}
|
||||
|
||||
void MainPage::loadMoreBlocks()
|
||||
{
|
||||
if (!m_oldestLoadedBlockId || *m_oldestLoadedBlockId <= 1)
|
||||
return;
|
||||
|
||||
auto blocks = m_indexer->getBlocks(m_oldestLoadedBlockId, 10);
|
||||
for (const auto& block : blocks) {
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (block.blockId < *m_oldestLoadedBlockId)
|
||||
m_oldestLoadedBlockId = block.blockId;
|
||||
}
|
||||
|
||||
m_loadMoreBtn->setVisible(*m_oldestLoadedBlockId > 1 && !blocks.isEmpty());
|
||||
}
|
||||
|
||||
void MainPage::showSearchResults(const SearchResults& results)
|
||||
{
|
||||
clearSearchResults();
|
||||
@ -210,7 +282,7 @@ void MainPage::showSearchResults(const SearchResults& results)
|
||||
bool hasResults = false;
|
||||
|
||||
if (!results.blocks.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Blocks"));
|
||||
layout->addWidget(createSectionHeader("Blocks", ":/icons/box.svg"));
|
||||
for (const auto& block : results.blocks) {
|
||||
addBlockRow(layout, block);
|
||||
}
|
||||
@ -218,7 +290,7 @@ void MainPage::showSearchResults(const SearchResults& results)
|
||||
}
|
||||
|
||||
if (!results.transactions.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Transactions"));
|
||||
layout->addWidget(createSectionHeader("Transactions", ":/icons/file-text.svg"));
|
||||
for (const auto& tx : results.transactions) {
|
||||
addTransactionRow(layout, tx);
|
||||
}
|
||||
@ -226,7 +298,7 @@ void MainPage::showSearchResults(const SearchResults& results)
|
||||
}
|
||||
|
||||
if (!results.accounts.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Accounts"));
|
||||
layout->addWidget(createSectionHeader("Accounts", ":/icons/user.svg"));
|
||||
for (const auto& acc : results.accounts) {
|
||||
addAccountRow(layout, acc);
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <optional>
|
||||
|
||||
class QVBoxLayout;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class MainPage : public QWidget {
|
||||
Q_OBJECT
|
||||
@ -26,11 +28,15 @@ 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);
|
||||
QWidget* createSectionHeader(const QString& title, const QString& iconPath = {});
|
||||
void loadMoreBlocks();
|
||||
|
||||
IndexerService* m_indexer = nullptr;
|
||||
QVBoxLayout* m_contentLayout = nullptr;
|
||||
QWidget* m_searchResultsWidget = nullptr;
|
||||
QWidget* m_recentBlocksWidget = nullptr;
|
||||
QVBoxLayout* m_blocksLayout = nullptr;
|
||||
QPushButton* m_loadMoreBtn = nullptr;
|
||||
QLabel* m_healthLabel = nullptr;
|
||||
std::optional<quint64> m_oldestLoadedBlockId;
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
|
||||
namespace {
|
||||
|
||||
@ -32,30 +33,45 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* outerLayout = new QVBoxLayout(this);
|
||||
outerLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
auto* scrollArea = new QScrollArea(this);
|
||||
scrollArea->setWidgetResizable(true);
|
||||
scrollArea->setFrameShape(QFrame::NoFrame);
|
||||
scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }");
|
||||
|
||||
auto* scrollContent = new QWidget();
|
||||
scrollContent->setObjectName("scrollContent");
|
||||
auto* layout = new QVBoxLayout(scrollContent);
|
||||
layout->setAlignment(Qt::AlignTop);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Title
|
||||
// Title with icon
|
||||
auto* titleRow = new QHBoxLayout();
|
||||
titleRow->setSpacing(10);
|
||||
auto* titleIcon = new QLabel();
|
||||
titleIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(30, 30));
|
||||
auto* title = new QLabel("Transaction Details");
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setPointSize(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
layout->addWidget(title);
|
||||
title->setStyleSheet(QString("color: %1;").arg(Style::Color::text()));
|
||||
titleRow->addWidget(titleIcon);
|
||||
titleRow->addWidget(title);
|
||||
titleRow->addStretch();
|
||||
layout->addLayout(titleRow);
|
||||
layout->addSpacing(8);
|
||||
|
||||
// Transaction info grid
|
||||
auto* infoFrame = new QFrame();
|
||||
infoFrame->setFrameShape(QFrame::StyledPanel);
|
||||
infoFrame->setFrameShape(QFrame::NoFrame);
|
||||
infoFrame->setStyleSheet(Style::cardFrameWithLabels());
|
||||
|
||||
auto* grid = new QGridLayout(infoFrame);
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setVerticalSpacing(10);
|
||||
grid->setHorizontalSpacing(20);
|
||||
int row = 0;
|
||||
|
||||
grid->addWidget(makeFieldLabel("Hash"), row, 0);
|
||||
@ -64,21 +80,17 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent)
|
||||
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;");
|
||||
QString typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
typeLabel->setMaximumWidth(180);
|
||||
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);
|
||||
{ auto* v = makeValueLabel(tx.programId); v->setStyleSheet(Style::monoText()); grid->addWidget(v, row++, 1); }
|
||||
|
||||
grid->addWidget(makeFieldLabel("Instruction Data"), row, 0);
|
||||
grid->addWidget(makeValueLabel(QString("%1 items").arg(tx.instructionData.size())), row++, 1);
|
||||
@ -120,22 +132,35 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent)
|
||||
|
||||
// Accounts section
|
||||
if (!tx.accounts.isEmpty()) {
|
||||
auto* headerRow = new QHBoxLayout();
|
||||
headerRow->setContentsMargins(0, 16, 0, 6);
|
||||
headerRow->setSpacing(8);
|
||||
auto* headerIcon = new QLabel();
|
||||
headerIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(24, 24));
|
||||
auto* accHeader = new QLabel("Accounts");
|
||||
QFont headerFont = accHeader->font();
|
||||
headerFont.setPointSize(16);
|
||||
headerFont.setPointSize(20);
|
||||
headerFont.setBold(true);
|
||||
accHeader->setFont(headerFont);
|
||||
accHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;");
|
||||
layout->addWidget(accHeader);
|
||||
accHeader->setStyleSheet(Style::sectionHeader());
|
||||
headerRow->addWidget(headerIcon);
|
||||
headerRow->addWidget(accHeader);
|
||||
headerRow->addStretch();
|
||||
layout->addLayout(headerRow);
|
||||
|
||||
for (const auto& accRef : tx.accounts) {
|
||||
auto* frame = new ClickableFrame();
|
||||
frame->setFrameShape(QFrame::StyledPanel);
|
||||
frame->setFrameShape(QFrame::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* accRow = new QHBoxLayout(frame);
|
||||
accRow->setSpacing(10);
|
||||
|
||||
auto* idLabel = new QLabel(accRef.accountId.left(20) + "...");
|
||||
auto* icon = new QLabel();
|
||||
icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20));
|
||||
accRow->addWidget(icon);
|
||||
|
||||
auto* idLabel = new QLabel(accRef.accountId);
|
||||
QFont boldFont = idLabel->font();
|
||||
boldFont.setBold(true);
|
||||
idLabel->setFont(boldFont);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "Style.h"
|
||||
|
||||
#include <QFrame>
|
||||
#include <QMouseEvent>
|
||||
|
||||
@ -24,4 +26,22 @@ protected:
|
||||
}
|
||||
QFrame::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void enterEvent(QEnterEvent* event) override
|
||||
{
|
||||
m_baseStyleSheet = styleSheet();
|
||||
setStyleSheet(m_baseStyleSheet +
|
||||
QString(" ClickableFrame { background: %1 !important; border-color: %2 !important; }")
|
||||
.arg(Style::Color::surfaceHover(), Style::Color::accent()));
|
||||
QFrame::enterEvent(event);
|
||||
}
|
||||
|
||||
void leaveEvent(QEvent* event) override
|
||||
{
|
||||
setStyleSheet(m_baseStyleSheet);
|
||||
QFrame::leaveEvent(event);
|
||||
}
|
||||
|
||||
private:
|
||||
QString m_baseStyleSheet;
|
||||
};
|
||||
|
||||
@ -1,32 +1,50 @@
|
||||
#include "NavigationBar.h"
|
||||
#include "Style.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QIcon>
|
||||
|
||||
NavigationBar::NavigationBar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(6);
|
||||
|
||||
m_backBtn = new QPushButton("<", this);
|
||||
m_backBtn->setFixedWidth(40);
|
||||
m_backBtn->setMinimumHeight(32);
|
||||
const QSize iconSize(22, 22);
|
||||
|
||||
m_backBtn = new QPushButton(this);
|
||||
m_backBtn->setIcon(QIcon(":/icons/arrow-left.svg"));
|
||||
m_backBtn->setIconSize(iconSize);
|
||||
m_backBtn->setFixedSize(42, 42);
|
||||
m_backBtn->setEnabled(false);
|
||||
m_backBtn->setToolTip("Back");
|
||||
m_backBtn->setStyleSheet(Style::navButton());
|
||||
|
||||
m_forwardBtn = new QPushButton(">", this);
|
||||
m_forwardBtn->setFixedWidth(40);
|
||||
m_forwardBtn->setMinimumHeight(32);
|
||||
m_forwardBtn = new QPushButton(this);
|
||||
m_forwardBtn->setIcon(QIcon(":/icons/arrow-right.svg"));
|
||||
m_forwardBtn->setIconSize(iconSize);
|
||||
m_forwardBtn->setFixedSize(42, 42);
|
||||
m_forwardBtn->setEnabled(false);
|
||||
m_forwardBtn->setToolTip("Forward");
|
||||
m_forwardBtn->setStyleSheet(Style::navButton());
|
||||
|
||||
auto* homeBtn = new QPushButton("Home", this);
|
||||
homeBtn->setMinimumHeight(32);
|
||||
auto* homeBtn = new QPushButton(this);
|
||||
homeBtn->setIcon(QIcon(":/icons/home.svg"));
|
||||
homeBtn->setIconSize(iconSize);
|
||||
homeBtn->setFixedSize(42, 42);
|
||||
homeBtn->setToolTip("Home");
|
||||
homeBtn->setStyleSheet(Style::navButton());
|
||||
|
||||
auto* titleLabel = new QLabel("LEZ Explorer");
|
||||
titleLabel->setStyleSheet(QString("color: %1; font-size: 14px; font-weight: bold; margin-left: 8px;").arg(Style::Color::accent()));
|
||||
|
||||
layout->addWidget(m_backBtn);
|
||||
layout->addWidget(m_forwardBtn);
|
||||
layout->addWidget(homeBtn);
|
||||
layout->addWidget(titleLabel);
|
||||
layout->addStretch();
|
||||
|
||||
connect(m_backBtn, &QPushButton::clicked, this, &NavigationBar::backClicked);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "SearchBar.h"
|
||||
#include "Style.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
@ -8,14 +9,17 @@ SearchBar::SearchBar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setContentsMargins(0, 4, 0, 4);
|
||||
layout->setSpacing(8);
|
||||
|
||||
m_input = new QLineEdit(this);
|
||||
m_input->setPlaceholderText("Search by block ID / block hash / tx hash / account ID...");
|
||||
m_input->setMinimumHeight(32);
|
||||
m_input->setMinimumHeight(38);
|
||||
m_input->setStyleSheet(Style::searchInput());
|
||||
|
||||
auto* searchBtn = new QPushButton("Search", this);
|
||||
searchBtn->setMinimumHeight(32);
|
||||
searchBtn->setMinimumHeight(38);
|
||||
searchBtn->setStyleSheet(Style::searchButton());
|
||||
|
||||
layout->addWidget(m_input, 1);
|
||||
layout->addWidget(searchBtn);
|
||||
|
||||