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