diff --git a/CMakeLists.txt b/CMakeLists.txt index e309dc5..76aa1c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,6 +82,10 @@ endif() # Source files set(SOURCES + src/LEZAccountFilterModel.cpp + src/LEZAccountFilterModel.h + src/LEZWalletAccountModel.cpp + src/LEZWalletAccountModel.h src/LEZWalletPlugin.cpp src/LEZWalletPlugin.h src/LEZWalletBackend.cpp diff --git a/app/main.cpp b/app/main.cpp index ee732b7..695a33d 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -3,8 +3,6 @@ #include #include #include -#include -#include extern "C" { void logos_core_set_plugins_dir(const char* plugins_dir); diff --git a/flake.lock b/flake.lock index eae8aaf..9d8419b 100644 --- a/flake.lock +++ b/flake.lock @@ -528,11 +528,11 @@ ] }, "locked": { - "lastModified": 1771705420, - "narHash": "sha256-DySEiVMYk2FWLJWar8rlPqfKDeWMqh5EqXSkn2csRO0=", + "lastModified": 1771838299, + "narHash": "sha256-Uf45wbh2q5ewoiw4u04YImc2Gij3OXIfbB5NYpUm5dw=", "owner": "logos-co", "repo": "logos-design-system", - "rev": "063c4b46accc621bc85fa8baab46b31ef65f3957", + "rev": "fc6f52d85a008aa1bb513f6b42648df4bcf0713d", "type": "github" }, "original": { diff --git a/src/LEZAccountFilterModel.cpp b/src/LEZAccountFilterModel.cpp new file mode 100644 index 0000000..c43046f --- /dev/null +++ b/src/LEZAccountFilterModel.cpp @@ -0,0 +1,38 @@ +#include "LEZAccountFilterModel.h" + +LEZAccountFilterModel::LEZAccountFilterModel(QObject* parent) + : QSortFilterProxyModel(parent) +{ + connect(this, &QAbstractItemModel::rowsInserted, this, &LEZAccountFilterModel::countChanged); + connect(this, &QAbstractItemModel::rowsRemoved, this, &LEZAccountFilterModel::countChanged); + connect(this, &QAbstractItemModel::modelReset, this, &LEZAccountFilterModel::countChanged); + connect(this, &QAbstractItemModel::layoutChanged, this, &LEZAccountFilterModel::countChanged); +} + +void LEZAccountFilterModel::setFilterByPublic(bool value) +{ + if (m_filterByPublic == value) + return; + m_filterByPublic = value; + invalidateFilter(); + emit filterByPublicChanged(); + emit countChanged(); +} + +bool LEZAccountFilterModel::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const +{ + if (!sourceModel()) + return false; + const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent); + const bool isPublic = sourceModel()->data(idx, LEZWalletAccountModel::IsPublicRole).toBool(); + return isPublic == m_filterByPublic; +} + +int LEZAccountFilterModel::rowForAddress(const QString& address) const +{ + for (int i = 0; i < rowCount(); ++i) { + if (data(index(i, 0), LEZWalletAccountModel::AddressRole).toString() == address) + return i; + } + return -1; +} diff --git a/src/LEZAccountFilterModel.h b/src/LEZAccountFilterModel.h new file mode 100644 index 0000000..827d1b4 --- /dev/null +++ b/src/LEZAccountFilterModel.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include "LEZWalletAccountModel.h" + +class LEZAccountFilterModel : public QSortFilterProxyModel { + Q_OBJECT + Q_PROPERTY(bool filterByPublic READ filterByPublic WRITE setFilterByPublic NOTIFY filterByPublicChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit LEZAccountFilterModel(QObject* parent = nullptr); + + bool filterByPublic() const { return m_filterByPublic; } + void setFilterByPublic(bool value); + + int count() const { return rowCount(); } + + Q_INVOKABLE int rowForAddress(const QString& address) const; + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override; + +signals: + void filterByPublicChanged(); + void countChanged(); + +private: + bool m_filterByPublic = true; +}; diff --git a/src/LEZWalletAccountModel.cpp b/src/LEZWalletAccountModel.cpp new file mode 100644 index 0000000..2381024 --- /dev/null +++ b/src/LEZWalletAccountModel.cpp @@ -0,0 +1,78 @@ +#include "LEZWalletAccountModel.h" +#include + +LEZWalletAccountModel::LEZWalletAccountModel(QObject* parent) + : QAbstractListModel(parent) +{ +} + +int LEZWalletAccountModel::rowCount(const QModelIndex& parent) const +{ + if (parent.isValid()) + return 0; + return m_entries.size(); +} + +QVariant LEZWalletAccountModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.size()) + return QVariant(); + + const LEZWalletAccountEntry& e = m_entries.at(index.row()); + switch (role) { + case NameRole: return e.name; + case AddressRole: return e.address; + case BalanceRole: return e.balance; + case IsPublicRole: return e.isPublic; + default: return QVariant(); + } +} + +QHash LEZWalletAccountModel::roleNames() const +{ + return { + { NameRole, "name" }, + { AddressRole, "address" }, + { BalanceRole, "balance" }, + { IsPublicRole, "isPublic" } + }; +} + +void LEZWalletAccountModel::replaceFromJsonArray(const QJsonArray& arr) +{ + beginResetModel(); + int oldCount = m_entries.size(); + m_entries.clear(); + int idx = 0; + for (const QJsonValue& v : arr) { + LEZWalletAccountEntry e; + e.name = QStringLiteral("Account %1").arg(++idx); + e.balance = QString(); + if (v.isObject()) { + const QJsonObject obj = v.toObject(); + e.address = obj.value(QStringLiteral("account_id")).toString(); + e.isPublic = obj.value(QStringLiteral("is_public")).toBool(true); + } else { + e.address = v.toString(); + e.isPublic = true; + } + m_entries.append(e); + } + endResetModel(); + if (oldCount != m_entries.size()) + emit countChanged(); +} + +void LEZWalletAccountModel::setBalanceByAddress(const QString& address, const QString& balance) +{ + for (int i = 0; i < m_entries.size(); ++i) { + if (m_entries.at(i).address == address) { + if (m_entries.at(i).balance != balance) { + m_entries[i].balance = balance; + QModelIndex idx = index(i, 0); + emit dataChanged(idx, idx, { BalanceRole }); + } + return; + } + } +} diff --git a/src/LEZWalletAccountModel.h b/src/LEZWalletAccountModel.h new file mode 100644 index 0000000..6092c8f --- /dev/null +++ b/src/LEZWalletAccountModel.h @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include + +struct LEZWalletAccountEntry { + QString name; + QString address; + QString balance; + bool isPublic = true; +}; + +class LEZWalletAccountModel : public QAbstractListModel { + Q_OBJECT + Q_PROPERTY(int count READ count NOTIFY countChanged) +public: + enum Role { + NameRole = Qt::UserRole + 1, + AddressRole, + BalanceRole, + IsPublicRole + }; + Q_ENUM(Role) + + explicit LEZWalletAccountModel(QObject* parent = nullptr); + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + + void replaceFromJsonArray(const QJsonArray& arr); + void setBalanceByAddress(const QString& address, const QString& balance); + int count() const { return m_entries.size(); } + +signals: + void countChanged(); + +private: + QVector m_entries; +}; diff --git a/src/LEZWalletBackend.cpp b/src/LEZWalletBackend.cpp index b1b7434..a09646d 100644 --- a/src/LEZWalletBackend.cpp +++ b/src/LEZWalletBackend.cpp @@ -1,5 +1,7 @@ #include "LEZWalletBackend.h" +#include #include +#include #include #include @@ -17,9 +19,13 @@ LEZWalletBackend::LEZWalletBackend(LogosAPI* logosAPI, QObject* parent) m_isWalletOpen(false), m_lastSyncedBlock(0), m_currentBlockHeight(0), + m_accountModel(new LEZWalletAccountModel(this)), + m_filteredAccountModel(new LEZAccountFilterModel(this)), m_logosAPI(nullptr), m_walletClient(nullptr) { + m_filteredAccountModel->setSourceModel(m_accountModel); + QSettings s(SETTINGS_ORG, SETTINGS_APP); m_configPath = s.value(CONFIG_PATH_KEY).toString(); m_storagePath = s.value(STORAGE_PATH_KEY).toString(); @@ -104,9 +110,18 @@ void LEZWalletBackend::refreshAccounts() if (result.isValid() && result.canConvert()) { arr = result.toJsonArray(); } - if (m_accounts != arr) { - m_accounts = std::move(arr); - emit accountsChanged(); + m_accountModel->replaceFromJsonArray(arr); + emit accountModelChanged(); +} + +void LEZWalletBackend::refreshBalances() +{ + if (!m_walletClient || !m_accountModel) return; + for (int i = 0; i < m_accountModel->count(); ++i) { + const QModelIndex idx = m_accountModel->index(i, 0); + const QString addr = m_accountModel->data(idx, LEZWalletAccountModel::AddressRole).toString(); + const bool isPub = m_accountModel->data(idx, LEZWalletAccountModel::IsPublicRole).toBool(); + m_accountModel->setBalanceByAddress(addr, getBalance(addr, isPub)); } } @@ -239,3 +254,18 @@ bool LEZWalletBackend::createNew( refreshSequencerAddr(); return true; } + +int LEZWalletBackend::indexOfAddressInModel(QObject* model, const QString& address) const +{ + auto* m = qobject_cast(model); + if (!m || address.isEmpty()) + return -1; + const int role = m->roleNames().key("address", -1); + if (role < 0) + return -1; + for (int i = 0; i < m->rowCount(); ++i) { + if (m->data(m->index(i, 0), role).toString() == address) + return i; + } + return -1; +} diff --git a/src/LEZWalletBackend.h b/src/LEZWalletBackend.h index d0dd488..bec5a36 100644 --- a/src/LEZWalletBackend.h +++ b/src/LEZWalletBackend.h @@ -2,10 +2,13 @@ #include #include -#include +#include "LEZAccountFilterModel.h" +#include "LEZWalletAccountModel.h" #include "logos_api.h" #include "logos_api_client.h" +class QAbstractItemModel; + class LEZWalletBackend : public QObject { Q_OBJECT @@ -13,7 +16,8 @@ public: Q_PROPERTY(bool isWalletOpen READ isWalletOpen NOTIFY isWalletOpenChanged) Q_PROPERTY(QString configPath READ configPath WRITE setConfigPath NOTIFY configPathChanged) Q_PROPERTY(QString storagePath READ storagePath WRITE setStoragePath NOTIFY storagePathChanged) - Q_PROPERTY(QJsonArray accounts READ accounts NOTIFY accountsChanged) + Q_PROPERTY(LEZWalletAccountModel* accountModel READ accountModel NOTIFY accountModelChanged) + Q_PROPERTY(LEZAccountFilterModel* filteredAccountModel READ filteredAccountModel NOTIFY filteredAccountModelChanged) Q_PROPERTY(quint64 lastSyncedBlock READ lastSyncedBlock NOTIFY lastSyncedBlockChanged) Q_PROPERTY(quint64 currentBlockHeight READ currentBlockHeight NOTIFY currentBlockHeightChanged) Q_PROPERTY(QString sequencerAddr READ sequencerAddr NOTIFY sequencerAddrChanged) @@ -24,7 +28,8 @@ public: bool isWalletOpen() const { return m_isWalletOpen; } QString configPath() const { return m_configPath; } QString storagePath() const { return m_storagePath; } - QJsonArray accounts() const { return m_accounts; } + LEZWalletAccountModel* accountModel() const { return m_accountModel; } + LEZAccountFilterModel* filteredAccountModel() const { return m_filteredAccountModel; } quint64 lastSyncedBlock() const { return m_lastSyncedBlock; } quint64 currentBlockHeight() const { return m_currentBlockHeight; } QString sequencerAddr() const { return m_sequencerAddr; } @@ -36,6 +41,7 @@ public: Q_INVOKABLE QString createAccountPrivate(); Q_INVOKABLE void refreshAccounts(); Q_INVOKABLE QString getBalance(const QString& accountIdHex, bool isPublic); + Q_INVOKABLE void refreshBalances(); Q_INVOKABLE QString getPublicAccountKey(const QString& accountIdHex); Q_INVOKABLE QString getPrivateAccountKeys(const QString& accountIdHex); Q_INVOKABLE bool syncToBlock(quint64 blockId); @@ -51,12 +57,14 @@ public: const QString& configPath, const QString& storagePath, const QString& password); + Q_INVOKABLE int indexOfAddressInModel(QObject* model, const QString& address) const; signals: void isWalletOpenChanged(); void configPathChanged(); void storagePathChanged(); - void accountsChanged(); + void accountModelChanged(); + void filteredAccountModelChanged(); void lastSyncedBlockChanged(); void currentBlockHeightChanged(); void sequencerAddrChanged(); @@ -70,7 +78,8 @@ private: bool m_isWalletOpen; QString m_configPath; QString m_storagePath; - QJsonArray m_accounts; + LEZWalletAccountModel* m_accountModel; + LEZAccountFilterModel* m_filteredAccountModel; quint64 m_lastSyncedBlock; quint64 m_currentBlockHeight; QString m_sequencerAddr; diff --git a/src/LEZWalletPlugin.cpp b/src/LEZWalletPlugin.cpp index 96f981b..9688a69 100644 --- a/src/LEZWalletPlugin.cpp +++ b/src/LEZWalletPlugin.cpp @@ -1,5 +1,6 @@ #include "LEZWalletPlugin.h" #include "LEZWalletBackend.h" +#include "LEZAccountFilterModel.h" #include #include #include @@ -16,6 +17,7 @@ QWidget* LEZWalletPlugin::createWidget(LogosAPI* logosAPI) { quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); qmlRegisterType("LEZWalletBackend", 1, 0, "LEZWalletBackend"); + qmlRegisterType("LEZWalletBackend", 1, 0, "LEZAccountFilterModel"); LEZWalletBackend* backend = new LEZWalletBackend(logosAPI, quickWidget); quickWidget->rootContext()->setContextProperty("backend", backend); diff --git a/src/qml/ExecutionZoneWalletView.qml b/src/qml/ExecutionZoneWalletView.qml index f8e179e..9d00358 100644 --- a/src/qml/ExecutionZoneWalletView.qml +++ b/src/qml/ExecutionZoneWalletView.qml @@ -1,7 +1,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts -import QtCore import LEZWalletBackend import Logos.Theme @@ -27,18 +26,42 @@ Rectangle { } } - - // Page 1: Main screen placeholder (AccountsView / SendView added later) Component { id: mainView - Rectangle { - anchors.fill: parent - color: Theme.palette.background - LogosText { - anchors.centerIn: parent - text: qsTr("Wallet") - font.pixelSize: Theme.typography.secondaryText - font.bold: true + DashboardView { + id: dashboardView + accountModel: backend ? backend.accountModel : null + filteredAccountModel: backend ? backend.filteredAccountModel : null + + onCreatePublicAccountRequested: { + if (!backend) { + console.warning("backend is null") + return + } + backend.createAccountPublic() + } + onCreatePrivateAccountRequested: { + if (!backend) { + console.warning("backend is null") + return + } + backend.createAccountPrivate() + } + onFetchBalancesRequested: { + if (!backend) { + console.warning("backend is null") + return + } + backend.refreshBalances() + } + onTransferRequested: function(isPublic, fromId, toAddress, amount) { + if (!backend) { + console.warning("backend is null") + return + } + dashboardView.transferResult = isPublic + ? backend.transferPublic(fromId, toAddress, amount) + : backend.transferPrivate(fromId, toAddress, amount) } } } diff --git a/src/qml/controls/AccountDelegate.qml b/src/qml/controls/AccountDelegate.qml new file mode 100644 index 0000000..1d774cb --- /dev/null +++ b/src/qml/controls/AccountDelegate.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +ItemDelegate { + id: root + + implicitHeight: 80 + leftPadding: Theme.spacing.medium + rightPadding: Theme.spacing.medium + topPadding: Theme.spacing.medium + bottomPadding: Theme.spacing.medium + + background: Rectangle { + color: root.highlighted ? Theme.palette.backgroundMuted : "transparent" + radius: Theme.spacing.radiusSmall + } + + contentItem: RowLayout { + spacing: Theme.spacing.small + + LogosText { + text: model.name + font.pixelSize: Theme.typography.secondaryText + font.bold: true + } + + Rectangle { + Layout.preferredWidth: tagLabel.implicitWidth + Theme.spacing.small * 2 + Layout.preferredHeight: tagLabel.implicitHeight + 4 + radius: 2 + color: model.isPublic ? Theme.palette.backgroundElevated : Theme.palette.backgroundSecondary + + LogosText { + id: tagLabel + anchors.centerIn: parent + text: model.isPublic ? qsTr("Public") : qsTr("Private") + font.pixelSize: Theme.typography.captionText + color: Theme.palette.textSecondary + } + } + + Item { Layout.fillWidth: true } + + LogosText { + text: model.balance && model.balance.length > 0 ? model.balance : "—" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + } +} diff --git a/src/qml/popups/CreateAccountDialog.qml b/src/qml/popups/CreateAccountDialog.qml new file mode 100644 index 0000000..363ff94 --- /dev/null +++ b/src/qml/popups/CreateAccountDialog.qml @@ -0,0 +1,100 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +Popup { + id: root + + signal createPublicRequested() + signal createPrivateRequested() + + modal: true + dim: true + padding: Theme.spacing.large + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + // Center in overlay (main window when modal) + parent: Overlay.overlay + anchors.centerIn: parent + // width: contentWrapper.width + leftPadding + rightPadding + // height: contentWrapper.height + topPadding + bottomPadding + + background: Rectangle { + color: Theme.palette.backgroundSecondary + radius: Theme.spacing.radiusXlarge + border.color: Theme.palette.backgroundElevated + } + + contentItem: ColumnLayout { + id: contentLayout + width: parent.width + spacing: Theme.spacing.large + + LogosText { + text: qsTr("Create account") + font.pixelSize: Theme.typography.titleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + + LogosText { + text: qsTr("Choose account type.") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + Layout.topMargin: -Theme.spacing.small + } + + TabBar { + id: tabBar + Layout.preferredWidth: 200 + currentIndex: 0 + + background: Rectangle { + color: Theme.palette.backgroundSecondary + radius: Theme.spacing.radiusSmall + } + + LogosTabButton { + text: qsTr("Public") + } + + LogosTabButton { + text: qsTr("Private") + } + } + + LogosText { + text: tabBar.currentIndex === 0 + ? qsTr("Address visible. Balance on-chain.") + : qsTr("Private balance and activity.") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + wrapMode: Text.WordWrap + Layout.fillWidth: true + } + + RowLayout { + Layout.topMargin: Theme.spacing.medium + spacing: Theme.spacing.medium + Layout.fillWidth: true + Item { Layout.fillWidth: true } + LogosButton { + text: qsTr("Cancel") + onClicked: root.close() + } + LogosButton { + text: qsTr("Create") + onClicked: { + if (tabBar.currentIndex === 0) + root.createPublicRequested() + else + root.createPrivateRequested() + root.close() + } + } + } + } +} diff --git a/src/qml/views/AccountsPanel.qml b/src/qml/views/AccountsPanel.qml new file mode 100644 index 0000000..b316b61 --- /dev/null +++ b/src/qml/views/AccountsPanel.qml @@ -0,0 +1,95 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls +// TODO: remove relative paths and use qmldir instead +import "../controls" +import "../popups" + +Rectangle { + id: root + + // --- Public API: data in --- + property var accountModel: null + + // --- Public API: signals out --- + signal createPublicAccountRequested() + signal createPrivateAccountRequested() + signal fetchBalancesRequested() + + radius: Theme.spacing.radiusXlarge + color: Theme.palette.backgroundSecondary + + CreateAccountDialog { + id: createAccountDialog + onCreatePublicRequested: root.createPublicAccountRequested() + onCreatePrivateRequested: root.createPrivateAccountRequested() + } + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.medium + + // Header row + RowLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Theme.spacing.medium + + LogosText { + text: qsTr("Accounts") + font.pixelSize: Theme.typography.titleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + + Item { Layout.fillWidth: true } + + LogosButton { + Layout.preferredHeight: 40 + Layout.preferredWidth: 80 + text: qsTr("+ Create") + onClicked: createAccountDialog.open() + } + } + + // Empty state (when no real model and we don't show showcase) + LogosText { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.topMargin: Theme.spacing.xlarge + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + text: qsTr("Add a new account to get started") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + visible: !listView.visible + } + + // Account ListView (real model when set and non-empty; otherwise showcase so delegate is visible) + ListView { + id: listView + Layout.fillWidth: true + Layout.fillHeight: true + visible: (accountModel && accountModel.count > 0) || !accountModel + clip: true + spacing: Theme.spacing.small + model: accountModel && accountModel.count > 0 ? root.accountModel: null + + delegate: AccountDelegate { + width: listView.width + } + } + + // Footer: Fetch / Refresh Balances + LogosButton { + Layout.fillWidth: true + text: qsTr("Refresh Balances") + onClicked: root.fetchBalancesRequested() + visible: listView.visible + } + } +} diff --git a/src/qml/views/DashboardView.qml b/src/qml/views/DashboardView.qml new file mode 100644 index 0000000..001d8b9 --- /dev/null +++ b/src/qml/views/DashboardView.qml @@ -0,0 +1,54 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + color: Theme.palette.background + + // --- Public API: input properties (set by parent / MainView) --- + property var accountModel: null + property var filteredAccountModel: null + property string transferResult: "" + + // --- Public API: output signals (parent connects and calls backend) --- + signal createPublicAccountRequested() + signal createPrivateAccountRequested() + signal fetchBalancesRequested() + signal transferRequested(bool isPublic, string fromAccountId, string toAddress, string amount) + + RowLayout { + anchors.fill: parent + anchors.margins: Theme.spacing.xlarge + spacing: Theme.spacing.large + + AccountsPanel { + id: accountsPanel + Layout.preferredWidth: parent ? parent.width * 0.40 : 400 + Layout.fillHeight: true + + accountModel: root.accountModel + + onCreatePublicAccountRequested: root.createPublicAccountRequested() + onCreatePrivateAccountRequested: root.createPrivateAccountRequested() + onFetchBalancesRequested: root.fetchBalancesRequested() + } + + TransferPanel { + id: transferPanel + Layout.fillWidth: true + Layout.fillHeight: true + + fromAccountModel: root.filteredAccountModel + transferResult: root.transferResult + + onTransferRequested: function(isPublic, fromId, toAddress, amount) { + root.transferRequested(isPublic, fromId, toAddress, amount) + } + } + } +} diff --git a/src/qml/views/OnboardingView.qml b/src/qml/views/OnboardingView.qml index ae32c1f..d2c122e 100644 --- a/src/qml/views/OnboardingView.qml +++ b/src/qml/views/OnboardingView.qml @@ -2,7 +2,6 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Dialogs -import QtCore import Logos.Theme import Logos.Controls diff --git a/src/qml/views/TransferPanel.qml b/src/qml/views/TransferPanel.qml new file mode 100644 index 0000000..8e260ae --- /dev/null +++ b/src/qml/views/TransferPanel.qml @@ -0,0 +1,237 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Logos.Theme +import Logos.Controls + +Rectangle { + id: root + + // --- Public API: data in --- + property var fromAccountModel: null // LEZAccountFilterModel from backend (filtered by public/private) + property string transferResult: "" + + // --- Public API: signals out --- + signal transferRequested(bool isPublic, string fromAccountId, string toAddress, string amount) + + readonly property int fromFilterCount: fromAccountModel ? fromAccountModel.count : 0 + + QtObject { + id: d + readonly property bool sendEnabled: toField && amountField && manualFromField + && toField.text.length > 0 && amountField.text.length > 0 + && ((fromFilterCount > 0 && fromCombo.currentIndex >= 0) + || (fromFilterCount === 0 && manualFromField.text.trim().length > 0)) + } + + Binding { + target: fromAccountModel + property: "filterByPublic" + value: transferTypeBar.currentIndex === 0 + when: fromAccountModel != null + } + + radius: Theme.spacing.radiusXlarge + color: Theme.palette.backgroundSecondary + + ColumnLayout { + anchors.fill: parent + anchors.margins: Theme.spacing.large + spacing: Theme.spacing.large + + LogosText { + text: qsTr("Transfer") + font.pixelSize: Theme.typography.titleText + font.weight: Theme.typography.weightBold + color: Theme.palette.text + } + + // Transfer type toggle + TabBar { + id: transferTypeBar + Layout.preferredWidth: 200 + currentIndex: 0 + + background: Rectangle { + color: Theme.palette.backgroundSecondary + radius: Theme.spacing.radiusSmall + } + + LogosTabButton { + text: qsTr("Public") + } + + LogosTabButton { + text: qsTr("Private") + } + } + + // From: dropdown when accounts exist, or manual entry when list is empty + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + text: qsTr("From") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + LogosTextField { + id: manualFromField + Layout.fillWidth: true + placeholderText: qsTr("Paste or type from address") + visible: fromFilterCount === 0 + } + + ComboBox { + id: fromCombo + Layout.fillWidth: true + leftPadding: 12 + rightPadding: 12 + implicitHeight: 40 + model: fromAccountModel + textRole: "name" + valueRole: "address" + visible: fromFilterCount > 0 + + background: Rectangle { + radius: Theme.spacing.radiusSmall + color: Theme.palette.backgroundSecondary + border.width: 1 + border.color: fromCombo.popup.visible ? Theme.palette.overlayOrange : Theme.palette.backgroundElevated + } + + indicator: LogosText { + id: indicatorText + text: "▼" + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + x: fromCombo.width - width - 12 + y: (fromCombo.height - height) / 2 + visible: fromCombo.count > 0 + } + + contentItem: TextInput { + readOnly: true + selectByMouse: true + width: fromCombo.width - indicatorText.width - 12 + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text + text: fromCombo.currentValue ?? "" + verticalAlignment: Text.AlignVCenter + clip: true + } + + delegate: ItemDelegate { + id: delegate + width: fromCombo.width + leftPadding: 12 + rightPadding: 12 + contentItem: LogosText { + width: parent.width - parent.leftPadding - parent.rightPadding + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.text + text: model.name + elide: Text.ElideRight + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignVCenter + } + background: Rectangle { + color: delegate.highlighted + ? Theme.palette.backgroundElevated + : Theme.palette.backgroundSecondary + } + highlighted: fromCombo.highlightedIndex === index + } + + popup: Popup { + y: fromCombo.height - 1 + width: fromCombo.width + height: Math.min(contentItem.implicitHeight + 8, 300) + padding: 0 + + contentItem: ListView { + clip: true + implicitHeight: contentHeight + model: fromCombo.popup.visible ? fromCombo.delegateModel : null + ScrollIndicator.vertical: ScrollIndicator { } + highlightFollowsCurrentItem: false + } + + background: Rectangle { + color: Theme.palette.backgroundSecondary + border.width: 1 + border.color: Theme.palette.backgroundElevated + radius: Theme.spacing.radiusSmall + } + } + } + } + + // To field + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + text: qsTr("To") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + LogosTextField { + id: toField + Layout.fillWidth: true + placeholderText: qsTr("Recipient public key") + } + } + + // Amount field + ColumnLayout { + Layout.fillWidth: true + spacing: Theme.spacing.small + + LogosText { + text: qsTr("Amount") + font.pixelSize: Theme.typography.secondaryText + color: Theme.palette.textSecondary + } + + LogosTextField { + id: amountField + Layout.fillWidth: true + placeholderText: "0.00" + } + } + + // Send button + LogosButton { + Layout.fillWidth: true + text: qsTr("Send") + font.pixelSize: Theme.typography.secondaryText + enabled: d.sendEnabled + onClicked: { + var fromId = fromFilterCount > 0 && fromCombo.currentIndex >= 0 + ? (fromCombo.currentValue ?? "") + : manualFromField.text.trim() + if (fromId.length > 0) + root.transferRequested(transferTypeBar.currentIndex === 0, fromId, toField.text.trim(), amountField.text.trim()) + } + } + + // Result label + LogosText { + Layout.fillWidth: true + text: root.transferResult + font.pixelSize: Theme.typography.secondaryText + color: root.transferResult.length > 0 ? Theme.palette.textSecondary : "transparent" + wrapMode: Text.WordWrap + } + + Item { + Layout.fillHeight: true + } + } +} diff --git a/src/qml/views/qmldir b/src/qml/views/qmldir index c331b23..bff80f1 100644 --- a/src/qml/views/qmldir +++ b/src/qml/views/qmldir @@ -1,2 +1,5 @@ module views OnboardingView 1.0 OnboardingView.qml +DashboardView 1.0 DashboardView.qml +AccountsPanel 1.0 AccountsPanel.qml +TransferPanel 1.0 TransferPanel.qml diff --git a/src/wallet_resources.qrc b/src/wallet_resources.qrc index 426e5e8..b548e26 100644 --- a/src/wallet_resources.qrc +++ b/src/wallet_resources.qrc @@ -1,7 +1,12 @@ qml/ExecutionZoneWalletView.qml + qml/controls/AccountDelegate.qml + qml/popups/CreateAccountDialog.qml qml/views/qmldir qml/views/OnboardingView.qml + qml/views/DashboardView.qml + qml/views/AccountsPanel.qml + qml/views/TransferPanel.qml