1 /* ============================================================
2 *
3 * This file is a part of digiKam project
4 * https://www.digikam.org
5 *
6 * Date : 2004-06-15
7 * Description : Albums manager interface.
8 *
9 * Copyright (C) 2004 by Renchi Raju <renchi dot raju at gmail dot com>
10 * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11 * Copyright (C) 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
12 * Copyright (C) 2015 by Mohamed_Anwer <m_dot_anwer at gmx dot com>
13 *
14 * This program is free software; you can redistribute it
15 * and/or modify it under the terms of the GNU General
16 * Public License as published by the Free Software Foundation;
17 * either version 2, or (at your option)
18 * any later version.
19 *
20 * This program is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * ============================================================ */
26
27 #include "albummanager_p.h"
28
29 namespace Digikam
30 {
31
32 Q_GLOBAL_STATIC(AlbumManagerCreator, creator)
33
34 /**
35 * A friend-class shortcut to circumvent accessing this from within the destructor
36 */
37 AlbumManager* AlbumManager::internalInstance = nullptr;
38
instance()39 AlbumManager* AlbumManager::instance()
40 {
41 return &creator->object;
42 }
43
44 // -----------------------------------------------------------------------------------
45
AlbumManager()46 AlbumManager::AlbumManager()
47 : d(new Private)
48 {
49 qRegisterMetaType<QHash<QDateTime,int>>("QHash<QDateTime,int>");
50 qRegisterMetaType<QMap<int,int>>("QMap<int,int>");
51 qRegisterMetaType<QMap<QString,QMap<int,int> >>("QMap<QString,QMap<int,int> >");
52
53 internalInstance = this;
54 d->albumWatch = new AlbumWatch(this);
55
56 // these operations are pretty fast, no need for long queuing
57
58 d->scanPAlbumsTimer = new QTimer(this);
59 d->scanPAlbumsTimer->setInterval(150);
60 d->scanPAlbumsTimer->setSingleShot(true);
61
62 connect(d->scanPAlbumsTimer, SIGNAL(timeout()),
63 this, SLOT(scanPAlbums()));
64
65 d->scanTAlbumsTimer = new QTimer(this);
66 d->scanTAlbumsTimer->setInterval(150);
67 d->scanTAlbumsTimer->setSingleShot(true);
68
69 connect(d->scanTAlbumsTimer, SIGNAL(timeout()),
70 this, SLOT(scanTAlbums()));
71
72 d->scanSAlbumsTimer = new QTimer(this);
73 d->scanSAlbumsTimer->setInterval(150);
74 d->scanSAlbumsTimer->setSingleShot(true);
75
76 connect(d->scanSAlbumsTimer, SIGNAL(timeout()),
77 this, SLOT(scanSAlbums()));
78
79 d->updatePAlbumsTimer = new QTimer(this);
80 d->updatePAlbumsTimer->setInterval(150);
81 d->updatePAlbumsTimer->setSingleShot(true);
82
83 connect(d->updatePAlbumsTimer, SIGNAL(timeout()),
84 this, SLOT(updateChangedPAlbums()));
85
86 // this operation is much more expensive than the other scan methods
87
88 d->scanDAlbumsTimer = new QTimer(this);
89 d->scanDAlbumsTimer->setInterval(30 * 1000);
90 d->scanDAlbumsTimer->setSingleShot(true);
91
92 connect(d->scanDAlbumsTimer, SIGNAL(timeout()),
93 this, SLOT(scanDAlbumsScheduled()));
94
95 // moderately expensive
96
97 d->albumItemCountTimer = new QTimer(this);
98 d->albumItemCountTimer->setInterval(1000);
99 d->albumItemCountTimer->setSingleShot(true);
100
101 connect(d->albumItemCountTimer, SIGNAL(timeout()),
102 this, SLOT(getAlbumItemsCount()));
103
104 // more expensive
105
106 d->tagItemCountTimer = new QTimer(this);
107 d->tagItemCountTimer->setInterval(2500);
108 d->tagItemCountTimer->setSingleShot(true);
109
110 connect(d->tagItemCountTimer, SIGNAL(timeout()),
111 this, SLOT(getTagItemsCount()));
112 }
113
~AlbumManager()114 AlbumManager::~AlbumManager()
115 {
116 delete d->rootPAlbum;
117 delete d->rootTAlbum;
118 delete d->rootDAlbum;
119 delete d->rootSAlbum;
120
121 internalInstance = nullptr;
122 delete d;
123 }
124
cleanUp()125 void AlbumManager::cleanUp()
126 {
127 // This is what we prefer to do before Application destruction
128
129 if (d->dateListJob)
130 {
131 d->dateListJob->cancel();
132 d->dateListJob = nullptr;
133 }
134
135 if (d->albumListJob)
136 {
137 d->albumListJob->cancel();
138 d->albumListJob = nullptr;
139 }
140
141 if (d->tagListJob)
142 {
143 d->tagListJob->cancel();
144 d->tagListJob = nullptr;
145 }
146
147 if (d->personListJob)
148 {
149 d->personListJob->cancel();
150 d->personListJob = nullptr;
151 }
152 }
153
startScan()154 void AlbumManager::startScan()
155 {
156 if (!d->changed)
157 {
158 return;
159 }
160
161 d->changed = false;
162
163 // create root albums
164
165 d->rootPAlbum = new PAlbum(i18n("Albums"));
166 insertPAlbum(d->rootPAlbum, nullptr);
167
168 d->rootTAlbum = new TAlbum(i18n("Tags"), 0, true);
169 insertTAlbum(d->rootTAlbum, nullptr);
170
171 d->rootSAlbum = new SAlbum(i18n("Searches"), 0, true);
172 emit signalAlbumAboutToBeAdded(d->rootSAlbum, nullptr, nullptr);
173 d->allAlbumsIdHash[d->rootSAlbum->globalID()] = d->rootSAlbum;
174 emit signalAlbumAdded(d->rootSAlbum);
175
176 d->rootDAlbum = new DAlbum(QDate(), true);
177 emit signalAlbumAboutToBeAdded(d->rootDAlbum, nullptr, nullptr);
178 d->allAlbumsIdHash[d->rootDAlbum->globalID()] = d->rootDAlbum;
179 emit signalAlbumAdded(d->rootDAlbum);
180
181 // Create albums for album roots. Reuse logic implemented in the method
182
183 foreach (const CollectionLocation& location, CollectionManager::instance()->allLocations())
184 {
185 handleCollectionStatusChange(location, CollectionLocation::LocationNull);
186 }
187
188 // listen to location status changes
189
190 connect(CollectionManager::instance(), SIGNAL(locationStatusChanged(CollectionLocation,int)),
191 this, SLOT(slotCollectionLocationStatusChanged(CollectionLocation,int)));
192
193 connect(CollectionManager::instance(), SIGNAL(locationPropertiesChanged(CollectionLocation)),
194 this, SLOT(slotCollectionLocationPropertiesChanged(CollectionLocation)));
195
196 // reload albums
197
198 refresh();
199
200 // listen to album database changes
201
202 connect(CoreDbAccess::databaseWatch(), SIGNAL(albumChange(AlbumChangeset)),
203 this, SLOT(slotAlbumChange(AlbumChangeset)));
204
205 connect(CoreDbAccess::databaseWatch(), SIGNAL(tagChange(TagChangeset)),
206 this, SLOT(slotTagChange(TagChangeset)));
207
208 connect(CoreDbAccess::databaseWatch(), SIGNAL(searchChange(SearchChangeset)),
209 this, SLOT(slotSearchChange(SearchChangeset)));
210
211 // listen to collection image changes
212
213 connect(CoreDbAccess::databaseWatch(), SIGNAL(collectionImageChange(CollectionImageChangeset)),
214 this, SLOT(slotCollectionImageChange(CollectionImageChangeset)));
215
216 connect(CoreDbAccess::databaseWatch(), SIGNAL(imageTagChange(ImageTagChangeset)),
217 this, SLOT(slotImageTagChange(ImageTagChangeset)));
218
219 // listen to image attribute changes
220
221 connect(ItemAttributesWatch::instance(), SIGNAL(signalImageDateChanged(qlonglong)),
222 d->scanDAlbumsTimer, SLOT(start()));
223
224 emit signalAllAlbumsLoaded();
225 }
226
isShowingOnlyAvailableAlbums() const227 bool AlbumManager::isShowingOnlyAvailableAlbums() const
228 {
229 return d->showOnlyAvailableAlbums;
230 }
231
setShowOnlyAvailableAlbums(bool onlyAvailable)232 void AlbumManager::setShowOnlyAvailableAlbums(bool onlyAvailable)
233 {
234 if (d->showOnlyAvailableAlbums == onlyAvailable)
235 {
236 return;
237 }
238
239 d->showOnlyAvailableAlbums = onlyAvailable;
240
241 emit signalShowOnlyAvailableAlbumsChanged(d->showOnlyAvailableAlbums);
242
243 // We need to update the unavailable locations.
244 // We assume the handleCollectionStatusChange does the right thing (even though old status == current status)
245
246 foreach (const CollectionLocation& location, CollectionManager::instance()->allLocations())
247 {
248 if (location.status() == CollectionLocation::LocationUnavailable)
249 {
250 handleCollectionStatusChange(location, CollectionLocation::LocationUnavailable);
251 }
252 }
253 }
254
refresh()255 void AlbumManager::refresh()
256 {
257 scanPAlbums();
258 scanTAlbums();
259 scanSAlbums();
260 scanDAlbums();
261 }
262
prepareItemCounts()263 void AlbumManager::prepareItemCounts()
264 {
265 // There is no way to find out if any data we had collected
266 // previously is still valid - recompute
267
268 scanDAlbums();
269 getAlbumItemsCount();
270 getTagItemsCount();
271 }
272
slotImagesDeleted(const QList<qlonglong> & imageIds)273 void AlbumManager::slotImagesDeleted(const QList<qlonglong>& imageIds)
274 {
275 qCDebug(DIGIKAM_GENERAL_LOG) << "Got image deletion notification from ItemViewUtilities for " << imageIds.size() << " images.";
276
277 QSet<SAlbum*> sAlbumsToUpdate;
278 QSet<qlonglong> deletedImages = imageIds.toSet();
279
280 QList<SAlbum*> sAlbums = findSAlbumsBySearchType(DatabaseSearch::DuplicatesSearch);
281
282 foreach (SAlbum* const sAlbum, sAlbums)
283 {
284 // Read the search query XML and save the image ids
285
286 SearchXmlReader reader(sAlbum->query());
287 SearchXml::Element element;
288 QSet<qlonglong> images;
289
290 while ((element = reader.readNext()) != SearchXml::End)
291 {
292 if ((element == SearchXml::Field) && (reader.fieldName().compare(QLatin1String("imageid")) == 0))
293 {
294 images = reader.valueToLongLongList().toSet();
295 }
296 }
297
298 // If the deleted images are part of the SAlbum,
299 // mark the album as ready for deletion and the images as ready for rescan.
300
301 if (images.intersects(deletedImages))
302 {
303 sAlbumsToUpdate.insert(sAlbum);
304 }
305 }
306
307 if (!sAlbumsToUpdate.isEmpty())
308 {
309 emit signalUpdateDuplicatesAlbums(sAlbumsToUpdate.values(), deletedImages.values());
310 }
311 }
312
313 } // namespace Digikam
314