From ee881d4889fc767c9b1662d991136ad8dccb7695 Mon Sep 17 00:00:00 2001 From: Samuel Lachance Date: Fri, 3 Oct 2025 20:12:26 -0400 Subject: [PATCH] Allow to filter RSS by simple string Adds a search bar for RSS items. It supports plain text search (no regex), applies only to the selected tab, updates results as you type, and shows all items when the field is empty. PR #23278. Resolves #14719, resolves #15538, resolves #18444, resolves #18183, resolves #22570. --------- Co-authored-by: Vladimir Golovnev Co-authored-by: Chocobo1 --- src/gui/rss/articlelistwidget.cpp | 4 +-- src/gui/rss/articlelistwidget.h | 2 +- src/gui/rss/rsswidget.cpp | 20 +++++++++++- src/gui/rss/rsswidget.h | 3 ++ src/webui/www/private/views/rss.html | 48 ++++++++++++++++++++++++++-- 5 files changed, 70 insertions(+), 7 deletions(-) diff --git a/src/gui/rss/articlelistwidget.cpp b/src/gui/rss/articlelistwidget.cpp index 561af742f..a7ba1d80e 100644 --- a/src/gui/rss/articlelistwidget.cpp +++ b/src/gui/rss/articlelistwidget.cpp @@ -64,7 +64,7 @@ QListWidgetItem *ArticleListWidget::mapRSSArticle(RSS::Article *rssArticle) cons return m_rssArticleToListItemMapping.value(rssArticle); } -void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly) +void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly, const QString &filter) { // Clear the list first clear(); @@ -82,7 +82,7 @@ void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly) for (auto *article : asConst(rssItem->articles())) { - if (!(m_unreadOnly && article->isRead())) + if (!(m_unreadOnly && article->isRead()) && (filter.isEmpty() || article->title().contains(filter, Qt::CaseInsensitive))) { auto *item = createItem(article); addItem(item); diff --git a/src/gui/rss/articlelistwidget.h b/src/gui/rss/articlelistwidget.h index b576b9d66..9c42801ff 100644 --- a/src/gui/rss/articlelistwidget.h +++ b/src/gui/rss/articlelistwidget.h @@ -48,7 +48,7 @@ public: RSS::Article *getRSSArticle(QListWidgetItem *item) const; QListWidgetItem *mapRSSArticle(RSS::Article *rssArticle) const; - void setRSSItem(RSS::Item *rssItem, bool unreadOnly = false); + void setRSSItem(RSS::Item *rssItem, bool unreadOnly, const QString &filter); private slots: void handleArticleAdded(RSS::Article *rssArticle); diff --git a/src/gui/rss/rsswidget.cpp b/src/gui/rss/rsswidget.cpp index fb711b0ac..5198b9fa6 100644 --- a/src/gui/rss/rsswidget.cpp +++ b/src/gui/rss/rsswidget.cpp @@ -49,6 +49,7 @@ #include "base/rss/rss_session.h" #include "gui/autoexpandabledialog.h" #include "gui/interfaces/iguiapplication.h" +#include "gui/lineedit.h" #include "gui/uithememanager.h" #include "gui/utils/keysequence.h" #include "articlelistwidget.h" @@ -108,6 +109,7 @@ namespace RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent) : GUIApplicationComponent(app, parent) , m_ui {new Ui::RSSWidget} + , m_rssFilter {new LineEdit(this)} { m_ui->setupUi(this); @@ -130,6 +132,13 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent) m_ui->rssDownloaderBtn->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_s, u"download"_s)); #endif + m_rssFilter->setMaximumWidth(200); + m_rssFilter->setPlaceholderText(tr("Filter feed items...")); + + const int spacerIndex = m_ui->horizontalLayout->indexOf(m_ui->spacer1); + m_ui->horizontalLayout->insertWidget((spacerIndex + 1), m_rssFilter); + + connect(m_rssFilter, &QLineEdit::textChanged, this, &RSSWidget::handleRSSFilterTextChanged); connect(m_ui->articleListWidget, &ArticleListWidget::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu); connect(m_ui->articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged); connect(m_ui->articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents); @@ -568,7 +577,8 @@ void RSSWidget::copySelectedFeedsURL() void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem) { m_ui->articleListWidget->setRSSItem(m_ui->feedListWidget->getRSSItem(currentItem) - , (currentItem == m_ui->feedListWidget->stickyUnreadItem())); + , (currentItem == m_ui->feedListWidget->stickyUnreadItem()) + , m_rssFilter->text()); } void RSSWidget::on_markReadButton_clicked() @@ -641,6 +651,14 @@ void RSSWidget::handleUnreadCountChanged() emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount()); } +void RSSWidget::handleRSSFilterTextChanged(const QString &newFilter) +{ + QTreeWidgetItem *currentItem = m_ui->feedListWidget->currentItem(); + m_ui->articleListWidget->setRSSItem(m_ui->feedListWidget->getRSSItem(currentItem) + , (currentItem == m_ui->feedListWidget->stickyUnreadItem()) + , newFilter); +} + bool RSSWidget::eventFilter(QObject *obj, QEvent *event) { if ((obj == m_ui->textBrowser) && (event->type() == QEvent::PaletteChange)) diff --git a/src/gui/rss/rsswidget.h b/src/gui/rss/rsswidget.h index 67d1e474c..1d6951d5c 100644 --- a/src/gui/rss/rsswidget.h +++ b/src/gui/rss/rsswidget.h @@ -34,6 +34,7 @@ #include "gui/guiapplicationcomponent.h" +class LineEdit; class QListWidgetItem; class QTreeWidgetItem; @@ -85,10 +86,12 @@ private slots: void on_rssDownloaderBtn_clicked(); void handleSessionProcessingStateChanged(bool enabled); void handleUnreadCountChanged(); + void handleRSSFilterTextChanged(const QString &newFilter); private: bool eventFilter(QObject *obj, QEvent *event) override; void renderArticle(const RSS::Article *article) const; Ui::RSSWidget *m_ui = nullptr; + LineEdit *m_rssFilter = nullptr; }; diff --git a/src/webui/www/private/views/rss.html b/src/webui/www/private/views/rss.html index f2e790878..e6ea67be7 100644 --- a/src/webui/www/private/views/rss.html +++ b/src/webui/www/private/views/rss.html @@ -89,10 +89,32 @@ padding: 3px 6px; } + #rssButtonBar input { + background-color: var(--color-background-default); + background-image: url("../images/edit-find.svg"); + background-position: 2px; + background-repeat: no-repeat; + background-size: 1.5em; + border: 1px solid var(--color-border-default); + border-radius: 3px; + min-width: 170px; + padding: 2px 2px 2px 25px; + } + + #rssButtonBar div { + display: inline-block; + vertical-align: top; + } + #rssButtonBar button img { margin: 0 5px -3px 0; } + #rssFilterToolbar { + float: right; + margin-right: 5px; + } + #rssContentView table { width: 100%; } @@ -120,9 +142,12 @@ - +
+ + +
@@ -277,6 +302,17 @@ rssFeedContextMenu.updateMenuItems(); } }); + document.getElementById("rssFilterInput").addEventListener("input", (event) => { + const rowId = rssFeedTable.selectedRows[0]; + let path = ""; + for (const row of rssFeedTable.getRowValues()) { + if (row.rowId === rowId) { + path = row.full_data.dataPath; + break; + } + } + qBittorrent.Rss.showRssFeed(path); + }); document.getElementById("CopyFeedURL").addEventListener("click", async (event) => { let joined = ""; @@ -406,6 +442,12 @@ if (path === "") visibleArticles = visibleArticles.filter((a) => !a.isRead); + const rssFilterInput = document.getElementById("rssFilterInput"); + if (rssFilterInput.value.length > 0) { + const lowerFilter = rssFilterInput.value.toLowerCase(); + visibleArticles = visibleArticles.filter((a) => a.title.toLowerCase().includes(lowerFilter)); + } + let rowID = -1; visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date)) .each((torrentEntry) => {