diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86af10ed..312daeef 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,233 +1,234 @@ configure_file(akregator-version.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/akregator-version.h @ONLY) include_directories( ${CMAKE_CURRENT_SOURCE_DIR}/command/ ${CMAKE_CURRENT_SOURCE_DIR}/formatter/ ${CMAKE_CURRENT_SOURCE_DIR}/subscription/ ${CMAKE_CURRENT_SOURCE_DIR}/feed/ ${CMAKE_CURRENT_SOURCE_DIR}/urlhandler/ ${CMAKE_CURRENT_SOURCE_DIR}/actions/ ${CMAKE_CURRENT_SOURCE_DIR}/frame/ ) add_subdirectory(icons) ########### next target ############### set(akregator_common_SRCS) ecm_qt_declare_logging_category(akregator_common_SRCS HEADER akregator_debug.h IDENTIFIER AKREGATOR_LOG CATEGORY_NAME org.kde.pim.akregator) set(akregator_SRCS main.cpp mainwindow.cpp ${akregator_common_SRCS}) file(GLOB ICONS_AKREGATOR_SRCS "${CMAKE_CURRENT_SOURCE_DIR}/icons/*-apps-akregator.png") ecm_add_app_icon(akregator_SRCS ICONS ${ICONS_AKREGATOR_SRCS}) add_executable(akregator ${akregator_SRCS}) target_link_libraries(akregator KF5::Crash KF5::Notifications KF5::KontactInterface KF5::Libkdepim akregatorprivate akregatorinterfaces KF5::PimCommon ) install(TARGETS akregator ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) ########### next target ############### set(akregator_articleviewer_ng_webengine_SRCS articleviewer-ng/webengine/articleviewerwebengine.cpp articleviewer-ng/webengine/articleviewerwebenginepage.cpp articleviewer-ng/webengine/articlehtmlwebenginewriter.cpp articleviewer-ng/webengine/articleviewerwebenginewidgetng.cpp ) set(akregator_job_SRCS job/downloadarticlejob.cpp ) set(akregatorprivate_formatter_SRCS formatter/articleformatter.cpp formatter/defaultnormalviewformatter.cpp formatter/defaultcombinedviewformatter.cpp formatter/grantleeviewformatter.cpp formatter/articlegrantleeobject.cpp ) set(akregatorprivate_frame_SRCS frame/webengine/webengineframe.cpp frame/webengine/akrwebengineviewer.cpp frame/frame.cpp frame/mainframe.cpp frame/framemanager.cpp ) set(akregatorprivate_urlhandlerwebengine_SRCS urlhandler/webengine/urlhandlerwebengine.cpp urlhandler/webengine/urlhandlerwebenginemanager.cpp ) set(akregatorprivate_LIB_SRCS akregratormigrateapplication.cpp ${akregatorprivate_frame_SRCS} ${akregatorprivate_urlhandler_SRCS} ${akregator_articleviewer_ng_SRCS} ${akregator_articleviewer_ng_webengine_SRCS} ${akregator_common_SRCS} ${akregatorprivate_formatter_SRCS} ${akregatorprivate_urlhandlerwebengine_SRCS} articleviewerwidget.cpp aboutdata.cpp trayicon.cpp unityservicemanager.cpp article.cpp feed/feed.cpp feed/feedlist.cpp + feed/feedretriever.cpp treenode.cpp treenodevisitor.cpp utils.cpp notificationmanager.cpp articlejobs.cpp folder.cpp kernel.cpp subscription/subscriptionlistjobs.cpp fetchqueue.cpp openurlrequest.cpp actions/actionmanager.cpp actions/actions.cpp ) qt5_add_resources(akregatorprivate_LIB_SRCS akregator.qrc) add_library(akregatorprivate ${akregatorprivate_LIB_SRCS}) generate_export_header(akregatorprivate BASE_NAME akregator) target_link_libraries(akregatorprivate PRIVATE KF5::Parts KF5::Notifications KF5::Libkdepim KF5::Syndication akregatorinterfaces KF5::PimCommon KF5::IconThemes KF5::PimTextEdit KF5::GrantleeTheme Grantlee5::Templates KF5::KIOGui KF5::MessageViewer Qt5::PrintSupport KF5::WebEngineViewer ) target_include_directories(akregatorprivate PUBLIC "$") set_target_properties(akregatorprivate PROPERTIES VERSION ${KDEPIM_LIB_VERSION} SOVERSION ${KDEPIM_LIB_SOVERSION} ) install(TARGETS akregatorprivate ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) ########### next target ############### set(akregator_crashwidget_SRCS crashwidget/crashwidget.cpp ) set(akregator_utils_SRCS utils/filtercolumnsproxymodel.cpp ) set(akregatorpart_command_SRCS command/deletesubscriptioncommand.cpp command/createfeedcommand.cpp command/createfoldercommand.cpp command/expireitemscommand.cpp command/loadfeedlistcommand.cpp command/editsubscriptioncommand.cpp command/importfeedlistcommand.cpp ) set(akregatorpart_widgets_SRCS widgets/statussearchline.cpp widgets/searchbar.cpp widgets/akregatorcentralwidget.cpp ) set(akregatorpart_subscription_SRCS subscription/subscriptionlistview.cpp subscription/subscriptionlistdelegate.cpp subscription/subscriptionlistmodel.cpp ) set(akregatorpart_PART_SRCS ${akregator_crashwidget_SRCS} ${akregatorpart_subscription_SRCS} ${akregatorpart_widgets_SRCS} ${akregatorpart_command_SRCS} ${akregator_utils_SRCS} ${akregator_common_SRCS} ${akregator_job_SRCS} abstractselectioncontroller.cpp articlematcher.cpp articlemodel.cpp pluginmanager.cpp selectioncontroller.cpp articlelistview.cpp actions/actionmanagerimpl.cpp addfeeddialog.cpp feed/feedpropertiesdialog.cpp tabwidget.cpp progressmanager.cpp akregator_part.cpp mainwidget.cpp dummystorage/storagedummyimpl.cpp dummystorage/storagefactorydummyimpl.cpp dummystorage/feedstoragedummyimpl.cpp ) qt5_add_dbus_adaptor(akregatorpart_PART_SRCS org.kde.akregator.part.xml akregator_part.h Akregator::Part) ki18n_wrap_ui(akregatorpart_PART_SRCS ui/addfeedwidgetbase.ui ui/feedpropertieswidgetbase.ui ) add_library(akregatorpart MODULE ${akregatorpart_PART_SRCS}) generate_export_header(akregatorpart BASE_NAME akregatorpart) target_link_libraries(akregatorpart akregatorinterfaces akregatorprivate KF5::KCMUtils KF5::NotifyConfig KF5::Libkdepim KF5::Syndication KF5::PimCommon KF5::PimTextEdit KF5::GrantleeTheme KF5::MessageViewer KF5::Completion KF5::IconThemes KF5::Parts KF5::Notifications Qt5::WebEngineWidgets KF5::WebEngineViewer ) install(TARGETS akregatorpart DESTINATION ${KDE_INSTALL_PLUGINDIR}) ########### install files ############### install(PROGRAMS data/org.kde.akregator.desktop DESTINATION ${KDE_INSTALL_APPDIR}) install(FILES data/akregator_part.desktop DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES data/org.kde.akregator.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR}) install(FILES data/akregator_plugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) install(FILES feed.protocol DESTINATION ${KDE_INSTALL_KSERVICES5DIR}) install(FILES org.kde.akregator.part.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR}) install(FILES data/akregator.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFY5RCDIR} ) add_subdirectory(formatter/html) #add_subdirectory(crashwidget/autotests) diff --git a/src/akregator_part.cpp b/src/akregator_part.cpp index 74acfabf..afde53f9 100644 --- a/src/akregator_part.cpp +++ b/src/akregator_part.cpp @@ -1,838 +1,828 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2005 Frank Osterfeld 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "akregator_part.h" #include "akregator_debug.h" #include "messageviewer/messageviewersettings.h" #include "akregatorconfig.h" #include "aboutdata.h" #include "actionmanagerimpl.h" #include "article.h" #include "fetchqueue.h" #include "feedlist.h" #include "framemanager.h" #include "kernel.h" #include "loadfeedlistcommand.h" #include "mainwidget.h" #include "notificationmanager.h" #include "plugin.h" #include "pluginmanager.h" #include "storage.h" #include "storagefactory.h" #include "storagefactoryregistry.h" #include "trayicon.h" #include "widgets/akregatorcentralwidget.h" #include "dummystorage/storagefactorydummyimpl.h" #include "utils.h" #include "akregator_options.h" #include #include "akregator-version.h" #include "unityservicemanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "akregratormigrateapplication.h" #include "partadaptor.h" #include #include #include namespace { static QDomDocument createDefaultFeedList() { QDomDocument doc; QDomProcessingInstruction z = doc.createProcessingInstruction(QStringLiteral("xml"), QStringLiteral("version=\"1.0\" encoding=\"UTF-8\"")); doc.appendChild(z); QDomElement root = doc.createElement(QStringLiteral("opml")); root.setAttribute(QStringLiteral("version"), QStringLiteral("1.0")); doc.appendChild(root); QDomElement head = doc.createElement(QStringLiteral("head")); root.appendChild(head); QDomElement text = doc.createElement(QStringLiteral("text")); text.appendChild(doc.createTextNode(i18n("Feeds"))); head.appendChild(text); QDomElement body = doc.createElement(QStringLiteral("body")); root.appendChild(body); QDomElement mainFolder = doc.createElement(QStringLiteral("outline")); mainFolder.setAttribute(QStringLiteral("text"), QStringLiteral("KDE")); body.appendChild(mainFolder); QDomElement dot = doc.createElement(QStringLiteral("outline")); dot.setAttribute(QStringLiteral("text"), i18n("KDE Dot News")); dot.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://dot.kde.org/rss.xml")); mainFolder.appendChild(dot); QDomElement linuxFeeds = doc.createElement(QStringLiteral("outline")); linuxFeeds.setAttribute(QStringLiteral("text"), i18n("Linux.com")); linuxFeeds.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://www.linux.com/feeds/rss")); mainFolder.appendChild(linuxFeeds); QDomElement planetkde = doc.createElement(QStringLiteral("outline")); planetkde.setAttribute(QStringLiteral("text"), i18n("Planet KDE")); planetkde.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://planetkde.org/rss20.xml")); mainFolder.appendChild(planetkde); QDomElement apps = doc.createElement(QStringLiteral("outline")); apps.setAttribute(QStringLiteral("text"), i18n("KDE Apps")); apps.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://store.kde.org/content.rdf")); mainFolder.appendChild(apps); // Brazilian Portuguese feeds QDomElement portugueuseFolder = doc.createElement(QStringLiteral("outline")); portugueuseFolder.setAttribute(QStringLiteral("text"), i18n("Brazilian Portuguese feeds")); mainFolder.appendChild(portugueuseFolder); QDomElement portugueuseKde = doc.createElement(QStringLiteral("outline")); portugueuseKde.setAttribute(QStringLiteral("text"), i18n("Planet KDE Brazilian Portuguese")); portugueuseKde.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://planetkde.org/pt-br/rss20.xml")); portugueuseFolder.appendChild(portugueuseKde); // spanish feed(s) QDomElement spanishFolder = doc.createElement(QStringLiteral("outline")); spanishFolder.setAttribute(QStringLiteral("text"), i18n("Spanish feeds")); mainFolder.appendChild(spanishFolder); QDomElement spanishKde = doc.createElement(QStringLiteral("outline")); spanishKde.setAttribute(QStringLiteral("text"), i18n("Planet KDE España")); spanishKde.setAttribute(QStringLiteral("xmlUrl"), QStringLiteral("https://planet.kde-espana.org/atom.xml")); spanishFolder.appendChild(spanishKde); return doc; } } namespace Akregator { K_PLUGIN_FACTORY(AkregatorFactory, registerPlugin(); ) static Part *mySelf = nullptr; BrowserExtension::BrowserExtension(Part *p, const char *name) : KParts::BrowserExtension(p) { AkregratorMigrateApplication migrate; migrate.migrate(); setObjectName(QLatin1String(name)); m_part = p; } void BrowserExtension::saveSettings() { m_part->saveSettings(); } Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList &) : KParts::ReadOnlyPart(parent) , m_standardListLoaded(false) , m_shuttingDown(false) , m_doCrashSave(false) , m_backedUpList(false) , m_mainWidget(nullptr) , m_storage(nullptr) , m_dialog(nullptr) { mySelf = this; //Make sure to initialize settings Part::config(); initFonts(); setPluginLoadingMode(LoadPluginsIfEnabled); setPluginInterfaceVersion(AKREGATOR_PLUGIN_INTERFACE_VERSION); setComponentName(QStringLiteral("akregator"), i18n("Akregator")); setXMLFile(QStringLiteral("akregator_part.rc"), true); new PartAdaptor(this); QDBusConnection::sessionBus().registerObject(QStringLiteral("/Akregator"), this); const QString path = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QStringLiteral("/akregator/data/"); QDir().mkpath(path); m_standardFeedList = path + QStringLiteral("/feeds.opml"); Backend::StorageFactoryDummyImpl *dummyFactory = new Backend::StorageFactoryDummyImpl(); if (!Backend::StorageFactoryRegistry::self()->registerFactory(dummyFactory, dummyFactory->key())) { // There was already a dummy factory registered. delete dummyFactory; } loadPlugins(QStringLiteral("storage")); // FIXME: also unload them! m_storage = nullptr; Backend::StorageFactory *storageFactory = Backend::StorageFactoryRegistry::self()->getFactory(Settings::archiveBackend()); if (storageFactory != nullptr) { m_storage = storageFactory->createStorage(QStringList()); } if (!m_storage) { // Houston, we have a problem m_storage = Backend::StorageFactoryRegistry::self()->getFactory(QStringLiteral("dummy"))->createStorage(QStringList()); KMessageBox::error(parentWidget, i18n("Unable to load storage backend plugin \"%1\". No feeds are archived.", Settings::archiveBackend()), i18n("Plugin error")); } m_storage->open(true); Kernel::self()->setStorage(m_storage); m_actionManager = new ActionManagerImpl(this); ActionManager::setInstance(m_actionManager); mCentralWidget = new Akregator::AkregatorCentralWidget(parentWidget); connect(mCentralWidget, &AkregatorCentralWidget::restoreSession, this, &Part::slotRestoreSession); m_mainWidget = new Akregator::MainWidget(this, parentWidget, m_actionManager, QStringLiteral("akregator_view")); mCentralWidget->setMainWidget(m_mainWidget); m_extension = new BrowserExtension(this, "ak_extension"); connect(Kernel::self()->frameManager(), &FrameManager::signalCaptionChanged, this, &Part::setWindowCaption); connect(Kernel::self()->frameManager(), &FrameManager::signalStatusText, this, &Part::slotSetStatusText); connect(Kernel::self()->frameManager(), &FrameManager::signalLoadingProgress, m_extension, &BrowserExtension::loadingProgress); connect(Kernel::self()->frameManager(), &FrameManager::signalCanceled, this, &ReadOnlyPart::canceled); connect(Kernel::self()->frameManager(), &FrameManager::signalStarted, this, &Part::slotStarted); connect(Kernel::self()->frameManager(), SIGNAL(signalCompleted()), this, SIGNAL(completed())); // notify the part that this is our internal widget setWidget(mCentralWidget); //Initialize instance. (void)UnityServiceManager::instance(); connect(m_mainWidget.data(), &MainWidget::signalUnreadCountChanged, UnityServiceManager::instance(), &UnityServiceManager::slotSetUnread); if (Settings::showTrayIcon() && !TrayIcon::getInstance()) { initializeTrayIcon(); QWidget *const notificationParent = isTrayIconEnabled() ? m_mainWidget->window() : nullptr; NotificationManager::self()->setWidget(notificationParent, componentData().componentName()); } connect(qApp, &QCoreApplication::aboutToQuit, this, &Part::slotOnShutdown); m_autosaveTimer = new QTimer(this); connect(m_autosaveTimer, &QTimer::timeout, this, &Part::slotSaveFeedList); m_autosaveTimer->start(5 * 60 * 1000); // 5 minutes - QString useragent = QStringLiteral("Akregator/%1; syndication").arg(QStringLiteral(AKREGATOR_VERSION)); - - if (!Settings::customUserAgent().isEmpty()) { - useragent = Settings::customUserAgent(); - } - - Syndication::FileRetriever::setUserAgent(useragent); - loadPlugins(QStringLiteral("extension")); // FIXME: also unload them! if (mCentralWidget->previousSessionCrashed()) { mCentralWidget->needToRestoreCrashedSession(); } else { m_doCrashSave = true; autoReadProperties(); } } KSharedConfig::Ptr Part::config() { assert(mySelf); if (!mySelf->mConfig) { mySelf->mConfig = KSharedConfig::openConfig(QStringLiteral("akregatorrc")); } return mySelf->mConfig; } void Part::updateQuickSearchLineText() { if (m_mainWidget) { m_mainWidget->updateQuickSearchLineText(); } } void Part::loadPlugins(const QString &type) { const KService::List offers = PluginManager::query(QStringLiteral("[X-KDE-akregator-plugintype] == '%1'").arg(type)); for (const KService::Ptr &i : offers) { Akregator::Plugin *plugin = PluginManager::createFromService(i, this); if (!plugin) { continue; } plugin->initialize(); plugin->insertGuiClients(this); } } void Part::slotStarted() { Q_EMIT started(nullptr); } void Part::slotOnShutdown() { autoSaveProperties(); m_shuttingDown = true; m_autosaveTimer->stop(); if (m_mainWidget) { saveSettings(); m_mainWidget->slotOnShutdown(); } //delete m_mainWidget; delete TrayIcon::getInstance(); TrayIcon::setInstance(nullptr); delete m_storage; m_storage = nullptr; //delete m_actionManager; } void Part::initializeTrayIcon() { TrayIcon *trayIcon = new TrayIcon(m_mainWidget->window()); TrayIcon::setInstance(trayIcon); m_actionManager->setTrayIcon(trayIcon); if (isTrayIconEnabled()) { trayIcon->setStatus(KStatusNotifierItem::Active); } connect(m_mainWidget.data(), &MainWidget::signalUnreadCountChanged, trayIcon, &TrayIcon::slotSetUnread); connect(m_mainWidget.data(), &MainWidget::signalArticlesSelected, this, &Part::signalArticlesSelected); m_mainWidget->slotSetTotalUnread(); } void Part::slotSettingsChanged() { NotificationManager::self()->setWidget(isTrayIconEnabled() ? m_mainWidget->window() : nullptr, componentData().componentName()); if (Settings::showTrayIcon()) { if (!TrayIcon::getInstance()) { initializeTrayIcon(); m_mainWidget->slotSetTotalUnread(); } } else { TrayIcon::getInstance()->disconnect(); delete TrayIcon::getInstance(); TrayIcon::setInstance(nullptr); m_actionManager->setTrayIcon(nullptr); } - Syndication::FileRetriever::setUseCache(Settings::useHTMLCache()); - const QStringList fonts { Settings::standardFont(), Settings::fixedFont(), Settings::sansSerifFont(), Settings::serifFont(), Settings::standardFont(), Settings::standardFont(), QStringLiteral("0") }; Settings::setFonts(fonts); if (Settings::minimumFontSize() > Settings::mediumFontSize()) { Settings::setMediumFontSize(Settings::minimumFontSize()); } saveSettings(); Q_EMIT signalSettingsChanged(); initFonts(); } void Part::slotSetStatusText(const QString &statusText) { KPIM::BroadcastStatus::instance()->setStatusMsg(statusText); } void Part::saveSettings() { if (m_mainWidget) { m_mainWidget->saveSettings(); } } Part::~Part() { disconnect(qApp, &QCoreApplication::aboutToQuit, this, &Part::slotOnShutdown); qCDebug(AKREGATOR_LOG) << "Part::~Part() enter"; // If the widget is destroyed for some reason, KParts::Part will set its // widget property to 0 and then delete itself (and therefore this object). // In this case, it's not safe to do our normal shutdown routine. if (widget() && !m_shuttingDown) { slotOnShutdown(); } qCDebug(AKREGATOR_LOG) << "Part::~Part(): leaving"; } void Part::readProperties(const KConfigGroup &config) { m_backedUpList = false; openStandardFeedList(); if (m_mainWidget) { m_mainWidget->readProperties(config); } } void Part::saveProperties(KConfigGroup &config) { if (m_mainWidget) { slotSaveFeedList(); m_mainWidget->saveProperties(config); } } void Part::exportFile(const QString &str) { exportFile(QUrl(str)); } bool Part::openUrl(const QUrl &url) { setLocalFilePath(url.toLocalFile()); return openFile(); } void Part::openStandardFeedList() { if (!m_standardFeedList.isEmpty()) { openUrl(QUrl::fromLocalFile(m_standardFeedList)); } } bool Part::openFile() { if (m_loadFeedListCommand || m_standardListLoaded) { return true; } QScopedPointer cmd(new LoadFeedListCommand(m_mainWidget)); cmd->setParentWidget(m_mainWidget); cmd->setStorage(Kernel::self()->storage()); cmd->setFileName(localFilePath()); cmd->setDefaultFeedList(createDefaultFeedList()); connect(cmd.data(), &LoadFeedListCommand::result, this, &Part::feedListLoaded); m_loadFeedListCommand = cmd.take(); m_loadFeedListCommand->start(); return true; } bool Part::writeToTextFile(const QString &data, const QString &filename) const { QSaveFile file(filename); if (!file.open(QIODevice::WriteOnly)) { return false; } QTextStream stream(&file); stream.setCodec("UTF-8"); stream << data << endl; return file.commit(); } void Part::feedListLoaded(const QSharedPointer &list) { Q_ASSERT(!m_standardListLoaded); if (!m_mainWidget) { return; } m_mainWidget->setFeedList(list); m_standardListLoaded = list != nullptr; if (Settings::markAllFeedsReadOnStartup()) { m_mainWidget->slotMarkAllFeedsRead(); } if (m_standardListLoaded) { QTimer::singleShot(0, this, &Part::flushAddFeedRequests); } if (Settings::fetchOnStartup()) { m_mainWidget->slotFetchAllFeeds(); } } void Part::flushAddFeedRequests() { if (!m_mainWidget) { return; } for (const AddFeedRequest &i : qAsConst(m_requests)) { Q_FOREACH (const QString &j, i.urls) { m_mainWidget->addFeedToGroup(j, i.group); } NotificationManager::self()->slotNotifyFeeds(i.urls); } m_requests.clear(); } void Part::slotSaveFeedList() { // don't save to the standard feed list, when it wasn't completely loaded before if (!m_standardListLoaded) { return; } // the first time we overwrite the feed list, we create a backup if (!m_backedUpList) { const QString backup = localFilePath() + QLatin1Char('~'); if (QFile::exists(backup)) { QFile::remove(backup); } if (QFile::copy(localFilePath(), backup)) { m_backedUpList = true; } } const QString xml = m_mainWidget->feedListToOPML().toString(); if (xml.isEmpty()) { return; } m_storage->storeFeedList(xml); if (writeToTextFile(xml, localFilePath())) { return; } KMessageBox::error(m_mainWidget, i18n("Access denied: Cannot save feed list to %1. Please check your permissions.", localFilePath()), i18n("Write Error")); } bool Part::isTrayIconEnabled() const { return Settings::showTrayIcon(); } void Part::importFile(const QUrl &url) { QString filename; QTemporaryFile tempFile; if (url.isLocalFile()) { filename = url.toLocalFile(); } else { if (!tempFile.open()) { return; } filename = tempFile.fileName(); auto job = KIO::file_copy(url, QUrl::fromLocalFile(filename), -1, KIO::Overwrite | KIO::HideProgressInfo); KJobWidgets::setWindow(job, m_mainWidget); if (!job->exec()) { KMessageBox::error(m_mainWidget, job->errorString()); return; } } QFile file(filename); if (file.open(QIODevice::ReadOnly)) { // Read OPML feeds list and build QDom tree. QDomDocument doc; if (doc.setContent(file.readAll())) { m_mainWidget->importFeedList(doc); } else { KMessageBox::error(m_mainWidget, i18n("Could not import the file %1 (no valid OPML)", filename), i18n("OPML Parsing Error")); } } else { KMessageBox::error(m_mainWidget, i18n("The file %1 could not be read, check if it exists or if it is readable for the current user.", filename), i18n("Read Error")); } } void Part::exportFile(const QUrl &url) { if (url.isLocalFile()) { const QString fname = url.toLocalFile(); if (!writeToTextFile(m_mainWidget->feedListToOPML().toString(), fname)) { KMessageBox::error(m_mainWidget, i18n("Access denied: cannot write to file %1. Please check your permissions.", fname), i18n("Write Error")); } return; } else { auto job = KIO::storedPut(m_mainWidget->feedListToOPML().toString().toUtf8(), url, -1); KJobWidgets::setWindow(job, m_mainWidget); if (!job->exec()) { KMessageBox::error(m_mainWidget, job->errorString()); } } } void Part::fileImport() { const QString filters = i18n("OPML Outlines (*.opml *.xml);;All Files (*)"); const QUrl url = QFileDialog::getOpenFileUrl(m_mainWidget, QString(), QUrl(), filters); if (!url.isEmpty()) { importFile(url); } } void Part::fileExport() { const QString filters = i18n("OPML Outlines (*.opml *.xml);;All Files (*)"); const QUrl url = QFileDialog::getSaveFileUrl(m_mainWidget, QString(), QUrl(), filters); if (!url.isEmpty()) { exportFile(url); } } void Part::fetchAllFeeds() { m_mainWidget->slotFetchAllFeeds(); } void Part::fetchFeedUrl(const QString &s) { qCDebug(AKREGATOR_LOG) << "fetchFeedURL==" << s; } void Part::addFeedsToGroup(const QStringList &urls, const QString &group) { AddFeedRequest req; req.group = group; req.urls = urls; m_requests.append(req); if (m_standardListLoaded) { flushAddFeedRequests(); } } void Part::addFeed() { m_mainWidget->slotFeedAdd(); } void Part::showNotificationOptions() { const Akregator::AboutData about; KNotifyConfigWidget::configure(m_mainWidget, about.productName()); } void Part::showOptions() { saveSettings(); if (!m_dialog) { m_dialog = new KCMultiDialog(m_mainWidget); connect(m_dialog, QOverload<>::of(&KCMultiDialog::configCommitted), this, &Part::slotSettingsChanged); if (TrayIcon::getInstance()) { connect(m_dialog, QOverload<>::of(&KCMultiDialog::configCommitted), TrayIcon::getInstance(), &TrayIcon::settingsChanged); } m_dialog->addModule(QStringLiteral("akregator_config_general")); m_dialog->addModule(QStringLiteral("akregator_config_appearance")); m_dialog->addModule(QStringLiteral("akregator_config_archive")); m_dialog->addModule(QStringLiteral("akregator_config_browser")); m_dialog->addModule(QStringLiteral("akregator_config_advanced")); m_dialog->addModule(QStringLiteral("akregator_config_plugins")); } m_dialog->show(); m_dialog->raise(); } void Part::initFonts() { QStringList fonts = Settings::fonts(); if (fonts.isEmpty()) { fonts.append(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family()); fonts.append(QFontDatabase::systemFont(QFontDatabase::FixedFont).family()); fonts.append(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family()); fonts.append(QFontDatabase::systemFont(QFontDatabase::GeneralFont).family()); fonts.append(QStringLiteral("0")); } Settings::setFonts(fonts); if (Settings::standardFont().isEmpty()) { Settings::setStandardFont(fonts[0]); } if (Settings::fixedFont().isEmpty()) { Settings::setFixedFont(fonts[1]); } if (Settings::sansSerifFont().isEmpty()) { Settings::setSansSerifFont(fonts[2]); } if (Settings::serifFont().isEmpty()) { Settings::setSerifFont(fonts[3]); } //TODO add CursiveFont, FantasyFont KConfigGroup conf(Settings::self()->config(), "HTML Settings"); KConfig _konq(QStringLiteral("konquerorrc"), KConfig::NoGlobals); KConfigGroup konq(&_konq, "HTML Settings"); if (!conf.hasKey("MinimumFontSize")) { int minfs; if (konq.hasKey("MinimumFontSize")) { minfs = konq.readEntry("MinimumFontSize", 8); } else { minfs = std::max(QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize() - 2, 4); } Settings::setMinimumFontSize(minfs); } if (!conf.hasKey("MediumFontSize")) { int medfs; if (konq.hasKey("MediumFontSize")) { medfs = konq.readEntry("MediumFontSize", 12); } else { medfs = QFontDatabase::systemFont(QFontDatabase::GeneralFont).pointSize(); } Settings::setMediumFontSize(medfs); } QWebEngineSettings::defaultSettings()->setFontFamily(QWebEngineSettings::StandardFont, Settings::standardFont()); QWebEngineSettings::defaultSettings()->setFontFamily(QWebEngineSettings::FixedFont, Settings::fixedFont()); QWebEngineSettings::defaultSettings()->setFontFamily(QWebEngineSettings::SerifFont, Settings::serifFont()); QWebEngineSettings::defaultSettings()->setFontFamily(QWebEngineSettings::SansSerifFont, Settings::sansSerifFont()); QWebEngineSettings::defaultSettings()->setFontSize(QWebEngineSettings::MinimumFontSize, Settings::minimumFontSize()); QWebEngineSettings::defaultSettings()->setFontSize(QWebEngineSettings::DefaultFontSize, Settings::mediumFontSize()); } bool Part::handleCommandLine(const QStringList &args) { QCommandLineParser parser; akregator_options(&parser); parser.process(args); const QString addFeedGroup = parser.isSet(QStringLiteral("group")) ? parser.value(QStringLiteral("group")) : i18n("Imported Folder"); QStringList feedsToAdd = parser.values(QStringLiteral("addfeed")); if (feedsToAdd.isEmpty() && !parser.positionalArguments().isEmpty()) { Q_FOREACH (const QString &url, parser.positionalArguments()) { feedsToAdd.append(url); } } if (!feedsToAdd.isEmpty()) { addFeedsToGroup(feedsToAdd, addFeedGroup); } return true; } void Part::clearCrashProperties() { if (!m_doCrashSave) { return; } KConfig config(QStringLiteral("crashed"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); KConfigGroup configGroup(&config, "Part"); configGroup.writeEntry("crashed", false); } void Part::saveCrashProperties() { if (!m_doCrashSave) { return; } KConfig config(QStringLiteral("crashed"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); KConfigGroup configGroup(&config, "Part"); configGroup.deleteGroup(); configGroup.writeEntry("crashed", true); saveProperties(configGroup); } void Part::slotAutoSave() { saveCrashProperties(); } void Part::autoSaveProperties() { KConfig config(QStringLiteral("autosaved"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); KConfigGroup configGroup(&config, "Part"); configGroup.deleteGroup(); saveProperties(configGroup); clearCrashProperties(); } void Part::autoReadProperties() { if (qGuiApp->isSessionRestored()) { return; } KConfig config(QStringLiteral("autosaved"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); KConfigGroup configGroup(&config, "Part"); readProperties(configGroup); } void Part::slotRestoreSession(Akregator::CrashWidget::CrashAction type) { switch (type) { case Akregator::CrashWidget::RestoreSession: { KConfig config(QStringLiteral("crashed"), KConfig::SimpleConfig, QStandardPaths::AppDataLocation); KConfigGroup configGroup(&config, "Part"); readProperties(configGroup); clearCrashProperties(); break; } case Akregator::CrashWidget::NotRestoreSession: clearCrashProperties(); break; case Akregator::CrashWidget::AskMeLater: break; } m_doCrashSave = true; } } // namespace Akregator #include "akregator_part.moc" diff --git a/src/feed/feed.cpp b/src/feed/feed.cpp index 87ba473e..774f5066 100644 --- a/src/feed/feed.cpp +++ b/src/feed/feed.cpp @@ -1,945 +1,946 @@ /* This file is part of Akregator. Copyright (C) 2004 Stanislav Karchebny 2005 Frank Osterfeld 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. As a special exception, permission is given to link this program with any edition of Qt, and distribute the resulting executable, without including the source code for Qt in the source distribution. */ #include "feed.h" #include "akregatorconfig.h" #include "article.h" #include "articlejobs.h" #include "feedstorage.h" #include "fetchqueue.h" #include "folder.h" #include "notificationmanager.h" #include "storage.h" #include "treenodevisitor.h" #include "types.h" #include "utils.h" +#include "feedretriever.h" #include #include "akregator_debug.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using Syndication::ItemPtr; using namespace Akregator; template class Container> QVector valuesToVector(const Container &container) { QVector values; values.reserve(container.size()); for (const Value &value : container) { values << value; } return values; } class Q_DECL_HIDDEN Akregator::Feed::Private { Akregator::Feed *const q; public: explicit Private(Backend::Storage *storage, Akregator::Feed *qq); Backend::Storage *storage = nullptr; bool autoFetch = false; int fetchInterval; ArchiveMode archiveMode; int maxArticleAge; int maxArticleNumber; bool markImmediatelyAsRead = false; bool useNotification = false; bool loadLinkedWebsite = false; int lastFetched; Syndication::ErrorCode fetchErrorCode; int fetchTries; bool followDiscovery = false; Syndication::Loader *loader = nullptr; bool articlesLoaded = false; Backend::FeedStorage *archive = nullptr; QString xmlUrl; QString htmlUrl; QString description; /** list of feed articles */ QHash articles; /** list of deleted articles. This contains **/ QVector
deletedArticles; /** caches guids of deleted articles for notification */ QVector
addedArticlesNotify; QVector
removedArticlesNotify; QVector
updatedArticlesNotify; QPixmap imagePixmap; Syndication::ImagePtr image; QIcon favicon; mutable int totalCount; void setTotalCountDirty() const { totalCount = -1; } }; QString Akregator::Feed::archiveModeToString(ArchiveMode mode) { switch (mode) { case keepAllArticles: return QStringLiteral("keepAllArticles"); case disableArchiving: return QStringLiteral("disableArchiving"); case limitArticleNumber: return QStringLiteral("limitArticleNumber"); case limitArticleAge: return QStringLiteral("limitArticleAge"); default: break; } return QStringLiteral("globalDefault"); } Akregator::Feed *Akregator::Feed::fromOPML(QDomElement e, Backend::Storage *storage) { if (!e.hasAttribute(QStringLiteral("xmlUrl")) && !e.hasAttribute(QStringLiteral("xmlurl")) && !e.hasAttribute(QStringLiteral("xmlURL"))) { return nullptr; } QString title = e.hasAttribute(QStringLiteral("text")) ? e.attribute(QStringLiteral("text")) : e.attribute(QStringLiteral("title")); QString xmlUrl = e.hasAttribute(QStringLiteral("xmlUrl")) ? e.attribute(QStringLiteral("xmlUrl")) : e.attribute(QStringLiteral("xmlurl")); if (xmlUrl.isEmpty()) { xmlUrl = e.attribute(QStringLiteral("xmlURL")); } bool useCustomFetchInterval = e.attribute(QStringLiteral("useCustomFetchInterval")) == QLatin1String("true"); QString htmlUrl = e.attribute(QStringLiteral("htmlUrl")); QString description = e.attribute(QStringLiteral("description")); int fetchInterval = e.attribute(QStringLiteral("fetchInterval")).toInt(); ArchiveMode archiveMode = stringToArchiveMode(e.attribute(QStringLiteral("archiveMode"))); int maxArticleAge = e.attribute(QStringLiteral("maxArticleAge")).toUInt(); int maxArticleNumber = e.attribute(QStringLiteral("maxArticleNumber")).toUInt(); bool markImmediatelyAsRead = e.attribute(QStringLiteral("markImmediatelyAsRead")) == QLatin1String("true"); bool useNotification = e.attribute(QStringLiteral("useNotification")) == QLatin1String("true"); bool loadLinkedWebsite = e.attribute(QStringLiteral("loadLinkedWebsite")) == QLatin1String("true"); uint id = e.attribute(QStringLiteral("id")).toUInt(); Feed *const feed = new Feed(storage); feed->setTitle(title); feed->setXmlUrl(xmlUrl); feed->setCustomFetchIntervalEnabled(useCustomFetchInterval); feed->setHtmlUrl(htmlUrl); feed->setId(id); feed->setDescription(description); feed->setArchiveMode(archiveMode); feed->setUseNotification(useNotification); feed->setFetchInterval(fetchInterval); feed->setMaxArticleAge(maxArticleAge); feed->setMaxArticleNumber(maxArticleNumber); feed->setMarkImmediatelyAsRead(markImmediatelyAsRead); feed->setLoadLinkedWebsite(loadLinkedWebsite); feed->loadArticles(); // TODO: make me fly: make this delayed return feed; } bool Akregator::Feed::accept(TreeNodeVisitor *visitor) { if (visitor->visitFeed(this)) { return true; } else { return visitor->visitTreeNode(this); } } QVector Akregator::Feed::folders() const { return QVector(); } QVector Akregator::Feed::folders() { return QVector(); } QVector Akregator::Feed::feeds() const { QVector list; list.append(this); return list; } QVector Akregator::Feed::feeds() { QVector list; list.append(this); return list; } Article Akregator::Feed::findArticle(const QString &guid) const { return d->articles.value(guid); } QVector
Akregator::Feed::articles() { if (!d->articlesLoaded) { loadArticles(); } return valuesToVector(d->articles); } Backend::Storage *Akregator::Feed::storage() { return d->storage; } void Akregator::Feed::loadArticles() { if (d->articlesLoaded) { return; } if (!d->archive && d->storage) { d->archive = d->storage->archiveFor(xmlUrl()); } QStringList list = d->archive->articles(); for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) { Article mya(*it, this); d->articles[mya.guid()] = mya; if (mya.isDeleted()) { d->deletedArticles.append(mya); } } d->articlesLoaded = true; enforceLimitArticleNumber(); recalcUnreadCount(); } void Akregator::Feed::recalcUnreadCount() { QVector
tarticles = articles(); QVector
::ConstIterator it; QVector
::ConstIterator en = tarticles.constEnd(); int oldUnread = d->archive->unread(); int unread = 0; for (it = tarticles.constBegin(); it != en; ++it) { if (!(*it).isDeleted() && (*it).status() != Read) { ++unread; } } if (unread != oldUnread) { d->archive->setUnread(unread); nodeModified(); } } Akregator::Feed::ArchiveMode Akregator::Feed::stringToArchiveMode(const QString &str) { if (str == QLatin1String("globalDefault")) { return globalDefault; } else if (str == QLatin1String("keepAllArticles")) { return keepAllArticles; } else if (str == QLatin1String("disableArchiving")) { return disableArchiving; } else if (str == QLatin1String("limitArticleNumber")) { return limitArticleNumber; } else if (str == QLatin1String("limitArticleAge")) { return limitArticleAge; } return globalDefault; } Akregator::Feed::Private::Private(Backend::Storage *storage_, Akregator::Feed *qq) : q(qq) , storage(storage_) , autoFetch(false) , fetchInterval(30) , archiveMode(globalDefault) , maxArticleAge(60) , maxArticleNumber(1000) , markImmediatelyAsRead(false) , useNotification(false) , loadLinkedWebsite(false) , lastFetched(0) , fetchErrorCode(Syndication::Success) , fetchTries(0) , followDiscovery(false) , loader(nullptr) , articlesLoaded(false) , archive(nullptr) , totalCount(-1) { Q_ASSERT(q); Q_ASSERT(storage); } Akregator::Feed::Feed(Backend::Storage *storage) : TreeNode() , d(new Private(storage, this)) { } Akregator::Feed::~Feed() { slotAbortFetch(); emitSignalDestroyed(); delete d; d = nullptr; } void Akregator::Feed::loadFavicon(const QUrl &url) { KIO::FavIconRequestJob *job = new KIO::FavIconRequestJob(url); connect(job, &KIO::FavIconRequestJob::result, this, [job, this](KJob *) { if (!job->error()) { setFavicon(QIcon(job->iconFile())); } }); } bool Akregator::Feed::useCustomFetchInterval() const { return d->autoFetch; } void Akregator::Feed::setCustomFetchIntervalEnabled(bool enabled) { d->autoFetch = enabled; } int Akregator::Feed::fetchInterval() const { return d->fetchInterval; } void Akregator::Feed::setFetchInterval(int interval) { d->fetchInterval = interval; } int Akregator::Feed::maxArticleAge() const { return d->maxArticleAge; } void Akregator::Feed::setMaxArticleAge(int maxArticleAge) { d->maxArticleAge = maxArticleAge; } int Akregator::Feed::maxArticleNumber() const { return d->maxArticleNumber; } void Akregator::Feed::setMaxArticleNumber(int maxArticleNumber) { d->maxArticleNumber = maxArticleNumber; } bool Akregator::Feed::markImmediatelyAsRead() const { return d->markImmediatelyAsRead; } bool Akregator::Feed::isFetching() const { return d->loader != nullptr; } void Akregator::Feed::setMarkImmediatelyAsRead(bool enabled) { d->markImmediatelyAsRead = enabled; if (enabled) { createMarkAsReadJob()->start(); } } void Akregator::Feed::setUseNotification(bool enabled) { d->useNotification = enabled; } bool Akregator::Feed::useNotification() const { return d->useNotification; } void Akregator::Feed::setLoadLinkedWebsite(bool enabled) { d->loadLinkedWebsite = enabled; } bool Akregator::Feed::loadLinkedWebsite() const { return d->loadLinkedWebsite; } QPixmap Akregator::Feed::image() const { return d->imagePixmap; } QString Akregator::Feed::xmlUrl() const { return d->xmlUrl; } void Akregator::Feed::setXmlUrl(const QString &s) { d->xmlUrl = s; if (!Settings::fetchOnStartup()) { QTimer::singleShot(KRandom::random() % 4000, this, &Feed::slotAddFeedIconListener); // TODO: let's give a gui some time to show up before starting the fetch when no fetch on startup is used. replace this with something proper later... } } QString Akregator::Feed::htmlUrl() const { return d->htmlUrl; } void Akregator::Feed::setHtmlUrl(const QString &s) { d->htmlUrl = s; } QString Akregator::Feed::description() const { return d->description; } void Akregator::Feed::setDescription(const QString &s) { d->description = s; } bool Akregator::Feed::fetchErrorOccurred() const { return d->fetchErrorCode != Syndication::Success; } Syndication::ErrorCode Akregator::Feed::fetchErrorCode() const { return d->fetchErrorCode; } bool Akregator::Feed::isArticlesLoaded() const { return d->articlesLoaded; } QDomElement Akregator::Feed::toOPML(QDomElement parent, QDomDocument document) const { QDomElement el = document.createElement(QStringLiteral("outline")); el.setAttribute(QStringLiteral("text"), title()); el.setAttribute(QStringLiteral("title"), title()); el.setAttribute(QStringLiteral("xmlUrl"), d->xmlUrl); el.setAttribute(QStringLiteral("htmlUrl"), d->htmlUrl); el.setAttribute(QStringLiteral("id"), QString::number(id())); el.setAttribute(QStringLiteral("description"), d->description); el.setAttribute(QStringLiteral("useCustomFetchInterval"), (useCustomFetchInterval() ? QStringLiteral("true") : QStringLiteral("false"))); el.setAttribute(QStringLiteral("fetchInterval"), QString::number(fetchInterval())); el.setAttribute(QStringLiteral("archiveMode"), archiveModeToString(d->archiveMode)); el.setAttribute(QStringLiteral("maxArticleAge"), d->maxArticleAge); el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber); if (d->markImmediatelyAsRead) { el.setAttribute(QStringLiteral("markImmediatelyAsRead"), QStringLiteral("true")); } if (d->useNotification) { el.setAttribute(QStringLiteral("useNotification"), QStringLiteral("true")); } if (d->loadLinkedWebsite) { el.setAttribute(QStringLiteral("loadLinkedWebsite"), QStringLiteral("true")); } el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber); el.setAttribute(QStringLiteral("type"), QStringLiteral("rss")); // despite some additional fields, it is still "rss" OPML el.setAttribute(QStringLiteral("version"), QStringLiteral("RSS")); parent.appendChild(el); return el; } KJob *Akregator::Feed::createMarkAsReadJob() { ArticleModifyJob *job = new ArticleModifyJob; Q_FOREACH (const Article &i, articles()) { const ArticleId aid = { xmlUrl(), i.guid() }; job->setStatus(aid, Read); } return job; } void Akregator::Feed::slotAddToFetchQueue(FetchQueue *queue, bool intervalFetchOnly) { if (!intervalFetchOnly) { queue->addFeed(this); } else { int interval = -1; if (useCustomFetchInterval()) { interval = fetchInterval() * 60; } else if (Settings::useIntervalFetch()) { interval = Settings::autoFetchInterval() * 60; } uint lastFetch = d->archive->lastFetch(); uint now = QDateTime::currentDateTimeUtc().toTime_t(); if (interval > 0 && (now - lastFetch) >= static_cast(interval)) { queue->addFeed(this); } } } void Akregator::Feed::slotAddFeedIconListener() { loadFavicon(QUrl(d->xmlUrl)); } void Akregator::Feed::appendArticles(const Syndication::FeedPtr &feed) { d->setTotalCountDirty(); bool changed = false; const bool notify = useNotification() || Settings::useNotifications(); QList items = feed->items(); QList::ConstIterator it = items.constBegin(); QList::ConstIterator en = items.constEnd(); int nudge = 0; QVector
deletedArticles = d->deletedArticles; for (; it != en; ++it) { if (!d->articles.contains((*it)->id())) { // article not in list Article mya(*it, this); mya.offsetPubDate(nudge); nudge--; appendArticle(mya); d->addedArticlesNotify.append(mya); if (!mya.isDeleted() && !markImmediatelyAsRead()) { mya.setStatus(New); } else { mya.setStatus(Read); } if (notify) { NotificationManager::self()->slotNotifyArticle(mya); } changed = true; } else { // article is in list // if the article's guid is no hash but an ID, we have to check if the article was updated. That's done by comparing the hash values. Article old = d->articles[(*it)->id()]; Article mya(*it, this); if (!mya.guidIsHash() && mya.hash() != old.hash() && !old.isDeleted()) { mya.setKeep(old.keep()); int oldstatus = old.status(); old.setStatus(Read); d->articles.remove(old.guid()); appendArticle(mya); mya.setStatus(oldstatus); d->updatedArticlesNotify.append(mya); changed = true; } else if (old.isDeleted()) { deletedArticles.removeAll(mya); } } } QVector
::ConstIterator dit = deletedArticles.constBegin(); QVector
::ConstIterator dtmp; QVector
::ConstIterator den = deletedArticles.constEnd(); // delete articles with delete flag set completely from archive, which aren't in the current feed source anymore while (dit != den) { dtmp = dit; ++dit; d->articles.remove((*dtmp).guid()); d->archive->deleteArticle((*dtmp).guid()); d->removedArticlesNotify.append(*dtmp); changed = true; d->deletedArticles.removeAll(*dtmp); } if (changed) { articlesModified(); } } bool Akregator::Feed::usesExpiryByAge() const { return (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) || d->archiveMode == limitArticleAge; } bool Akregator::Feed::isExpired(const Article &a) const { QDateTime now = QDateTime::currentDateTime(); int expiryAge = -1; // check whether the feed uses the global default and the default is limitArticleAge if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) { expiryAge = Settings::maxArticleAge() * 24 * 3600; } else // otherwise check if this feed has limitArticleAge set if (d->archiveMode == limitArticleAge) { expiryAge = d->maxArticleAge * 24 * 3600; } return expiryAge != -1 && a.pubDate().secsTo(now) > expiryAge; } void Akregator::Feed::appendArticle(const Article &a) { if ((a.keep() && Settings::doNotExpireImportantArticles()) || (!usesExpiryByAge() || !isExpired(a))) { // if not expired if (!d->articles.contains(a.guid())) { d->articles[a.guid()] = a; if (!a.isDeleted() && a.status() != Read) { setUnread(unread() + 1); } } } } void Akregator::Feed::fetch(bool followDiscovery) { d->followDiscovery = followDiscovery; d->fetchTries = 0; // mark all new as unread for (auto it = d->articles.begin(), end = d->articles.end(); it != end; ++it) { if ((*it).status() == New) { (*it).setStatus(Unread); } } Q_EMIT fetchStarted(this); tryFetch(); } void Akregator::Feed::slotAbortFetch() { if (d->loader) { d->loader->abort(); } } void Akregator::Feed::tryFetch() { d->fetchErrorCode = Syndication::Success; d->loader = Syndication::Loader::create(this, SLOT(fetchCompleted(Syndication::Loader *, Syndication::FeedPtr, Syndication::ErrorCode))); - d->loader->loadFrom(QUrl(d->xmlUrl)); + d->loader->loadFrom(QUrl(d->xmlUrl), new FeedRetriever()); } void Akregator::Feed::slotImageFetched(const QPixmap &image) { setImage(image); } void Akregator::Feed::fetchCompleted(Syndication::Loader *l, Syndication::FeedPtr doc, Syndication::ErrorCode status) { // Note that loader instances delete themselves d->loader = nullptr; // fetching wasn't successful: if (status != Syndication::Success) { if (status == Syndication::Aborted) { d->fetchErrorCode = Syndication::Success; Q_EMIT fetchAborted(this); } else if (d->followDiscovery && (status == Syndication::InvalidXml) && (d->fetchTries < 3) && (l->discoveredFeedURL().isValid())) { d->fetchTries++; d->xmlUrl = l->discoveredFeedURL().url(); Q_EMIT fetchDiscovery(this); tryFetch(); } else { d->fetchErrorCode = status; Q_EMIT fetchError(this); } markAsFetchedNow(); return; } loadArticles(); // TODO: make me fly: make this delayed loadFavicon(QUrl(xmlUrl())); d->fetchErrorCode = Syndication::Success; if (d->imagePixmap.isNull()) { const QString imageFileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png"); d->imagePixmap = QPixmap(imageFileName, "PNG"); } if (title().isEmpty()) { setTitle(Syndication::htmlToPlainText(doc->title())); } d->description = doc->description(); d->htmlUrl = doc->link(); appendArticles(doc); markAsFetchedNow(); Q_EMIT fetched(this); } void Akregator::Feed::markAsFetchedNow() { if (d->archive) { d->archive->setLastFetch(QDateTime::currentDateTimeUtc().toTime_t()); } } QIcon Akregator::Feed::icon() const { if (fetchErrorOccurred()) { return QIcon::fromTheme(QStringLiteral("dialog-error")); } return !d->favicon.isNull() ? d->favicon : QIcon::fromTheme(QStringLiteral("text-html")); } void Akregator::Feed::deleteExpiredArticles(ArticleDeleteJob *deleteJob) { if (!usesExpiryByAge()) { return; } setNotificationMode(false); QList toDelete; const QString feedUrl = xmlUrl(); const bool useKeep = Settings::doNotExpireImportantArticles(); for (const Article &i : qAsConst(d->articles)) { if ((!useKeep || !i.keep()) && isExpired(i)) { const ArticleId aid = { feedUrl, i.guid() }; toDelete.append(aid); } } deleteJob->appendArticleIds(toDelete); setNotificationMode(true); } void Akregator::Feed::setFavicon(const QIcon &icon) { d->favicon = icon; nodeModified(); } void Akregator::Feed::setImage(const QPixmap &p) { if (p.isNull()) { return; } d->imagePixmap = p; const QString filename = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1String("/akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png"); QFileInfo fileInfo(filename); QDir().mkpath(fileInfo.absolutePath()); d->imagePixmap.save(filename, "PNG"); nodeModified(); } Akregator::Feed::ArchiveMode Akregator::Feed::archiveMode() const { return d->archiveMode; } void Akregator::Feed::setArchiveMode(ArchiveMode archiveMode) { d->archiveMode = archiveMode; } int Akregator::Feed::unread() const { return d->archive ? d->archive->unread() : 0; } void Akregator::Feed::setUnread(int unread) { if (d->archive && unread != d->archive->unread()) { d->archive->setUnread(unread); nodeModified(); } } void Akregator::Feed::setArticleDeleted(Article &a) { d->setTotalCountDirty(); if (!d->deletedArticles.contains(a)) { d->deletedArticles.append(a); } d->updatedArticlesNotify.append(a); articlesModified(); } void Akregator::Feed::setArticleChanged(Article &a, int oldStatus, bool process) { int newStatus = a.status(); if (oldStatus != -1) { if (oldStatus == Read && newStatus != Read) { setUnread(unread() + 1); } else if (oldStatus != Read && newStatus == Read) { setUnread(unread() - 1); } } d->updatedArticlesNotify.append(a); if (process) { articlesModified(); } } int Akregator::Feed::totalCount() const { if (d->totalCount == -1) { d->totalCount = std::count_if(d->articles.constBegin(), d->articles.constEnd(), [](const Article &art) -> bool { return !art.isDeleted(); }); } return d->totalCount; } TreeNode *Akregator::Feed::next() { if (nextSibling()) { return nextSibling(); } Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } const TreeNode *Akregator::Feed::next() const { if (nextSibling()) { return nextSibling(); } const Folder *p = parent(); while (p) { if (p->nextSibling()) { return p->nextSibling(); } else { p = p->parent(); } } return nullptr; } void Akregator::Feed::doArticleNotification() { if (!d->addedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->addedArticlesNotify; Q_EMIT signalArticlesAdded(this, l); d->addedArticlesNotify.clear(); } if (!d->updatedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->updatedArticlesNotify; Q_EMIT signalArticlesUpdated(this, l); d->updatedArticlesNotify.clear(); } if (!d->removedArticlesNotify.isEmpty()) { // copy list, otherwise the refcounting in Article::Private breaks for // some reason (causing segfaults) QVector
l = d->removedArticlesNotify; Q_EMIT signalArticlesRemoved(this, l); d->removedArticlesNotify.clear(); } TreeNode::doArticleNotification(); } void Akregator::Feed::enforceLimitArticleNumber() { int limit = -1; if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleNumber) { limit = Settings::maxArticleNumber(); } else if (d->archiveMode == limitArticleNumber) { limit = maxArticleNumber(); } if (limit == -1 || limit >= d->articles.count() - d->deletedArticles.count()) { return; } QVector
articles = valuesToVector(d->articles); std::sort(articles.begin(), articles.end()); int c = 0; const bool useKeep = Settings::doNotExpireImportantArticles(); for (Article i : qAsConst(articles)) { if (c < limit) { if (!i.isDeleted() && (!useKeep || !i.keep())) { ++c; } } else if (!useKeep || !i.keep()) { i.setDeleted(); } } } diff --git a/src/feed/feedretriever.cpp b/src/feed/feedretriever.cpp new file mode 100644 index 00000000..62526c43 --- /dev/null +++ b/src/feed/feedretriever.cpp @@ -0,0 +1,78 @@ +/* + This file is part of Akregator. + + Copyright (C) 2018 Daniel Vrátil + + 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. + + As a special exception, permission is given to link this program + with any edition of Qt, and distribute the resulting executable, + without including the source code for Qt in the source distribution. +*/ + +#include "feedretriever.h" +#include "akregatorconfig.h" +#include "akregator-version.h" + +#include + +#include + +using namespace Akregator; + +FeedRetriever::FeedRetriever() + : Syndication::DataRetriever() +{ +} + +void FeedRetriever::retrieveData(const QUrl &url) +{ + QString userAgent = QStringLiteral("Akregator/%1; syndication").arg(QStringLiteral(AKREGATOR_VERSION)); + if (!Settings::customUserAgent().isEmpty()) { + userAgent = Settings::customUserAgent(); + } + bool useCache = Settings::useHTMLCache(); + + auto job = KIO::storedGet(url, KIO::NoReload, KIO::HideProgressInfo); + job->addMetaData(QStringLiteral("UserAgent"), userAgent); + job->addMetaData(QStringLiteral("cache"), useCache ? QStringLiteral("refresh") : QStringLiteral("reload")); + connect(job, &KJob::result, this, &FeedRetriever::getFinished); + mJob = job; + mJob->start(); +} + +int FeedRetriever::errorCode() const +{ + return mError; +} + +void FeedRetriever::abort() +{ + if (mJob) { + mJob->kill(); + mJob = nullptr; + } +} + +void FeedRetriever::getFinished(KJob *job) +{ + if (job->error()) { + mError = job->error(); + Q_EMIT dataRetrieved({}, false); + return; + } + + Q_EMIT dataRetrieved(static_cast(job)->data(), true); +} diff --git a/src/feed/feedretriever.h b/src/feed/feedretriever.h new file mode 100644 index 00000000..3a0ff3d8 --- /dev/null +++ b/src/feed/feedretriever.h @@ -0,0 +1,54 @@ +/* + This file is part of Akregator. + + Copyright (C) 2018 Daniel Vrátil + + 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. + + As a special exception, permission is given to link this program + with any edition of Qt, and distribute the resulting executable, + without including the source code for Qt in the source distribution. +*/ + +#ifndef FEEDRETRIEVER_H_ +#define FEEDRETRIEVER_H_ + +#include + +class KJob; + +namespace Akregator { + +class FeedRetriever : public Syndication::DataRetriever +{ + Q_OBJECT +public: + explicit FeedRetriever(); + + void retrieveData(const QUrl &url) override; + void abort() override; + int errorCode() const override; + +private Q_SLOTS: + void getFinished(KJob *job); + +private: + KJob *mJob = nullptr; + int mError = 0; +}; + +} + +#endif