File indexing completed on 2024-04-28 05:11:01

0001 /*
0002     This file is part of Akregator.
0003 
0004     SPDX-FileCopyrightText: 2007 Frank Osterfeld <osterfeld@kde.org>
0005 
0006     SPDX-License-Identifier: GPL-2.0-or-later WITH Qt-Commercial-exception-1.0
0007 */
0008 #include "articlemodel.h"
0009 
0010 #include "akregatorconfig.h"
0011 #include "articlematcher.h"
0012 #include "feed.h"
0013 #include "utils.h"
0014 
0015 #include <Syndication/Tools>
0016 
0017 #include <QList>
0018 #include <QMimeData>
0019 #include <QString>
0020 
0021 #include <KLocalizedString>
0022 #include <QUrl>
0023 
0024 #include <memory>
0025 
0026 #include <QLocale>
0027 #include <cassert>
0028 #include <cmath>
0029 
0030 using namespace Akregator;
0031 
0032 // like Syndication::htmlToPlainText, but without linebreaks
0033 
0034 static QString stripHtml(const QString &html)
0035 {
0036     QString str(html);
0037     // TODO: preserve some formatting, such as line breaks
0038     str = Akregator::Utils::stripTags(str); // remove tags
0039     str = Syndication::resolveEntities(str);
0040     return str.simplified();
0041 }
0042 
0043 ArticleModel::ArticleModel(const QList<Article> &articles, QObject *parent)
0044     : QAbstractTableModel(parent)
0045     , m_articles(articles)
0046 {
0047     const int articlesCount(articles.count());
0048     m_titleCache.resize(articlesCount);
0049     for (int i = 0; i < articlesCount; ++i) {
0050         m_titleCache[i] = stripHtml(articles[i].title());
0051     }
0052 }
0053 
0054 ArticleModel::~ArticleModel() = default;
0055 
0056 int ArticleModel::columnCount(const QModelIndex &parent) const
0057 {
0058     return parent.isValid() ? 0 : ColumnCount;
0059 }
0060 
0061 int ArticleModel::rowCount(const QModelIndex &parent) const
0062 {
0063     return parent.isValid() ? 0 : m_articles.count();
0064 }
0065 
0066 QVariant ArticleModel::headerData(int section, Qt::Orientation, int role) const
0067 {
0068     if (role != Qt::DisplayRole) {
0069         return {};
0070     }
0071 
0072     switch (section) {
0073     case ItemTitleColumn:
0074         return i18nc("Articlelist's column header", "Title");
0075     case FeedTitleColumn:
0076         return i18nc("Articlelist's column header", "Feed");
0077     case DateColumn:
0078         return i18nc("Articlelist's column header", "Date");
0079     case AuthorColumn:
0080         return i18nc("Articlelist's column header", "Author");
0081     case DescriptionColumn:
0082         return i18nc("Articlelist's column header", "Description");
0083     case ContentColumn:
0084         return i18nc("Articlelist's column header", "Content");
0085     }
0086 
0087     return {};
0088 }
0089 
0090 QVariant ArticleModel::data(const QModelIndex &index, int role) const
0091 {
0092     if (!index.isValid() || index.row() < 0 || index.row() >= m_articles.count()) {
0093         return {};
0094     }
0095     const int row = index.row();
0096     const Article &article(m_articles[row]);
0097 
0098     switch (role) {
0099     case SortRole:
0100         if (index.column() == DateColumn) {
0101             return article.pubDate();
0102         }
0103         [[fallthrough]];
0104     // no break
0105     case Qt::DisplayRole:
0106         switch (index.column()) {
0107         case FeedTitleColumn:
0108             return article.feed() ? article.feed()->title() : QVariant();
0109         case DateColumn:
0110             return QLocale().toString(article.pubDate(), QLocale::ShortFormat);
0111         case ItemTitleColumn:
0112             return m_titleCache[row];
0113         case AuthorColumn:
0114             return article.authorShort();
0115         case DescriptionColumn:
0116         case ContentColumn:
0117             return article.description();
0118         }
0119     case LinkRole:
0120         return article.link();
0121     case ItemIdRole:
0122     case GuidRole:
0123         return article.guid();
0124     case FeedIdRole:
0125         return article.feed() ? article.feed()->xmlUrl() : QVariant();
0126     case StatusRole:
0127         return article.status();
0128     case IsImportantRole:
0129         return article.keep();
0130     case IsDeletedRole:
0131         return article.isDeleted();
0132     }
0133 
0134     return {};
0135 }
0136 
0137 void ArticleModel::clear()
0138 {
0139     beginResetModel();
0140     m_articles.clear();
0141     m_titleCache.clear();
0142     endResetModel();
0143 }
0144 
0145 void ArticleModel::articlesAdded(Akregator::TreeNode *, const QList<Article> &l)
0146 {
0147     if (l.isEmpty()) { // assert?
0148         return;
0149     }
0150     const int first = m_articles.count();
0151     beginInsertRows(QModelIndex(), first, first + l.size() - 1);
0152 
0153     const int oldSize = m_articles.size();
0154     m_articles << l;
0155 
0156     const int newArticlesCount(m_articles.count());
0157     m_titleCache.resize(newArticlesCount);
0158     for (int i = oldSize; i < newArticlesCount; ++i) {
0159         m_titleCache[i] = stripHtml(m_articles[i].title());
0160     }
0161     endInsertRows();
0162 }
0163 
0164 void ArticleModel::articlesRemoved(Akregator::TreeNode *, const QList<Article> &l)
0165 {
0166     // might want to avoid indexOf() in case of performance problems
0167     for (const Article &i : l) {
0168         const int row = m_articles.indexOf(i);
0169         Q_ASSERT(row != -1);
0170         removeRow(row, QModelIndex());
0171     }
0172 }
0173 
0174 void ArticleModel::articlesUpdated(Akregator::TreeNode *, const QList<Article> &l)
0175 {
0176     int rmin = 0;
0177     int rmax = 0;
0178 
0179     const int numberOfArticles(m_articles.count());
0180     if (numberOfArticles > 0) {
0181         rmin = numberOfArticles - 1;
0182         // might want to avoid indexOf() in case of performance problems
0183         for (const Article &i : l) {
0184             const int row = m_articles.indexOf(i);
0185             // TODO: figure out how why the Article might not be found in
0186             // TODO: the articles list because we should need this conditional.
0187             if (row >= 0) {
0188                 m_titleCache[row] = stripHtml(m_articles[row].title());
0189                 rmin = std::min(row, rmin);
0190                 rmax = std::max(row, rmax);
0191             }
0192         }
0193     }
0194     Q_EMIT dataChanged(index(rmin, 0), index(rmax, ColumnCount - 1));
0195 }
0196 
0197 bool ArticleModel::rowMatches(int row, const QSharedPointer<const Filters::AbstractMatcher> &matcher) const
0198 {
0199     Q_ASSERT(matcher);
0200     return matcher->matches(article(row));
0201 }
0202 
0203 Article ArticleModel::article(int row) const
0204 {
0205     if (row < 0 || row >= m_articles.count()) {
0206         return {};
0207     }
0208     return m_articles[row];
0209 }
0210 
0211 QStringList ArticleModel::mimeTypes() const
0212 {
0213     return QStringList() << QStringLiteral("text/uri-list");
0214 }
0215 
0216 QMimeData *ArticleModel::mimeData(const QModelIndexList &indexes) const
0217 {
0218     std::unique_ptr<QMimeData> md(new QMimeData);
0219     QList<QUrl> urls;
0220     QList<int> seenArticles;
0221     for (const QModelIndex &i : indexes) {
0222         const int rowIndex = i.row();
0223         if (seenArticles.contains(rowIndex)) {
0224             continue;
0225         }
0226         seenArticles.append(rowIndex);
0227         const QUrl url = i.data(ArticleModel::LinkRole).toUrl();
0228         if (url.isValid()) {
0229             urls.push_back(url);
0230         } else {
0231             const QUrl guid(i.data(ArticleModel::GuidRole).toString());
0232             if (guid.isValid()) {
0233                 urls.push_back(guid);
0234             }
0235         }
0236     }
0237     md->setUrls(urls);
0238     return md.release();
0239 }
0240 
0241 Qt::ItemFlags ArticleModel::flags(const QModelIndex &idx) const
0242 {
0243     const Qt::ItemFlags f = QAbstractTableModel::flags(idx);
0244     if (!idx.isValid()) {
0245         return f;
0246     }
0247     return f | Qt::ItemIsDragEnabled;
0248 }
0249 
0250 #include "moc_articlemodel.cpp"