Merge pull request #3 from logos-blockchain/feat/DashboardView

feat: add main wallet views
This commit is contained in:
Khushboo-dev-cpp 2026-02-23 15:11:04 +05:30 committed by GitHub
commit b4795bce70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 825 additions and 25 deletions

View File

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

View File

@ -3,8 +3,6 @@
#include <QApplication>
#include <QDir>
#include <QDebug>
#include <iostream>
#include <memory>
extern "C" {
void logos_core_set_plugins_dir(const char* plugins_dir);

6
flake.lock generated
View File

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

View File

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

View File

@ -0,0 +1,30 @@
#pragma once
#include <QSortFilterProxyModel>
#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;
};

View File

@ -0,0 +1,78 @@
#include "LEZWalletAccountModel.h"
#include <QJsonObject>
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<int, QByteArray> 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;
}
}
}

View File

@ -0,0 +1,41 @@
#pragma once
#include <QAbstractListModel>
#include <QJsonArray>
#include <QString>
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<int, QByteArray> 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<LEZWalletAccountEntry> m_entries;
};

View File

@ -1,5 +1,7 @@
#include "LEZWalletBackend.h"
#include <QAbstractItemModel>
#include <QDebug>
#include <QJsonArray>
#include <QSettings>
#include <QUrl>
@ -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<QJsonArray>()) {
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<QAbstractItemModel*>(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;
}

View File

@ -2,10 +2,13 @@
#include <QObject>
#include <QString>
#include <QJsonArray>
#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;

View File

@ -1,5 +1,6 @@
#include "LEZWalletPlugin.h"
#include "LEZWalletBackend.h"
#include "LEZAccountFilterModel.h"
#include <QQuickWidget>
#include <QQmlContext>
#include <QQmlEngine>
@ -16,6 +17,7 @@ QWidget* LEZWalletPlugin::createWidget(LogosAPI* logosAPI) {
quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView);
qmlRegisterType<LEZWalletBackend>("LEZWalletBackend", 1, 0, "LEZWalletBackend");
qmlRegisterType<LEZAccountFilterModel>("LEZWalletBackend", 1, 0, "LEZAccountFilterModel");
LEZWalletBackend* backend = new LEZWalletBackend(logosAPI, quickWidget);
quickWidget->rootContext()->setContextProperty("backend", backend);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,6 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Dialogs
import QtCore
import Logos.Theme
import Logos.Controls

View File

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

View File

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

View File

@ -1,7 +1,12 @@
<RCC>
<qresource prefix="/">
<file>qml/ExecutionZoneWalletView.qml</file>
<file>qml/controls/AccountDelegate.qml</file>
<file>qml/popups/CreateAccountDialog.qml</file>
<file>qml/views/qmldir</file>
<file>qml/views/OnboardingView.qml</file>
<file>qml/views/DashboardView.qml</file>
<file>qml/views/AccountsPanel.qml</file>
<file>qml/views/TransferPanel.qml</file>
</qresource>
</RCC>