// For license of this file, see /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 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>::progressValueChanged, this, &FormDiscoverFeeds::onDiscoveryProgress); connect(&m_watcherLookup, &QFutureWatcher>::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(it); } RootItem* FormDiscoverFeeds::targetParent() const { return m_ui.m_cmbParentCategory->currentData().value(); } FormDiscoverFeeds::~FormDiscoverFeeds() { qDeleteAll(m_parsers); m_discoveredModel->setRootItem(nullptr); } QList 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(const FeedParser*)> func = [=](const FeedParser* parser) -> QList { return discoverFeedsWithParser(parser, url, greedy_discover); }; std::function(QList&, const QList&)> reducer = [=](QList& res, const QList& interm) -> QList { res.append(interm); return res; }; #if QT_VERSION_MAJOR == 5 QFuture> fut = QtConcurrent::mappedReduced>(m_parsers, func, reducer); #else QFuture> fut = QtConcurrent::mappedReduced>(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& 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 form_pointer(new FormStandardFeedDetails(m_serviceRoot, targetParent(), fd->source(), qApp->mainFormWidget())); if (!form_pointer->addEditFeed().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& 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(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; }