mirror of
https://github.com/wownero/wowlet.git
synced 2026-11-09 01:24:04 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7fca31653 | ||
|
|
081c01ada9 | ||
|
|
613d4e7f61 | ||
|
|
79daae6796 | ||
|
|
b1e751a6b7 | ||
|
|
068e4c2aa5 | ||
|
|
f1144c483e | ||
|
|
e5b8ef6a00 |
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
cmake_minimum_required(VERSION 3.18)
|
cmake_minimum_required(VERSION 3.18)
|
||||||
|
|
||||||
project(feather
|
project(feather
|
||||||
VERSION "5.1.1"
|
VERSION "5.1.2"
|
||||||
DESCRIPTION "A free Monero desktop wallet"
|
DESCRIPTION "A free Monero desktop wallet"
|
||||||
LANGUAGES CXX C ASM
|
LANGUAGES CXX C ASM
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -252,7 +252,7 @@ void MainWindow::initWidgets() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner)
|
// [Mining] — wowlet: the pixel-art mining tab (mining.qml + embedded-node solo miner)
|
||||||
m_miningWidget = new MiningWidget(m_wallet, this);
|
m_miningWidget = new MiningWidget(m_wallet, m_nodes, this);
|
||||||
ui->tabWidget->addTab(m_miningWidget, "Mining");
|
ui->tabWidget->addTab(m_miningWidget, "Mining");
|
||||||
|
|
||||||
// [Notes]
|
// [Notes]
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent)
|
|||||||
|
|
||||||
// wowlet: ambient scene as a short banner under the address table. A layout
|
// wowlet: ambient scene as a short banner under the address table. A layout
|
||||||
// item, never an overlay over the table.
|
// item, never an overlay over the table.
|
||||||
auto *scene = new SceneWidget(this, SceneWidget::Dawn);
|
auto *scene = new SceneWidget(this, SceneWidget::Dawn, SceneWidget::Sit);
|
||||||
scene->setMinimumHeight(110);
|
scene->setMinimumHeight(110);
|
||||||
scene->setMaximumHeight(130);
|
scene->setMaximumHeight(130);
|
||||||
ui->verticalLayout->addWidget(scene);
|
ui->verticalLayout->addWidget(scene);
|
||||||
|
|||||||
+10
-3
@@ -29,10 +29,17 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent)
|
|||||||
// wowlet: the form only needs the top of the tab — give the rest of it to
|
// wowlet: the form only needs the top of the tab — give the rest of it to
|
||||||
// the ambient scene. A layout item under the form, never an overlay: the
|
// the ambient scene. A layout item under the form, never an overlay: the
|
||||||
// send form itself stays untouched.
|
// send form itself stays untouched.
|
||||||
auto *scene = new SceneWidget(this, SceneWidget::Night);
|
//
|
||||||
|
// Centred in the space below the form rather than stacked straight under
|
||||||
|
// it: butted up against the Clear/Send row the scene reads as part of the
|
||||||
|
// form, and a band of night sky touching a button looks like a mistake.
|
||||||
|
auto *scene = new SceneWidget(this, SceneWidget::Night, SceneWidget::Money);
|
||||||
scene->setMinimumHeight(160);
|
scene->setMinimumHeight(160);
|
||||||
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
scene->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
|
||||||
ui->verticalLayoutOuter->addWidget(scene, 1);
|
ui->verticalLayoutOuter->addSpacing(14); // never touches the Send row
|
||||||
|
ui->verticalLayoutOuter->addStretch(1);
|
||||||
|
ui->verticalLayoutOuter->addWidget(scene);
|
||||||
|
ui->verticalLayoutOuter->addStretch(1);
|
||||||
|
|
||||||
QString amount_rx = R"(^\d{0,8}[\.,]\d{0,12}|(all)$)";
|
QString amount_rx = R"(^\d{0,8}[\.,]\d{0,12}|(all)$)";
|
||||||
QRegularExpression rx;
|
QRegularExpression rx;
|
||||||
|
|||||||
@@ -512,6 +512,10 @@ Rectangle {
|
|||||||
Image {
|
Image {
|
||||||
id: imgAxe
|
id: imgAxe
|
||||||
visible: mining.daemonMiningState === 0
|
visible: mining.daemonMiningState === 0
|
||||||
|
// Greyed out while the wallet is on someone else's node:
|
||||||
|
// solo mining is an RPC to the daemon we are connected
|
||||||
|
// to, and that daemon has to be ours.
|
||||||
|
opacity: mining.canMine ? 1.0 : 0.35
|
||||||
source: "qrc:/mining/axe.png"
|
source: "qrc:/mining/axe.png"
|
||||||
width: 73
|
width: 73
|
||||||
height: 75
|
height: 75
|
||||||
@@ -551,7 +555,11 @@ Rectangle {
|
|||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if(mining.daemonMiningState === 0) {
|
if (mining.daemonMiningState === 0) {
|
||||||
|
if (!mining.canMine) {
|
||||||
|
remoteNodeNotice.flash();
|
||||||
|
return;
|
||||||
|
}
|
||||||
root.startMining();
|
root.startMining();
|
||||||
root.calcAvailableHeightConsoleLines();
|
root.calcAvailableHeightConsoleLines();
|
||||||
}
|
}
|
||||||
@@ -559,14 +567,56 @@ Rectangle {
|
|||||||
root.stopMining();
|
root.stopMining();
|
||||||
}
|
}
|
||||||
onEntered: {
|
onEntered: {
|
||||||
imgAxe.height = 64
|
if (mining.canMine) {
|
||||||
imgAxe.width = 64
|
imgAxe.height = 64
|
||||||
|
imgAxe.width = 64
|
||||||
|
} else {
|
||||||
|
remoteNodeNotice.show();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onExited: {
|
onExited: {
|
||||||
imgAxe.height = 75
|
imgAxe.height = 75
|
||||||
imgAxe.width = 73
|
imgAxe.width = 73
|
||||||
|
remoteNodeNotice.hideSoon();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Why the axe is greyed out. Shown on hover, and flashed if
|
||||||
|
// the axe is clicked anyway.
|
||||||
|
Rectangle {
|
||||||
|
id: remoteNodeNotice
|
||||||
|
z: parent.z + 5
|
||||||
|
visible: opacity > 0.01
|
||||||
|
opacity: 0.0
|
||||||
|
width: 250
|
||||||
|
height: noticeText.implicitHeight + 16
|
||||||
|
radius: 6
|
||||||
|
color: "#231a10"
|
||||||
|
border.color: "#e0b062"
|
||||||
|
border.width: 1
|
||||||
|
anchors.right: parent.left
|
||||||
|
anchors.rightMargin: 8
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: noticeText
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width - 16
|
||||||
|
wrapMode: Text.WordWrap
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
color: "#f2dcb4"
|
||||||
|
font.pointSize: 9
|
||||||
|
text: "Mining needs your own node.\nYou are connected to a remote one — " +
|
||||||
|
"start the built-in node or point the wallet at a local daemon " +
|
||||||
|
"under Settings to mine."
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 160 } }
|
||||||
|
Timer { id: noticeHide; interval: 2600; onTriggered: remoteNodeNotice.opacity = 0.0 }
|
||||||
|
function show() { noticeHide.stop(); opacity = 1.0 }
|
||||||
|
function hideSoon() { noticeHide.restart() }
|
||||||
|
function flash() { show(); noticeHide.restart() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+797
-84
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include "MiningWidget.h"
|
#include "MiningWidget.h"
|
||||||
|
|
||||||
|
#include "utils/nodes.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
#include <QVBoxLayout>
|
#include <QVBoxLayout>
|
||||||
@@ -20,9 +22,10 @@ static QString formatUptime(qint64 secs) {
|
|||||||
.arg(secs % 60, 2, 10, QChar('0'));
|
.arg(secs % 60, 2, 10, QChar('0'));
|
||||||
}
|
}
|
||||||
|
|
||||||
MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
|
MiningWidget::MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent)
|
||||||
: QWidget(parent)
|
: QWidget(parent)
|
||||||
, m_wallet(wallet)
|
, m_wallet(wallet)
|
||||||
|
, m_nodes(nodes)
|
||||||
, m_quickWidget(new QQuickWidget(this))
|
, m_quickWidget(new QQuickWidget(this))
|
||||||
, m_backend(new MiningBackend(this))
|
, m_backend(new MiningBackend(this))
|
||||||
, m_timer(new QTimer(this))
|
, m_timer(new QTimer(this))
|
||||||
@@ -46,6 +49,10 @@ MiningWidget::MiningWidget(Wallet *wallet, QWidget *parent)
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MiningWidget::onStartMining() {
|
void MiningWidget::onStartMining() {
|
||||||
|
// The QML disables the control, but the signal is not a promise: refuse
|
||||||
|
// here too rather than send start_mining to a stranger's daemon.
|
||||||
|
if (!m_backend->canMine())
|
||||||
|
return;
|
||||||
int threads = std::max(1, QThread::idealThreadCount() / 2);
|
int threads = std::max(1, QThread::idealThreadCount() / 2);
|
||||||
if (m_wallet->startMining(threads))
|
if (m_wallet->startMining(threads))
|
||||||
m_backend->setState(3);
|
m_backend->setState(3);
|
||||||
@@ -57,6 +64,8 @@ void MiningWidget::onStopMining() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void MiningWidget::poll() {
|
void MiningWidget::poll() {
|
||||||
|
m_backend->setCanMine(m_nodes && m_nodes->connection().isLocal());
|
||||||
|
|
||||||
bool mining = m_wallet->isMining();
|
bool mining = m_wallet->isMining();
|
||||||
m_backend->setState(mining ? 3 : 0);
|
m_backend->setState(mining ? 3 : 0);
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
#define WOWLET_MININGWIDGET_H
|
#define WOWLET_MININGWIDGET_H
|
||||||
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
|
class Nodes;
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QQuickWidget>
|
#include <QQuickWidget>
|
||||||
|
|
||||||
@@ -16,16 +18,24 @@ class MiningBackend : public QObject {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged)
|
Q_PROPERTY(int daemonMiningState READ daemonMiningState NOTIFY daemonMiningStateChanged)
|
||||||
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
||||||
|
Q_PROPERTY(bool canMine READ canMine NOTIFY canMineChanged)
|
||||||
public:
|
public:
|
||||||
explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {}
|
explicit MiningBackend(QObject *parent = nullptr) : QObject(parent) {}
|
||||||
int daemonMiningState() const { return m_state; }
|
int daemonMiningState() const { return m_state; }
|
||||||
void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } }
|
void setState(int s) { if (m_state != s) { m_state = s; emit daemonMiningStateChanged(); } }
|
||||||
bool reducedMotion() const { return m_reducedMotion; }
|
bool reducedMotion() const { return m_reducedMotion; }
|
||||||
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
||||||
|
// Solo mining is a start_mining RPC to whichever daemon the wallet is
|
||||||
|
// connected to. Against a remote node that is either refused outright or —
|
||||||
|
// worse, if the operator left the RPC open — it puts someone else's machine
|
||||||
|
// to work on our address. It is only ever ours to ask of our own node.
|
||||||
|
bool canMine() const { return m_canMine; }
|
||||||
|
void setCanMine(bool c) { if (m_canMine != c) { m_canMine = c; emit canMineChanged(); } }
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void daemonMiningStateChanged();
|
void daemonMiningStateChanged();
|
||||||
void reducedMotionChanged();
|
void reducedMotionChanged();
|
||||||
|
void canMineChanged();
|
||||||
void daemonOutput(const QString &line);
|
void daemonOutput(const QString &line);
|
||||||
void syncStatus(int from, int to, int pct);
|
void syncStatus(int from, int to, int pct);
|
||||||
void uptimeChanged(const QString &uptime);
|
void uptimeChanged(const QString &uptime);
|
||||||
@@ -34,6 +44,7 @@ signals:
|
|||||||
private:
|
private:
|
||||||
int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining
|
int m_state = 0; // 0:idle 1:startup 2:syncing 3:mining
|
||||||
bool m_reducedMotion = false;
|
bool m_reducedMotion = false;
|
||||||
|
bool m_canMine = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// wowlet: the pixel-art mining tab. Hosts mining.qml and wires its pick-axe to the wallet's solo
|
// wowlet: the pixel-art mining tab. Hosts mining.qml and wires its pick-axe to the wallet's solo
|
||||||
@@ -41,7 +52,7 @@ private:
|
|||||||
class MiningWidget : public QWidget {
|
class MiningWidget : public QWidget {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
explicit MiningWidget(Wallet *wallet, QWidget *parent = nullptr);
|
explicit MiningWidget(Wallet *wallet, Nodes *nodes, QWidget *parent = nullptr);
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onStartMining();
|
void onStartMining();
|
||||||
@@ -50,6 +61,7 @@ private slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
Wallet *m_wallet;
|
Wallet *m_wallet;
|
||||||
|
Nodes *m_nodes;
|
||||||
QQuickWidget *m_quickWidget;
|
QQuickWidget *m_quickWidget;
|
||||||
MiningBackend *m_backend;
|
MiningBackend *m_backend;
|
||||||
QTimer *m_timer;
|
QTimer *m_timer;
|
||||||
|
|||||||
@@ -4,18 +4,44 @@
|
|||||||
#include "SceneWidget.h"
|
#include "SceneWidget.h"
|
||||||
|
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
#include <QQuickItem>
|
||||||
|
|
||||||
#include "utils/config.h"
|
#include "utils/config.h"
|
||||||
|
|
||||||
SceneWidget::SceneWidget(QWidget *parent, Variant variant)
|
SceneWidget::SceneWidget(QWidget *parent, Variant variant, Mode mode)
|
||||||
: QQuickWidget(parent)
|
: QQuickWidget(parent)
|
||||||
, m_backend(new SceneBackend(this))
|
, m_backend(new SceneBackend(this))
|
||||||
{
|
{
|
||||||
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
|
m_backend->setReducedMotion(conf()->get(Config::reducedMotion).toBool());
|
||||||
m_backend->setVariant(static_cast<int>(variant));
|
m_backend->setVariant(static_cast<int>(variant));
|
||||||
|
m_backend->setMode(mode == Sit ? QStringLiteral("sit")
|
||||||
|
: mode == Money ? QStringLiteral("money")
|
||||||
|
: QStringLiteral("run"));
|
||||||
this->rootContext()->setContextProperty("sceneCtx", m_backend);
|
this->rootContext()->setContextProperty("sceneCtx", m_backend);
|
||||||
this->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
this->setResizeMode(QQuickWidget::SizeRootObjectToView);
|
||||||
this->setSource(QUrl("qrc:/qml/scene.qml"));
|
this->setSource(QUrl("qrc:/qml/scene.qml"));
|
||||||
|
this->callScene("suspend");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneWidget::showEvent(QShowEvent *event) {
|
||||||
|
QQuickWidget::showEvent(event);
|
||||||
|
this->callScene("restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneWidget::hideEvent(QHideEvent *event) {
|
||||||
|
this->callScene("suspend");
|
||||||
|
QQuickWidget::hideEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneWidget::resizeEvent(QResizeEvent *event) {
|
||||||
|
QQuickWidget::resizeEvent(event);
|
||||||
|
if (this->isVisible())
|
||||||
|
this->callScene("restart");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SceneWidget::callScene(const char *method) {
|
||||||
|
if (auto *root = this->rootObject())
|
||||||
|
QMetaObject::invokeMethod(root, method);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SceneWidget::refreshReducedMotion() {
|
void SceneWidget::refreshReducedMotion() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#define WOWLET_SCENEWIDGET_H
|
#define WOWLET_SCENEWIDGET_H
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
#include <QString>
|
||||||
#include <QQuickWidget>
|
#include <QQuickWidget>
|
||||||
|
|
||||||
// Context object exposed to scene.qml as `sceneCtx`. Carries the reduced-motion
|
// Context object exposed to scene.qml as `sceneCtx`. Carries the reduced-motion
|
||||||
@@ -14,18 +15,23 @@ class SceneBackend : public QObject {
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
Q_PROPERTY(bool reducedMotion READ reducedMotion NOTIFY reducedMotionChanged)
|
||||||
Q_PROPERTY(int variant READ variant NOTIFY variantChanged)
|
Q_PROPERTY(int variant READ variant NOTIFY variantChanged)
|
||||||
|
Q_PROPERTY(QString mode READ mode NOTIFY modeChanged)
|
||||||
public:
|
public:
|
||||||
explicit SceneBackend(QObject *parent = nullptr) : QObject(parent) {}
|
explicit SceneBackend(QObject *parent = nullptr) : QObject(parent) {}
|
||||||
bool reducedMotion() const { return m_reducedMotion; }
|
bool reducedMotion() const { return m_reducedMotion; }
|
||||||
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
void setReducedMotion(bool r) { if (m_reducedMotion != r) { m_reducedMotion = r; emit reducedMotionChanged(); } }
|
||||||
int variant() const { return m_variant; }
|
int variant() const { return m_variant; }
|
||||||
void setVariant(int v) { if (m_variant != v) { m_variant = v; emit variantChanged(); } }
|
void setVariant(int v) { if (m_variant != v) { m_variant = v; emit variantChanged(); } }
|
||||||
|
QString mode() const { return m_mode; }
|
||||||
|
void setMode(const QString &m) { if (m_mode != m) { m_mode = m; emit modeChanged(); } }
|
||||||
signals:
|
signals:
|
||||||
void reducedMotionChanged();
|
void reducedMotionChanged();
|
||||||
void variantChanged();
|
void variantChanged();
|
||||||
|
void modeChanged();
|
||||||
private:
|
private:
|
||||||
bool m_reducedMotion = false;
|
bool m_reducedMotion = false;
|
||||||
int m_variant = 0;
|
int m_variant = 0;
|
||||||
|
QString m_mode = QStringLiteral("run");
|
||||||
};
|
};
|
||||||
|
|
||||||
// wowlet: reusable ambient-scene surface. Hosts scene.qml and reads the
|
// wowlet: reusable ambient-scene surface. Hosts scene.qml and reads the
|
||||||
@@ -38,9 +44,32 @@ public:
|
|||||||
// same field. Must match the `skies` table in scene.qml.
|
// same field. Must match the `skies` table in scene.qml.
|
||||||
enum Variant { Dusk = 0, Dawn = 1, Night = 2 };
|
enum Variant { Dusk = 0, Dawn = 1, Night = 2 };
|
||||||
|
|
||||||
explicit SceneWidget(QWidget *parent = nullptr, Variant variant = Dusk);
|
// How much scene a surface wants. History has room for the whole run;
|
||||||
|
// Receive gets a doge sitting in the field; Send gets money blowing past and
|
||||||
|
// nothing else, so the form it sits under is not competing with a dog.
|
||||||
|
enum Mode { Run, Sit, Money };
|
||||||
|
|
||||||
|
explicit SceneWidget(QWidget *parent = nullptr, Variant variant = Dusk,
|
||||||
|
Mode mode = Run);
|
||||||
void refreshReducedMotion();
|
void refreshReducedMotion();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// A scene on a hidden tab is parked rather than left running: an unshown
|
||||||
|
// QQuickWidget is 0px wide, and a lap run against a 0px field collects every
|
||||||
|
// coin at once and leaves its litter (+1s, barks) to surface on the real lap
|
||||||
|
// when the tab is finally shown. Parking also keeps a background tab from
|
||||||
|
// burning CPU on animation nobody is looking at.
|
||||||
|
void showEvent(QShowEvent *event) override;
|
||||||
|
void hideEvent(QHideEvent *event) override;
|
||||||
|
// Belt and braces: a scene that is on screen and has just been given a real
|
||||||
|
// size is running, whether or not a show event was the thing that got it
|
||||||
|
// there. `restart` only sets a flag, so re-arming an already-running scene
|
||||||
|
// costs nothing and does not interrupt its lap.
|
||||||
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void callScene(const char *method);
|
||||||
|
|
||||||
SceneBackend *m_backend;
|
SceneBackend *m_backend;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user