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 <glassez@yandex.ru>
Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
This commit is contained in:
Samuel Lachance
2025-10-03 20:12:26 -04:00
committed by GitHub
parent eed0e56d1a
commit ee881d4889
5 changed files with 70 additions and 7 deletions

View File

@@ -64,7 +64,7 @@ QListWidgetItem *ArticleListWidget::mapRSSArticle(RSS::Article *rssArticle) cons
return m_rssArticleToListItemMapping.value(rssArticle); 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 the list first
clear(); clear();
@@ -82,7 +82,7 @@ void ArticleListWidget::setRSSItem(RSS::Item *rssItem, bool unreadOnly)
for (auto *article : asConst(rssItem->articles())) 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); auto *item = createItem(article);
addItem(item); addItem(item);

View File

@@ -48,7 +48,7 @@ public:
RSS::Article *getRSSArticle(QListWidgetItem *item) const; RSS::Article *getRSSArticle(QListWidgetItem *item) const;
QListWidgetItem *mapRSSArticle(RSS::Article *rssArticle) 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: private slots:
void handleArticleAdded(RSS::Article *rssArticle); void handleArticleAdded(RSS::Article *rssArticle);

View File

@@ -49,6 +49,7 @@
#include "base/rss/rss_session.h" #include "base/rss/rss_session.h"
#include "gui/autoexpandabledialog.h" #include "gui/autoexpandabledialog.h"
#include "gui/interfaces/iguiapplication.h" #include "gui/interfaces/iguiapplication.h"
#include "gui/lineedit.h"
#include "gui/uithememanager.h" #include "gui/uithememanager.h"
#include "gui/utils/keysequence.h" #include "gui/utils/keysequence.h"
#include "articlelistwidget.h" #include "articlelistwidget.h"
@@ -108,6 +109,7 @@ namespace
RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent) RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent)
: GUIApplicationComponent(app, parent) : GUIApplicationComponent(app, parent)
, m_ui {new Ui::RSSWidget} , m_ui {new Ui::RSSWidget}
, m_rssFilter {new LineEdit(this)}
{ {
m_ui->setupUi(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)); m_ui->rssDownloaderBtn->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_s, u"download"_s));
#endif #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::customContextMenuRequested, this, &RSSWidget::displayItemsListMenu);
connect(m_ui->articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged); connect(m_ui->articleListWidget, &ArticleListWidget::currentItemChanged, this, &RSSWidget::handleCurrentArticleItemChanged);
connect(m_ui->articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents); connect(m_ui->articleListWidget, &ArticleListWidget::itemDoubleClicked, this, &RSSWidget::downloadSelectedTorrents);
@@ -568,7 +577,8 @@ void RSSWidget::copySelectedFeedsURL()
void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem) void RSSWidget::handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem)
{ {
m_ui->articleListWidget->setRSSItem(m_ui->feedListWidget->getRSSItem(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() void RSSWidget::on_markReadButton_clicked()
@@ -641,6 +651,14 @@ void RSSWidget::handleUnreadCountChanged()
emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount()); 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) bool RSSWidget::eventFilter(QObject *obj, QEvent *event)
{ {
if ((obj == m_ui->textBrowser) && (event->type() == QEvent::PaletteChange)) if ((obj == m_ui->textBrowser) && (event->type() == QEvent::PaletteChange))

View File

@@ -34,6 +34,7 @@
#include "gui/guiapplicationcomponent.h" #include "gui/guiapplicationcomponent.h"
class LineEdit;
class QListWidgetItem; class QListWidgetItem;
class QTreeWidgetItem; class QTreeWidgetItem;
@@ -85,10 +86,12 @@ private slots:
void on_rssDownloaderBtn_clicked(); void on_rssDownloaderBtn_clicked();
void handleSessionProcessingStateChanged(bool enabled); void handleSessionProcessingStateChanged(bool enabled);
void handleUnreadCountChanged(); void handleUnreadCountChanged();
void handleRSSFilterTextChanged(const QString &newFilter);
private: private:
bool eventFilter(QObject *obj, QEvent *event) override; bool eventFilter(QObject *obj, QEvent *event) override;
void renderArticle(const RSS::Article *article) const; void renderArticle(const RSS::Article *article) const;
Ui::RSSWidget *m_ui = nullptr; Ui::RSSWidget *m_ui = nullptr;
LineEdit *m_rssFilter = nullptr;
}; };

View File

@@ -89,10 +89,32 @@
padding: 3px 6px; 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 { #rssButtonBar button img {
margin: 0 5px -3px 0; margin: 0 5px -3px 0;
} }
#rssFilterToolbar {
float: right;
margin-right: 5px;
}
#rssContentView table { #rssContentView table {
width: 100%; width: 100%;
} }
@@ -120,9 +142,12 @@
<button type="button" id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()"> <button type="button" id="updateAllButton" onclick="qBittorrent.Rss.refreshAllFeeds()">
<img alt="QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]" src="images/view-refresh.svg" width="16" height="16">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget] <img alt="QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]" src="images/view-refresh.svg" width="16" height="16">QBT_TR(Update all)QBT_TR[CONTEXT=RSSWidget]
</button> </button>
<button type="button" id="rssDownloaderButton" class="alignRight" onclick="qBittorrent.Rss.openRssDownloader()"> <div id="rssFilterToolbar" class="alignRight">
<img alt="QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]" src="images/downloading.svg" width="16" height="16">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget] <input type="search" id="rssFilterInput" placeholder="QBT_TR(Filter feed items...)QBT_TR[CONTEXT=MainWindow]" aria-label="QBT_TR(Filter feed items...)QBT_TR[CONTEXT=MainWindow]" autocorrect="off" autocapitalize="none">
</button> <button type="button" id="rssDownloaderButton" onclick="qBittorrent.Rss.openRssDownloader()">
<img alt="QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]" src="images/downloading.svg" width="16" height="16">QBT_TR(RSS Downloader...)QBT_TR[CONTEXT=RSSWidget]
</button>
</div>
</div> </div>
</div> </div>
<div id="rssContentView"> <div id="rssContentView">
@@ -277,6 +302,17 @@
rssFeedContextMenu.updateMenuItems(); 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) => { document.getElementById("CopyFeedURL").addEventListener("click", async (event) => {
let joined = ""; let joined = "";
@@ -406,6 +442,12 @@
if (path === "") if (path === "")
visibleArticles = visibleArticles.filter((a) => !a.isRead); 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; let rowID = -1;
visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date)) visibleArticles.sort((e1, e2) => new Date(e2.date) - new Date(e1.date))
.each((torrentEntry) => { .each((torrentEntry) => {