mirror of
https://github.com/logos-blockchain/logos-execution-zone-wallet-ui.git
synced 2026-02-27 19:53:10 +00:00
Merge pull request #3 from logos-blockchain/feat/DashboardView
feat: add main wallet views
This commit is contained in:
commit
b4795bce70
@ -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
|
||||
|
||||
@ -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
6
flake.lock
generated
@ -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": {
|
||||
|
||||
38
src/LEZAccountFilterModel.cpp
Normal file
38
src/LEZAccountFilterModel.cpp
Normal 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;
|
||||
}
|
||||
30
src/LEZAccountFilterModel.h
Normal file
30
src/LEZAccountFilterModel.h
Normal 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;
|
||||
};
|
||||
78
src/LEZWalletAccountModel.cpp
Normal file
78
src/LEZWalletAccountModel.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/LEZWalletAccountModel.h
Normal file
41
src/LEZWalletAccountModel.h
Normal 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;
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
54
src/qml/controls/AccountDelegate.qml
Normal file
54
src/qml/controls/AccountDelegate.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/qml/popups/CreateAccountDialog.qml
Normal file
100
src/qml/popups/CreateAccountDialog.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/qml/views/AccountsPanel.qml
Normal file
95
src/qml/views/AccountsPanel.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
54
src/qml/views/DashboardView.qml
Normal file
54
src/qml/views/DashboardView.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,6 @@ import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Dialogs
|
||||
import QtCore
|
||||
|
||||
import Logos.Theme
|
||||
import Logos.Controls
|
||||
|
||||
237
src/qml/views/TransferPanel.qml
Normal file
237
src/qml/views/TransferPanel.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user