From 5906e69bc13b0e7607a7fae7fa6e97ff624dc887 Mon Sep 17 00:00:00 2001
From: johnny9 <985648+johnny9@users.noreply.github.com>
Date: Sat, 18 May 2024 23:40:27 -0400
Subject: [PATCH 1/2] qml: Introduce the WalletBadge component
The WalletBadge is an Item that shows a wallet's name, type and
balance. The WalletBadge balance shows all of the decimal places
for a whole bitcoin and highlights the satoshis available. The
component can be configured to not show the Icon or the Balance.
---
src/Makefile.qt.include | 3 +-
src/qml/bitcoin_qml.qrc | 1 +
src/qml/pages/wallet/DesktopWallets.qml | 25 +---
src/qml/pages/wallet/WalletBadge.qml | 164 ++++++++++++++++++++++++
4 files changed, 171 insertions(+), 22 deletions(-)
create mode 100644 src/qml/pages/wallet/WalletBadge.qml
diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include
index 29c329028f..d7e7d5b8d0 100644
--- a/src/Makefile.qt.include
+++ b/src/Makefile.qt.include
@@ -423,7 +423,8 @@ QML_RES_QML = \
qml/pages/wallet/CreateIntro.qml \
qml/pages/wallet/CreateName.qml \
qml/pages/wallet/CreatePassword.qml \
- qml/pages/wallet/DesktopWallets.qml
+ qml/pages/wallet/DesktopWallets.qml \
+ qml/pages/wallet/WalletBadge.qml
if TARGET_ANDROID
BITCOIN_QT_H += qml/androidnotifier.h
diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc
index 2f36fe8776..e7ae19417f 100644
--- a/src/qml/bitcoin_qml.qrc
+++ b/src/qml/bitcoin_qml.qrc
@@ -73,6 +73,7 @@
pages/wallet/CreateName.qml
pages/wallet/CreatePassword.qml
pages/wallet/DesktopWallets.qml
+ pages/wallet/WalletBadge.qml
res/icons/add-wallet-dark.png
diff --git a/src/qml/pages/wallet/DesktopWallets.qml b/src/qml/pages/wallet/DesktopWallets.qml
index 87b90e0166..14dad40d96 100644
--- a/src/qml/pages/wallet/DesktopWallets.qml
+++ b/src/qml/pages/wallet/DesktopWallets.qml
@@ -21,27 +21,10 @@ Page {
header: NavigationBar2 {
id: navBar
- leftItem: RowLayout {
- spacing: 5
- Icon {
- source: "image://images/singlesig-wallet"
- color: Theme.color.neutral8
- Layout.preferredWidth: 30
- Layout.preferredHeight: 30
- Layout.leftMargin: 10
- }
- Column {
- spacing: 2
- CoreText {
- text: "Singlesig Wallet"
- color: Theme.color.neutral7
- bold: true
- }
- CoreText {
- text: "₿ 0.00 167 599"
- color: Theme.color.neutral7
- }
- }
+ leftItem: WalletBadge {
+ implicitWidth: 154
+ implicitHeight: 46
+ text: qsTr("Singlesig Wallet")
}
centerItem: RowLayout {
NavigationTab {
diff --git a/src/qml/pages/wallet/WalletBadge.qml b/src/qml/pages/wallet/WalletBadge.qml
new file mode 100644
index 0000000000..7a8e0c8219
--- /dev/null
+++ b/src/qml/pages/wallet/WalletBadge.qml
@@ -0,0 +1,164 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import org.bitcoincore.qt 1.0
+
+import "../../controls"
+
+Button {
+ id: root
+
+ function formatSatoshis(satoshis) {
+ var highlightColor = Theme.color.neutral9
+ var zeroColor = Theme.color.neutral7
+
+ if (root.checked || root.hovered) {
+ highlightColor = zeroColor = Theme.color.orange
+ }
+
+ // Convert satoshis to bitcoins
+ var bitcoins = satoshis / 100000000;
+
+ // Format bitcoins to a fixed 8 decimal places string
+ var bitcoinStr = bitcoins.toFixed(8);
+
+ // Split the bitcoin string into integer and fractional parts
+ var parts = bitcoinStr.split('.');
+
+ // Add spaces for every 3 digits in the integer part
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
+
+ // Highlight the first significant digit and all following digits in the integer part
+ var significantFound = false;
+ parts[0] = parts[0].replace(/(\d)/g, function(match) {
+ if (!significantFound && match !== '0') {
+ significantFound = true;
+ }
+ if (significantFound) {
+ return '' + match + '';
+ }
+ return match;
+ });
+
+ // Add spaces for every 3 digits in the decimal part
+ parts[1] = parts[1].replace(/\B(?=(\d{3})+(?!\d))/g, ' ');
+ if (significantFound) {
+ parts[1] = '' + parts[1] + '';
+ } else {
+ // Highlight the first significant digit and all following digits in the fractional part
+ significantFound = false;
+ parts[1] = parts[1].replace(/(\d)/g, function(match) {
+ if (!significantFound && match !== '0') {
+ significantFound = true;
+ }
+ if (significantFound) {
+ return '' + match + '';
+ }
+ return match;
+ });
+ }
+
+ // Concatenate the parts back together
+ var formattedBitcoins = parts.join('.');
+
+ // Format the text with the Bitcoin symbol
+ var formattedText = `₿ ${formattedBitcoins}`;
+
+ // Highlight zero in a different color if satoshis are zero
+ if (satoshis === 0) {
+ formattedText = `₿ 0.00`;
+ }
+
+ return formattedText;
+ }
+
+ property color bgActiveColor: Theme.color.neutral2
+ property color textColor: Theme.color.neutral7
+ property color textHoverColor: Theme.color.orange
+ property color textActiveColor: Theme.color.orange
+ property color iconColor: "transparent"
+ property string iconSource: ""
+ property bool showBalance: true
+ property bool showIcon: true
+
+ checkable: true
+ hoverEnabled: AppMode.isDesktop
+ implicitHeight: 60
+ implicitWidth: 220
+ bottomPadding: 0
+ topPadding: 0
+ clip: true
+
+ contentItem: RowLayout {
+ anchors.fill: parent
+ anchors.leftMargin: 5
+ anchors.rightMargin: 5
+ clip: true
+ spacing: 5
+ Icon {
+ id: icon
+ visible: root.showIcon
+ source: "image://images/singlesig-wallet"
+ color: Theme.color.neutral8
+ Layout.preferredWidth: 30
+ Layout.preferredHeight: 30
+ }
+ ColumnLayout {
+ spacing: 2
+ CoreText {
+ horizontalAlignment: Text.AlignLeft
+ Layout.fillWidth: true
+ wrap: false
+ id: buttonText
+ font.pixelSize: 13
+ text: root.text
+ color: root.textColor
+ bold: true
+ visible: root.text !== ""
+ }
+ CoreText {
+ id: balanceText
+ visible: root.showBalance
+ text: formatSatoshis(12300)
+ color: Theme.color.neutral7
+ }
+ }
+ }
+
+ background: Rectangle {
+ id: bg
+ height: root.height
+ width: root.width
+ radius: 5
+ color: Theme.color.neutral3
+ visible: root.hovered || root.checked
+
+ FocusBorder {
+ visible: root.visualFocus
+ }
+
+ Behavior on color {
+ ColorAnimation { duration: 150 }
+ }
+ }
+
+ states: [
+ State {
+ name: "CHECKED"; when: root.checked
+ PropertyChanges { target: buttonText; color: root.textActiveColor }
+ PropertyChanges { target: icon; color: root.textActiveColor }
+ PropertyChanges { target: balanceText; color: root.textActiveColor }
+ },
+ State {
+ name: "HOVER"; when: root.hovered
+ PropertyChanges { target: buttonText; color: root.textHoverColor }
+ PropertyChanges { target: icon; color: root.textHoverColor }
+ PropertyChanges { target: balanceText; color: root.textHoverColor }
+ }
+ ]
+}
From 0939f2bfd7586c04ad35aabffc5028511c1a236f Mon Sep 17 00:00:00 2001
From: johnny9 <985648+johnny9@users.noreply.github.com>
Date: Sat, 18 May 2024 23:49:03 -0400
Subject: [PATCH 2/2] qml: Introduce the WalletSelect component
WalletSelect is a Popup that appears after clicking the main
WalletBadge in the DesktopNavigation bar. It contains a ListView
that allows the user to select one of the wallets listed in the
wallet directory.
---
src/Makefile.qt.include | 7 +-
src/qml/bitcoin.cpp | 4 +
src/qml/bitcoin_qml.qrc | 2 +
src/qml/controls/Icon.qml | 2 +
src/qml/imageprovider.cpp | 4 +
src/qml/models/walletlistmodel.cpp | 81 +++++++++++++++++
src/qml/models/walletlistmodel.h | 55 ++++++++++++
src/qml/pages/wallet/DesktopWallets.qml | 18 +++-
src/qml/pages/wallet/WalletBadge.qml | 4 +-
src/qml/pages/wallet/WalletSelect.qml | 115 ++++++++++++++++++++++++
src/qml/res/icons/plus.png | Bin 0 -> 278 bytes
src/qml/res/src/plus.svg | 3 +
12 files changed, 292 insertions(+), 3 deletions(-)
create mode 100644 src/qml/models/walletlistmodel.cpp
create mode 100644 src/qml/models/walletlistmodel.h
create mode 100644 src/qml/pages/wallet/WalletSelect.qml
create mode 100644 src/qml/res/icons/plus.png
create mode 100644 src/qml/res/src/plus.svg
diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include
index d7e7d5b8d0..f371a5b5aa 100644
--- a/src/Makefile.qt.include
+++ b/src/Makefile.qt.include
@@ -42,6 +42,7 @@ QT_MOC_CPP = \
qml/models/moc_nodemodel.cpp \
qml/models/moc_options_model.cpp \
qml/models/moc_peerlistsortproxy.cpp \
+ qml/models/moc_walletlistmodel.cpp \
qml/moc_appmode.cpp \
qml/moc_walletcontroller.cpp \
qt/moc_addressbookpage.cpp \
@@ -122,6 +123,7 @@ BITCOIN_QT_H = \
qml/models/nodemodel.h \
qml/models/options_model.h \
qml/models/peerlistsortproxy.h \
+ qml/models/walletlistmodel.h \
qml/appmode.h \
qml/bitcoin.h \
qml/guiconstants.h \
@@ -311,6 +313,7 @@ BITCOIN_QML_BASE_CPP = \
qml/models/nodemodel.cpp \
qml/models/options_model.cpp \
qml/models/peerlistsortproxy.cpp \
+ qml/models/walletlistmodel.cpp \
qml/imageprovider.cpp \
qml/util.cpp \
qml/walletcontroller.cpp
@@ -339,6 +342,7 @@ QML_RES_ICONS = \
qml/res/icons/info.png \
qml/res/icons/network-dark.png \
qml/res/icons/network-light.png \
+ qml/res/icons/plus.png \
qml/res/icons/shutdown.png \
qml/res/icons/singlesig-wallet.png \
qml/res/icons/storage-dark.png \
@@ -424,7 +428,8 @@ QML_RES_QML = \
qml/pages/wallet/CreateName.qml \
qml/pages/wallet/CreatePassword.qml \
qml/pages/wallet/DesktopWallets.qml \
- qml/pages/wallet/WalletBadge.qml
+ qml/pages/wallet/WalletBadge.qml \
+ qml/pages/wallet/WalletSelect.qml
if TARGET_ANDROID
BITCOIN_QT_H += qml/androidnotifier.h
diff --git a/src/qml/bitcoin.cpp b/src/qml/bitcoin.cpp
index 80c38e7f95..367512cbe1 100644
--- a/src/qml/bitcoin.cpp
+++ b/src/qml/bitcoin.cpp
@@ -26,6 +26,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -295,12 +296,15 @@ int QmlGuiMain(int argc, char* argv[])
assert(!network_style.isNull());
engine.addImageProvider(QStringLiteral("images"), new ImageProvider{network_style.data()});
+ WalletListModel wallet_list_model{*node, nullptr};
+
engine.rootContext()->setContextProperty("networkTrafficTower", &network_traffic_tower);
engine.rootContext()->setContextProperty("nodeModel", &node_model);
engine.rootContext()->setContextProperty("chainModel", &chain_model);
engine.rootContext()->setContextProperty("peerTableModel", &peer_model);
engine.rootContext()->setContextProperty("peerListModelProxy", &peer_model_sort_proxy);
engine.rootContext()->setContextProperty("walletController", &wallet_controller);
+ engine.rootContext()->setContextProperty("walletListModel", &wallet_list_model);
OptionsQmlModel options_model(*node, !need_onboarding.toBool());
engine.rootContext()->setContextProperty("optionsModel", &options_model);
diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc
index e7ae19417f..3d20c0109d 100644
--- a/src/qml/bitcoin_qml.qrc
+++ b/src/qml/bitcoin_qml.qrc
@@ -74,6 +74,7 @@
pages/wallet/CreatePassword.qml
pages/wallet/DesktopWallets.qml
pages/wallet/WalletBadge.qml
+ pages/wallet/WalletSelect.qml
res/icons/add-wallet-dark.png
@@ -96,6 +97,7 @@
res/icons/info.png
res/icons/network-dark.png
res/icons/network-light.png
+ res/icons/plus.png
res/icons/shutdown.png
res/icons/singlesig-wallet.png
res/icons/storage-dark.png
diff --git a/src/qml/controls/Icon.qml b/src/qml/controls/Icon.qml
index a051b2971a..599728ff7a 100644
--- a/src/qml/controls/Icon.qml
+++ b/src/qml/controls/Icon.qml
@@ -7,6 +7,8 @@ import QtQuick.Controls 2.15
Button {
id: root
+ width: icon.width
+ height: icon.height
required property color color
required property url source
property int size: 32
diff --git a/src/qml/imageprovider.cpp b/src/qml/imageprovider.cpp
index 1804c37171..f00168acec 100644
--- a/src/qml/imageprovider.cpp
+++ b/src/qml/imageprovider.cpp
@@ -162,5 +162,9 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize
return QIcon(":/icons/hidden").pixmap(requested_size);
}
+ if (id == "plus") {
+ *size = requested_size;
+ return QIcon(":/icons/plus").pixmap(requested_size);
+ }
return {};
}
diff --git a/src/qml/models/walletlistmodel.cpp b/src/qml/models/walletlistmodel.cpp
new file mode 100644
index 0000000000..ecf97b025a
--- /dev/null
+++ b/src/qml/models/walletlistmodel.cpp
@@ -0,0 +1,81 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+
+#include
+
+#include
+
+WalletListModel::WalletListModel(interfaces::Node& node, QObject *parent)
+: QAbstractListModel(parent)
+, m_node(node)
+{
+ setSelectedWallet("Singlesig Wallet");
+}
+
+void WalletListModel::listWalletDir()
+{
+ QSet existing_names;
+ for (int i = 0; i < rowCount(); ++i) {
+ QModelIndex index = this->index(i, 0);
+ QString name = data(index, NameRole).toString();
+ existing_names.insert(name);
+ }
+
+ for (const std::string &name : m_node.walletLoader().listWalletDir()) {
+ QString qname = QString::fromStdString(name);
+ if (!existing_names.contains(qname)) {
+ addItem({ qname });
+ }
+ }
+}
+
+void WalletListModel::setSelectedWallet(QString wallet_name)
+{
+ if (m_selected_wallet != wallet_name) {
+ m_selected_wallet = wallet_name;
+ Q_EMIT selectedWalletChanged();
+ }
+}
+
+QString WalletListModel::selectedWallet() const
+{
+ return m_selected_wallet;
+}
+
+int WalletListModel::rowCount(const QModelIndex &parent) const
+{
+ Q_UNUSED(parent);
+ return m_items.size();
+}
+
+QVariant WalletListModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_items.size())
+ return QVariant();
+
+ const auto &item = m_items[index.row()];
+ switch (role) {
+ case Qt::DisplayRole:
+ case NameRole:
+ return item.name;
+ default:
+ return QVariant();
+ }
+}
+
+QHash WalletListModel::roleNames() const
+{
+ QHash roles;
+ roles[NameRole] = "name";
+ return roles;
+}
+
+void WalletListModel::addItem(const Item &item)
+{
+ beginInsertRows(QModelIndex(), rowCount(), rowCount());
+ m_items.append(item);
+ endInsertRows();
+}
diff --git a/src/qml/models/walletlistmodel.h b/src/qml/models/walletlistmodel.h
new file mode 100644
index 0000000000..ae1451b21a
--- /dev/null
+++ b/src/qml/models/walletlistmodel.h
@@ -0,0 +1,55 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_QML_MODELS_WALLETLISTMODEL_H
+#define BITCOIN_QML_MODELS_WALLETLISTMODEL_H
+
+#include
+#include
+#include
+
+namespace interfaces {
+class Node;
+}
+
+class WalletListModel : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QString selectedWallet READ selectedWallet WRITE setSelectedWallet NOTIFY selectedWalletChanged)
+
+public:
+ WalletListModel(interfaces::Node& node, QObject *parent = nullptr);
+ ~WalletListModel() = default;
+
+ enum Roles {
+ NameRole = Qt::UserRole + 1
+ };
+
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QHash roleNames() const override;
+
+ void setSelectedWallet(QString wallet_name);
+ QString selectedWallet() const;
+
+public Q_SLOTS:
+ void listWalletDir();
+
+Q_SIGNALS:
+ void selectedWalletChanged();
+
+private:
+ struct Item {
+ QString name;
+ };
+
+ void addItem(const Item &item);
+
+ QList- m_items;
+ interfaces::Node& m_node;
+ QString m_selected_wallet;
+
+};
+
+#endif // BITCOIN_QML_MODELS_WALLETLISTMODEL_H
diff --git a/src/qml/pages/wallet/DesktopWallets.qml b/src/qml/pages/wallet/DesktopWallets.qml
index 14dad40d96..59a7ac15e4 100644
--- a/src/qml/pages/wallet/DesktopWallets.qml
+++ b/src/qml/pages/wallet/DesktopWallets.qml
@@ -24,7 +24,23 @@ Page {
leftItem: WalletBadge {
implicitWidth: 154
implicitHeight: 46
- text: qsTr("Singlesig Wallet")
+ text: walletListModel.selectedWallet
+
+ MouseArea {
+ anchors.fill: parent
+ onClicked: {
+ walletListModel.listWalletDir()
+ walletSelect.opened ? walletSelect.close() : walletSelect.open()
+ }
+ }
+
+ WalletSelect {
+ id: walletSelect
+ model: walletListModel
+ closePolicy: Popup.CloseOnPressOutside
+ x: 0
+ y: parent.height
+ }
}
centerItem: RowLayout {
NavigationTab {
diff --git a/src/qml/pages/wallet/WalletBadge.qml b/src/qml/pages/wallet/WalletBadge.qml
index 7a8e0c8219..fe28d58f47 100644
--- a/src/qml/pages/wallet/WalletBadge.qml
+++ b/src/qml/pages/wallet/WalletBadge.qml
@@ -105,8 +105,10 @@ Button {
visible: root.showIcon
source: "image://images/singlesig-wallet"
color: Theme.color.neutral8
+ size: 30
+ Layout.minimumWidth: 30
Layout.preferredWidth: 30
- Layout.preferredHeight: 30
+ Layout.maximumWidth: 30
}
ColumnLayout {
spacing: 2
diff --git a/src/qml/pages/wallet/WalletSelect.qml b/src/qml/pages/wallet/WalletSelect.qml
new file mode 100644
index 0000000000..9f226ad0b0
--- /dev/null
+++ b/src/qml/pages/wallet/WalletSelect.qml
@@ -0,0 +1,115 @@
+// Copyright (c) 2024 The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+import QtQuick 2.15
+import QtQuick.Controls 2.15
+import QtQuick.Layouts 1.15
+
+import "../../controls"
+
+Popup {
+ id: root
+
+ property alias model: listView.model
+ implicitHeight: layout.height + arrow.height
+ implicitWidth: 250
+ clip: true
+
+ background: Item {
+ anchors.fill: parent
+ Rectangle {
+ id: tooltipBg
+ color: Theme.color.neutral0
+ border.color: Theme.color.neutral4
+ radius: 5
+ border.width: 1
+ width: parent.width
+ height: parent.height - arrow.height - 1
+ anchors.top: arrow.bottom
+ anchors.horizontalCenter: root.horizontalCenter
+ anchors.topMargin: -1
+ }
+ Image {
+ id: arrow
+ source: Theme.image.tooltipArrow
+ width: 22
+ height: 10
+ anchors.left: parent.left
+ anchors.leftMargin: 10
+ anchors.top: parent.top
+ }
+ }
+
+ ButtonGroup {
+ id: buttonGroup
+ }
+
+ ColumnLayout {
+ id: layout
+ width: 220
+ anchors.topMargin: arrow.height
+ CoreText {
+ Layout.alignment: Qt.AlignHCenter
+ Layout.preferredWidth: 220
+ Layout.preferredHeight: 30
+ id: label
+ text: qsTr("Wallets")
+ visible: listView.count > 0
+ bold: true
+ color: Theme.color.neutral9
+ font.pixelSize: 14
+ topPadding: 10
+ bottomPadding: 5
+ }
+
+ ListView {
+ Layout.preferredWidth: 220
+ Layout.preferredHeight: Math.min(listView.count * 34, 300)
+ id: listView
+ interactive: true
+ spacing: 2
+ model: walletListModel
+
+ delegate: WalletBadge {
+ required property string name;
+
+ width: 220
+ height: 32
+ text: name
+ ButtonGroup.group: buttonGroup
+ showBalance: false
+ showIcon: false
+ onClicked: {
+ walletListModel.selectedWallet = name
+ root.close()
+ }
+ }
+ }
+
+ RowLayout {
+ id: addWallet
+ Layout.preferredWidth: addIcon.size + addText.width
+ Layout.preferredHeight: 45
+ Layout.alignment: Qt.AlignHCenter
+ Icon {
+ id: addIcon
+ Layout.alignment: Qt.AlignHCenter
+ source: "image://images/plus"
+ color: Theme.color.neutral8
+ size: 14
+ topPadding: 5
+ bottomPadding: 10
+ }
+ CoreText {
+ id: addText
+ Layout.alignment: Qt.AlignHCenter
+ text: qsTr("Add Wallet")
+ color: Theme.color.neutral9
+ font.pixelSize: 15
+ topPadding: 5
+ bottomPadding: 10
+ }
+ }
+ }
+}
diff --git a/src/qml/res/icons/plus.png b/src/qml/res/icons/plus.png
new file mode 100644
index 0000000000000000000000000000000000000000..6bbe9a4d923a6e98dec328cf487fdc3fbe2c9c63
GIT binary patch
literal 278
zcmeAS@N?(olHy`uVBq!ia0vp^G9b*s1|*Ak?@s|zoCO|{#S9E$svykh8Km+7D9BhG
zHJV@L(#+i4eh4;k>d=y%jhC@LE;NgvR-
zyFmGXMvjAf0`o3LgGS44`AMs??yUOxX|kS|o!{~kn}iQ^bjYkw|GFq@4X^XNNt?F+
zFyA1&`SYvB?hg+9Uk)%v$6Dz2Ntk_+s9Ze%#ebjM7up4^cz1i)-ddoowaD~MK!5u^
zgQCj;yPYmeX3FMY*?nMn>Api
+
+