# Plugins
option(WITH_PLUGIN_HOME "Include Home tab plugin" ON)
option(WITH_PLUGIN_TICKERS "Include Tickers Home plugin" ON)
+option(WITH_PLUGIN_CROWDFUNDING "Include Crowdfunding Home plugin" ON)
option(WITH_PLUGIN_REVUO "Include Revuo Home plugin" ON)
option(WITH_PLUGIN_CALC "Include Calc tab plugin" ON)
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef FEATHER_CCSENTRY_H
+#define FEATHER_CCSENTRY_H
+
+#include <QString>
+
+struct CCSEntry {
+ CCSEntry()= default;;
+
+ QString title;
+ QString date;
+ QString address;
+ QString author;
+ QString state;
+ QString url;
+ QString organizer;
+ QString currency;
+ double target_amount = 0;
+ double raised_amount = 0;
+ double percentage_funded = 0;
+ int contributions = 0;
+};
+
+#endif //FEATHER_CCSENTRY_H
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "CCSModel.h"
+
+CCSModel::CCSModel(QObject *parent)
+ : QAbstractTableModel(parent)
+{
+
+}
+
+void CCSModel::clear() {
+ beginResetModel();
+
+ m_entries.clear();
+
+ endResetModel();
+}
+
+void CCSModel::updateEntries(const QList<QSharedPointer<CCSEntry>>& entries) {
+ beginResetModel();
+
+ m_entries.clear();
+ for (const auto& entry : entries) {
+ m_entries.push_back(entry);
+ }
+
+ endResetModel();
+}
+
+int CCSModel::rowCount(const QModelIndex &parent) const{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return m_entries.count();
+}
+
+int CCSModel::columnCount(const QModelIndex &parent) const
+{
+ if (parent.isValid()) {
+ return 0;
+ }
+ return ModelColumn::COUNT;
+}
+
+QVariant CCSModel::data(const QModelIndex &index, int role) const
+{
+ if (!index.isValid() || index.row() < 0 || index.row() >= m_entries.count())
+ return QVariant();
+
+ QSharedPointer<CCSEntry> entry = m_entries.at(index.row());
+
+ if(role == Qt::DisplayRole) {
+ switch(index.column()) {
+ case Title:
+ return entry->title;
+ case Organizer:
+ return entry->organizer;
+ case Author:
+ return QString("%1 ").arg(entry->author);
+ case Progress:
+ return QString("%1/%2 %3").arg(QString::number(entry->raised_amount), QString::number(entry->target_amount), entry->currency);
+ default:
+ return QVariant();
+ }
+ }
+ return QVariant();
+}
+
+QVariant CCSModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role != Qt::DisplayRole) {
+ return QVariant();
+ }
+ if (orientation == Qt::Horizontal)
+ {
+ switch(section) {
+ case Title:
+ return QString("Proposal");
+ case Organizer:
+ return QString("Organizer ");
+ case Author:
+ return QString("Author");
+ case Progress:
+ return QString("Progress");
+ default:
+ return QVariant();
+ }
+ }
+ return QVariant();
+}
+
+QSharedPointer<CCSEntry> CCSModel::entry(int row) {
+ if (row < 0 || row >= m_entries.size()) {
+ qCritical("%s: no reddit post for index %d", __FUNCTION__, row);
+ return QSharedPointer<CCSEntry>();
+ }
+
+ return m_entries.at(row);
+}
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef FEATHER_CCSMODEL_H
+#define FEATHER_CCSMODEL_H
+
+#include <QAbstractTableModel>
+#include <QSharedPointer>
+
+#include "CCSEntry.h"
+
+class CCSModel : public QAbstractTableModel
+{
+Q_OBJECT
+
+public:
+ enum ModelColumn
+ {
+ Title = 0,
+ Organizer,
+ Author,
+ Progress,
+ COUNT
+ };
+
+ explicit CCSModel(QObject *parent);
+
+ int rowCount(const QModelIndex &parent) const override;
+ int columnCount(const QModelIndex &parent) const override;
+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+
+ void clear();
+ void updateEntries(const QList<QSharedPointer<CCSEntry>>& entries);
+
+ QSharedPointer<CCSEntry> entry(int row);
+
+private:
+ QList<QSharedPointer<CCSEntry>> m_entries;
+};
+
+
+#endif //FEATHER_CCSMODEL_H
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "CCSProgressDelegate.h"
+
+#include <QApplication>
+
+CCSProgressDelegate::CCSProgressDelegate(CCSModel *model, QWidget *parent)
+ : QStyledItemDelegate(parent)
+ , m_model(model)
+{
+}
+
+void CCSProgressDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option,
+ const QModelIndex &index) const {
+
+ if (index.column() != CCSModel::Progress) {
+ QStyledItemDelegate::paint(painter, option, index);
+ return;
+ }
+
+ QStyleOptionProgressBar progressBarOption;
+ progressBarOption.state = QStyle::State_Enabled;
+ progressBarOption.direction = QApplication::layoutDirection();
+ progressBarOption.rect = option.rect;
+ progressBarOption.fontMetrics = QApplication::fontMetrics();
+ progressBarOption.minimum = 0;
+ progressBarOption.maximum = 100;
+ progressBarOption.textAlignment = Qt::AlignCenter;
+ progressBarOption.textVisible = true;
+
+ QSharedPointer<CCSEntry> entry = m_model->entry(index.row());
+ auto target = QString("%1/%2 XMR").arg(entry->raised_amount).arg(entry->target_amount);
+ auto progress = (int)entry->percentage_funded;
+ progressBarOption.progress = progress < 0 ? 0 : progress;
+ progressBarOption.text = target;
+
+ QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter); // Draw the progress bar onto the view.
+}
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef FEATHER_CSSPROGRESSDELEGATE_H
+#define FEATHER_CSSPROGRESSDELEGATE_H
+
+#include <QStyledItemDelegate>
+
+#include "CCSModel.h"
+
+class CCSProgressDelegate : public QStyledItemDelegate
+{
+ Q_OBJECT
+
+public:
+ explicit CCSProgressDelegate(CCSModel *model, QWidget *parent = nullptr);
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
+
+private:
+ CCSModel *m_model;
+};
+
+
+#endif //FEATHER_CSSPROGRESSDELEGATE_H
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "CCSWidget.h"
+#include "ui_CCSWidget.h"
+
+#include <QTableWidget>
+#include <QJsonArray>
+
+#include "CCSProgressDelegate.h"
+#include "utils/Utils.h"
+#include "utils/WebsocketNotifier.h"
+
+CCSWidget::CCSWidget(QWidget *parent)
+ : QWidget(parent)
+ , ui(new Ui::CSSWidget)
+ , m_model(new CCSModel(this))
+ , m_contextMenu(new QMenu(this))
+{
+ ui->setupUi(this);
+ ui->treeView->setModel(m_model);
+
+ m_contextMenu->addAction("View proposal", this, &CCSWidget::linkClicked);
+ m_contextMenu->addAction("Donate", this, &CCSWidget::donateClicked);
+
+ connect(ui->treeView, &QHeaderView::customContextMenuRequested, this, &CCSWidget::showContextMenu);
+ connect(ui->treeView, &QTreeView::doubleClicked, this, &CCSWidget::linkClicked);
+
+ ui->treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
+ ui->treeView->header()->setSectionResizeMode(QHeaderView::ResizeToContents);
+ ui->treeView->header()->setSectionResizeMode(CCSModel::Title, QHeaderView::Stretch);
+
+ connect(websocketNotifier(), &WebsocketNotifier::dataReceived, this, [this](const QString& type, const QJsonValue& json) {
+ if (type == "ccs") {
+ QJsonArray ccs_data = json.toArray();
+ QList<QSharedPointer<CCSEntry>> l;
+
+ for (const auto& entry: ccs_data) {
+ auto obj = entry.toObject();
+ auto c = QSharedPointer<CCSEntry>(new CCSEntry());
+
+ if (obj.value("state").toString() != "FUNDING-REQUIRED")
+ continue;
+
+ c->state = obj.value("state").toString();
+ c->address = obj.value("address").toString();
+ c->author = obj.value("author").toString();
+ c->date = obj.value("date").toString();
+ c->title = obj.value("title").toString();
+ c->target_amount = obj.value("target_amount").toDouble();
+ c->raised_amount = obj.value("raised_amount").toDouble();
+ c->percentage_funded = obj.value("percentage_funded").toDouble();
+ c->contributions = obj.value("contributions").toInt();
+ c->organizer = obj.value("organizer").toString();
+ c->currency = obj.value("currency").toString();
+
+ QString urlpath = obj.value("urlpath").toString();
+ if (c->organizer == "CCS") {
+ c->url = QString("https://ccs.getmonero.org/%1").arg(urlpath);
+ }
+ else if (c->organizer == "MAGIC") {
+ c->url = QString("https://donate.magicgrants.org/%1").arg(urlpath);
+ }
+ else {
+ continue;
+ }
+
+ l.append(c);
+ }
+
+ m_model->updateEntries(l);
+ }
+ });
+}
+
+void CCSWidget::linkClicked() {
+ QModelIndex index = ui->treeView->currentIndex();
+ auto entry = m_model->entry(index.row());
+
+ if (entry) {
+ Utils::externalLinkWarning(this, entry->url);
+ }
+}
+
+void CCSWidget::donateClicked() {
+ QModelIndex index = ui->treeView->currentIndex();
+ auto entry = m_model->entry(index.row());
+
+ if (entry) {
+ emit fillSendTab(entry->address, QString("Donation to %1: %2").arg(entry->organizer, entry->title));
+ }
+}
+
+void CCSWidget::showContextMenu(const QPoint &pos) {
+ QModelIndex index = ui->treeView->indexAt(pos);
+ if (!index.isValid()) {
+ return;
+ }
+
+ m_contextMenu->exec(ui->treeView->viewport()->mapToGlobal(pos));
+}
+
+CCSWidget::~CCSWidget() = default;
\ No newline at end of file
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef FEATHER_CSSWIDGET_H
+#define FEATHER_CSSWIDGET_H
+
+#include <QItemDelegate>
+#include <QMenu>
+#include <QObject>
+#include <QProgressBar>
+#include <QWidget>
+
+#include "CCSModel.h"
+#include "CCSEntry.h"
+
+namespace Ui {
+ class CSSWidget;
+}
+
+class CCSWidget : public QWidget
+{
+Q_OBJECT
+
+public:
+ explicit CCSWidget(QWidget *parent = nullptr);
+ ~CCSWidget();
+
+signals:
+ void fillSendTab(const QString &address, const QString &description);
+
+public slots:
+ void donateClicked();
+
+private slots:
+ void linkClicked();
+
+private:
+ void showContextMenu(const QPoint &pos);
+
+ QScopedPointer<Ui::CSSWidget> ui;
+ CCSModel *m_model;
+ QMenu *m_contextMenu;
+};
+
+#endif // FEATHER_CSSWIDGET_H
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>CSSWidget</class>
+ <widget class="QWidget" name="CSSWidget">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>893</width>
+ <height>396</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Form</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>4</number>
+ </property>
+ <property name="leftMargin">
+ <number>0</number>
+ </property>
+ <property name="topMargin">
+ <number>0</number>
+ </property>
+ <property name="rightMargin">
+ <number>0</number>
+ </property>
+ <property name="bottomMargin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QTreeView" name="treeView">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="rootIsDecorated">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+ <slots>
+ <slot>donateClicked()</slot>
+ <slot>linkClicked()</slot>
+ </slots>
+</ui>
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#include "CrowdfundingPlugin.h"
+
+#include "plugins/PluginRegistry.h"
+#include "CCSWidget.h"
+
+CrowdfundingPlugin::CrowdfundingPlugin()
+{
+}
+
+void CrowdfundingPlugin::initialize(Wallet *wallet, QObject *parent) {
+ this->setParent(parent);
+ m_tab = new CCSWidget(nullptr);
+ connect(m_tab, &CCSWidget::fillSendTab, this, &Plugin::fillSendTab);
+}
+
+QString CrowdfundingPlugin::id() {
+ return "crowdfunding";
+}
+
+int CrowdfundingPlugin::idx() const {
+ return 10;
+}
+
+QString CrowdfundingPlugin::parent() {
+ return "home";
+}
+
+QString CrowdfundingPlugin::displayName() {
+ return "Crowdfunding";
+}
+
+QString CrowdfundingPlugin::description() {
+ return {};
+}
+
+QString CrowdfundingPlugin::icon() {
+ return {};
+}
+
+QStringList CrowdfundingPlugin::socketData() {
+ return {"ccs"};
+}
+
+Plugin::PluginType CrowdfundingPlugin::type() {
+ return Plugin::PluginType::TAB;
+}
+
+QWidget* CrowdfundingPlugin::tab() {
+ return m_tab;
+}
+
+const bool CrowdfundingPlugin::registered = [] {
+ PluginRegistry::registerPlugin(CrowdfundingPlugin::create());
+ PluginRegistry::getInstance().registerPluginCreator(&CrowdfundingPlugin::create);
+ return true;
+}();
--- /dev/null
+// SPDX-License-Identifier: BSD-3-Clause
+// SPDX-FileCopyrightText: The Monero Project
+
+#ifndef CROWDFUNDINGPLUGIN_H
+#define CROWDFUNDINGPLUGIN_H
+
+#include "plugins/Plugin.h"
+#include "CCSWidget.h"
+
+class CrowdfundingPlugin : public Plugin {
+ Q_OBJECT
+
+public:
+ explicit CrowdfundingPlugin();
+
+ QString id() override;
+ int idx() const override;
+ QString parent() override;
+ QString displayName() override;
+ QString description() override;
+ QString icon() override;
+ QStringList socketData() override;
+ PluginType type() override;
+ QWidget* tab() override;
+
+ void initialize(Wallet *wallet, QObject *parent) override;
+
+ static CrowdfundingPlugin* create() { return new CrowdfundingPlugin(); }
+
+private:
+ CCSWidget* m_tab = nullptr;
+ static const bool registered;
+};
+
+#endif //CROWDFUNDINGPLUGIN_H
{Config::warnOnKiImport,{QS("warnOnKiImport"), true}},
{Config::logLevel,{QS("logLevel"), 0}},
- {Config::homeWidget,{QS("homeWidget"), "revuo"}},
+ {Config::homeWidget,{QS("homeWidget"), "ccs"}},
{Config::donateBeg,{QS("donateBeg"), 1}},
{Config::showHistorySyncNotice, {QS("showHistorySyncNotice"), true}},
{Config::useLocalTor, {QS("useLocalTor"), false}},
{Config::initSyncThreshold, {QS("initSyncThreshold"), 360}},
- {Config::enabledPlugins, {QS("enabledPlugins"), QStringList{"tickers", "revuo", "calc"}}},
+ {Config::enabledPlugins, {QS("enabledPlugins"), QStringList{"tickers", "crowdfunding", "revuo", "calc"}}},
{Config::restartRequired, {QS("restartRequired"), false}},
{Config::tickers, {QS("tickers"), QStringList{"XMR", "BTC", "XMR/BTC"}}},