diff --git a/src/base/rss/rss_feed.cpp b/src/base/rss/rss_feed.cpp index 7b8d4e3fa..2df5c2417 100644 --- a/src/base/rss/rss_feed.cpp +++ b/src/base/rss/rss_feed.cpp @@ -1,7 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015-2025 Vladimir Golovnev * Copyright (C) 2024 Jonathan Ketchker - * Copyright (C) 2015-2022 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -56,19 +56,22 @@ const QString KEY_UID = u"uid"_s; const QString KEY_URL = u"url"_s; +const QString KEY_REFRESHINTERVAL = u"refreshInterval"_s; const QString KEY_TITLE = u"title"_s; const QString KEY_LASTBUILDDATE = u"lastBuildDate"_s; const QString KEY_ISLOADING = u"isLoading"_s; const QString KEY_HASERROR = u"hasError"_s; const QString KEY_ARTICLES = u"articles"_s; +using namespace std::chrono_literals; using namespace RSS; -Feed::Feed(const QUuid &uid, const QString &url, const QString &path, Session *session) +Feed::Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, const std::chrono::seconds refreshInterval) : Item(path) - , m_session(session) - , m_uid(uid) - , m_url(url) + , m_session {session} + , m_uid {uid} + , m_url {url} + , m_refreshInterval {refreshInterval} { const auto uidHex = QString::fromLatin1(m_uid.toRfc4122().toHex()); m_dataFileName = Path(uidHex + u".json"); @@ -462,6 +465,20 @@ Path Feed::iconPath() const return m_iconPath; } +std::chrono::seconds Feed::refreshInterval() const +{ + return m_refreshInterval; +} + +void Feed::setRefreshInterval(const std::chrono::seconds refreshInterval) +{ + if (refreshInterval == m_refreshInterval) + return; + + const std::chrono::seconds oldRefreshInterval = std::exchange(m_refreshInterval, refreshInterval); + emit refreshIntervalChanged(oldRefreshInterval); +} + void Feed::setURL(const QString &url) { const QString oldURL = m_url; @@ -474,6 +491,8 @@ QJsonValue Feed::toJsonValue(const bool withData) const QJsonObject jsonObj; jsonObj.insert(KEY_UID, uid().toString()); jsonObj.insert(KEY_URL, url()); + if (refreshInterval() > 0s) + jsonObj.insert(KEY_REFRESHINTERVAL, static_cast(refreshInterval().count())); if (withData) { diff --git a/src/base/rss/rss_feed.h b/src/base/rss/rss_feed.h index b85231b34..639b8bda4 100644 --- a/src/base/rss/rss_feed.h +++ b/src/base/rss/rss_feed.h @@ -1,7 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2015-2025 Vladimir Golovnev * Copyright (C) 2024 Jonathan Ketchker - * Copyright (C) 2015-2022 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -31,6 +31,8 @@ #pragma once +#include + #include #include #include @@ -68,7 +70,7 @@ namespace RSS friend class Session; - Feed(const QUuid &uid, const QString &url, const QString &path, Session *session); + Feed(Session *session, const QUuid &uid, const QString &url, const QString &path, std::chrono::seconds refreshInterval); ~Feed() override; public: @@ -87,6 +89,9 @@ namespace RSS Article *articleByGUID(const QString &guid) const; Path iconPath() const; + std::chrono::seconds refreshInterval() const; + void setRefreshInterval(std::chrono::seconds refreshInterval); + QJsonValue toJsonValue(bool withData = false) const override; signals: @@ -94,6 +99,7 @@ namespace RSS void titleChanged(Feed *feed = nullptr); void stateChanged(Feed *feed = nullptr); void urlChanged(const QString &oldURL); + void refreshIntervalChanged(std::chrono::seconds oldRefreshInterval); private slots: void handleSessionProcessingEnabledChanged(bool enabled); @@ -123,6 +129,7 @@ namespace RSS Private::FeedSerializer *m_serializer = nullptr; const QUuid m_uid; QString m_url; + std::chrono::seconds m_refreshInterval; QString m_title; QString m_lastBuildDate; bool m_hasError = false; diff --git a/src/base/rss/rss_parser.cpp b/src/base/rss/rss_parser.cpp index ec646ecc3..d34212b40 100644 --- a/src/base/rss/rss_parser.cpp +++ b/src/base/rss/rss_parser.cpp @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015-2024 Vladimir Golovnev + * Copyright (C) 2015-2025 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or diff --git a/src/base/rss/rss_parser.h b/src/base/rss/rss_parser.h index 17d4a2464..1569049a1 100644 --- a/src/base/rss/rss_parser.h +++ b/src/base/rss/rss_parser.h @@ -1,6 +1,6 @@ /* * Bittorrent Client using Qt and libtorrent. - * Copyright (C) 2015-2024 Vladimir Golovnev + * Copyright (C) 2015-2025 Vladimir Golovnev * Copyright (C) 2012 Christophe Dumez * * This program is free software; you can redistribute it and/or diff --git a/src/base/rss/rss_session.cpp b/src/base/rss/rss_session.cpp index c15ff1708..9fd2ada0f 100644 --- a/src/base/rss/rss_session.cpp +++ b/src/base/rss/rss_session.cpp @@ -1,7 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017-2025 Vladimir Golovnev * Copyright (C) 2024 Jonathan Ketchker - * Copyright (C) 2017 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -56,6 +56,7 @@ const QString CONF_FOLDER_NAME = u"rss"_s; const QString DATA_FOLDER_NAME = u"rss/articles"_s; const QString FEEDS_FILE_NAME = u"feeds.json"_s; +using namespace std::chrono_literals; using namespace RSS; QPointer Session::m_instance = nullptr; @@ -94,12 +95,10 @@ Session::Session() m_workingThread->start(); load(); + m_refreshTimer.setSingleShot(true); connect(&m_refreshTimer, &QTimer::timeout, this, &Session::refresh); if (isProcessingEnabled()) - { - m_refreshTimer.start(std::chrono::minutes(refreshInterval())); refresh(); - } // Remove legacy/corrupted settings // (at least on Windows, QSettings is case-insensitive and it can get @@ -138,19 +137,20 @@ Session *Session::instance() return m_instance; } -nonstd::expected Session::addFolder(const QString &path) +nonstd::expected Session::addFolder(const QString &path) { const nonstd::expected result = prepareItemDest(path); if (!result) return result.get_unexpected(); auto *destFolder = result.value(); - addItem(new Folder(path), destFolder); + auto *folder = new Folder(path); + addItem(folder, destFolder); store(); - return {}; + return folder; } -nonstd::expected Session::addFeed(const QString &url, const QString &path) +nonstd::expected Session::addFeed(const QString &url, const QString &path, const std::chrono::seconds refreshInterval) { if (m_feedsByURL.contains(url)) return nonstd::make_unexpected(tr("RSS feed with given URL already exists: %1.").arg(url)); @@ -160,13 +160,13 @@ nonstd::expected Session::addFeed(const QString &url, const QStri return result.get_unexpected(); auto *destFolder = result.value(); - auto *feed = new Feed(generateUID(), url, path, this); + auto *feed = new Feed(this, generateUID(), url, path, refreshInterval); addItem(feed, destFolder); store(); if (isProcessingEnabled()) - feed->refresh(); + refreshFeed(feed, std::chrono::system_clock::now()); - return {}; + return feed; } nonstd::expected Session::setFeedURL(const QString &path, const QString &url) @@ -192,7 +192,7 @@ nonstd::expected Session::setFeedURL(Feed *feed, const QString &u feed->setURL(url); store(); if (isProcessingEnabled()) - feed->refresh(); + refreshFeed(feed, std::chrono::system_clock::now()); return {}; } @@ -314,7 +314,7 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder) QString url = val.toString(); if (url.isEmpty()) url = key; - addFeedToFolder(generateUID(), url, key, folder); + addFeedToFolder(generateUID(), url, key, folder, 0s); updated = true; } else if (val.isObject()) @@ -354,7 +354,9 @@ bool Session::loadFolder(const QJsonObject &jsonObj, Folder *folder) updated = true; } - addFeedToFolder(uid, valObj[u"url"].toString(), key, folder); + const auto refreshInterval = std::chrono::seconds(valObj[u"refreshInterval"].toInteger()); + + addFeedToFolder(uid, valObj[u"url"].toString(), key, folder, refreshInterval); } else { @@ -410,7 +412,7 @@ void Session::loadLegacy() void Session::store() { m_confFileStorage->store(Path(FEEDS_FILE_NAME) - , QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson()); + , QJsonDocument(rootFolder()->toJsonValue().toObject()).toJson()); } nonstd::expected Session::prepareItemDest(const QString &path) @@ -436,9 +438,9 @@ Folder *Session::addSubfolder(const QString &name, Folder *parentFolder) return folder; } -Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder) +Feed *Session::addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, const std::chrono::seconds refreshInterval) { - auto *feed = new Feed(uid, url, Item::joinPath(parentFolder->path(), name), this); + auto *feed = new Feed(this, uid, url, Item::joinPath(parentFolder->path(), name), refreshInterval); addItem(feed, parentFolder); return feed; } @@ -460,8 +462,25 @@ void Session::addItem(Item *item, Folder *destFolder) emit feedURLChanged(feed, oldURL); }); + connect(feed, &Feed::refreshIntervalChanged, this, [this, feed](const std::chrono::seconds oldRefreshInterval) + { + store(); + + std::chrono::system_clock::time_point &nextRefresh = m_refreshTimepoints[feed]; + if (nextRefresh > std::chrono::system_clock::time_point()) + nextRefresh += feed->refreshInterval() - oldRefreshInterval; + + if (isProcessingEnabled()) + { + const std::chrono::seconds oldEffectiveRefreshInterval = (oldRefreshInterval > 0s) + ? oldRefreshInterval : std::chrono::minutes(refreshInterval()); + if (feed->refreshInterval() < oldEffectiveRefreshInterval) + refresh(); + } + }); m_feedsByUID[feed->uid()] = feed; m_feedsByURL[feed->url()] = feed; + m_refreshTimepoints.emplace(feed, std::chrono::system_clock::time_point()); } connect(item, &Item::pathChanged, this, &Session::itemPathChanged); @@ -482,14 +501,9 @@ void Session::setProcessingEnabled(const bool enabled) { m_storeProcessingEnabled = enabled; if (enabled) - { - m_refreshTimer.start(std::chrono::minutes(refreshInterval())); refresh(); - } else - { m_refreshTimer.stop(); - } emit processingStateChanged(enabled); } @@ -560,6 +574,7 @@ void Session::handleItemAboutToBeDestroyed(Item *item) { m_feedsByUID.remove(feed->uid()); m_feedsByURL.remove(feed->url()); + m_refreshTimepoints.remove(feed); } } @@ -598,6 +613,28 @@ void Session::setMaxArticlesPerFeed(const int n) void Session::refresh() { - // NOTE: Should we allow manually refreshing for disabled session? - rootFolder()->refresh(); + const auto currentTimepoint = std::chrono::system_clock::now(); + std::chrono::seconds nextRefreshInterval = 0s; + for (auto it = m_refreshTimepoints.begin(); it != m_refreshTimepoints.end(); ++it) + { + Feed *feed = it.key(); + std::chrono::system_clock::time_point &timepoint = it.value(); + + if (timepoint <= currentTimepoint) + timepoint = refreshFeed(feed, currentTimepoint); + + const auto interval = std::chrono::duration_cast(timepoint - currentTimepoint); + if ((interval < nextRefreshInterval) || (nextRefreshInterval == 0s)) + nextRefreshInterval = interval; + } + + m_refreshTimer.start(nextRefreshInterval); +} + +std::chrono::system_clock::time_point Session::refreshFeed(Feed *feed, const std::chrono::system_clock::time_point ¤tTimepoint) +{ + feed->refresh(); + const std::chrono::seconds feedRefreshInterval = feed->refreshInterval(); + const std::chrono::seconds effectiveRefreshInterval = (feedRefreshInterval > 0s) ? feedRefreshInterval : std::chrono::minutes(refreshInterval()); + return currentTimepoint + effectiveRefreshInterval; } diff --git a/src/base/rss/rss_session.h b/src/base/rss/rss_session.h index 97241c56f..b8d2e28c4 100644 --- a/src/base/rss/rss_session.h +++ b/src/base/rss/rss_session.h @@ -1,7 +1,7 @@ /* * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2017-2025 Vladimir Golovnev * Copyright (C) 2024 Jonathan Ketchker - * Copyright (C) 2017 Vladimir Golovnev * Copyright (C) 2010 Christophe Dumez * Copyright (C) 2010 Arnaud Demaiziere * @@ -35,26 +35,20 @@ * RSS Session configuration file format (JSON): * * =============== BEGIN =============== - * - { - * "folder1": - { - * "subfolder1": - { - * "Feed name 1 (Alias)": - { + * { + * "folder1": { + * "subfolder1": { + * "Feed name 1 (Alias)": { * "uid": "feed unique identifier", * "url": "http://some-feed-url1" * } - * "Feed name 2 (Alias)": - { + * "Feed name 2 (Alias)": { * "uid": "feed unique identifier", * "url": "http://some-feed-url2" * } * }, * "subfolder2": {}, - * "Feed name 3 (Alias)": - { + * "Feed name 3 (Alias)": { * "uid": "feed unique identifier", * "url": "http://some-feed-url3" * } @@ -120,8 +114,8 @@ namespace RSS std::chrono::seconds fetchDelay() const; void setFetchDelay(std::chrono::seconds delay); - nonstd::expected addFolder(const QString &path); - nonstd::expected addFeed(const QString &url, const QString &path); + nonstd::expected addFolder(const QString &path); + nonstd::expected addFeed(const QString &url, const QString &path, std::chrono::seconds refreshInterval = {}); nonstd::expected setFeedURL(const QString &path, const QString &url); nonstd::expected setFeedURL(Feed *feed, const QString &url); nonstd::expected moveItem(const QString &itemPath, const QString &destPath); @@ -135,9 +129,6 @@ namespace RSS Folder *rootFolder() const; - public slots: - void refresh(); - signals: void processingStateChanged(bool enabled); void maxArticlesPerFeedChanged(int n); @@ -160,8 +151,10 @@ namespace RSS void store(); nonstd::expected prepareItemDest(const QString &path); Folder *addSubfolder(const QString &name, Folder *parentFolder); - Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder); + Feed *addFeedToFolder(const QUuid &uid, const QString &url, const QString &name, Folder *parentFolder, std::chrono::seconds refreshInterval); void addItem(Item *item, Folder *destFolder); + void refresh(); + std::chrono::system_clock::time_point refreshFeed(Feed *feed, const std::chrono::system_clock::time_point ¤tTimepoint); static QPointer m_instance; @@ -176,5 +169,6 @@ namespace RSS QHash m_itemsByPath; QHash m_feedsByUID; QHash m_feedsByURL; + QHash m_refreshTimepoints; }; } diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index cbbccd5a7..a7b3a5b91 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -18,6 +18,7 @@ qt_wrap_ui(UI_HEADERS properties/peersadditiondialog.ui properties/propertieswidget.ui rss/automatedrssdownloader.ui + rss/rssfeeddialog.ui rss/rsswidget.ui search/pluginselectdialog.ui search/pluginsourcedialog.ui @@ -89,6 +90,7 @@ add_library(qbt_gui STATIC rss/automatedrssdownloader.h rss/feedlistwidget.h rss/htmlbrowser.h + rss/rssfeeddialog.h rss/rsswidget.h search/pluginselectdialog.h search/pluginsourcedialog.h @@ -188,6 +190,7 @@ add_library(qbt_gui STATIC rss/automatedrssdownloader.cpp rss/feedlistwidget.cpp rss/htmlbrowser.cpp + rss/rssfeeddialog.cpp rss/rsswidget.cpp search/pluginselectdialog.cpp search/pluginsourcedialog.cpp diff --git a/src/gui/rss/rssfeeddialog.cpp b/src/gui/rss/rssfeeddialog.cpp new file mode 100644 index 000000000..e179617ff --- /dev/null +++ b/src/gui/rss/rssfeeddialog.cpp @@ -0,0 +1,82 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "rssfeeddialog.h" + +#include + +#include "ui_rssfeeddialog.h" + +RSSFeedDialog::RSSFeedDialog(QWidget *parent) + : QDialog(parent) + , m_ui {new Ui::RSSFeedDialog} +{ + m_ui->setupUi(this); + + m_ui->spinRefreshInterval->setMaximum(std::numeric_limits::max()); + m_ui->spinRefreshInterval->setStepType(QAbstractSpinBox::AdaptiveDecimalStepType); + m_ui->spinRefreshInterval->setSuffix(tr(" sec")); + m_ui->spinRefreshInterval->setSpecialValueText(tr("Default")); + + // disable Ok button + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + connect(m_ui->textFeedURL, &QLineEdit::textChanged, this, &RSSFeedDialog::feedURLChanged); +} + +RSSFeedDialog::~RSSFeedDialog() +{ + delete m_ui; +} + +QString RSSFeedDialog::feedURL() const +{ + return m_ui->textFeedURL->text(); +} + +void RSSFeedDialog::setFeedURL(const QString &feedURL) +{ + m_ui->textFeedURL->setText(feedURL); +} + +std::chrono::seconds RSSFeedDialog::refreshInterval() const +{ + return std::chrono::seconds(m_ui->spinRefreshInterval->value()); +} + +void RSSFeedDialog::setRefreshInterval(const std::chrono::seconds refreshInterval) +{ + m_ui->spinRefreshInterval->setValue(refreshInterval.count()); +} + +void RSSFeedDialog::feedURLChanged(const QString &feedURL) +{ + m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!feedURL.isEmpty()); +} diff --git a/src/gui/rss/rssfeeddialog.h b/src/gui/rss/rssfeeddialog.h new file mode 100644 index 000000000..d97f605a0 --- /dev/null +++ b/src/gui/rss/rssfeeddialog.h @@ -0,0 +1,57 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2025 Vladimir Golovnev + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include +#include + +namespace Ui +{ + class RSSFeedDialog; +} + +class RSSFeedDialog final : public QDialog +{ + Q_OBJECT + Q_DISABLE_COPY_MOVE(RSSFeedDialog) + +public: + explicit RSSFeedDialog(QWidget *parent = nullptr); + ~RSSFeedDialog() override; + + QString feedURL() const; + void setFeedURL(const QString &feedURL); + std::chrono::seconds refreshInterval() const; + void setRefreshInterval(std::chrono::seconds refreshInterval); + +private: + void feedURLChanged(const QString &feedURL); + + Ui::RSSFeedDialog *m_ui = nullptr; +}; diff --git a/src/gui/rss/rssfeeddialog.ui b/src/gui/rss/rssfeeddialog.ui new file mode 100644 index 000000000..601d4c798 --- /dev/null +++ b/src/gui/rss/rssfeeddialog.ui @@ -0,0 +1,75 @@ + + + RSSFeedDialog + + + + 0 + 0 + 555 + 106 + + + + RSS Feed Options + + + + + + + + URL: + + + + + + + + + + Refresh interval: + + + + + + + + 0 + 0 + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Qt::Orientation::Horizontal + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + + + + + + diff --git a/src/gui/rss/rsswidget.cpp b/src/gui/rss/rsswidget.cpp index 67da4cfdb..75c5d171b 100644 --- a/src/gui/rss/rsswidget.cpp +++ b/src/gui/rss/rsswidget.cpp @@ -52,6 +52,7 @@ #include "articlelistwidget.h" #include "automatedrssdownloader.h" #include "feedlistwidget.h" +#include "rssfeeddialog.h" #include "ui_rsswidget.h" namespace @@ -112,7 +113,7 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent) m_ui->actionCopyFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-copy"_s)); m_ui->actionDelete->setIcon(UIThemeManager::instance()->getIcon(u"edit-clear"_s)); m_ui->actionDownloadTorrent->setIcon(UIThemeManager::instance()->getIcon(u"downloading"_s, u"download"_s)); - m_ui->actionEditFeedURL->setIcon(UIThemeManager::instance()->getIcon(u"edit-rename"_s)); + m_ui->actionEditFeed->setIcon(UIThemeManager::instance()->getIcon(u"edit-rename"_s)); m_ui->actionMarkItemsRead->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_s, u"mail-mark-read"_s)); m_ui->actionNewFolder->setIcon(UIThemeManager::instance()->getIcon(u"folder-new"_s)); m_ui->actionNewSubscription->setIcon(UIThemeManager::instance()->getIcon(u"list-add"_s)); @@ -145,7 +146,7 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent) // Feeds list actions connect(m_ui->actionDelete, &QAction::triggered, this, &RSSWidget::deleteSelectedItems); connect(m_ui->actionRename, &QAction::triggered, this, &RSSWidget::renameSelectedRSSItem); - connect(m_ui->actionEditFeedURL, &QAction::triggered, this, &RSSWidget::editSelectedRSSFeedURL); + connect(m_ui->actionEditFeed, &QAction::triggered, this, &RSSWidget::editSelectedRSSFeed); connect(m_ui->actionUpdate, &QAction::triggered, this, &RSSWidget::refreshSelectedItems); connect(m_ui->actionNewFolder, &QAction::triggered, this, &RSSWidget::askNewFolder); connect(m_ui->actionNewSubscription, &QAction::triggered, this, &RSSWidget::on_newFeedButton_clicked); @@ -209,7 +210,7 @@ void RSSWidget::displayRSSListMenu(const QPoint &pos) { menu->addAction(m_ui->actionRename); if (m_ui->feedListWidget->isFeed(selectedItem)) - menu->addAction(m_ui->actionEditFeedURL); + menu->addAction(m_ui->actionEditFeed); menu->addAction(m_ui->actionDelete); menu->addSeparator(); if (m_ui->feedListWidget->isFolder(selectedItem)) @@ -292,36 +293,29 @@ void RSSWidget::askNewFolder() } // Consider the case where the user clicked on Unread item RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem())) - ? RSS::Session::instance()->rootFolder() - : qobject_cast(m_ui->feedListWidget->getRSSItem(destItem))); + ? RSS::Session::instance()->rootFolder() + : qobject_cast(m_ui->feedListWidget->getRSSItem(destItem))); const QString newFolderPath = RSS::Item::joinPath(rssDestFolder->path(), newName); - const nonstd::expected result = RSS::Session::instance()->addFolder(newFolderPath); + const nonstd::expected result = RSS::Session::instance()->addFolder(newFolderPath); if (!result) + { QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok); + return; + } + + RSS::Folder *newFolder = result.value(); // Expand destination folder to display new feed if (destItem && (destItem != m_ui->feedListWidget->stickyUnreadItem())) destItem->setExpanded(true); // As new RSS items are added synchronously, we can do the following here. - m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFolderPath))); + m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(newFolder)); } // add a stream by a button void RSSWidget::on_newFeedButton_clicked() { - // Ask for feed URL - const QString clipText = qApp->clipboard()->text(); - const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"http://"_s; - - bool ok = false; - QString newURL = AutoExpandableDialog::getText( - this, tr("Please type a RSS feed URL"), tr("Feed URL:"), QLineEdit::Normal, defaultURL, &ok); - if (!ok) return; - - newURL = newURL.trimmed(); - if (newURL.isEmpty()) return; - // Determine destination folder for new item QTreeWidgetItem *destItem = nullptr; QList selectedItems = m_ui->feedListWidget->selectedItems(); @@ -332,21 +326,38 @@ void RSSWidget::on_newFeedButton_clicked() destItem = destItem->parent(); } // Consider the case where the user clicked on Unread item - RSS::Folder *rssDestFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem())) - ? RSS::Session::instance()->rootFolder() - : qobject_cast(m_ui->feedListWidget->getRSSItem(destItem))); + RSS::Folder *destFolder = ((!destItem || (destItem == m_ui->feedListWidget->stickyUnreadItem())) + ? RSS::Session::instance()->rootFolder() + : qobject_cast(m_ui->feedListWidget->getRSSItem(destItem))); - // NOTE: We still add feed using legacy way (with URL as feed name) - const QString newFeedPath = RSS::Item::joinPath(rssDestFolder->path(), newURL); - const nonstd::expected result = RSS::Session::instance()->addFeed(newURL, newFeedPath); - if (!result) - QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok); + // Ask for feed URL + const QString clipText = qApp->clipboard()->text(); + const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"http://"_s; + + RSS::Feed *newFeed = nullptr; + RSSFeedDialog dialog {this}; + dialog.setFeedURL(defaultURL); + while (!newFeed && (dialog.exec() == RSSFeedDialog::Accepted)) + { + const QString feedURL = dialog.feedURL().trimmed(); + const std::chrono::seconds refreshInterval = dialog.refreshInterval(); + + const QString feedPath = RSS::Item::joinPath(destFolder->path(), feedURL); + const nonstd::expected result = RSS::Session::instance()->addFeed(feedURL, feedPath, refreshInterval); + if (result) + newFeed = result.value(); + else + QMessageBox::warning(&dialog, u"qBittorrent"_s, result.error(), QMessageBox::Ok); + } + + if (!newFeed) + return; // Expand destination folder to display new feed if (destItem && (destItem != m_ui->feedListWidget->stickyUnreadItem())) destItem->setExpanded(true); // As new RSS items are added synchronously, we can do the following here. - m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(RSS::Session::instance()->itemByPath(newFeedPath))); + m_ui->feedListWidget->setCurrentItem(m_ui->feedListWidget->mapRSSItem(newFeed)); } void RSSWidget::deleteSelectedItems() @@ -401,7 +412,7 @@ void RSSWidget::saveFoldersOpenState() void RSSWidget::refreshAllFeeds() { - RSS::Session::instance()->refresh(); + RSS::Session::instance()->rootFolder()->refresh(); } void RSSWidget::downloadSelectedTorrents() @@ -463,7 +474,7 @@ void RSSWidget::renameSelectedRSSItem() } while (!ok); } -void RSSWidget::editSelectedRSSFeedURL() +void RSSWidget::editSelectedRSSFeed() { QList selectedItems = m_ui->feedListWidget->selectedItems(); if (selectedItems.size() != 1) @@ -475,15 +486,20 @@ void RSSWidget::editSelectedRSSFeedURL() if (!rssFeed) [[unlikely]] return; - bool ok = false; - QString newURL = AutoExpandableDialog::getText(this, tr("Please type a RSS feed URL") - , tr("Feed URL:"), QLineEdit::Normal, rssFeed->url(), &ok).trimmed(); - if (!ok || newURL.isEmpty()) - return; + auto *dialog = new RSSFeedDialog(this); + dialog->setAttribute(Qt::WA_DeleteOnClose); + dialog->setFeedURL(rssFeed->url()); + dialog->setRefreshInterval(rssFeed->refreshInterval()); + connect(dialog, &RSSFeedDialog::accepted, this, [this, dialog, rssFeed] + { + rssFeed->setRefreshInterval(dialog->refreshInterval()); - const nonstd::expected result = RSS::Session::instance()->setFeedURL(rssFeed, newURL); - if (!result) - QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok); + const QString newURL = dialog->feedURL(); + const nonstd::expected result = RSS::Session::instance()->setFeedURL(rssFeed, newURL); + if (!result) + QMessageBox::warning(this, u"qBittorrent"_s, result.error(), QMessageBox::Ok); + }); + dialog->open(); } void RSSWidget::refreshSelectedItems() diff --git a/src/gui/rss/rsswidget.h b/src/gui/rss/rsswidget.h index 98bceac30..67d1e474c 100644 --- a/src/gui/rss/rsswidget.h +++ b/src/gui/rss/rsswidget.h @@ -70,7 +70,7 @@ private slots: void displayRSSListMenu(const QPoint &pos); void displayItemsListMenu(); void renameSelectedRSSItem(); - void editSelectedRSSFeedURL(); + void editSelectedRSSFeed(); void refreshSelectedItems(); void copySelectedFeedsURL(); void handleCurrentFeedItemChanged(QTreeWidgetItem *currentItem); diff --git a/src/gui/rss/rsswidget.ui b/src/gui/rss/rsswidget.ui index 97fc97e83..964b0bae1 100644 --- a/src/gui/rss/rsswidget.ui +++ b/src/gui/rss/rsswidget.ui @@ -198,12 +198,9 @@ New folder... - + - Edit feed URL... - - - Edit feed URL + Feed options... diff --git a/src/webui/api/rsscontroller.cpp b/src/webui/api/rsscontroller.cpp index 0dd5d1dc3..d4308db46 100644 --- a/src/webui/api/rsscontroller.cpp +++ b/src/webui/api/rsscontroller.cpp @@ -50,7 +50,7 @@ void RSSController::addFolderAction() requireParams({u"path"_s}); const QString path = params()[u"path"_s]; - const nonstd::expected result = RSS::Session::instance()->addFolder(path); + const nonstd::expected result = RSS::Session::instance()->addFolder(path); if (!result) throw APIError(APIErrorType::Conflict, result.error()); } @@ -61,7 +61,8 @@ void RSSController::addFeedAction() const QString url = params()[u"url"_s]; const QString path = params()[u"path"_s]; - const nonstd::expected result = RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path)); + const auto refreshInterval = std::max(params()[u"refreshInterval"_s].toLongLong(), 0); + const nonstd::expected result = RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path), std::chrono::seconds(refreshInterval)); if (!result) throw APIError(APIErrorType::Conflict, result.error()); } @@ -77,6 +78,23 @@ void RSSController::setFeedURLAction() throw APIError(APIErrorType::Conflict, result.error()); } +void RSSController::setFeedRefreshIntervalAction() +{ + requireParams({u"path"_s, u"refreshInterval"_s}); + + bool ok = false; + const auto refreshInterval = params()[u"refreshInterval"_s].toLongLong(&ok); + if (!ok || (refreshInterval < 0)) + throw APIError(APIErrorType::BadParams, tr("Invalid 'refreshInterval' value")); + + const QString path = params()[u"path"_s]; + auto *feed = qobject_cast(RSS::Session::instance()->itemByPath(path)); + if (!feed) + throw APIError(APIErrorType::Conflict, tr("Feed doesn't exist: %1.").arg(path)); + + feed->setRefreshInterval(std::chrono::seconds(refreshInterval)); +} + void RSSController::removeItemAction() { requireParams({u"path"_s}); diff --git a/src/webui/api/rsscontroller.h b/src/webui/api/rsscontroller.h index 0d10ba5c5..9c9e5f1fd 100644 --- a/src/webui/api/rsscontroller.h +++ b/src/webui/api/rsscontroller.h @@ -42,6 +42,7 @@ private slots: void addFolderAction(); void addFeedAction(); void setFeedURLAction(); + void setFeedRefreshIntervalAction(); void removeItemAction(); void moveItemAction(); void itemsAction(); diff --git a/src/webui/webapplication.h b/src/webui/webapplication.h index 295919e22..026394ee2 100644 --- a/src/webui/webapplication.h +++ b/src/webui/webapplication.h @@ -53,7 +53,7 @@ #include "base/utils/version.h" #include "api/isessionmanager.h" -inline const Utils::Version<3, 2> API_VERSION {2, 11, 4}; +inline const Utils::Version<3, 2> API_VERSION {2, 11, 5}; class APIController; class AuthController;