Revert "plugins: remove Crowdfunding and Bounties"
authortobtoht <tob@featherwallet.org>
Wed, 12 Nov 2025 12:35:11 +0000 (13:35 +0100)
committertobtoht <tob@featherwallet.org>
Wed, 12 Nov 2025 14:25:16 +0000 (15:25 +0100)
partial revert, keep CCS for now

12 files changed:
CMakeLists.txt
src/plugins/crowdfunding/CCSEntry.h [new file with mode: 0644]
src/plugins/crowdfunding/CCSModel.cpp [new file with mode: 0644]
src/plugins/crowdfunding/CCSModel.h [new file with mode: 0644]
src/plugins/crowdfunding/CCSProgressDelegate.cpp [new file with mode: 0644]
src/plugins/crowdfunding/CCSProgressDelegate.h [new file with mode: 0644]
src/plugins/crowdfunding/CCSWidget.cpp [new file with mode: 0644]
src/plugins/crowdfunding/CCSWidget.h [new file with mode: 0644]
src/plugins/crowdfunding/CCSWidget.ui [new file with mode: 0644]
src/plugins/crowdfunding/CrowdfundingPlugin.cpp [new file with mode: 0644]
src/plugins/crowdfunding/CrowdfundingPlugin.h [new file with mode: 0644]
src/utils/config.cpp

index 0f7f5bdbf43a7faa68b57acb9b8640a64cbd1124..dd2087adec942951e743faea3d20f821f9de0c93 100644 (file)
@@ -33,6 +33,7 @@ option(TOR_INSTALLED "Is Tor installed on the filesystem?" OFF)
 # 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)
 
diff --git a/src/plugins/crowdfunding/CCSEntry.h b/src/plugins/crowdfunding/CCSEntry.h
new file mode 100644 (file)
index 0000000..49c8f63
--- /dev/null
@@ -0,0 +1,26 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSModel.cpp b/src/plugins/crowdfunding/CCSModel.cpp
new file mode 100644 (file)
index 0000000..baaaf2e
--- /dev/null
@@ -0,0 +1,100 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSModel.h b/src/plugins/crowdfunding/CCSModel.h
new file mode 100644 (file)
index 0000000..e310878
--- /dev/null
@@ -0,0 +1,43 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSProgressDelegate.cpp b/src/plugins/crowdfunding/CCSProgressDelegate.cpp
new file mode 100644 (file)
index 0000000..c915d24
--- /dev/null
@@ -0,0 +1,39 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSProgressDelegate.h b/src/plugins/crowdfunding/CCSProgressDelegate.h
new file mode 100644 (file)
index 0000000..4c0f5cb
--- /dev/null
@@ -0,0 +1,24 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSWidget.cpp b/src/plugins/crowdfunding/CCSWidget.cpp
new file mode 100644 (file)
index 0000000..92d8e94
--- /dev/null
@@ -0,0 +1,103 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSWidget.h b/src/plugins/crowdfunding/CCSWidget.h
new file mode 100644 (file)
index 0000000..5a763da
--- /dev/null
@@ -0,0 +1,45 @@
+// 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
diff --git a/src/plugins/crowdfunding/CCSWidget.ui b/src/plugins/crowdfunding/CCSWidget.ui
new file mode 100644 (file)
index 0000000..51163e3
--- /dev/null
@@ -0,0 +1,56 @@
+<?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>
diff --git a/src/plugins/crowdfunding/CrowdfundingPlugin.cpp b/src/plugins/crowdfunding/CrowdfundingPlugin.cpp
new file mode 100644 (file)
index 0000000..8cebc63
--- /dev/null
@@ -0,0 +1,59 @@
+// 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;
+}();
diff --git a/src/plugins/crowdfunding/CrowdfundingPlugin.h b/src/plugins/crowdfunding/CrowdfundingPlugin.h
new file mode 100644 (file)
index 0000000..9bd4736
--- /dev/null
@@ -0,0 +1,35 @@
+// 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
index dfae745b323ad7fe960a0023286b089f2e4380fd..b5baa886d797ece9c12bcef280d41dd9bd6dab06 100644 (file)
@@ -27,7 +27,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
         {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}},
 
@@ -123,7 +123,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
         {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"}}},