mirror of
https://github.com/logos-blockchain/logos-blockchain-ui.git
synced 2026-04-01 17:03:31 +00:00
feat: Adding version 1 of UI for the Blockchain app
This commit is contained in:
parent
6c331f161f
commit
f679c0bb52
@ -92,6 +92,8 @@ set(SOURCES
|
||||
src/BlockchainPlugin.h
|
||||
src/BlockchainBackend.cpp
|
||||
src/BlockchainBackend.h
|
||||
src/LogModel.cpp
|
||||
src/LogModel.h
|
||||
src/blockchain_resources.qrc
|
||||
)
|
||||
|
||||
|
||||
@ -115,6 +115,6 @@ Example configuration file can be found in the logos-blockchain-module repositor
|
||||
|
||||
During development, you can enable QML hot reload by setting an environment variable:
|
||||
```bash
|
||||
export BLOCKCHAIN_UI_QML_PATH=/path/to/logos-blockchain-ui-new/src/BlockchainView.qml
|
||||
export BLOCKCHAIN_UI_QML_PATH=/path/to/logos-blockchain-ui/src/qml
|
||||
```
|
||||
This allows you to edit the QML file and see changes by reloading the plugin without recompiling.
|
||||
28
flake.lock
generated
28
flake.lock
generated
@ -23,11 +23,11 @@
|
||||
"rust-overlay": "rust-overlay"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770911078,
|
||||
"narHash": "sha256-+asG6HJ/9vuUprjgstZDrvfIo1Zm9Msox3EV8auMDwg=",
|
||||
"lastModified": 1770888466,
|
||||
"narHash": "sha256-7IJz+UIwa8QPg81cvqunpnpO87VUdWt0TcPz7HGBnYE=",
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain",
|
||||
"rev": "71b75c6711779937a0aed383232a6a1920dc13e6",
|
||||
"rev": "3ef7a137b91c4a5a7708405815354ff58e0e179c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -67,14 +67,18 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770917148,
|
||||
"narHash": "sha256-fz8KwDBCa8ZCTdPxRqaNrjeY/kGSZRrPJC1llG22CN0=",
|
||||
"path": "/Users/khushboomehta/Documents/logos/logos-blockchain-module",
|
||||
"type": "path"
|
||||
"lastModified": 1770934619,
|
||||
"narHash": "sha256-DyksgOrea/gktElcOZmMDUcYW45JPpkDA5BnPkwVmmc=",
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain-module",
|
||||
"rev": "578308270ecfe7463a94ac50cae0584451c135ef",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"path": "/Users/khushboomehta/Documents/logos/logos-blockchain-module",
|
||||
"type": "path"
|
||||
"owner": "logos-blockchain",
|
||||
"repo": "logos-blockchain-module",
|
||||
"rev": "578308270ecfe7463a94ac50cae0584451c135ef",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"logos-capability-module": {
|
||||
@ -590,11 +594,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770058741,
|
||||
"narHash": "sha256-9Gx5zJqLZKCuI5BXoCQt39IYrqPR8VSLjT/NQawKdxk=",
|
||||
"lastModified": 1770997148,
|
||||
"narHash": "sha256-plHuPEFyOPrUv1Dyk/2D9Ppc71Xby0LiCIOAzbFCi+I=",
|
||||
"owner": "logos-co",
|
||||
"repo": "logos-design-system",
|
||||
"rev": "7d3ae424b77adef1bfa4a728e951c3b029887e45",
|
||||
"rev": "ede76f156852321f3793fa417295813994e6c9e4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
nixpkgs.follows = "logos-liblogos/nixpkgs";
|
||||
logos-cpp-sdk.url = "github:logos-co/logos-cpp-sdk";
|
||||
logos-liblogos.url = "github:logos-co/logos-liblogos";
|
||||
logos-blockchain-module.url = "github:logos-co/logos-blockchain-module";
|
||||
logos-blockchain-module.url = "github:logos-blockchain/logos-blockchain-module/578308270ecfe7463a94ac50cae0584451c135ef";
|
||||
logos-capability-module.url = "github:logos-co/logos-capability-module";
|
||||
logos-design-system.url = "github:logos-co/logos-design-system";
|
||||
logos-design-system.inputs.nixpkgs.follows = "nixpkgs";
|
||||
|
||||
@ -14,6 +14,8 @@
|
||||
"src/BlockchainPlugin.h",
|
||||
"src/BlockchainBackend.cpp",
|
||||
"src/BlockchainBackend.h",
|
||||
"src/LogModel.cpp",
|
||||
"src/LogModel.h",
|
||||
"src/blockchain_resources.qrc"
|
||||
]
|
||||
},
|
||||
|
||||
@ -7,6 +7,7 @@ BlockchainBackend::BlockchainBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
: QObject(parent),
|
||||
m_status(NotStarted),
|
||||
m_configPath(""),
|
||||
m_logModel(new LogModel(this)),
|
||||
m_logos(nullptr),
|
||||
m_blockchainModule(nullptr)
|
||||
{
|
||||
@ -46,11 +47,6 @@ void BlockchainBackend::setStatus(BlockchainStatus newStatus)
|
||||
}
|
||||
}
|
||||
|
||||
void BlockchainBackend::clearLogs()
|
||||
{
|
||||
emit logsCleared();
|
||||
}
|
||||
|
||||
void BlockchainBackend::setConfigPath(const QString& path)
|
||||
{
|
||||
const QString localPath = QUrl::fromUserInput(path).toLocalFile();
|
||||
@ -60,6 +56,36 @@ void BlockchainBackend::setConfigPath(const QString& path)
|
||||
}
|
||||
}
|
||||
|
||||
void BlockchainBackend::clearLogs()
|
||||
{
|
||||
m_logModel->clear();
|
||||
}
|
||||
|
||||
QString BlockchainBackend::getBalance(const QString& addressHex)
|
||||
{
|
||||
if (!m_blockchainModule) {
|
||||
return QStringLiteral("Error: Module not initialized.");
|
||||
}
|
||||
// The generated proxy converts C pointer params (uint8_t*, BalanceResult*) to QVariant,
|
||||
// which cannot carry raw C pointers. The module needs to expose a QString-based
|
||||
// wrapper (e.g. getWalletBalanceQ) for this to work through the proxy.
|
||||
Q_UNUSED(addressHex)
|
||||
return QStringLiteral("Not yet available: module needs Qt-friendly wallet API.");
|
||||
}
|
||||
|
||||
QString BlockchainBackend::transferFunds(const QString& fromKeyHex, const QString& toKeyHex, const QString& amountStr)
|
||||
{
|
||||
if (!m_blockchainModule) {
|
||||
return QStringLiteral("Error: Module not initialized.");
|
||||
}
|
||||
// Same limitation: TransferFundsArguments and Hash are C types that cannot
|
||||
// pass through the QVariant-based generated proxy.
|
||||
Q_UNUSED(fromKeyHex)
|
||||
Q_UNUSED(toKeyHex)
|
||||
Q_UNUSED(amountStr)
|
||||
return QStringLiteral("Not yet available: module needs Qt-friendly wallet API.");
|
||||
}
|
||||
|
||||
void BlockchainBackend::startBlockchain()
|
||||
{
|
||||
if (!m_blockchainModule) {
|
||||
@ -107,14 +133,16 @@ void BlockchainBackend::stopBlockchain()
|
||||
void BlockchainBackend::onNewBlock(const QVariantList& data)
|
||||
{
|
||||
QString timestamp = QDateTime::currentDateTime().toString("HH:mm:ss");
|
||||
QString line;
|
||||
if (!data.isEmpty()) {
|
||||
QString blockInfo = data.first().toString();
|
||||
QString shortInfo = blockInfo.left(80);
|
||||
if (blockInfo.length() > 80) {
|
||||
shortInfo += "...";
|
||||
}
|
||||
emit newBlockMessage(QString("[%1] 📦 New block: %2\n").arg(timestamp, shortInfo));
|
||||
line = QString("[%1] 📦 New block: %2").arg(timestamp, shortInfo);
|
||||
} else {
|
||||
emit newBlockMessage(QString("[%1] 📦 New block (no data)\n").arg(timestamp));
|
||||
line = QString("[%1] 📦 New block (no data)").arg(timestamp);
|
||||
}
|
||||
m_logModel->append(line);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
#include "logos_api.h"
|
||||
#include "logos_api_client.h"
|
||||
#include "logos_sdk.h"
|
||||
#include "LogModel.h"
|
||||
|
||||
// Type of the blockchain module proxy (has start(), stop(), on() etc.)
|
||||
using BlockchainModuleProxy = std::remove_reference_t<decltype(std::declval<LogosModules>().liblogos_blockchain_module)>;
|
||||
@ -31,32 +32,35 @@ public:
|
||||
|
||||
Q_PROPERTY(BlockchainStatus status READ status NOTIFY statusChanged)
|
||||
Q_PROPERTY(QString configPath READ configPath WRITE setConfigPath NOTIFY configPathChanged)
|
||||
Q_PROPERTY(LogModel* logModel READ logModel CONSTANT)
|
||||
|
||||
explicit BlockchainBackend(LogosAPI* logosAPI = nullptr, QObject* parent = nullptr);
|
||||
~BlockchainBackend();
|
||||
|
||||
BlockchainStatus status() const { return m_status; }
|
||||
QString configPath() const { return m_configPath; }
|
||||
LogModel* logModel() const { return m_logModel; }
|
||||
|
||||
void setConfigPath(const QString& path);
|
||||
Q_INVOKABLE void clearLogs();
|
||||
|
||||
public slots:
|
||||
Q_INVOKABLE QString getBalance(const QString& addressHex);
|
||||
Q_INVOKABLE QString transferFunds(const QString& fromKeyHex, const QString& toKeyHex, const QString& amountStr);
|
||||
Q_INVOKABLE void startBlockchain();
|
||||
Q_INVOKABLE void stopBlockchain();
|
||||
|
||||
public slots:
|
||||
void onNewBlock(const QVariantList& data);
|
||||
|
||||
signals:
|
||||
void statusChanged();
|
||||
void configPathChanged();
|
||||
void newBlockMessage(const QString& message);
|
||||
void logsCleared();
|
||||
|
||||
private:
|
||||
void setStatus(BlockchainStatus newStatus);
|
||||
|
||||
BlockchainStatus m_status;
|
||||
QString m_configPath;
|
||||
LogModel* m_logModel;
|
||||
|
||||
LogosModules* m_logos;
|
||||
BlockchainModuleProxy* m_blockchainModule;
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
#include "BlockchainPlugin.h"
|
||||
#include "BlockchainBackend.h"
|
||||
#include "LogModel.h"
|
||||
#include <QQuickWidget>
|
||||
#include <QQmlContext>
|
||||
#include <QQmlEngine>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QUrl>
|
||||
|
||||
QWidget* BlockchainPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
qDebug() << "BlockchainPlugin::createWidget called";
|
||||
@ -14,20 +17,31 @@ QWidget* BlockchainPlugin::createWidget(LogosAPI* logosAPI) {
|
||||
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||
|
||||
qmlRegisterType<BlockchainBackend>("BlockchainBackend", 1, 0, "BlockchainBackend");
|
||||
qmlRegisterType<LogModel>("BlockchainBackend", 1, 0, "LogModel");
|
||||
|
||||
BlockchainBackend* backend = new BlockchainBackend(logosAPI, quickWidget);
|
||||
|
||||
quickWidget->rootContext()->setContextProperty("backend", backend);
|
||||
|
||||
QString qmlPath = "qrc:/BlockchainView.qml";
|
||||
QString envPath = qgetenv("BLOCKCHAIN_UI_QML_PATH");
|
||||
if (!envPath.isEmpty() && QFile::exists(envPath)) {
|
||||
qmlPath = QUrl::fromLocalFile(QFileInfo(envPath).absoluteFilePath()).toString();
|
||||
qDebug() << "Loading QML from file system:" << qmlPath;
|
||||
QString qmlSource = "qrc:/qml/BlockchainView.qml";
|
||||
QString importPath = "qrc:/qml";
|
||||
|
||||
QString envPath = QString::fromUtf8(qgetenv("BLOCKCHAIN_UI_QML_PATH")).trimmed();
|
||||
if (!envPath.isEmpty()) {
|
||||
QFileInfo info(envPath);
|
||||
if (info.isDir()) {
|
||||
QString main = QDir(info.absoluteFilePath()).absoluteFilePath("BlockchainView.qml");
|
||||
if (QFile::exists(main)) {
|
||||
importPath = info.absoluteFilePath();
|
||||
qmlSource = QUrl::fromLocalFile(main).toString();
|
||||
} else {
|
||||
qWarning() << "BLOCKCHAIN_UI_QML_PATH: BlockchainView.qml not found in" << info.absoluteFilePath();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quickWidget->setSource(QUrl(qmlPath));
|
||||
|
||||
|
||||
quickWidget->engine()->addImportPath(importPath);
|
||||
quickWidget->setSource(QUrl(qmlSource));
|
||||
|
||||
if (quickWidget->status() == QQuickWidget::Error) {
|
||||
qWarning() << "BlockchainPlugin: Failed to load QML:" << quickWidget->errors();
|
||||
}
|
||||
|
||||
@ -1,291 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import QtCore
|
||||
|
||||
import BlockchainBackend
|
||||
import Logos.DesignSystem
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
QtObject {
|
||||
id: _d
|
||||
function getStatusString(status) {
|
||||
switch(status) {
|
||||
case BlockchainBackend.NotStarted: return "Not Started";
|
||||
case BlockchainBackend.Starting: return "Starting...";
|
||||
case BlockchainBackend.Running: return "Running";
|
||||
case BlockchainBackend.Stopping: return "Stopping...";
|
||||
case BlockchainBackend.Stopped: return "Stopped";
|
||||
case BlockchainBackend.Error: return "Error";
|
||||
case BlockchainBackend.ErrorNotInitialized: return "Error: Module not initialized";
|
||||
case BlockchainBackend.ErrorConfigMissing: return "Error: Config path missing";
|
||||
case BlockchainBackend.ErrorStartFailed: return "Error: Failed to start node";
|
||||
case BlockchainBackend.ErrorStopFailed: return "Error: Failed to stop node";
|
||||
case BlockchainBackend.ErrorSubscribeFailed: return "Error: Failed to subscribe to events";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
function getStatusColor(status) {
|
||||
switch(status) {
|
||||
case BlockchainBackend.Running: return Theme.palette.success;
|
||||
case BlockchainBackend.Starting: return Theme.palette.warning;
|
||||
case BlockchainBackend.Stopping: return Theme.palette.warning;
|
||||
case BlockchainBackend.NotStarted: return Theme.palette.error;
|
||||
case BlockchainBackend.Stopped: return Theme.palette.error;
|
||||
case BlockchainBackend.Error:
|
||||
case BlockchainBackend.ErrorNotInitialized:
|
||||
case BlockchainBackend.ErrorConfigMissing:
|
||||
case BlockchainBackend.ErrorStartFailed:
|
||||
case BlockchainBackend.ErrorStopFailed:
|
||||
case BlockchainBackend.ErrorSubscribeFailed: return Theme.palette.error;
|
||||
default: return Theme.palette.textSecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Theme.palette.background
|
||||
|
||||
Connections {
|
||||
target: backend
|
||||
function onLogMessage(message) {
|
||||
logsContainer.logsText += message
|
||||
}
|
||||
function onNewBlockMessage(message) {
|
||||
logsContainer.logsText += message
|
||||
}
|
||||
function onLogsCleared() {
|
||||
logsContainer.logsText = ""
|
||||
}
|
||||
}
|
||||
|
||||
SplitView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
orientation: Qt.Vertical
|
||||
|
||||
// Tpp: Status and Controls
|
||||
ColumnLayout {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 200
|
||||
SplitView.preferredHeight: implicitHeight
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// Status Card
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width * 0.9
|
||||
implicitHeight: content.implicitHeight + 2 * Theme.spacing.large
|
||||
Layout.preferredHeight: implicitHeight
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: 14//Theme.typography.primaryText
|
||||
font.bold: true
|
||||
text: _d.getStatusString(backend.status)
|
||||
color: _d.getStatusColor(backend.status)
|
||||
}
|
||||
|
||||
// Chain info
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.topMargin: -Theme.spacing.medium
|
||||
text: "Mainnet - chain ID 1"
|
||||
font.pixelSize: 12//Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
// Start/Stop Button
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 50
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed || parent.hovered ?
|
||||
Theme.palette.backgroundMuted:
|
||||
Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
enabled: !!backend.configPath && backend.status !== BlockchainBackend.Starting && backend.status !== BlockchainBackend.Stopping
|
||||
text: backend.status === BlockchainBackend.Running ? "Stop Node" : "Start Node"
|
||||
onClicked: {
|
||||
if (backend.status === BlockchainBackend.Running) {
|
||||
backend.stopBlockchain()
|
||||
} else {
|
||||
backend.startBlockchain()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Status Card
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width * 0.9
|
||||
implicitHeight: content2.implicitHeight + 2 * Theme.spacing.large
|
||||
Layout.preferredHeight: implicitHeight
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
RowLayout {
|
||||
id: content2
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
ColumnLayout {
|
||||
Text {
|
||||
text: "Current Config: "
|
||||
font.pixelSize: 14//Theme.typography.primary
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
// Config Path (collapsible/minimal)
|
||||
Text {
|
||||
text: (backend.configPath || "No file selected")
|
||||
font.pixelSize: 12//Theme.typography.secondary
|
||||
color: Theme.palette.textSecondary
|
||||
elide: Text.ElideMiddle
|
||||
}
|
||||
}
|
||||
|
||||
// Choose New Config file
|
||||
Button {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
Layout.preferredWidth: 100
|
||||
Layout.preferredHeight: 50
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed || parent.hovered ?
|
||||
Theme.palette.backgroundMuted:
|
||||
Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
text: qsTr("Change")
|
||||
onClicked: {
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
}
|
||||
|
||||
// Right: Logs
|
||||
Item {
|
||||
id: logsPane
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 200
|
||||
SplitView.fillHeight: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Logs header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
Text {
|
||||
text: "Logs"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
Button {
|
||||
text: "Clear"
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
Layout.preferredWidth: 80
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: backend.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
// Logs view (accumulated from logMessage signals)
|
||||
Item {
|
||||
id: logsContainer
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
property string logsText: ""
|
||||
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
TextArea {
|
||||
id: logsTextArea
|
||||
readOnly: true
|
||||
text: logsContainer.logsText || "No logs yet..."
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: "Monaco, Menlo, Courier, monospace"
|
||||
wrapMode: TextArea.Wrap
|
||||
selectByMouse: true
|
||||
color: Theme.palette.text
|
||||
padding: Theme.spacing.medium
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
onTextChanged: {
|
||||
cursorPosition = text.length
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
modality: Qt.NonModal
|
||||
nameFilters: ["YAML files (*.yaml)"]
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
|
||||
onAccepted: {
|
||||
backend.configPath = selectedFile
|
||||
}
|
||||
}
|
||||
}
|
||||
43
src/LogModel.cpp
Normal file
43
src/LogModel.cpp
Normal file
@ -0,0 +1,43 @@
|
||||
#include "LogModel.h"
|
||||
|
||||
int LogModel::rowCount(const QModelIndex& parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_lines.size();
|
||||
}
|
||||
|
||||
QVariant LogModel::data(const QModelIndex& index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() < 0 || index.row() >= m_lines.size())
|
||||
return QVariant();
|
||||
if (role == TextRole || role == Qt::DisplayRole)
|
||||
return m_lines.at(index.row());
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> LogModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> names;
|
||||
names[TextRole] = "text";
|
||||
return names;
|
||||
}
|
||||
|
||||
void LogModel::append(const QString& line)
|
||||
{
|
||||
const int row = m_lines.size();
|
||||
beginInsertRows(QModelIndex(), row, row);
|
||||
m_lines.append(line);
|
||||
endInsertRows();
|
||||
emit countChanged();
|
||||
}
|
||||
|
||||
void LogModel::clear()
|
||||
{
|
||||
if (m_lines.isEmpty())
|
||||
return;
|
||||
beginResetModel();
|
||||
m_lines.clear();
|
||||
endResetModel();
|
||||
emit countChanged();
|
||||
}
|
||||
26
src/LogModel.h
Normal file
26
src/LogModel.h
Normal file
@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QStringList>
|
||||
|
||||
class LogModel : public QAbstractListModel {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
|
||||
public:
|
||||
enum Roles { TextRole = Qt::UserRole + 1 };
|
||||
|
||||
explicit LogModel(QObject* parent = nullptr) : QAbstractListModel(parent) {}
|
||||
|
||||
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
Q_INVOKABLE void append(const QString& line);
|
||||
Q_INVOKABLE void clear();
|
||||
|
||||
signals:
|
||||
void countChanged();
|
||||
|
||||
private:
|
||||
QStringList m_lines;
|
||||
};
|
||||
@ -1,5 +1,11 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>BlockchainView.qml</file>
|
||||
<file>qml/BlockchainView.qml</file>
|
||||
<file>qml/controls/qmldir</file>
|
||||
<file>qml/controls/LogosButton.qml</file>
|
||||
<file>qml/views/qmldir</file>
|
||||
<file>qml/views/StatusConfigView.qml</file>
|
||||
<file>qml/views/LogsView.qml</file>
|
||||
<file>qml/views/WalletView.qml</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
|
||||
110
src/qml/BlockchainView.qml
Normal file
110
src/qml/BlockchainView.qml
Normal file
@ -0,0 +1,110 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import QtCore
|
||||
|
||||
import BlockchainBackend
|
||||
import Logos.DesignSystem
|
||||
|
||||
import views
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
QtObject {
|
||||
id: _d
|
||||
function getStatusString(status) {
|
||||
switch(status) {
|
||||
case BlockchainBackend.NotStarted: return qsTr("Not Started");
|
||||
case BlockchainBackend.Starting: return qsTr("Starting...");
|
||||
case BlockchainBackend.Running: return qsTr("Running");
|
||||
case BlockchainBackend.Stopping: return qsTr("Stopping...");
|
||||
case BlockchainBackend.Stopped: return qsTr("Stopped");
|
||||
case BlockchainBackend.Error: return qsTr("Error");
|
||||
case BlockchainBackend.ErrorNotInitialized: return qsTr("Error: Module not initialized");
|
||||
case BlockchainBackend.ErrorConfigMissing: return qsTr("Error: Config path missing");
|
||||
case BlockchainBackend.ErrorStartFailed: return qsTr("Error: Failed to start node");
|
||||
case BlockchainBackend.ErrorStopFailed: return qsTr("Error: Failed to stop node");
|
||||
case BlockchainBackend.ErrorSubscribeFailed: return qsTr("Error: Failed to subscribe to events");
|
||||
default: return qsTr("Unknown");
|
||||
}
|
||||
}
|
||||
function getStatusColor(status) {
|
||||
switch(status) {
|
||||
case BlockchainBackend.Running: return Theme.palette.success;
|
||||
case BlockchainBackend.Starting: return Theme.palette.warning;
|
||||
case BlockchainBackend.Stopping: return Theme.palette.warning;
|
||||
case BlockchainBackend.NotStarted: return Theme.palette.error;
|
||||
case BlockchainBackend.Stopped: return Theme.palette.error;
|
||||
case BlockchainBackend.Error:
|
||||
case BlockchainBackend.ErrorNotInitialized:
|
||||
case BlockchainBackend.ErrorConfigMissing:
|
||||
case BlockchainBackend.ErrorStartFailed:
|
||||
case BlockchainBackend.ErrorStopFailed:
|
||||
case BlockchainBackend.ErrorSubscribeFailed: return Theme.palette.error;
|
||||
default: return Theme.palette.textSecondary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
color: Theme.palette.background
|
||||
|
||||
SplitView {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
orientation: Qt.Vertical
|
||||
|
||||
// Top: Status/Config + Wallet side-by-side
|
||||
RowLayout {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 200
|
||||
|
||||
StatusConfigView {
|
||||
Layout.preferredWidth: parent.width / 2
|
||||
statusText: _d.getStatusString(backend.status)
|
||||
statusColor: _d.getStatusColor(backend.status)
|
||||
configPath: backend.configPath
|
||||
canStart: !!backend.configPath
|
||||
&& backend.status !== BlockchainBackend.Starting
|
||||
&& backend.status !== BlockchainBackend.Stopping
|
||||
isRunning: backend.status === BlockchainBackend.Running
|
||||
|
||||
onStartRequested: backend.startBlockchain()
|
||||
onStopRequested: backend.stopBlockchain()
|
||||
onChangeConfigRequested: fileDialog.open()
|
||||
}
|
||||
|
||||
WalletView {
|
||||
id: walletView
|
||||
Layout.preferredWidth: parent.width / 2
|
||||
|
||||
onGetBalanceRequested: function(addressHex) {
|
||||
walletView.setBalanceResult(backend.getBalance(addressHex))
|
||||
}
|
||||
onTransferRequested: function(fromKeyHex, toKeyHex, amount) {
|
||||
walletView.setTransferResult(backend.transferFunds(fromKeyHex, toKeyHex, amount))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bottom: Logs
|
||||
LogsView {
|
||||
SplitView.fillWidth: true
|
||||
SplitView.minimumHeight: 150
|
||||
|
||||
logModel: backend.logModel
|
||||
onClearRequested: backend.clearLogs()
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
modality: Qt.NonModal
|
||||
nameFilters: ["YAML files (*.yaml)"]
|
||||
currentFolder: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
|
||||
onAccepted: {
|
||||
backend.configPath = selectedFile
|
||||
}
|
||||
}
|
||||
}
|
||||
20
src/qml/controls/LogosButton.qml
Normal file
20
src/qml/controls/LogosButton.qml
Normal file
@ -0,0 +1,20 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
import Logos.DesignSystem
|
||||
|
||||
Button {
|
||||
implicitWidth: 200
|
||||
implicitHeight: 50
|
||||
|
||||
background: Rectangle {
|
||||
color: parent.pressed || parent.hovered ?
|
||||
Theme.palette.backgroundMuted :
|
||||
Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusXlarge
|
||||
border.color: parent.pressed || parent.hovered ?
|
||||
Theme.palette.overlayOrange :
|
||||
Theme.palette.border
|
||||
border.width: 1
|
||||
}
|
||||
}
|
||||
2
src/qml/controls/qmldir
Normal file
2
src/qml/controls/qmldir
Normal file
@ -0,0 +1,2 @@
|
||||
module controls
|
||||
LogosButton 1.0 LogosButton.qml
|
||||
92
src/qml/views/LogsView.qml
Normal file
92
src/qml/views/LogsView.qml
Normal file
@ -0,0 +1,92 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.DesignSystem
|
||||
|
||||
import controls
|
||||
|
||||
Control {
|
||||
id: root
|
||||
|
||||
// --- Public API ---
|
||||
required property var logModel // LogModel (QAbstractListModel with "text" role)
|
||||
|
||||
signal clearRequested()
|
||||
|
||||
background: Rectangle {
|
||||
color: Theme.palette.background
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Header
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
Text {
|
||||
text: qsTr("Logs")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
Item { Layout.fillWidth: true }
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Clear")
|
||||
Layout.preferredWidth: 80
|
||||
Layout.preferredHeight: 32
|
||||
onClicked: root.clearRequested()
|
||||
}
|
||||
}
|
||||
|
||||
// Log list
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
color: Theme.palette.backgroundSecondary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ListView {
|
||||
id: logsListView
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
model: root.logModel
|
||||
spacing: 2
|
||||
|
||||
delegate: Text {
|
||||
text: model.text
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.family: Theme.typography.publicSans
|
||||
color: Theme.palette.text
|
||||
width: logsListView.width
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: !root.logModel || root.logModel.count === 0
|
||||
anchors.centerIn: parent
|
||||
text: qsTr("No logs yet...")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root.logModel
|
||||
function onCountChanged() {
|
||||
if (root.logModel.count > 0)
|
||||
logsListView.positionViewAtEnd()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
117
src/qml/views/StatusConfigView.qml
Normal file
117
src/qml/views/StatusConfigView.qml
Normal file
@ -0,0 +1,117 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.DesignSystem
|
||||
import controls
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// --- Public API ---
|
||||
required property string statusText
|
||||
required property color statusColor
|
||||
required property string configPath
|
||||
required property bool canStart
|
||||
required property bool isRunning
|
||||
|
||||
signal startRequested()
|
||||
signal stopRequested()
|
||||
signal changeConfigRequested()
|
||||
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
// Status Card
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.preferredWidth: parent.width * 0.9
|
||||
Layout.preferredHeight: implicitHeight
|
||||
implicitHeight: statusContent.implicitHeight + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: statusContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
font.bold: true
|
||||
text: root.statusText
|
||||
color: root.statusColor
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.alignment: Qt.AlignLeft
|
||||
Layout.topMargin: -Theme.spacing.medium
|
||||
text: qsTr("Mainnet - chain ID 1")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 50
|
||||
enabled: root.canStart
|
||||
text: root.isRunning ? qsTr("Stop Node") : qsTr("Start Node")
|
||||
onClicked: root.isRunning ? root.stopRequested() : root.startRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Config Card
|
||||
Rectangle {
|
||||
Layout.preferredWidth: parent.width * 0.9
|
||||
Layout.preferredHeight: implicitHeight
|
||||
implicitHeight: configContent.implicitHeight + 2 * Theme.spacing.large
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: configContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
Text {
|
||||
text: qsTr("Current Config: ")
|
||||
font.pixelSize: Theme.typography.primaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
Text {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -Theme.spacing.medium
|
||||
text: root.configPath || qsTr("No file selected")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: 50
|
||||
text: qsTr("Change")
|
||||
onClicked: root.changeConfigRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item { Layout.fillHeight: true }
|
||||
}
|
||||
141
src/qml/views/WalletView.qml
Normal file
141
src/qml/views/WalletView.qml
Normal file
@ -0,0 +1,141 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import Logos.DesignSystem
|
||||
|
||||
import controls
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
|
||||
// --- Public API ---
|
||||
signal getBalanceRequested(string addressHex)
|
||||
signal transferRequested(string fromKeyHex, string toKeyHex, string amount)
|
||||
|
||||
// Call these from the parent to display results
|
||||
function setBalanceResult(text) {
|
||||
balanceResultText.text = text
|
||||
}
|
||||
function setTransferResult(text) {
|
||||
transferResultText.text = text
|
||||
}
|
||||
|
||||
spacing: Theme.spacing.medium
|
||||
|
||||
// Get balance card
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: balanceCol.implicitHeight
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: balanceCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
Text {
|
||||
text: qsTr("Get balance")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
CustomTextFeild {
|
||||
id: balanceAddressField
|
||||
placeholderText: qsTr("Wallet address (64 hex chars)")
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Get balance")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
onClicked: root.getBalanceRequested(balanceAddressField.text)
|
||||
}
|
||||
|
||||
Text {
|
||||
id: balanceResultText
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer funds card
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: transferCol.height
|
||||
color: Theme.palette.backgroundTertiary
|
||||
radius: Theme.spacing.radiusLarge
|
||||
border.color: Theme.palette.border
|
||||
border.width: 1
|
||||
|
||||
ColumnLayout {
|
||||
id: transferCol
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacing.large
|
||||
spacing: Theme.spacing.large
|
||||
|
||||
Text {
|
||||
text: qsTr("Transfer funds")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
font.bold: true
|
||||
color: Theme.palette.text
|
||||
}
|
||||
|
||||
CustomTextFeild {
|
||||
placeholderText: qsTr("From key (64 hex chars)")
|
||||
}
|
||||
|
||||
CustomTextFeild {
|
||||
id: transferToField
|
||||
placeholderText: qsTr("To key (64 hex chars)")
|
||||
}
|
||||
|
||||
CustomTextFeild {
|
||||
placeholderText: qsTr("Amount")
|
||||
}
|
||||
|
||||
LogosButton {
|
||||
text: qsTr("Transfer")
|
||||
Layout.alignment: Qt.AlignRight
|
||||
onClicked: root.transferRequested(transferFromField.text, transferToField.text, transferAmountField.text)
|
||||
}
|
||||
|
||||
Text {
|
||||
id: transferResultText
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
color: Theme.palette.textSecondary
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Theme.spacing.small
|
||||
}
|
||||
|
||||
component CustomTextFeild: TextField {
|
||||
id: textField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: qsTr("From key (64 hex chars)")
|
||||
font.pixelSize: Theme.typography.secondaryText
|
||||
|
||||
background: Rectangle {
|
||||
radius: Theme.spacing.radiusSmall
|
||||
color: Theme.palette.backgroundSecondary
|
||||
border.color: textField.activeFocus ?
|
||||
Theme.palette.overlayOrange :
|
||||
Theme.palette.backgroundElevated
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/qml/views/qmldir
Normal file
4
src/qml/views/qmldir
Normal file
@ -0,0 +1,4 @@
|
||||
module views
|
||||
StatusConfigView 1.0 StatusConfigView.qml
|
||||
LogsView 1.0 LogsView.qml
|
||||
WalletView 1.0 WalletView.qml
|
||||
Loading…
x
Reference in New Issue
Block a user