rssguard/src/librssguard/services/standard/gui/formdiscoverfeeds.cpp

354 lines
12 KiB
C++

// For license of this file, see <project-root-folder>/LICENSE.md.
#include "services/standard/gui/formdiscoverfeeds.h"
#include "3rd-party/boolinq/boolinq.h"
#include "database/databasequeries.h"
#include "gui/guiutilities.h"
#include "miscellaneous/application.h"
#include "miscellaneous/iconfactory.h"
#include "miscellaneous/settings.h"
#include "services/abstract/category.h"
#include "services/abstract/serviceroot.h"
#include "services/standard/definitions.h"
#include "services/standard/gui/formstandardfeeddetails.h"
#include "services/standard/standardfeed.h"
#include "services/standard/parsers/atomparser.h"
#include "services/standard/parsers/icalparser.h"
#include "services/standard/parsers/jsonparser.h"
#include "services/standard/parsers/rdfparser.h"
#include "services/standard/parsers/rssparser.h"
#include "services/standard/parsers/sitemapparser.h"
#include <QtConcurrentMap>
FormDiscoverFeeds::FormDiscoverFeeds(ServiceRoot* service_root,
RootItem* parent_to_select,
const QString& url,
QWidget* parent)
: QDialog(parent), m_serviceRoot(service_root), m_discoveredModel(new DiscoveredFeedsModel(this)) {
m_ui.setupUi(this);
GuiUtilities::applyDialogProperties(*this, qApp->icons()->fromTheme(QSL("application-rss+xml")));
m_parsers = {new AtomParser({}),
new RssParser({}),
new RdfParser({}),
new IcalParser({}),
new JsonParser({}),
new SitemapParser({})};
m_btnGoAdvanced = m_ui.m_buttonBox->addButton(tr("Switch to &advanced mode"), QDialogButtonBox::ButtonRole::NoRole);
m_btnGoAdvanced
->setToolTip(tr("Close this dialog and display dialog for adding individual feeds with advanced options."));
m_ui.m_btnSelecAll->setIcon(qApp->icons()->fromTheme(QSL("dialog-yes"), QSL("edit-select-all")));
m_ui.m_btnSelectNone->setIcon(qApp->icons()->fromTheme(QSL("dialog-no"), QSL("edit-select-none")));
m_ui.m_btnAddIndividually->setIcon(qApp->icons()->fromTheme(QSL("list-add")));
m_btnGoAdvanced->setIcon(qApp->icons()->fromTheme(QSL("system-upgrade")));
m_ui.m_btnImportSelected->setIcon(qApp->icons()->fromTheme(QSL("document-import")));
m_ui.m_buttonBox->button(QDialogButtonBox::StandardButton::Close)
->setIcon(qApp->icons()->fromTheme(QSL("window-close")));
m_ui.m_btnDiscover->setIcon(qApp->icons()->fromTheme(QSL("system-search")));
connect(m_ui.m_txtUrl->lineEdit(), &QLineEdit::textChanged, this, &FormDiscoverFeeds::onUrlChanged);
connect(m_ui.m_btnImportSelected, &QPushButton::clicked, this, &FormDiscoverFeeds::importSelectedFeeds);
connect(m_ui.m_btnSelecAll, &QPushButton::clicked, m_discoveredModel, &DiscoveredFeedsModel::checkAllItems);
connect(m_ui.m_btnSelectNone, &QPushButton::clicked, m_discoveredModel, &DiscoveredFeedsModel::uncheckAllItems);
connect(m_ui.m_btnAddIndividually, &QPushButton::clicked, this, &FormDiscoverFeeds::addSingleFeed);
connect(m_btnGoAdvanced, &QPushButton::clicked, this, &FormDiscoverFeeds::userWantsAdvanced);
connect(m_ui.m_btnDiscover, &QPushButton::clicked, this, &FormDiscoverFeeds::discoverFeeds);
connect(&m_watcherLookup,
&QFutureWatcher<QList<StandardFeed*>>::progressValueChanged,
this,
&FormDiscoverFeeds::onDiscoveryProgress);
connect(&m_watcherLookup,
&QFutureWatcher<QList<StandardFeed*>>::finished,
this,
&FormDiscoverFeeds::onDiscoveryFinished);
loadCategories(m_serviceRoot->getSubTreeCategories(), m_serviceRoot);
m_ui.m_tvFeeds->setModel(m_discoveredModel);
m_ui.m_tvFeeds->header()->setSectionResizeMode(0, QHeaderView::ResizeMode::Stretch);
m_ui.m_tvFeeds->header()->setSectionResizeMode(1, QHeaderView::ResizeMode::ResizeToContents);
connect(m_ui.m_tvFeeds->selectionModel(),
&QItemSelectionModel::selectionChanged,
this,
&FormDiscoverFeeds::onFeedSelectionChanged);
m_ui.m_pbDiscovery->setVisible(false);
if (QUrl(url).isValid()) {
m_ui.m_txtUrl->lineEdit()->setText(url);
}
if (url.isEmpty()) {
emit m_ui.m_txtUrl->lineEdit()->textChanged(url);
}
m_ui.m_txtUrl->lineEdit()->selectAll();
m_ui.m_txtUrl->setFocus();
if (parent_to_select != nullptr) {
if (parent_to_select->kind() == RootItem::Kind::Category) {
m_ui.m_cmbParentCategory
->setCurrentIndex(m_ui.m_cmbParentCategory->findData(QVariant::fromValue(parent_to_select)));
}
else if (parent_to_select->kind() == RootItem::Kind::Feed) {
int target_item = m_ui.m_cmbParentCategory->findData(QVariant::fromValue(parent_to_select->parent()));
if (target_item >= 0) {
m_ui.m_cmbParentCategory->setCurrentIndex(target_item);
}
}
else {
m_ui.m_cmbParentCategory->setCurrentIndex(0);
}
}
}
void FormDiscoverFeeds::onDiscoveryProgress(int progress) {
m_ui.m_pbDiscovery->setValue(progress);
}
void FormDiscoverFeeds::onDiscoveryFinished() {
try {
auto res = m_watcherLookup.future().result();
loadDiscoveredFeeds(res);
}
catch (const ApplicationException& ex) {
qApp->showGuiMessage(Notification::Event::GeneralEvent,
{tr("Cannot discover feeds"),
tr("Error: %1").arg(ex.message()),
QSystemTrayIcon::MessageIcon::Critical});
}
setEnabled(true);
}
StandardFeed* FormDiscoverFeeds::selectedFeed() const {
RootItem* it = m_discoveredModel->itemForIndex(m_ui.m_tvFeeds->currentIndex());
return qobject_cast<StandardFeed*>(it);
}
RootItem* FormDiscoverFeeds::targetParent() const {
return m_ui.m_cmbParentCategory->currentData().value<RootItem*>();
}
FormDiscoverFeeds::~FormDiscoverFeeds() {
qDeleteAll(m_parsers);
m_discoveredModel->setRootItem(nullptr);
}
QList<StandardFeed*> FormDiscoverFeeds::discoverFeedsWithParser(const FeedParser* parser,
const QString& url,
bool greedy) {
auto feeds = parser->discoverFeeds(m_serviceRoot, QUrl::fromUserInput(url), greedy);
QPixmap icon;
int timeout = qApp->settings()->value(GROUP(Feeds), SETTING(Feeds::UpdateTimeout)).toInt();
if (NetworkFactory::downloadIcon({{url, false}}, timeout, icon, {}, m_serviceRoot->networkProxy()) ==
QNetworkReply::NetworkError::NoError) {
for (Feed* feed : feeds) {
feed->setIcon(icon);
}
}
return feeds;
}
void FormDiscoverFeeds::discoverFeeds() {
QString url = m_ui.m_txtUrl->lineEdit()->text();
bool greedy_discover = m_ui.m_cbDiscoverRecursive->isChecked();
std::function<QList<StandardFeed*>(const FeedParser*)> func = [=](const FeedParser* parser) -> QList<StandardFeed*> {
return discoverFeedsWithParser(parser, url, greedy_discover);
};
std::function<QList<StandardFeed*>(QList<StandardFeed*>&, const QList<StandardFeed*>&)> reducer =
[=](QList<StandardFeed*>& res, const QList<StandardFeed*>& interm) -> QList<StandardFeed*> {
res.append(interm);
return res;
};
#if QT_VERSION_MAJOR == 5
QFuture<QList<StandardFeed*>> fut = QtConcurrent::mappedReduced<QList<StandardFeed*>>(m_parsers, func, reducer);
#else
QFuture<QList<StandardFeed*>> fut =
QtConcurrent::mappedReduced<QList<StandardFeed*>>(qApp->workHorsePool(), m_parsers, func, reducer);
#endif
m_watcherLookup.setFuture(fut);
m_ui.m_pbDiscovery->setMaximum(m_parsers.size());
m_ui.m_pbDiscovery->setValue(0);
m_ui.m_pbDiscovery->setVisible(true);
setEnabled(false);
}
void FormDiscoverFeeds::onUrlChanged(const QString& new_url) {
if (QUrl(new_url).isValid()) {
m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Ok, tr("URL is valid."));
}
else {
m_ui.m_txtUrl->setStatus(WidgetWithStatus::StatusType::Error, tr("URL is NOT valid."));
}
}
void FormDiscoverFeeds::loadCategories(const QList<Category*>& categories, RootItem* root_item) {
m_ui.m_cmbParentCategory->addItem(root_item->fullIcon(), root_item->title(), QVariant::fromValue(root_item));
for (Category* category : categories) {
m_ui.m_cmbParentCategory->addItem(category->fullIcon(), category->title(), QVariant::fromValue(category));
}
}
void FormDiscoverFeeds::addSingleFeed() {
auto* fd = selectedFeed();
if (fd == nullptr) {
return;
}
auto idx = m_ui.m_tvFeeds->currentIndex();
QScopedPointer<FormStandardFeedDetails> form_pointer(new FormStandardFeedDetails(m_serviceRoot,
targetParent(),
fd->source(),
qApp->mainFormWidget()));
if (!form_pointer->addEditFeed<StandardFeed>().isEmpty()) {
// Feed was added, remove from list.
if (m_discoveredModel->removeItem(idx) != nullptr) {
// Feed was guessed by the dialog, we do not need this object.
fd->deleteLater();
}
}
}
void FormDiscoverFeeds::importSelectedFeeds() {
for (RootItem* it : m_discoveredModel->checkedItems()) {
Feed* std_feed = it->toFeed();
RootItem* parent = targetParent();
QSqlDatabase database = qApp->database()->driver()->connection(metaObject()->className());
try {
DatabaseQueries::createOverwriteFeed(database, std_feed, m_serviceRoot->accountId(), parent->id());
m_discoveredModel->removeItem(std_feed);
m_serviceRoot->requestItemReassignment(std_feed, parent);
m_serviceRoot->itemChanged({std_feed});
}
catch (const ApplicationException& ex) {
qFatal("Cannot save feed: '%s'.", qPrintable(ex.message()));
}
}
}
void FormDiscoverFeeds::onFeedSelectionChanged() {
m_ui.m_btnAddIndividually->setEnabled(selectedFeed() != nullptr);
}
void FormDiscoverFeeds::userWantsAdvanced() {
setResult(ADVANCED_FEED_ADD_DIALOG_CODE);
close();
}
void FormDiscoverFeeds::loadDiscoveredFeeds(const QList<StandardFeed*>& feeds) {
RootItem* root = new RootItem();
for (Feed* feed : feeds) {
if (feed->title().isEmpty()) {
feed->setTitle(tr("No title"));
}
root->appendChild(feed);
}
m_ui.m_pbDiscovery->setVisible(false);
m_discoveredModel->setRootItem(root);
}
DiscoveredFeedsModel::DiscoveredFeedsModel(QObject* parent) : AccountCheckModel(parent) {}
int DiscoveredFeedsModel::columnCount(const QModelIndex& parent) const {
return 2;
}
QVariant DiscoveredFeedsModel::data(const QModelIndex& index, int role) const {
if (role == Qt::ItemDataRole::DisplayRole && index.column() == 1) {
StandardFeed* fd = qobject_cast<StandardFeed*>(itemForIndex(index));
if (fd != nullptr) {
return StandardFeed::typeToString(fd->type());
}
}
return AccountCheckModel::data(index, role);
}
QVariant DiscoveredFeedsModel::headerData(int section, Qt::Orientation orientation, int role) const {
if (orientation == Qt::Orientation::Vertical) {
return {};
}
static QStringList headers = {tr("Title"), tr("Type")};
if (role == Qt::ItemDataRole::DisplayRole) {
return headers.at(section);
}
return {};
}
void FormDiscoverFeeds::closeEvent(QCloseEvent* event) {
try {
// Wait for discovery to finish.
if (m_watcherLookup.isRunning()) {
m_watcherLookup.result();
}
}
catch (...) {
}
// Clear all remaining items.
m_discoveredModel->setRootItem(nullptr);
QDialog::closeEvent(event);
}
RootItem* DiscoveredFeedsModel::removeItem(RootItem* it) {
auto idx = indexForItem(it);
if (it == nullptr || it == m_rootItem || it->parent() == nullptr) {
return nullptr;
}
beginRemoveRows(idx.parent(), idx.row(), idx.row());
it->parent()->removeChild(it);
endRemoveRows();
return it;
}
RootItem* DiscoveredFeedsModel::removeItem(const QModelIndex& idx) {
RootItem* it = itemForIndex(idx);
if (it == nullptr || it == m_rootItem || it->parent() == nullptr) {
return nullptr;
}
beginRemoveRows(idx.parent(), idx.row(), idx.row());
it->parent()->removeChild(it);
endRemoveRows();
return it;
}