Fix show total article.
[kdepim.git] / akregator / src / feed.cpp
bloba161389c9f81f6bdee4efa5e0add9de602093b4c
1 /*
2 This file is part of Akregator.
4 Copyright (C) 2004 Stanislav Karchebny <Stanislav.Karchebny@kdemail.net>
5 2005 Frank Osterfeld <osterfeld@kde.org>
7 This program is free software; you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation; either version 2 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program; if not, write to the Free Software
19 Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 As a special exception, permission is given to link this program
22 with any edition of Qt, and distribute the resulting executable,
23 without including the source code for Qt in the source distribution.
26 #include "feed.h"
27 #include "akregatorconfig.h"
28 #include "article.h"
29 #include "articlejobs.h"
30 #include "feediconmanager.h"
31 #include "feedstorage.h"
32 #include "fetchqueue.h"
33 #include "folder.h"
34 #include "notificationmanager.h"
35 #include "storage.h"
36 #include "treenodevisitor.h"
37 #include "types.h"
38 #include "utils.h"
40 #include <Syndication/Syndication>
42 #include "akregator_debug.h"
43 #include <QIcon>
44 #include <QFileInfo>
45 #include <QDir>
47 #include <QUrl>
48 #include <KRandom>
50 #include <QDateTime>
51 #include <QDomDocument>
52 #include <QDomElement>
53 #include <QHash>
54 #include <QList>
55 #include <QPixmap>
56 #include <QTimer>
58 #include <memory>
59 #include <QStandardPaths>
61 using Syndication::ItemPtr;
62 using namespace Akregator;
64 template<typename Key, typename Value, template<typename, typename> class Container>
65 QVector<Value> valuesToVector(const Container<Key, Value> &container)
67 QVector<Value> values;
68 values.reserve(container.size());
69 foreach (const Value &value, container) {
70 values << value;
72 return values;
75 class Q_DECL_HIDDEN Akregator::Feed::Private
77 Akregator::Feed *const q;
78 public:
79 explicit Private(Backend::Storage *storage, Akregator::Feed *qq);
81 Backend::Storage *storage;
82 bool autoFetch;
83 int fetchInterval;
84 ArchiveMode archiveMode;
85 int maxArticleAge;
86 int maxArticleNumber;
87 bool markImmediatelyAsRead;
88 bool useNotification;
89 bool loadLinkedWebsite;
90 int lastFetched;
92 Syndication::ErrorCode fetchErrorCode;
93 int fetchTries;
94 bool followDiscovery;
95 Syndication::Loader *loader;
96 bool articlesLoaded;
97 Backend::FeedStorage *archive;
99 QString xmlUrl;
100 QString htmlUrl;
101 QString description;
103 /** list of feed articles */
104 QHash<QString, Article> articles;
106 /** list of deleted articles. This contains **/
107 QVector<Article> deletedArticles;
109 /** caches guids of deleted articles for notification */
111 QVector<Article> addedArticlesNotify;
112 QVector<Article> removedArticlesNotify;
113 QVector<Article> updatedArticlesNotify;
115 QPixmap imagePixmap;
116 Syndication::ImagePtr image;
117 QIcon favicon;
118 mutable int totalCount;
119 void setTotalCountDirty() const
121 totalCount = -1;
125 QString Akregator::Feed::archiveModeToString(ArchiveMode mode)
127 switch (mode) {
128 case keepAllArticles:
129 return QStringLiteral("keepAllArticles");
130 case disableArchiving:
131 return QStringLiteral("disableArchiving");
132 case limitArticleNumber:
133 return QStringLiteral("limitArticleNumber");
134 case limitArticleAge:
135 return QStringLiteral("limitArticleAge");
136 default:
137 break;
139 return QStringLiteral("globalDefault");
142 Akregator::Feed *Akregator::Feed::fromOPML(QDomElement e, Backend::Storage *storage)
145 if (!e.hasAttribute(QStringLiteral("xmlUrl")) && !e.hasAttribute(QStringLiteral("xmlurl")) && !e.hasAttribute(QStringLiteral("xmlURL"))) {
146 return 0;
149 QString title = e.hasAttribute(QStringLiteral("text")) ? e.attribute(QStringLiteral("text")) : e.attribute(QStringLiteral("title"));
151 QString xmlUrl = e.hasAttribute(QStringLiteral("xmlUrl")) ? e.attribute(QStringLiteral("xmlUrl")) : e.attribute(QStringLiteral("xmlurl"));
152 if (xmlUrl.isEmpty()) {
153 xmlUrl = e.attribute(QStringLiteral("xmlURL"));
156 bool useCustomFetchInterval = e.attribute(QStringLiteral("useCustomFetchInterval")) == QLatin1String("true");
158 QString htmlUrl = e.attribute(QStringLiteral("htmlUrl"));
159 QString description = e.attribute(QStringLiteral("description"));
160 int fetchInterval = e.attribute(QStringLiteral("fetchInterval")).toInt();
161 ArchiveMode archiveMode = stringToArchiveMode(e.attribute(QStringLiteral("archiveMode")));
162 int maxArticleAge = e.attribute(QStringLiteral("maxArticleAge")).toUInt();
163 int maxArticleNumber = e.attribute(QStringLiteral("maxArticleNumber")).toUInt();
164 bool markImmediatelyAsRead = e.attribute(QStringLiteral("markImmediatelyAsRead")) == QLatin1String("true");
165 bool useNotification = e.attribute(QStringLiteral("useNotification")) == QLatin1String("true");
166 bool loadLinkedWebsite = e.attribute(QStringLiteral("loadLinkedWebsite")) == QLatin1String("true");
167 uint id = e.attribute(QStringLiteral("id")).toUInt();
169 Feed *const feed = new Feed(storage);
170 feed->setTitle(title);
171 feed->setXmlUrl(xmlUrl);
172 feed->setCustomFetchIntervalEnabled(useCustomFetchInterval);
173 feed->setHtmlUrl(htmlUrl);
174 feed->setId(id);
175 feed->setDescription(description);
176 feed->setArchiveMode(archiveMode);
177 feed->setUseNotification(useNotification);
178 feed->setFetchInterval(fetchInterval);
179 feed->setMaxArticleAge(maxArticleAge);
180 feed->setMaxArticleNumber(maxArticleNumber);
181 feed->setMarkImmediatelyAsRead(markImmediatelyAsRead);
182 feed->setLoadLinkedWebsite(loadLinkedWebsite);
183 feed->loadArticles(); // TODO: make me fly: make this delayed
185 return feed;
188 bool Akregator::Feed::accept(TreeNodeVisitor *visitor)
190 if (visitor->visitFeed(this)) {
191 return true;
192 } else {
193 return visitor->visitTreeNode(this);
197 QVector<const Folder *> Akregator::Feed::folders() const
199 return QVector<const Folder *>();
202 QVector<Folder *> Akregator::Feed::folders()
204 return QVector<Folder *>();
207 QVector<const Akregator::Feed *> Akregator::Feed::feeds() const
209 QVector<const Akregator::Feed *> list;
210 list.append(this);
211 return list;
214 QVector<Akregator::Feed *> Akregator::Feed::feeds()
216 QVector<Feed *> list;
217 list.append(this);
218 return list;
221 Article Akregator::Feed::findArticle(const QString &guid) const
223 return d->articles.value(guid);
226 QVector<Article> Akregator::Feed::articles()
228 if (!d->articlesLoaded) {
229 loadArticles();
231 return valuesToVector(d->articles);
234 Backend::Storage *Akregator::Feed::storage()
236 return d->storage;
239 void Akregator::Feed::loadArticles()
241 if (d->articlesLoaded) {
242 return;
245 if (!d->archive && d->storage) {
246 d->archive = d->storage->archiveFor(xmlUrl());
249 QStringList list = d->archive->articles();
250 for (QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); ++it) {
251 Article mya(*it, this);
252 d->articles[mya.guid()] = mya;
253 if (mya.isDeleted()) {
254 d->deletedArticles.append(mya);
258 d->articlesLoaded = true;
259 enforceLimitArticleNumber();
260 recalcUnreadCount();
263 void Akregator::Feed::recalcUnreadCount()
265 QVector<Article> tarticles = articles();
266 QVector<Article>::ConstIterator it;
267 QVector<Article>::ConstIterator en = tarticles.constEnd();
269 int oldUnread = d->archive->unread();
271 int unread = 0;
273 for (it = tarticles.constBegin(); it != en; ++it)
274 if (!(*it).isDeleted() && (*it).status() != Read) {
275 ++unread;
278 if (unread != oldUnread) {
279 d->archive->setUnread(unread);
280 nodeModified();
284 Akregator::Feed::ArchiveMode Akregator::Feed::stringToArchiveMode(const QString &str)
286 if (str == QLatin1String("globalDefault")) {
287 return globalDefault;
289 if (str == QLatin1String("keepAllArticles")) {
290 return keepAllArticles;
292 if (str == QLatin1String("disableArchiving")) {
293 return disableArchiving;
295 if (str == QLatin1String("limitArticleNumber")) {
296 return limitArticleNumber;
298 if (str == QLatin1String("limitArticleAge")) {
299 return limitArticleAge;
302 return globalDefault;
305 Akregator::Feed::Private::Private(Backend::Storage *storage_, Akregator::Feed *qq)
306 : q(qq),
307 storage(storage_),
308 autoFetch(false),
309 fetchInterval(30),
310 archiveMode(globalDefault),
311 maxArticleAge(60),
312 maxArticleNumber(1000),
313 markImmediatelyAsRead(false),
314 useNotification(false),
315 loadLinkedWebsite(false),
316 lastFetched(0),
317 fetchErrorCode(Syndication::Success),
318 fetchTries(0),
319 followDiscovery(false),
320 loader(0),
321 articlesLoaded(false),
322 archive(0),
323 totalCount(-1)
325 Q_ASSERT(q);
326 Q_ASSERT(storage);
329 Akregator::Feed::Feed(Backend::Storage *storage) : TreeNode(), d(new Private(storage, this))
333 Akregator::Feed::~Feed()
335 FeedIconManager::self()->removeListener(this);
336 slotAbortFetch();
337 emitSignalDestroyed();
338 delete d;
339 d = 0;
342 bool Akregator::Feed::useCustomFetchInterval() const
344 return d->autoFetch;
347 void Akregator::Feed::setCustomFetchIntervalEnabled(bool enabled)
349 d->autoFetch = enabled;
352 int Akregator::Feed::fetchInterval() const
354 return d->fetchInterval;
357 void Akregator::Feed::setFetchInterval(int interval)
359 d->fetchInterval = interval;
362 int Akregator::Feed::maxArticleAge() const
364 return d->maxArticleAge;
367 void Akregator::Feed::setMaxArticleAge(int maxArticleAge)
369 d->maxArticleAge = maxArticleAge;
372 int Akregator::Feed::maxArticleNumber() const
374 return d->maxArticleNumber;
377 void Akregator::Feed::setMaxArticleNumber(int maxArticleNumber)
379 d->maxArticleNumber = maxArticleNumber;
382 bool Akregator::Feed::markImmediatelyAsRead() const
384 return d->markImmediatelyAsRead;
387 bool Akregator::Feed::isFetching() const
389 return d->loader != 0;
392 void Akregator::Feed::setMarkImmediatelyAsRead(bool enabled)
394 d->markImmediatelyAsRead = enabled;
395 if (enabled) {
396 createMarkAsReadJob()->start();
400 void Akregator::Feed::setUseNotification(bool enabled)
402 d->useNotification = enabled;
405 bool Akregator::Feed::useNotification() const
407 return d->useNotification;
410 void Akregator::Feed::setLoadLinkedWebsite(bool enabled)
412 d->loadLinkedWebsite = enabled;
415 bool Akregator::Feed::loadLinkedWebsite() const
417 return d->loadLinkedWebsite;
420 QPixmap Akregator::Feed::image() const
422 return d->imagePixmap;
425 QString Akregator::Feed::xmlUrl() const
427 return d->xmlUrl;
430 void Akregator::Feed::setXmlUrl(const QString &s)
432 d->xmlUrl = s;
433 if (! Settings::fetchOnStartup()) {
434 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...
438 QString Akregator::Feed::htmlUrl() const
440 return d->htmlUrl;
443 void Akregator::Feed::setHtmlUrl(const QString &s)
445 d->htmlUrl = s;
448 QString Akregator::Feed::description() const
450 return d->description;
453 void Akregator::Feed::setDescription(const QString &s)
455 d->description = s;
458 bool Akregator::Feed::fetchErrorOccurred() const
460 return d->fetchErrorCode != Syndication::Success;
463 Syndication::ErrorCode Akregator::Feed::fetchErrorCode() const
465 return d->fetchErrorCode;
468 bool Akregator::Feed::isArticlesLoaded() const
470 return d->articlesLoaded;
473 QDomElement Akregator::Feed::toOPML(QDomElement parent, QDomDocument document) const
475 QDomElement el = document.createElement(QStringLiteral("outline"));
476 el.setAttribute(QStringLiteral("text"), title());
477 el.setAttribute(QStringLiteral("title"), title());
478 el.setAttribute(QStringLiteral("xmlUrl"), d->xmlUrl);
479 el.setAttribute(QStringLiteral("htmlUrl"), d->htmlUrl);
480 el.setAttribute(QStringLiteral("id"), QString::number(id()));
481 el.setAttribute(QStringLiteral("description"), d->description);
482 el.setAttribute(QStringLiteral("useCustomFetchInterval"), (useCustomFetchInterval() ? QStringLiteral("true") : QStringLiteral("false")));
483 el.setAttribute(QStringLiteral("fetchInterval"), QString::number(fetchInterval()));
484 el.setAttribute(QStringLiteral("archiveMode"), archiveModeToString(d->archiveMode));
485 el.setAttribute(QStringLiteral("maxArticleAge"), d->maxArticleAge);
486 el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber);
487 if (d->markImmediatelyAsRead) {
488 el.setAttribute(QStringLiteral("markImmediatelyAsRead"), QStringLiteral("true"));
490 if (d->useNotification) {
491 el.setAttribute(QStringLiteral("useNotification"), QStringLiteral("true"));
493 if (d->loadLinkedWebsite) {
494 el.setAttribute(QStringLiteral("loadLinkedWebsite"), QStringLiteral("true"));
496 el.setAttribute(QStringLiteral("maxArticleNumber"), d->maxArticleNumber);
497 el.setAttribute(QStringLiteral("type"), QStringLiteral("rss")); // despite some additional fields, it is still "rss" OPML
498 el.setAttribute(QStringLiteral("version"), QStringLiteral("RSS"));
499 parent.appendChild(el);
500 return el;
503 KJob *Akregator::Feed::createMarkAsReadJob()
505 ArticleModifyJob *job = new ArticleModifyJob;
506 Q_FOREACH (const Article &i, articles()) {
507 const ArticleId aid = { xmlUrl(), i.guid() };
508 job->setStatus(aid, Read);
510 return job;
513 void Akregator::Feed::slotAddToFetchQueue(FetchQueue *queue, bool intervalFetchOnly)
515 if (!intervalFetchOnly) {
516 queue->addFeed(this);
517 } else {
518 int interval = -1;
520 if (useCustomFetchInterval()) {
521 interval = fetchInterval() * 60;
522 } else if (Settings::useIntervalFetch()) {
523 interval = Settings::autoFetchInterval() * 60;
526 uint lastFetch = d->archive->lastFetch();
528 uint now = QDateTime::currentDateTime().toTime_t();
530 if (interval > 0 && now - lastFetch >= (uint)interval) {
531 queue->addFeed(this);
536 void Akregator::Feed::slotAddFeedIconListener()
538 FeedIconManager::self()->addListener(QUrl(d->xmlUrl), this);
541 void Akregator::Feed::appendArticles(const Syndication::FeedPtr &feed)
543 d->setTotalCountDirty();
544 bool changed = false;
545 const bool notify = useNotification() || Settings::useNotifications();
547 QList<ItemPtr> items = feed->items();
548 QList<ItemPtr>::ConstIterator it = items.constBegin();
549 QList<ItemPtr>::ConstIterator en = items.constEnd();
551 int nudge = 0;
553 QVector<Article> deletedArticles = d->deletedArticles;
555 for (; it != en; ++it) {
556 if (!d->articles.contains((*it)->id())) { // article not in list
557 Article mya(*it, this);
558 mya.offsetPubDate(nudge);
559 nudge--;
560 appendArticle(mya);
561 d->addedArticlesNotify.append(mya);
563 if (!mya.isDeleted() && !markImmediatelyAsRead()) {
564 mya.setStatus(New);
565 } else {
566 mya.setStatus(Read);
568 if (notify) {
569 NotificationManager::self()->slotNotifyArticle(mya);
571 changed = true;
572 } else { // article is in list
573 // 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.
574 Article old = d->articles[(*it)->id()];
575 Article mya(*it, this);
576 if (!mya.guidIsHash() && mya.hash() != old.hash() && !old.isDeleted()) {
577 mya.setKeep(old.keep());
578 int oldstatus = old.status();
579 old.setStatus(Read);
581 d->articles.remove(old.guid());
582 appendArticle(mya);
584 mya.setStatus(oldstatus);
586 d->updatedArticlesNotify.append(mya);
587 changed = true;
588 } else if (old.isDeleted()) {
589 deletedArticles.removeAll(mya);
594 QVector<Article>::ConstIterator dit = deletedArticles.constBegin();
595 QVector<Article>::ConstIterator dtmp;
596 QVector<Article>::ConstIterator den = deletedArticles.constEnd();
598 // delete articles with delete flag set completely from archive, which aren't in the current feed source anymore
599 while (dit != den) {
600 dtmp = dit;
601 ++dit;
602 d->articles.remove((*dtmp).guid());
603 d->archive->deleteArticle((*dtmp).guid());
604 d->removedArticlesNotify.append(*dtmp);
605 changed = true;
606 d->deletedArticles.removeAll(*dtmp);
609 if (changed) {
610 articlesModified();
614 bool Akregator::Feed::usesExpiryByAge() const
616 return (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) || d->archiveMode == limitArticleAge;
619 bool Akregator::Feed::isExpired(const Article &a) const
621 QDateTime now = QDateTime::currentDateTime();
622 int expiryAge = -1;
623 // check whether the feed uses the global default and the default is limitArticleAge
624 if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleAge) {
625 expiryAge = Settings::maxArticleAge() * 24 * 3600;
626 } else // otherwise check if this feed has limitArticleAge set
627 if (d->archiveMode == limitArticleAge) {
628 expiryAge = d->maxArticleAge * 24 * 3600;
631 return (expiryAge != -1 && a.pubDate().secsTo(now) > expiryAge);
634 void Akregator::Feed::appendArticle(const Article &a)
636 if ((a.keep() && Settings::doNotExpireImportantArticles()) || (!usesExpiryByAge() || !isExpired(a))) { // if not expired
637 if (!d->articles.contains(a.guid())) {
638 d->articles[a.guid()] = a;
639 if (!a.isDeleted() && a.status() != Read) {
640 setUnread(unread() + 1);
646 void Akregator::Feed::fetch(bool followDiscovery)
648 d->followDiscovery = followDiscovery;
649 d->fetchTries = 0;
651 // mark all new as unread
652 for (auto it = d->articles.begin(), end = d->articles.end(); it != end; ++it) {
653 if ((*it).status() == New) {
654 (*it).setStatus(Unread);
658 Q_EMIT fetchStarted(this);
660 tryFetch();
663 void Akregator::Feed::slotAbortFetch()
665 if (d->loader) {
666 d->loader->abort();
670 void Akregator::Feed::tryFetch()
672 d->fetchErrorCode = Syndication::Success;
674 d->loader = Syndication::Loader::create(this, SLOT(fetchCompleted(Syndication::Loader *,
675 Syndication::FeedPtr,
676 Syndication::ErrorCode)));
677 //connect(d->loader, SIGNAL(progress(ulong)), this, SLOT(slotSetProgress(ulong)));
678 d->loader->loadFrom(d->xmlUrl);
681 void Akregator::Feed::slotImageFetched(const QPixmap &image)
683 setImage(image);
686 void Akregator::Feed::fetchCompleted(Syndication::Loader *l, Syndication::FeedPtr doc, Syndication::ErrorCode status)
688 // Note that loader instances delete themselves
689 d->loader = 0;
691 // fetching wasn't successful:
692 if (status != Syndication::Success) {
693 if (status == Syndication::Aborted) {
694 d->fetchErrorCode = Syndication::Success;
695 Q_EMIT fetchAborted(this);
696 } else if (d->followDiscovery && (status == Syndication::InvalidXml) && (d->fetchTries < 3) && (l->discoveredFeedURL().isValid())) {
697 d->fetchTries++;
698 d->xmlUrl = l->discoveredFeedURL().url();
699 Q_EMIT fetchDiscovery(this);
700 tryFetch();
701 } else {
702 d->fetchErrorCode = status;
703 Q_EMIT fetchError(this);
705 markAsFetchedNow();
706 return;
709 loadArticles(); // TODO: make me fly: make this delayed
711 FeedIconManager::self()->addListener(QUrl(xmlUrl()), this);
713 d->fetchErrorCode = Syndication::Success;
715 if (d->imagePixmap.isNull()) {
716 //QString u = d->xmlUrl;
717 QString imageFileName = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + QLatin1String("akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png");
718 d->imagePixmap = QPixmap(imageFileName, "PNG");
720 // if we ain't got the image and the feed provides one, get it....
721 // TODO: reenable image fetching!
722 if (false) { // d->imagePixmap.isNull() && doc.image())
723 //d->image = *doc.image();
724 //connect(&d->image, SIGNAL(gotPixmap(QPixmap)), this, SLOT(slotImageFetched(QPixmap)));
725 //d->image.getPixmap();
729 if (title().isEmpty()) {
730 setTitle(Syndication::htmlToPlainText(doc->title()));
733 d->description = doc->description();
734 d->htmlUrl = doc->link();
736 appendArticles(doc);
738 markAsFetchedNow();
739 Q_EMIT fetched(this);
742 void Akregator::Feed::markAsFetchedNow()
744 if (d->archive) {
745 d->archive->setLastFetch(QDateTime::currentDateTime().toTime_t());
749 QIcon Akregator::Feed::icon() const
751 if (fetchErrorOccurred()) {
752 return QIcon::fromTheme(QStringLiteral("dialog-error"));
755 return !d->favicon.isNull() ? d->favicon : QIcon::fromTheme(QStringLiteral("text-html"));
758 void Akregator::Feed::deleteExpiredArticles(ArticleDeleteJob *deleteJob)
760 if (!usesExpiryByAge()) {
761 return;
764 setNotificationMode(false);
766 QList<ArticleId> toDelete;
767 const QString feedUrl = xmlUrl();
768 const bool useKeep = Settings::doNotExpireImportantArticles();
770 Q_FOREACH (const Article &i, d->articles) {
771 if ((!useKeep || !i.keep()) && isExpired(i)) {
772 const ArticleId aid = { feedUrl, i.guid() };
773 toDelete.append(aid);
777 deleteJob->appendArticleIds(toDelete);
778 setNotificationMode(true);
781 void Akregator::Feed::setFavicon(const QIcon &icon)
783 d->favicon = icon;
784 nodeModified();
787 void Akregator::Feed::setImage(const QPixmap &p)
789 if (p.isNull()) {
790 return;
792 d->imagePixmap = p;
793 const QString filename = QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + QLatin1Char('/') + QString(QLatin1String("akregator/Media/") + Utils::fileNameForUrl(d->xmlUrl) + QLatin1String(".png"));
794 QFileInfo fileInfo(filename);
795 QDir().mkpath(fileInfo.absolutePath());
796 d->imagePixmap.save(filename, "PNG");
797 nodeModified();
800 Akregator::Feed::ArchiveMode Akregator::Feed::archiveMode() const
802 return d->archiveMode;
805 void Akregator::Feed::setArchiveMode(ArchiveMode archiveMode)
807 d->archiveMode = archiveMode;
810 int Akregator::Feed::unread() const
812 return d->archive ? d->archive->unread() : 0;
815 void Akregator::Feed::setUnread(int unread)
817 if (d->archive && unread != d->archive->unread()) {
818 d->archive->setUnread(unread);
819 nodeModified();
823 void Akregator::Feed::setArticleDeleted(Article &a)
825 d->setTotalCountDirty();
826 if (!d->deletedArticles.contains(a)) {
827 d->deletedArticles.append(a);
830 d->updatedArticlesNotify.append(a);
831 articlesModified();
834 void Akregator::Feed::setArticleChanged(Article &a, int oldStatus)
836 if (oldStatus != -1) {
837 int newStatus = a.status();
838 if (oldStatus == Read && newStatus != Read) {
839 setUnread(unread() + 1);
840 } else if (oldStatus != Read && newStatus == Read) {
841 setUnread(unread() - 1);
844 d->updatedArticlesNotify.append(a);
845 articlesModified();
848 int Akregator::Feed::totalCount() const
850 if (d->totalCount == -1) {
851 d->totalCount = std::count_if(d->articles.constBegin(), d->articles.constEnd(), [](const Article & art) -> bool { return !art.isDeleted(); });
853 return d->totalCount;
856 TreeNode *Akregator::Feed::next()
858 if (nextSibling()) {
859 return nextSibling();
862 Folder *p = parent();
863 while (p) {
864 if (p->nextSibling()) {
865 return p->nextSibling();
866 } else {
867 p = p->parent();
870 return 0;
873 const TreeNode *Akregator::Feed::next() const
875 if (nextSibling()) {
876 return nextSibling();
879 const Folder *p = parent();
880 while (p) {
881 if (p->nextSibling()) {
882 return p->nextSibling();
883 } else {
884 p = p->parent();
887 return 0;
890 void Akregator::Feed::doArticleNotification()
892 if (!d->addedArticlesNotify.isEmpty()) {
893 // copy list, otherwise the refcounting in Article::Private breaks for
894 // some reason (causing segfaults)
895 QVector<Article> l = d->addedArticlesNotify;
896 Q_EMIT signalArticlesAdded(this, l);
897 d->addedArticlesNotify.clear();
899 if (!d->updatedArticlesNotify.isEmpty()) {
900 // copy list, otherwise the refcounting in Article::Private breaks for
901 // some reason (causing segfaults)
902 QVector<Article> l = d->updatedArticlesNotify;
903 Q_EMIT signalArticlesUpdated(this, l);
904 d->updatedArticlesNotify.clear();
906 if (!d->removedArticlesNotify.isEmpty()) {
907 // copy list, otherwise the refcounting in Article::Private breaks for
908 // some reason (causing segfaults)
909 QVector<Article> l = d->removedArticlesNotify;
910 Q_EMIT signalArticlesRemoved(this, l);
911 d->removedArticlesNotify.clear();
913 TreeNode::doArticleNotification();
916 void Akregator::Feed::enforceLimitArticleNumber()
918 int limit = -1;
919 if (d->archiveMode == globalDefault && Settings::archiveMode() == Settings::EnumArchiveMode::limitArticleNumber) {
920 limit = Settings::maxArticleNumber();
921 } else if (d->archiveMode == limitArticleNumber) {
922 limit = maxArticleNumber();
925 if (limit == -1 || limit >= d->articles.count() - d->deletedArticles.count()) {
926 return;
929 QVector<Article> articles = valuesToVector(d->articles);
930 qSort(articles);
932 int c = 0;
933 const bool useKeep = Settings::doNotExpireImportantArticles();
935 Q_FOREACH (Article i, articles) { //krazy:exclude=foreach
936 if (c < limit) {
937 if (!i.isDeleted() && (!useKeep || !i.keep())) {
938 ++c;
940 } else if (!useKeep || !i.keep()) {
941 i.setDeleted();