feat: Adding version 1 of UI for the Blockchain app

This commit is contained in:
Khushboo Mehta 2026-02-13 18:06:21 +01:00
parent 6c331f161f
commit f679c0bb52
19 changed files with 651 additions and 327 deletions

View File

@ -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
)

View File

@ -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
View File

@ -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": {

View File

@ -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";

View File

@ -14,6 +14,8 @@
"src/BlockchainPlugin.h",
"src/BlockchainBackend.cpp",
"src/BlockchainBackend.h",
"src/LogModel.cpp",
"src/LogModel.h",
"src/blockchain_resources.qrc"
]
},

View File

@ -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);
}

View File

@ -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;

View File

@ -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();
}

View File

@ -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
View 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
View 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;
};

View File

@ -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
View 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
}
}
}

View 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
View File

@ -0,0 +1,2 @@
module controls
LogosButton 1.0 LogosButton.qml

View 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()
}
}
}
}
}
}

View 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 }
}

View 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
View File

@ -0,0 +1,4 @@
module views
StatusConfigView 1.0 StatusConfigView.qml
LogsView 1.0 LogsView.qml
WalletView 1.0 WalletView.qml