Merge 35ccd60be1eec8e2e4eeea2945f6284a9b2bd5b8 into 65a3a59cf7aee43202f32714a7b2f712e7e77e80
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
result
|
||||
112
CMakeLists.txt
Normal file
@ -0,0 +1,112 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(LEZExplorerUIPlugin VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets)
|
||||
|
||||
# Try to find the component-interfaces package first
|
||||
find_package(component-interfaces QUIET)
|
||||
|
||||
# If not found, use the local interfaces folder
|
||||
if(NOT component-interfaces_FOUND)
|
||||
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
|
||||
add_library(component-interfaces INTERFACE)
|
||||
target_include_directories(component-interfaces INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/interfaces)
|
||||
endif()
|
||||
|
||||
# Source files
|
||||
set(SOURCES
|
||||
src/ExplorerPlugin.cpp
|
||||
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
|
||||
src/explorer_resources.qrc
|
||||
)
|
||||
|
||||
# Create the plugin library
|
||||
add_library(lez_explorer_ui SHARED ${SOURCES})
|
||||
|
||||
# Set output name without lib prefix
|
||||
set_target_properties(lez_explorer_ui PROPERTIES
|
||||
PREFIX ""
|
||||
OUTPUT_NAME "lez_explorer_ui"
|
||||
)
|
||||
|
||||
# Include directories
|
||||
target_include_directories(lez_explorer_ui PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
# Link against libraries
|
||||
target_link_libraries(lez_explorer_ui PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
Qt6::Svg
|
||||
Qt6::SvgWidgets
|
||||
component-interfaces
|
||||
)
|
||||
|
||||
# Set common properties for both platforms
|
||||
set_target_properties(lez_explorer_ui PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/modules"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE
|
||||
SKIP_BUILD_RPATH FALSE
|
||||
)
|
||||
|
||||
if(APPLE)
|
||||
set_target_properties(lez_explorer_ui PROPERTIES
|
||||
INSTALL_RPATH "@loader_path"
|
||||
INSTALL_NAME_DIR "@rpath"
|
||||
BUILD_WITH_INSTALL_NAME_DIR TRUE
|
||||
)
|
||||
add_custom_command(TARGET lez_explorer_ui POST_BUILD
|
||||
COMMAND install_name_tool -id "@rpath/lez_explorer_ui.dylib" $<TARGET_FILE:lez_explorer_ui>
|
||||
COMMENT "Updating library paths for macOS"
|
||||
)
|
||||
else()
|
||||
set_target_properties(lez_explorer_ui PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN"
|
||||
INSTALL_RPATH_USE_LINK_PATH FALSE
|
||||
)
|
||||
endif()
|
||||
|
||||
install(TARGETS lez_explorer_ui
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}/logos/modules
|
||||
)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/metadata.json
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/lez-explorer-ui
|
||||
)
|
||||
|
||||
message(STATUS "LEZ Explorer UI Plugin configured successfully")
|
||||
45
README.md
Normal file
@ -0,0 +1,45 @@
|
||||
# LEZ Explorer UI
|
||||
|
||||
A Qt6/C++ block explorer for the **Logos Execution Zone** (LEZ). Builds as both a shared library (Qt plugin) and a standalone desktop application.
|
||||
|
||||
## Features
|
||||
|
||||
- Browse recent blocks with live streaming of new blocks
|
||||
- View block details: hash, previous hash, timestamp, status, signature, transactions
|
||||
- Navigate to previous blocks via clickable hash links
|
||||
- View transaction details (Public, Privacy-Preserving, Program Deployment)
|
||||
- View account details with paginated transaction history
|
||||
- Full-text search by block ID, block hash, transaction hash, or account ID
|
||||
- Browser-style back/forward navigation
|
||||
- Pagination with "Load more" for older blocks
|
||||
- Dark theme (Catppuccin Mocha)
|
||||
|
||||
## Building with Nix
|
||||
|
||||
### Standalone application
|
||||
|
||||
```sh
|
||||
nix build '.#app'
|
||||
nix run '.#app'
|
||||
```
|
||||
|
||||
### Shared library (Qt plugin)
|
||||
|
||||
```sh
|
||||
nix build '.#lib'
|
||||
```
|
||||
|
||||
### Development shell
|
||||
|
||||
```sh
|
||||
nix develop
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE-v2](LICENSE-APACHE-v2))
|
||||
- MIT License ([LICENSE-MIT](LICENSE-MIT))
|
||||
|
||||
at your option.
|
||||
44
app/CMakeLists.txt
Normal file
@ -0,0 +1,44 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
project(LEZExplorerUIApp LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
# Find Qt packages
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core Widgets)
|
||||
|
||||
# Set output directories
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
|
||||
|
||||
# Create the executable
|
||||
add_executable(lez-explorer-ui-app
|
||||
main.cpp
|
||||
mainwindow.cpp
|
||||
mainwindow.h
|
||||
)
|
||||
|
||||
# Link libraries
|
||||
target_link_libraries(lez-explorer-ui-app PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
)
|
||||
|
||||
# Set RPATH settings for the executable
|
||||
if(APPLE)
|
||||
set_target_properties(lez-explorer-ui-app PROPERTIES
|
||||
INSTALL_RPATH "@executable_path/../lib"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE
|
||||
)
|
||||
elseif(UNIX)
|
||||
set_target_properties(lez-explorer-ui-app PROPERTIES
|
||||
INSTALL_RPATH "$ORIGIN/../lib"
|
||||
BUILD_WITH_INSTALL_RPATH TRUE
|
||||
)
|
||||
endif()
|
||||
|
||||
# Install rules
|
||||
install(TARGETS lez-explorer-ui-app
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
13
app/main.cpp
Normal file
@ -0,0 +1,13 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
60
app/mainwindow.cpp
Normal file
@ -0,0 +1,60 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include <QtWidgets>
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow() = default;
|
||||
|
||||
void MainWindow::setupUi()
|
||||
{
|
||||
// Determine the appropriate plugin extension based on the platform
|
||||
QString pluginExtension;
|
||||
#if defined(Q_OS_WIN)
|
||||
pluginExtension = ".dll";
|
||||
#elif defined(Q_OS_MAC)
|
||||
pluginExtension = ".dylib";
|
||||
#else
|
||||
pluginExtension = ".so";
|
||||
#endif
|
||||
|
||||
QString pluginPath = QCoreApplication::applicationDirPath() + "/../lez_explorer_ui" + pluginExtension;
|
||||
QPluginLoader loader(pluginPath);
|
||||
|
||||
QWidget* explorerWidget = nullptr;
|
||||
|
||||
if (loader.load()) {
|
||||
QObject* plugin = loader.instance();
|
||||
if (plugin) {
|
||||
QMetaObject::invokeMethod(plugin, "createWidget",
|
||||
Qt::DirectConnection,
|
||||
Q_RETURN_ARG(QWidget*, explorerWidget));
|
||||
}
|
||||
}
|
||||
|
||||
if (explorerWidget) {
|
||||
setCentralWidget(explorerWidget);
|
||||
} else {
|
||||
qWarning() << "Failed to load LEZ Explorer UI plugin from:" << pluginPath;
|
||||
qWarning() << "Error:" << loader.errorString();
|
||||
|
||||
auto* fallbackWidget = new QWidget(this);
|
||||
auto* layout = new QVBoxLayout(fallbackWidget);
|
||||
|
||||
auto* messageLabel = new QLabel("LEZ Explorer UI module not loaded", fallbackWidget);
|
||||
QFont font = messageLabel->font();
|
||||
font.setPointSize(14);
|
||||
messageLabel->setFont(font);
|
||||
messageLabel->setAlignment(Qt::AlignCenter);
|
||||
|
||||
layout->addWidget(messageLabel);
|
||||
setCentralWidget(fallbackWidget);
|
||||
}
|
||||
|
||||
setWindowTitle("LEZ Explorer");
|
||||
resize(1024, 768);
|
||||
}
|
||||
14
app/mainwindow.h
Normal file
@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
|
||||
class MainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
~MainWindow() override;
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
};
|
||||
27
flake.lock
generated
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1751274312,
|
||||
"narHash": "sha256-/bVBlRpECLVzjV19t5KMdMFWSwKLtb5RyXdjz3LJT+g=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "50ab793786d9de88ee30ec4e4c24fb4236fc2674",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
53
flake.nix
Normal file
@ -0,0 +1,53 @@
|
||||
{
|
||||
description = "LEZ Explorer UI - A Qt block explorer for the Logos Execution Zone";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
systems = [ "aarch64-darwin" "x86_64-darwin" "aarch64-linux" "x86_64-linux" ];
|
||||
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f {
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
});
|
||||
in
|
||||
{
|
||||
packages = forAllSystems ({ pkgs }:
|
||||
let
|
||||
common = import ./nix/default.nix { inherit pkgs; };
|
||||
src = ./.;
|
||||
|
||||
lib = import ./nix/lib.nix { inherit pkgs common src; };
|
||||
|
||||
app = import ./nix/app.nix {
|
||||
inherit pkgs common src;
|
||||
lezExplorerUI = lib;
|
||||
};
|
||||
in
|
||||
{
|
||||
lez-explorer-ui-lib = lib;
|
||||
app = app;
|
||||
lib = lib;
|
||||
default = lib;
|
||||
}
|
||||
);
|
||||
|
||||
devShells = forAllSystems ({ pkgs }: {
|
||||
default = pkgs.mkShell {
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
];
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "LEZ Explorer UI development environment"
|
||||
'';
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
17
interfaces/IComponent.h
Normal file
@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QWidget>
|
||||
#include <QtPlugin>
|
||||
|
||||
class LogosAPI;
|
||||
|
||||
class IComponent {
|
||||
public:
|
||||
virtual ~IComponent() = default;
|
||||
virtual QWidget* createWidget(LogosAPI* logosAPI = nullptr) = 0;
|
||||
virtual void destroyWidget(QWidget* widget) = 0;
|
||||
};
|
||||
|
||||
#define IComponent_iid "com.logos.component.IComponent"
|
||||
Q_DECLARE_INTERFACE(IComponent, IComponent_iid)
|
||||
10
metadata.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "lez_explorer_ui",
|
||||
"version": "1.0.0",
|
||||
"description": "LEZ Explorer UI - A block explorer for the Logos Execution Zone",
|
||||
"type": "ui",
|
||||
"main": "lez_explorer_ui",
|
||||
"dependencies": [],
|
||||
"category": "explorer",
|
||||
"capabilities": ["ui_components"]
|
||||
}
|
||||
64
nix/app.nix
Normal file
@ -0,0 +1,64 @@
|
||||
# Builds the lez-explorer-ui-app standalone application
|
||||
{ pkgs, common, src, lezExplorerUI }:
|
||||
|
||||
pkgs.stdenv.mkDerivation rec {
|
||||
pname = "lez-explorer-ui-app";
|
||||
version = common.version;
|
||||
|
||||
inherit src;
|
||||
inherit (common) buildInputs meta;
|
||||
|
||||
nativeBuildInputs = common.nativeBuildInputs ++ pkgs.lib.optionals pkgs.stdenv.isLinux [ pkgs.patchelf ];
|
||||
|
||||
# This is a GUI application, enable Qt wrapping
|
||||
dontWrapQtApps = false;
|
||||
|
||||
qtLibPath = pkgs.lib.makeLibraryPath ([
|
||||
pkgs.qt6.qtbase
|
||||
] ++ pkgs.lib.optionals pkgs.stdenv.isLinux [
|
||||
pkgs.libglvnd
|
||||
]);
|
||||
|
||||
qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins:${pkgs.qt6.qtsvg}/lib/qt-6/plugins";
|
||||
|
||||
qtWrapperArgs = [
|
||||
"--prefix" "LD_LIBRARY_PATH" ":" qtLibPath
|
||||
"--prefix" "QT_PLUGIN_PATH" ":" qtPluginPath
|
||||
];
|
||||
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
cmake -S app -B build \
|
||||
-GNinja \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
cmake --build build
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/bin $out/lib
|
||||
|
||||
# Install the app binary
|
||||
cp build/bin/lez-explorer-ui-app $out/bin/
|
||||
|
||||
# Determine platform-specific plugin extension
|
||||
OS_EXT="so"
|
||||
case "$(uname -s)" in
|
||||
Darwin) OS_EXT="dylib";;
|
||||
esac
|
||||
|
||||
# Copy the explorer UI plugin to root directory (loaded relative to binary)
|
||||
if [ -f "${lezExplorerUI}/lib/lez_explorer_ui.$OS_EXT" ]; then
|
||||
cp -L "${lezExplorerUI}/lib/lez_explorer_ui.$OS_EXT" "$out/"
|
||||
fi
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
28
nix/default.nix
Normal file
@ -0,0 +1,28 @@
|
||||
# Common build configuration shared across all packages
|
||||
{ pkgs }:
|
||||
|
||||
{
|
||||
pname = "lez-explorer-ui";
|
||||
version = "1.0.0";
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.ninja
|
||||
pkgs.pkg-config
|
||||
pkgs.qt6.wrapQtAppsHook
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pkgs.qt6.qtbase
|
||||
pkgs.qt6.qtsvg
|
||||
];
|
||||
|
||||
cmakeFlags = [
|
||||
"-GNinja"
|
||||
];
|
||||
|
||||
meta = with pkgs.lib; {
|
||||
description = "LEZ Explorer UI - A Qt block explorer for the Logos Execution Zone";
|
||||
platforms = platforms.unix;
|
||||
};
|
||||
}
|
||||
45
nix/lib.nix
Normal file
@ -0,0 +1,45 @@
|
||||
# Builds the lez-explorer-ui library (Qt plugin)
|
||||
{ pkgs, common, src }:
|
||||
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "${common.pname}-lib";
|
||||
version = common.version;
|
||||
|
||||
inherit src;
|
||||
inherit (common) buildInputs cmakeFlags meta;
|
||||
nativeBuildInputs = common.nativeBuildInputs;
|
||||
|
||||
# Library (Qt plugin), not an app — no Qt wrapper
|
||||
dontWrapQtApps = true;
|
||||
|
||||
configurePhase = ''
|
||||
runHook preConfigure
|
||||
cmake -S . -B build \
|
||||
-GNinja \
|
||||
-DCMAKE_BUILD_TYPE=Release
|
||||
runHook postConfigure
|
||||
'';
|
||||
|
||||
buildPhase = ''
|
||||
runHook preBuild
|
||||
cmake --build build
|
||||
runHook postBuild
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
|
||||
mkdir -p $out/lib
|
||||
if [ -f build/modules/lez_explorer_ui.dylib ]; then
|
||||
cp build/modules/lez_explorer_ui.dylib $out/lib/
|
||||
elif [ -f build/modules/lez_explorer_ui.so ]; then
|
||||
cp build/modules/lez_explorer_ui.so $out/lib/
|
||||
else
|
||||
echo "Error: No library file found in build/modules/"
|
||||
ls -la build/modules/ 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
runHook postInstall
|
||||
'';
|
||||
}
|
||||
17
src/ExplorerPlugin.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include "ExplorerPlugin.h"
|
||||
#include "ExplorerWidget.h"
|
||||
|
||||
ExplorerPlugin::ExplorerPlugin(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QWidget* ExplorerPlugin::createWidget(LogosAPI* /*logosAPI*/)
|
||||
{
|
||||
return new ExplorerWidget();
|
||||
}
|
||||
|
||||
void ExplorerPlugin::destroyWidget(QWidget* widget)
|
||||
{
|
||||
delete widget;
|
||||
}
|
||||
18
src/ExplorerPlugin.h
Normal file
@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "IComponent.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QtPlugin>
|
||||
|
||||
class ExplorerPlugin : public QObject, public IComponent {
|
||||
Q_OBJECT
|
||||
Q_INTERFACES(IComponent)
|
||||
Q_PLUGIN_METADATA(IID IComponent_iid FILE "metadata.json")
|
||||
|
||||
public:
|
||||
explicit ExplorerPlugin(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE QWidget* createWidget(LogosAPI* logosAPI = nullptr) override;
|
||||
void destroyWidget(QWidget* widget) override;
|
||||
};
|
||||
213
src/ExplorerWidget.cpp
Normal file
@ -0,0 +1,213 @@
|
||||
#include "ExplorerWidget.h"
|
||||
#include "Style.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 <QVBoxLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QLabel>
|
||||
|
||||
ExplorerWidget::ExplorerWidget(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_indexer(std::make_unique<MockIndexerService>())
|
||||
{
|
||||
setStyleSheet(Style::appBackground());
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(16, 12, 16, 12);
|
||||
layout->setSpacing(8);
|
||||
|
||||
// Navigation bar
|
||||
m_navBar = new NavigationBar(this);
|
||||
layout->addWidget(m_navBar);
|
||||
|
||||
// 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);
|
||||
|
||||
connect(m_indexer.get(), &IndexerService::newBlockAdded, this, [this](const Block& block) {
|
||||
m_mainPage->onNewBlock(block);
|
||||
});
|
||||
|
||||
updateNavButtons();
|
||||
}
|
||||
|
||||
ExplorerWidget::~ExplorerWidget() = default;
|
||||
|
||||
void ExplorerWidget::connectPageSignals(QWidget* page)
|
||||
{
|
||||
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, this);
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
} else {
|
||||
qWarning("Block #%llu not found", static_cast<unsigned long long>(t.blockId));
|
||||
showPage(m_mainPage);
|
||||
}
|
||||
} else if constexpr (std::is_same_v<T, NavTransaction>) {
|
||||
auto tx = m_indexer->getTransaction(t.hash);
|
||||
if (tx) {
|
||||
auto* page = new TransactionPage(*tx, this);
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
} else {
|
||||
qWarning("Transaction %s not found", qPrintable(t.hash));
|
||||
showPage(m_mainPage);
|
||||
}
|
||||
} 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(), this);
|
||||
connectPageSignals(page);
|
||||
showPage(page);
|
||||
} else {
|
||||
qWarning("Account %s not found", qPrintable(t.accountId));
|
||||
showPage(m_mainPage);
|
||||
}
|
||||
}
|
||||
}, 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());
|
||||
}
|
||||
49
src/ExplorerWidget.h
Normal file
@ -0,0 +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;
|
||||
|
||||
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;
|
||||
};
|
||||
142
src/Style.h
Normal file
@ -0,0 +1,142 @@
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
|
||||
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 QString(
|
||||
"background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px;"
|
||||
).arg(Color::surface(), Color::border());
|
||||
}
|
||||
|
||||
inline QString cardFrameWithLabels()
|
||||
{
|
||||
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: %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 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: %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 |
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
@ -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
@ -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;
|
||||
};
|
||||
160
src/pages/AccountPage.cpp
Normal file
@ -0,0 +1,160 @@
|
||||
#include "AccountPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
|
||||
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);
|
||||
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 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(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
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::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);
|
||||
auto* idVal = makeValueLabel(account.accountId);
|
||||
idVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(idVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Balance"), row, 0);
|
||||
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);
|
||||
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* 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(20);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
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::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
txRow->setSpacing(10);
|
||||
|
||||
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 typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
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
@ -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);
|
||||
};
|
||||
218
src/pages/BlockPage.cpp
Normal file
@ -0,0 +1,218 @@
|
||||
#include "BlockPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#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);
|
||||
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);
|
||||
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 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(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
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::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);
|
||||
auto* hashVal = makeValueLabel(block.hash);
|
||||
hashVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(hashVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Previous Hash"), row, 0);
|
||||
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;
|
||||
prevHashLabel->installEventFilter(new PrevHashClickFilter(prevBlockId, this, this));
|
||||
grid->addWidget(prevHashLabel, row++, 1);
|
||||
} else {
|
||||
grid->addWidget(makeValueLabel(block.prevBlockHash), 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 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);
|
||||
// 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);
|
||||
grid->addWidget(makeValueLabel(QString::number(block.transactions.size())), row++, 1);
|
||||
|
||||
layout->addWidget(infoFrame);
|
||||
|
||||
// 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(20);
|
||||
headerFont.setBold(true);
|
||||
txHeader->setFont(headerFont);
|
||||
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::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* txRow = new QHBoxLayout(frame);
|
||||
txRow->setSpacing(10);
|
||||
|
||||
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 typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
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
@ -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);
|
||||
};
|
||||
347
src/pages/MainPage.cpp
Normal file
@ -0,0 +1,347 @@
|
||||
#include "MainPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#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);
|
||||
|
||||
// Scroll area for content
|
||||
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);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
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(20);
|
||||
font.setBold(true);
|
||||
label->setFont(font);
|
||||
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::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 statusStr = bedrockStatusToString(block.bedrockStatus);
|
||||
auto* statusLabel = new QLabel(statusStr);
|
||||
statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr)));
|
||||
|
||||
auto* hashLabel = new QLabel(block.hash);
|
||||
hashLabel->setStyleSheet(Style::mutedText() + " font-family: 'Menlo', 'Courier New'; font-size: 11px;");
|
||||
hashLabel->setFixedWidth(470);
|
||||
|
||||
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(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]() {
|
||||
emit blockClicked(blockId);
|
||||
});
|
||||
|
||||
layout->addWidget(frame);
|
||||
}
|
||||
|
||||
void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx)
|
||||
{
|
||||
auto* frame = new ClickableFrame();
|
||||
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/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 typeStr = transactionTypeToString(tx.type);
|
||||
auto* typeLabel = new QLabel(typeStr);
|
||||
typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr)));
|
||||
|
||||
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::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* row = new QHBoxLayout(frame);
|
||||
row->setSpacing(10);
|
||||
|
||||
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);
|
||||
|
||||
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(Style::healthLabel());
|
||||
|
||||
if (m_recentBlocksWidget) {
|
||||
m_contentLayout->removeWidget(m_recentBlocksWidget);
|
||||
m_recentBlocksWidget->deleteLater();
|
||||
m_recentBlocksWidget = nullptr;
|
||||
m_blocksLayout = nullptr;
|
||||
m_loadMoreBtn = nullptr;
|
||||
}
|
||||
|
||||
m_oldestLoadedBlockId = std::nullopt;
|
||||
|
||||
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(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();
|
||||
|
||||
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", ":/icons/box.svg"));
|
||||
for (const auto& block : results.blocks) {
|
||||
addBlockRow(layout, block);
|
||||
}
|
||||
hasResults = true;
|
||||
}
|
||||
|
||||
if (!results.transactions.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Transactions", ":/icons/file-text.svg"));
|
||||
for (const auto& tx : results.transactions) {
|
||||
addTransactionRow(layout, tx);
|
||||
}
|
||||
hasResults = true;
|
||||
}
|
||||
|
||||
if (!results.accounts.isEmpty()) {
|
||||
layout->addWidget(createSectionHeader("Accounts", ":/icons/user.svg"));
|
||||
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::onNewBlock(const Block& block)
|
||||
{
|
||||
if (!m_blocksLayout) return;
|
||||
|
||||
m_healthLabel->setText(QString("Chain height: %1").arg(block.blockId));
|
||||
|
||||
// Append to layout then move to top (index 0)
|
||||
int countBefore = m_blocksLayout->count();
|
||||
addBlockRow(m_blocksLayout, block);
|
||||
if (m_blocksLayout->count() <= countBefore) return;
|
||||
|
||||
QLayoutItem* item = m_blocksLayout->itemAt(m_blocksLayout->count() - 1);
|
||||
if (!item || !item->widget()) return;
|
||||
|
||||
QWidget* newRow = item->widget();
|
||||
m_blocksLayout->removeWidget(newRow);
|
||||
m_blocksLayout->insertWidget(0, newRow);
|
||||
}
|
||||
|
||||
void MainPage::clearSearchResults()
|
||||
{
|
||||
if (m_searchResultsWidget) {
|
||||
m_contentLayout->removeWidget(m_searchResultsWidget);
|
||||
m_searchResultsWidget->deleteLater();
|
||||
m_searchResultsWidget = nullptr;
|
||||
}
|
||||
if (m_recentBlocksWidget) {
|
||||
m_recentBlocksWidget->show();
|
||||
}
|
||||
}
|
||||
43
src/pages/MainPage.h
Normal file
@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "services/IndexerService.h"
|
||||
|
||||
#include <QWidget>
|
||||
#include <optional>
|
||||
|
||||
class QVBoxLayout;
|
||||
class QLabel;
|
||||
class QPushButton;
|
||||
|
||||
class MainPage : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainPage(IndexerService* indexer, QWidget* parent = nullptr);
|
||||
|
||||
void refresh();
|
||||
void showSearchResults(const SearchResults& results);
|
||||
void clearSearchResults();
|
||||
void onNewBlock(const Block& block);
|
||||
|
||||
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, 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;
|
||||
};
|
||||
185
src/pages/TransactionPage.cpp
Normal file
@ -0,0 +1,185 @@
|
||||
#include "TransactionPage.h"
|
||||
#include "Style.h"
|
||||
#include "widgets/ClickableFrame.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QScrollArea>
|
||||
#include <QFrame>
|
||||
#include <QGridLayout>
|
||||
#include <QIcon>
|
||||
|
||||
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);
|
||||
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 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(24);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
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::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);
|
||||
auto* hashVal = makeValueLabel(tx.hash);
|
||||
hashVal->setStyleSheet(Style::monoText());
|
||||
grid->addWidget(hashVal, row++, 1);
|
||||
|
||||
grid->addWidget(makeFieldLabel("Type"), row, 0);
|
||||
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);
|
||||
{ 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);
|
||||
|
||||
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* 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(20);
|
||||
headerFont.setBold(true);
|
||||
accHeader->setFont(headerFont);
|
||||
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::NoFrame);
|
||||
frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame"));
|
||||
|
||||
auto* accRow = new QHBoxLayout(frame);
|
||||
accRow->setSpacing(10);
|
||||
|
||||
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);
|
||||
|
||||
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
@ -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);
|
||||
};
|
||||
35
src/services/IndexerService.h
Normal file
@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "models/Block.h"
|
||||
#include "models/Transaction.h"
|
||||
#include "models/Account.h"
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include <optional>
|
||||
|
||||
struct SearchResults {
|
||||
QVector<Block> blocks;
|
||||
QVector<Transaction> transactions;
|
||||
QVector<Account> accounts;
|
||||
};
|
||||
|
||||
class IndexerService : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IndexerService(QObject* parent = nullptr) : QObject(parent) {}
|
||||
~IndexerService() override = 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;
|
||||
|
||||
signals:
|
||||
void newBlockAdded(Block block);
|
||||
};
|
||||
367
src/services/MockIndexerService.cpp
Normal file
@ -0,0 +1,367 @@
|
||||
#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(QObject* parent)
|
||||
: IndexerService(parent)
|
||||
{
|
||||
generateData();
|
||||
|
||||
connect(&m_blockTimer, &QTimer::timeout, this, &MockIndexerService::onGenerateBlock);
|
||||
m_blockTimer.start(30000); // 30 seconds
|
||||
}
|
||||
|
||||
Block MockIndexerService::generateBlock(quint64 blockId, const QString& prevHash)
|
||||
{
|
||||
auto* rng = QRandomGenerator::global();
|
||||
|
||||
Block block;
|
||||
block.blockId = blockId;
|
||||
block.prevBlockHash = prevHash;
|
||||
block.hash = randomHash();
|
||||
block.timestamp = QDateTime::currentDateTimeUtc();
|
||||
block.signature = randomHexString(64);
|
||||
block.bedrockParentId = randomHexString(32);
|
||||
block.bedrockStatus = BedrockStatus::Pending;
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return block;
|
||||
}
|
||||
|
||||
void MockIndexerService::onGenerateBlock()
|
||||
{
|
||||
quint64 newId = m_blocks.isEmpty() ? 1 : m_blocks.last().blockId + 1;
|
||||
QString prevHash = m_blocks.isEmpty() ? QString(64, '0') : m_blocks.last().hash;
|
||||
|
||||
Block block = generateBlock(newId, prevHash);
|
||||
m_blocks.append(block);
|
||||
m_blocksByHash[block.hash] = block;
|
||||
|
||||
emit newBlockAdded(block);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
40
src/services/MockIndexerService.h
Normal file
@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "IndexerService.h"
|
||||
|
||||
#include <QMap>
|
||||
#include <QTimer>
|
||||
|
||||
class MockIndexerService : public IndexerService {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MockIndexerService(QObject* parent = nullptr);
|
||||
|
||||
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 slots:
|
||||
void onGenerateBlock();
|
||||
|
||||
private:
|
||||
void generateData();
|
||||
QString randomHash();
|
||||
QString randomAccountId();
|
||||
Transaction generatePublicTransaction();
|
||||
Transaction generatePrivacyPreservingTransaction();
|
||||
Transaction generateProgramDeploymentTransaction();
|
||||
Block generateBlock(quint64 blockId, const QString& prevHash);
|
||||
|
||||
QVector<Block> m_blocks;
|
||||
QMap<QString, Block> m_blocksByHash;
|
||||
QMap<QString, Transaction> m_transactionsByHash;
|
||||
QMap<QString, Account> m_accounts;
|
||||
QTimer m_blockTimer;
|
||||
};
|
||||
47
src/widgets/ClickableFrame.h
Normal file
@ -0,0 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "Style.h"
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
63
src/widgets/NavigationBar.cpp
Normal file
@ -0,0 +1,63 @@
|
||||
#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);
|
||||
|
||||
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->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(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);
|
||||
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
@ -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;
|
||||
};
|
||||
38
src/widgets/SearchBar.cpp
Normal file
@ -0,0 +1,38 @@
|
||||
#include "SearchBar.h"
|
||||
#include "Style.h"
|
||||
|
||||
#include <QHBoxLayout>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
|
||||
SearchBar::SearchBar(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
auto* layout = new QHBoxLayout(this);
|
||||
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(38);
|
||||
m_input->setStyleSheet(Style::searchInput());
|
||||
|
||||
auto* searchBtn = new QPushButton("Search", this);
|
||||
searchBtn->setMinimumHeight(38);
|
||||
searchBtn->setStyleSheet(Style::searchButton());
|
||||
|
||||
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
@ -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;
|
||||
};
|
||||