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