diff --git a/CMakeLists.txt b/CMakeLists.txt index f17745d..43bd1da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ) diff --git a/README.md b/README.md index 116bc47..f04238d 100644 --- a/README.md +++ b/README.md @@ -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. \ No newline at end of file diff --git a/flake.lock b/flake.lock index a9c102c..451c2f7 100644 --- a/flake.lock +++ b/flake.lock @@ -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": { diff --git a/flake.nix b/flake.nix index bdc1009..6c28395 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; diff --git a/metadata.json b/metadata.json index 981bbc5..60ecd3b 100644 --- a/metadata.json +++ b/metadata.json @@ -14,6 +14,8 @@ "src/BlockchainPlugin.h", "src/BlockchainBackend.cpp", "src/BlockchainBackend.h", + "src/LogModel.cpp", + "src/LogModel.h", "src/blockchain_resources.qrc" ] }, diff --git a/src/BlockchainBackend.cpp b/src/BlockchainBackend.cpp index 32fe282..099d6bc 100644 --- a/src/BlockchainBackend.cpp +++ b/src/BlockchainBackend.cpp @@ -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); } diff --git a/src/BlockchainBackend.h b/src/BlockchainBackend.h index 1e16da3..c6b6dc9 100644 --- a/src/BlockchainBackend.h +++ b/src/BlockchainBackend.h @@ -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().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; diff --git a/src/BlockchainPlugin.cpp b/src/BlockchainPlugin.cpp index 896777b..b52f7a6 100644 --- a/src/BlockchainPlugin.cpp +++ b/src/BlockchainPlugin.cpp @@ -1,11 +1,14 @@ #include "BlockchainPlugin.h" #include "BlockchainBackend.h" +#include "LogModel.h" #include #include #include #include -#include +#include #include +#include +#include QWidget* BlockchainPlugin::createWidget(LogosAPI* logosAPI) { qDebug() << "BlockchainPlugin::createWidget called"; @@ -14,20 +17,31 @@ QWidget* BlockchainPlugin::createWidget(LogosAPI* logosAPI) { quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); qmlRegisterType("BlockchainBackend", 1, 0, "BlockchainBackend"); + qmlRegisterType("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(); } diff --git a/src/BlockchainView.qml b/src/BlockchainView.qml deleted file mode 100644 index e82abc0..0000000 --- a/src/BlockchainView.qml +++ /dev/null @@ -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 - } - } -} diff --git a/src/LogModel.cpp b/src/LogModel.cpp new file mode 100644 index 0000000..35d9281 --- /dev/null +++ b/src/LogModel.cpp @@ -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 LogModel::roleNames() const +{ + QHash 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(); +} diff --git a/src/LogModel.h b/src/LogModel.h new file mode 100644 index 0000000..215de63 --- /dev/null +++ b/src/LogModel.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +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 roleNames() const override; + + Q_INVOKABLE void append(const QString& line); + Q_INVOKABLE void clear(); + +signals: + void countChanged(); + +private: + QStringList m_lines; +}; diff --git a/src/blockchain_resources.qrc b/src/blockchain_resources.qrc index 3eaa825..d60a6e9 100644 --- a/src/blockchain_resources.qrc +++ b/src/blockchain_resources.qrc @@ -1,5 +1,11 @@ - BlockchainView.qml + qml/BlockchainView.qml + qml/controls/qmldir + qml/controls/LogosButton.qml + qml/views/qmldir + qml/views/StatusConfigView.qml + qml/views/LogsView.qml + qml/views/WalletView.qml diff --git a/src/qml/BlockchainView.qml b/src/qml/BlockchainView.qml new file mode 100644 index 0000000..86d6a52 --- /dev/null +++ b/src/qml/BlockchainView.qml @@ -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 + } + } +} diff --git a/src/qml/controls/LogosButton.qml b/src/qml/controls/LogosButton.qml new file mode 100644 index 0000000..bcb820e --- /dev/null +++ b/src/qml/controls/LogosButton.qml @@ -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 + } +} diff --git a/src/qml/controls/qmldir b/src/qml/controls/qmldir new file mode 100644 index 0000000..57997a8 --- /dev/null +++ b/src/qml/controls/qmldir @@ -0,0 +1,2 @@ +module controls +LogosButton 1.0 LogosButton.qml diff --git a/src/qml/views/LogsView.qml b/src/qml/views/LogsView.qml new file mode 100644 index 0000000..2dcd083 --- /dev/null +++ b/src/qml/views/LogsView.qml @@ -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() + } + } + } + } + } +} diff --git a/src/qml/views/StatusConfigView.qml b/src/qml/views/StatusConfigView.qml new file mode 100644 index 0000000..f71f42b --- /dev/null +++ b/src/qml/views/StatusConfigView.qml @@ -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 } +} diff --git a/src/qml/views/WalletView.qml b/src/qml/views/WalletView.qml new file mode 100644 index 0000000..31297f5 --- /dev/null +++ b/src/qml/views/WalletView.qml @@ -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 + } + } +} diff --git a/src/qml/views/qmldir b/src/qml/views/qmldir new file mode 100644 index 0000000..8de2446 --- /dev/null +++ b/src/qml/views/qmldir @@ -0,0 +1,4 @@ +module views +StatusConfigView 1.0 StatusConfigView.qml +LogsView 1.0 LogsView.qml +WalletView 1.0 WalletView.qml \ No newline at end of file