1 // vim: set tabstop=4 shiftwidth=4 expandtab:
2 /*
3 Gwenview: an image viewer
4 Copyright 2009 Aurélien Gâteau <agateau@kde.org>
5
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License
8 as published by the Free Software Foundation; either version 2
9 of the License, or (at your option) any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; if not, write to the Free Software
18 Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA.
19
20 */
21 // Self
22 #include "historymodel.h"
23
24 // Qt
25 #include <QDir>
26 #include <QFile>
27 #include <QMimeDatabase>
28 #include <QRegularExpression>
29 #include <QTemporaryFile>
30 #include <QUrl>
31
32 // KF
33 #include <KConfig>
34 #include <KConfigGroup>
35 #include <KDirModel>
36 #include <KFileItem>
37 #include <KFilePlacesModel>
38 #include <KFormat>
39 #include <KLocalizedString>
40
41 // Local
42 #include "gwenview_lib_debug.h"
43 #include <lib/urlutils.h>
44
45 namespace Gwenview
46 {
47 struct HistoryItem : public QStandardItem {
saveGwenview::HistoryItem48 void save() const
49 {
50 KConfig config(mConfigPath, KConfig::SimpleConfig);
51 KConfigGroup group(&config, "general");
52 group.writeEntry("url", mUrl.toString());
53 group.writeEntry("dateTime", mDateTime.toString(Qt::ISODate));
54 config.sync();
55 }
56
createGwenview::HistoryItem57 static HistoryItem *create(const QUrl &url, const QDateTime &dateTime, const QString &storageDir)
58 {
59 if (!QDir().mkpath(storageDir)) {
60 qCCritical(GWENVIEW_LIB_LOG) << "Could not create history dir" << storageDir;
61 return nullptr;
62 }
63 QTemporaryFile file(storageDir + QStringLiteral("/gvhistoryXXXXXXrc"));
64 file.setAutoRemove(false);
65 if (!file.open()) {
66 qCCritical(GWENVIEW_LIB_LOG) << "Could not create history file";
67 return nullptr;
68 }
69
70 auto *item = new HistoryItem(url, dateTime, file.fileName());
71 item->save();
72 return item;
73 }
74
loadGwenview::HistoryItem75 static HistoryItem *load(const QString &fileName)
76 {
77 KConfig config(fileName, KConfig::SimpleConfig);
78 KConfigGroup group(&config, "general");
79
80 QUrl url(group.readEntry("url"));
81 if (!url.isValid()) {
82 qCCritical(GWENVIEW_LIB_LOG) << "Invalid url" << url;
83 return nullptr;
84 }
85 QDateTime dateTime = QDateTime::fromString(group.readEntry("dateTime"), Qt::ISODate);
86 if (!dateTime.isValid()) {
87 qCCritical(GWENVIEW_LIB_LOG) << "Invalid dateTime" << dateTime;
88 return nullptr;
89 }
90
91 return new HistoryItem(url, dateTime, fileName);
92 }
93
urlGwenview::HistoryItem94 QUrl url() const
95 {
96 return mUrl;
97 }
98
dateTimeGwenview::HistoryItem99 QDateTime dateTime() const
100 {
101 return mDateTime;
102 }
103
setDateTimeGwenview::HistoryItem104 void setDateTime(const QDateTime &dateTime)
105 {
106 if (mDateTime != dateTime) {
107 mDateTime = dateTime;
108 save();
109 }
110 }
111
unlinkGwenview::HistoryItem112 void unlink()
113 {
114 QFile::remove(mConfigPath);
115 }
116
117 private:
118 QUrl mUrl;
119 QDateTime mDateTime;
120 QString mConfigPath;
121
HistoryItemGwenview::HistoryItem122 HistoryItem(const QUrl &url, const QDateTime &dateTime, const QString &configPath)
123 : mUrl(url)
124 , mDateTime(dateTime)
125 , mConfigPath(configPath)
126 {
127 QString text(mUrl.toDisplayString(QUrl::PreferLocalFile));
128 #ifdef Q_OS_UNIX
129 // shorten home directory, but avoid showing a cryptic "~/"
130 if (text.length() > QDir::homePath().length() + 1) {
131 text.replace(QRegularExpression('^' + QDir::homePath()), QStringLiteral("~"));
132 }
133 #endif
134 setText(text);
135
136 QMimeDatabase db;
137 const QString iconName = db.mimeTypeForUrl(mUrl).iconName();
138 setIcon(QIcon::fromTheme(iconName));
139
140 setData(mUrl, KFilePlacesModel::UrlRole);
141
142 KFileItem fileItem(mUrl);
143 setData(QVariant(fileItem), KDirModel::FileItemRole);
144
145 const QString date = KFormat().formatRelativeDateTime(mDateTime, QLocale::LongFormat);
146 setData(i18n("Last visited: %1", date), Qt::ToolTipRole);
147 }
148
operator <Gwenview::HistoryItem149 bool operator<(const QStandardItem &other) const override
150 {
151 return mDateTime > static_cast<const HistoryItem *>(&other)->mDateTime;
152 }
153 };
154
155 struct HistoryModelPrivate {
156 HistoryModel *q;
157 QString mStorageDir;
158 int mMaxCount;
159
160 QMap<QUrl, HistoryItem *> mHistoryItemForUrl;
161
loadGwenview::HistoryModelPrivate162 void load()
163 {
164 QDir dir(mStorageDir);
165 if (!dir.exists()) {
166 return;
167 }
168 const QStringList rcFilesList = dir.entryList(QStringList() << QStringLiteral("*rc"));
169 for (const QString &name : rcFilesList) {
170 HistoryItem *item = HistoryItem::load(dir.filePath(name));
171 if (!item) {
172 continue;
173 }
174
175 QUrl itemUrl = item->url();
176 if (UrlUtils::urlIsFastLocalFile(itemUrl)) {
177 if (!QFile::exists(itemUrl.path())) {
178 qCDebug(GWENVIEW_LIB_LOG) << "Removing" << itemUrl.path() << "from recent folders. It does not exist anymore";
179 item->unlink();
180 delete item;
181 continue;
182 }
183 }
184
185 HistoryItem *existingItem = mHistoryItemForUrl.value(item->url());
186 if (existingItem) {
187 // We already know this url(!) update existing item dateTime
188 // and get rid of duplicate
189 if (existingItem->dateTime() < item->dateTime()) {
190 existingItem->setDateTime(item->dateTime());
191 }
192 item->unlink();
193 delete item;
194 } else {
195 mHistoryItemForUrl.insert(item->url(), item);
196 q->appendRow(item);
197 }
198 }
199 q->sort(0);
200 }
201
garbageCollectGwenview::HistoryModelPrivate202 void garbageCollect()
203 {
204 while (q->rowCount() > mMaxCount) {
205 HistoryItem *item = static_cast<HistoryItem *>(q->takeRow(q->rowCount() - 1).at(0));
206 mHistoryItemForUrl.remove(item->url());
207 item->unlink();
208 delete item;
209 }
210 }
211 };
212
HistoryModel(QObject * parent,const QString & storageDir,int maxCount)213 HistoryModel::HistoryModel(QObject *parent, const QString &storageDir, int maxCount)
214 : QStandardItemModel(parent)
215 , d(new HistoryModelPrivate)
216 {
217 d->q = this;
218 d->mStorageDir = storageDir;
219 d->mMaxCount = maxCount;
220 d->load();
221 }
222
~HistoryModel()223 HistoryModel::~HistoryModel()
224 {
225 delete d;
226 }
227
addUrl(const QUrl & url,const QDateTime & _dateTime)228 void HistoryModel::addUrl(const QUrl &url, const QDateTime &_dateTime)
229 {
230 QDateTime dateTime = _dateTime.isValid() ? _dateTime : QDateTime::currentDateTime();
231 HistoryItem *historyItem = d->mHistoryItemForUrl.value(url);
232 if (historyItem) {
233 historyItem->setDateTime(dateTime);
234 sort(0);
235 } else {
236 historyItem = HistoryItem::create(url, dateTime, d->mStorageDir);
237 if (!historyItem) {
238 qCCritical(GWENVIEW_LIB_LOG) << "Could not save history for url" << url;
239 return;
240 }
241 d->mHistoryItemForUrl.insert(url, historyItem);
242 appendRow(historyItem);
243 sort(0);
244 d->garbageCollect();
245 }
246 }
247
removeRows(int start,int count,const QModelIndex & parent)248 bool HistoryModel::removeRows(int start, int count, const QModelIndex &parent)
249 {
250 Q_ASSERT(!parent.isValid());
251 for (int row = start + count - 1; row >= start; --row) {
252 auto *historyItem = static_cast<HistoryItem *>(item(row, 0));
253 Q_ASSERT(historyItem);
254 d->mHistoryItemForUrl.remove(historyItem->url());
255 historyItem->unlink();
256 }
257 return QStandardItemModel::removeRows(start, count, parent);
258 }
259
260 } // namespace
261