1 /* ============================================================
2 * QuiteRSS is a open-source cross-platform RSS/Atom news feeds reader
3 * Copyright (C) 2011-2020 QuiteRSS Team <quiterssteam@gmail.com>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
17 * ============================================================ */
18 #include "feedsmodel.h"
19 #include "feedsproxymodel.h"
20 
21 #include <QtCore>
22 #include <QPainter>
23 
FeedsModel(QObject * parent)24 FeedsModel::FeedsModel(QObject *parent)
25   : QAbstractItemModel(parent)
26   , defaultIconFeeds_(false)
27   , view_(0)
28   , rootParentId_(0)
29 {
30   setObjectName("FeedsModel");
31 
32   refresh();
33 }
34 
~FeedsModel()35 FeedsModel::~FeedsModel()
36 {
37   clear();
38 }
39 
clear()40 void FeedsModel::clear()
41 {
42   id2RowList_.clear();
43   parid2RowList_.clear();
44   columnsList_.clear();
45 
46   qDeleteAll(userDataList_);
47   userDataList_.clear();
48 }
49 
refresh()50 void FeedsModel::refresh()
51 {
52 #ifdef HAVE_QT5
53   beginResetModel();
54   clear();
55   endResetModel();
56 #else
57   reset();
58   clear();
59 #endif
60 
61   queryModel_.setQuery("SELECT * FROM feeds ORDER BY parentId, rowToParent");
62   while (queryModel_.canFetchMore())
63     queryModel_.fetchMore();
64 
65   indexId_ = queryModel_.record().indexOf("id");
66   indexParid_ = queryModel_.record().indexOf("parentId");
67   for (int i = 0; i < queryModel_.record().count(); i++) {
68     columnsList_[i] = i;
69   }
70   columnsList_[0] = queryModel_.record().indexOf("text");
71   columnsList_[queryModel_.record().indexOf("text")] = 0;
72 
73   for (int i = 0; i < queryModel_.rowCount(); i++) {
74     int id = queryModel_.record(i).value(indexId_).toInt();
75     id2RowList_[id] = i;
76     int parid = queryModel_.record(i).value(indexParid_).toInt();
77     parid2RowList_[i] = parid;
78     userDataList_[id] = new UserData(id, parid, queryModel_.record(i));
79   }
80 }
81 
userDataById(int id) const82 UserData * FeedsModel::userDataById(int id) const
83 {
84   return userDataList_.value(id, 0);
85 }
86 
rowById(int id) const87 int FeedsModel::rowById(int id) const
88 {
89   return id2RowList_.value(id, -1);
90 }
91 
rowByParid(int parid) const92 int FeedsModel::rowByParid(int parid) const
93 {
94   return parid2RowList_.key(parid, -1);
95 }
96 
rowCount(const QModelIndex & parent) const97 int FeedsModel::rowCount(const QModelIndex &parent) const
98 {
99   if (parent.isValid())
100     return parid2RowList_.keys(idByIndex(parent)).count();
101   else
102     return parid2RowList_.keys(rootParentId_).count();
103 }
104 
columnCount(const QModelIndex &) const105 int FeedsModel::columnCount(const QModelIndex&) const
106 {
107   return queryModel_.record().count();
108 }
109 
index(int row,int column,const QModelIndex & parent) const110 QModelIndex FeedsModel::index(int row, int column, const QModelIndex &parent) const
111 {
112   if (row == -1)
113     return QModelIndex();
114 
115   int id = 0;
116   if (parent.isValid())
117     id = id2RowList_.key(row + rowByParid(idByIndex(parent)), 0);
118   else
119     id = id2RowList_.key(row, 0);
120 
121   UserData *userData = userDataById(id);
122   if (userData)
123     return createIndex(row, column, userData);
124   else
125     return QModelIndex();
126 }
127 
parent(const QModelIndex & index) const128 QModelIndex FeedsModel::parent(const QModelIndex &index) const
129 {
130   if (!index.isValid())
131     return QModelIndex();
132 
133   int parid = paridByIndex(index);
134   if (parid == rootParentId_)
135     return QModelIndex();
136 
137   UserData *userData = userDataById(parid);
138   if (userData) {
139     int row = rowById(parid) - rowByParid(userData->parid);
140     return createIndex(row, 0, userData);
141   } else {
142     return QModelIndex();
143   }
144 }
145 
data(const QModelIndex & index,int role) const146 QVariant FeedsModel::data(const QModelIndex &index, int role) const
147 {
148   if (role == Qt::FontRole) {
149     QFont font = font_;
150     if (indexColumnOf("text") == index.column()) {
151       if (0 < indexSibling(index, "unread").data(Qt::EditRole).toInt())
152         font.setBold(true);
153     }
154     return font;
155   } else if (role == Qt::DisplayRole){
156     if (indexColumnOf("unread") == index.column()) {
157       int unread = indexSibling(index, "unread").data(Qt::EditRole).toInt();
158       if (0 == unread) {
159         return QVariant();
160       } else {
161         QString qStr = QString("(%1)").arg(unread);
162         return qStr;
163       }
164     } else if (indexColumnOf("undeleteCount") == index.column()) {
165       QString qStr = QString("(%1)").
166           arg(indexSibling(index, "undeleteCount").data(Qt::EditRole).toInt());
167       return qStr;
168     } else if (indexColumnOf("updated") == index.column()) {
169       QDateTime dtLocal;
170       QString strDate = indexSibling(index, "updated").data(Qt::EditRole).toString();
171 
172       if (!strDate.isNull()) {
173         QDateTime dtLocalTime = QDateTime::currentDateTime();
174         QDateTime dtUTC = QDateTime(dtLocalTime.date(), dtLocalTime.time(), Qt::UTC);
175         int nTimeShift = dtLocalTime.secsTo(dtUTC);
176 
177         QDateTime dt = QDateTime::fromString(strDate, Qt::ISODate);
178         dtLocal = dt.addSecs(nTimeShift);
179 
180         QString strResult;
181         if (QDateTime::currentDateTime().date() <= dtLocal.date())
182           strResult = dtLocal.toString(formatTime_);
183         else
184           strResult = dtLocal.toString(formatDate_);
185         return strResult;
186       } else {
187         return QVariant();
188       }
189     }
190   } else if (role == Qt::TextColorRole) {
191     if (indexColumnOf("unread") == index.column()) {
192       return QColor(countNewsUnreadColor_);
193     }
194 
195     QModelIndex currentIndex = ((FeedsProxyModel*)view_->model())->mapToSource(view_->currentIndex());
196     if ((index.row() == currentIndex.row()) && (index.parent() == currentIndex.parent()) &&
197         view_->selectionModel()->selectedRows(0).count()) {
198       return QColor(focusedFeedTextColor_);
199     }
200 
201     if (indexColumnOf("text") == index.column()) {
202       if (indexSibling(index, "newCount").data(Qt::EditRole).toInt() > 0) {
203         return QColor(feedWithNewNewsColor_);
204       }
205     }
206 
207     if (indexColumnOf("text") == index.column()) {
208       if (indexSibling(index, "disableUpdate").data(Qt::EditRole).toBool()) {
209         return QColor(feedDisabledUpdateColor_);
210       }
211     }
212 
213     return QColor(textColor_);
214   } else if (role == Qt::BackgroundRole) {
215     QModelIndex currentIndex = ((FeedsProxyModel*)view_->model())->mapToSource(view_->currentIndex());
216     if ((index.row() == currentIndex.row()) && (index.parent() == currentIndex.parent()) &&
217         view_->selectionModel()->selectedRows(0).count()) {
218       if (!focusedFeedBGColor_.isEmpty())
219         return QColor(focusedFeedBGColor_);
220     }
221   } else if (role == Qt::DecorationRole) {
222     if (indexColumnOf("text") == index.column()) {
223       if (isFolder(index)) {
224         return QPixmap(":/images/folder");
225       } else {
226         if (!defaultIconFeeds_) {
227           QByteArray byteArray = indexSibling(index, "image").data(Qt::EditRole).toByteArray();
228           if (!byteArray.isNull()) {
229             QImage resultImage;
230             if (resultImage.loadFromData(QByteArray::fromBase64(byteArray))) {
231               QString strStatus = indexSibling(index, "status").data(Qt::EditRole).toString();
232               if (strStatus.section(" ", 0, 0).toInt() != 0) {
233                 QImage image;
234                 if (strStatus.section(" ", 0, 0).toInt() < 0)
235                   image.load(":/images/bulletError");
236                 else if (strStatus.section(" ", 0, 0).toInt() == 1)
237                   image.load(":/images/bulletUpdate");
238                 QPainter resultPainter(&resultImage);
239                 resultPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
240                 resultPainter.drawImage(0, 0, image);
241                 resultPainter.end();
242               }
243               return resultImage;
244             }
245           }
246         }
247         QImage resultImage(":/images/feed");
248         QString strStatus = indexSibling(index, "status").data(Qt::EditRole).toString();
249         if (strStatus.section(" ", 0, 0).toInt() != 0) {
250           QImage image;
251           if (strStatus.section(" ", 0, 0).toInt() < 0)
252             image.load(":/images/bulletError");
253           else if (strStatus.section(" ", 0, 0).toInt() == 1)
254             image.load(":/images/bulletUpdate");
255 
256           QPainter resultPainter(&resultImage);
257           resultPainter.setCompositionMode(QPainter::CompositionMode_SourceOver);
258           resultPainter.drawImage(0, 0, image);
259           resultPainter.end();
260         }
261         return resultImage;
262       }
263     }
264   } else if (role == Qt::TextAlignmentRole) {
265     if (indexColumnOf("id") == index.column()) {
266       int flag = Qt::AlignRight|Qt::AlignVCenter;
267       return flag;
268     }
269   } else if (role == Qt::ToolTipRole) {
270     if (indexColumnOf("text") == index.column()) {
271       QString title = index.data(Qt::EditRole).toString();
272       QRect rectText = view_->visualRect(index);
273       int width = rectText.width() - 16 - 12;
274       QFont font = font_;
275       if (0 < indexSibling(index, "unread").data(Qt::EditRole).toInt())
276         font.setBold(true);
277       QFontMetrics fontMetrics(font);
278 
279       if (width < fontMetrics.width(title))
280         return title;
281     }
282     return QString("");
283   }
284 
285   if (!((role == Qt::EditRole) || (role == Qt::DisplayRole)))
286     return QVariant();
287 
288   QSqlRecord record = static_cast<UserData*>(index.internalPointer())->record;
289   return record.value(indexColumnOf(index.column()));
290 }
291 
setData(const QModelIndex & index,const QVariant & value,int)292 bool FeedsModel::setData(const QModelIndex &index, const QVariant &value, int)
293 {
294   if (!index.isValid())
295     return false;
296 
297   QSqlRecord *record = &static_cast<UserData*>(index.internalPointer())->record;
298   record->setValue(indexColumnOf(index.column()), value);
299   return true;
300 }
301 
flags(const QModelIndex & index) const302 Qt::ItemFlags FeedsModel::flags(const QModelIndex &index) const
303 {
304   Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index);
305 
306   if (index.isValid())
307     return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
308   else
309     return Qt::ItemIsDropEnabled | defaultFlags;
310 }
311 
supportedDropActions() const312 Qt::DropActions FeedsModel::supportedDropActions() const
313 {
314   return Qt::MoveAction;
315 }
316 
indexById(int id) const317 QModelIndex FeedsModel::indexById(int id) const
318 {
319   QModelIndex parentIndex = QModelIndex();
320   UserData *userData = userDataById(id);
321   if (userData)
322     parentIndex = indexById(userData->parid);
323   for (int i = 0; i < rowCount(parentIndex); i++) {
324     if (idByIndex(index(i, 0, parentIndex)) == id)
325       return index(i,0,parentIndex);
326   }
327   return QModelIndex();
328 }
329 
idByIndex(const QModelIndex & index) const330 int FeedsModel::idByIndex(const QModelIndex &index) const
331 {
332   if (index.isValid())
333     return static_cast<UserData*>(index.internalPointer())->id;
334   return 0;
335 }
336 
paridByIndex(const QModelIndex & index) const337 int FeedsModel::paridByIndex(const QModelIndex &index) const
338 {
339   if (index.isValid())
340     return static_cast<UserData*>(index.internalPointer())->parid;
341   return 0;
342 }
343 
indexColumnOf(int column) const344 int FeedsModel::indexColumnOf(int column) const
345 {
346   return columnsList_.value(column, column);
347 }
348 
indexColumnOf(const QString & name) const349 int FeedsModel::indexColumnOf(const QString &name) const
350 {
351   return indexColumnOf(queryModel_.record().indexOf(name));
352 }
353 
setView(QTreeView * view)354 void FeedsModel::setView(QTreeView *view)
355 {
356   view_ = view;
357 }
358 
dataField(const QModelIndex & index,const QString & fieldName) const359 QVariant FeedsModel::dataField(const QModelIndex &index, const QString &fieldName) const
360 {
361   return indexSibling(index, fieldName).data(Qt::EditRole);
362 }
363 
364 /** @brief Check if item is folder
365  *
366  *  If xmlUrl field is empty, than item is considered folder
367  * @param index Item to check
368  * @return Is folder sign
369  * @retval true Index item is category
370  * @retval false Index item is feed
371  *---------------------------------------------------------------------------*/
isFolder(const QModelIndex & index) const372 bool FeedsModel::isFolder(const QModelIndex &index) const
373 {
374   return indexSibling(index, "xmlUrl").data(Qt::EditRole).toString().isEmpty();
375 }
376 
indexSibling(const QModelIndex & index,const QString & fieldName) const377 QModelIndex FeedsModel::indexSibling(const QModelIndex &index, const QString &fieldName) const
378 {
379   return this->index(index.row(), indexColumnOf(fieldName), index.parent());
380 }
381