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 - Physical Album helpers.
8 *
9 * Copyright (C) 2006-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
10 * Copyright (C) 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
11 * Copyright (C) 2015 by Mohamed_Anwer <m_dot_anwer at gmx dot com>
12 *
13 * This program is free software; you can redistribute it
14 * and/or modify it under the terms of the GNU General
15 * Public License as published by the Free Software Foundation;
16 * either version 2, or (at your option)
17 * any later version.
18 *
19 * This program is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
23 *
24 * ============================================================ */
25
26 #include "albummanager_p.h"
27
28 namespace Digikam
29 {
30
scanPAlbums()31 void AlbumManager::scanPAlbums()
32 {
33 d->scanPAlbumsTimer->stop();
34
35 // first insert all the current normal PAlbums into a map for quick lookup
36
37 QHash<int, PAlbum*> oldAlbums;
38 AlbumIterator it(d->rootPAlbum);
39
40 while (it.current())
41 {
42 PAlbum* const a = (PAlbum*)(*it);
43 oldAlbums[a->id()] = a;
44 ++it;
45 }
46
47 // scan db and get a list of all albums
48
49 QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
50
51 // sort by relative path so that parents are created before children
52
53 std::sort(currentAlbums.begin(), currentAlbums.end());
54
55 QList<AlbumInfo> newAlbums;
56
57 // go through all the Albums and see which ones are already present
58
59 foreach (const AlbumInfo& info, currentAlbums)
60 {
61 // check that location of album is available
62
63 if (d->showOnlyAvailableAlbums &&
64 !CollectionManager::instance()->locationForAlbumRootId(info.albumRootId).isAvailable())
65 {
66 continue;
67 }
68
69 if (oldAlbums.contains(info.id))
70 {
71 oldAlbums.remove(info.id);
72 }
73 else
74 {
75 newAlbums << info;
76 }
77 }
78
79 // now oldAlbums contains all the deleted albums and
80 // newAlbums contains all the new albums
81
82 // delete old albums, informing all frontends
83
84 // The albums have to be removed with children being removed first,
85 // removePAlbum takes care of that.
86 // So we only feed it the albums from oldAlbums topmost in hierarchy.
87
88 QSet<PAlbum*> topMostOldAlbums;
89
90 foreach (PAlbum* const album, oldAlbums)
91 {
92 if (album->isTrashAlbum())
93 {
94 continue;
95 }
96
97 if (!album->parent() || !oldAlbums.contains(album->parent()->id()))
98 {
99 topMostOldAlbums << album;
100 }
101 }
102
103 foreach (PAlbum* const album, topMostOldAlbums)
104 {
105 // recursively removes all children and the album
106
107 removePAlbum(album);
108 }
109
110 // sort by relative path so that parents are created before children
111
112 std::sort(newAlbums.begin(), newAlbums.end());
113
114 // create all new albums
115
116 foreach (const AlbumInfo& info, newAlbums)
117 {
118 if (info.relativePath.isEmpty())
119 {
120 continue;
121 }
122
123 PAlbum* album = nullptr;
124 PAlbum* parent = nullptr;
125
126 if (info.relativePath == QLatin1String("/"))
127 {
128 // Albums that represent the root directory of an album root
129 // We have them as here new albums first time after their creation
130
131 parent = d->rootPAlbum;
132 album = d->albumRootAlbumHash.value(info.albumRootId);
133
134 if (!album)
135 {
136 qCDebug(DIGIKAM_GENERAL_LOG) << "Did not find album root album in hash";
137 continue;
138 }
139
140 // it has been created from the collection location
141 // with album root id, parentPath "/" and a name, but no album id yet.
142
143 album->m_id = info.id;
144 }
145 else
146 {
147 // last section, no slash
148
149 QString name = info.relativePath.section(QLatin1Char('/'), -1, -1);
150
151 // all but last sections, leading slash, no trailing slash
152
153 QString parentPath = info.relativePath.section(QLatin1Char('/'), 0, -2);
154
155 if (parentPath.isEmpty())
156 {
157 parent = d->albumRootAlbumHash.value(info.albumRootId);
158 }
159 else
160 {
161 parent = d->albumPathHash.value(PAlbumPath(info.albumRootId, parentPath));
162 }
163
164 if (!parent)
165 {
166 qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find parent with url: "
167 << parentPath << " for: "
168 << info.relativePath;
169 continue;
170 }
171
172 // Create the new album
173
174 album = new PAlbum(info.albumRootId, parentPath, name, info.id);
175 }
176
177 album->m_caption = info.caption;
178 album->m_category = info.category;
179 album->m_date = info.date;
180 album->m_iconId = info.iconId;
181
182 insertPAlbum(album, parent);
183
184 if (album->isAlbumRoot())
185 {
186 // Inserting virtual Trash PAlbum for AlbumsRootAlbum using special constructor
187
188 PAlbum* trashAlbum = new PAlbum(album->title(), album->id());
189 insertPAlbum(trashAlbum, album);
190 }
191 }
192
193 if (!topMostOldAlbums.isEmpty() || !newAlbums.isEmpty())
194 {
195 emit signalAlbumsUpdated(Album::PHYSICAL);
196 }
197
198 getAlbumItemsCount();
199 }
200
updateChangedPAlbums()201 void AlbumManager::updateChangedPAlbums()
202 {
203 d->updatePAlbumsTimer->stop();
204
205 // scan db and get a list of all albums
206
207 QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
208 bool needScanPAlbums = false;
209
210 // Find the AlbumInfo for each id in changedPAlbums
211
212 foreach (int id, d->changedPAlbums)
213 {
214 foreach (const AlbumInfo& info, currentAlbums)
215 {
216 if (info.id == id)
217 {
218 d->changedPAlbums.remove(info.id);
219
220 PAlbum* album = findPAlbum(info.id);
221
222 if (album)
223 {
224 // Renamed?
225
226 if (info.relativePath != QLatin1String("/"))
227 {
228 // Handle rename of album name
229 // last section, no slash
230
231 QString name = info.relativePath.section(QLatin1Char('/'), -1, -1);
232 QString parentPath = info.relativePath;
233 parentPath.chop(name.length());
234
235 if (parentPath != album->m_parentPath || info.albumRootId != album->albumRootId())
236 {
237 // Handle actual move operations: trigger ScanPAlbums
238
239 needScanPAlbums = true;
240 removePAlbum(album);
241 break;
242 }
243 else if (name != album->title())
244 {
245 album->setTitle(name);
246 updateAlbumPathHash();
247 emit signalAlbumRenamed(album);
248 }
249 }
250
251 // Update caption, collection, date
252
253 album->m_caption = info.caption;
254 album->m_category = info.category;
255 album->m_date = info.date;
256
257 // Icon changed?
258
259 if (album->m_iconId != info.iconId)
260 {
261 album->m_iconId = info.iconId;
262 emit signalAlbumIconChanged(album);
263 }
264 }
265 }
266 }
267 }
268
269 if (needScanPAlbums)
270 {
271 scanPAlbums();
272 }
273 }
274
allPAlbums() const275 AlbumList AlbumManager::allPAlbums() const
276 {
277 AlbumList list;
278
279 if (d->rootPAlbum)
280 {
281 list.append(d->rootPAlbum);
282 }
283
284 AlbumIterator it(d->rootPAlbum);
285
286 while (it.current())
287 {
288 list.append(*it);
289 ++it;
290 }
291
292 return list;
293 }
294
currentPAlbum() const295 PAlbum* AlbumManager::currentPAlbum() const
296 {
297 /**
298 * Temporary fix, to return multiple items,
299 * iterate and cast each element
300 */
301 if (!d->currentAlbums.isEmpty())
302 {
303 return dynamic_cast<PAlbum*>(d->currentAlbums.first());
304 }
305 else
306 {
307 return nullptr;
308 }
309 }
310
findPAlbum(const QUrl & url) const311 PAlbum* AlbumManager::findPAlbum(const QUrl& url) const
312 {
313 CollectionLocation location = CollectionManager::instance()->locationForUrl(url);
314
315 if (location.isNull())
316 {
317 return nullptr;
318 }
319
320 return d->albumPathHash.value(PAlbumPath(location.id(), CollectionManager::instance()->album(location, url)));
321 }
322
findPAlbum(int id) const323 PAlbum* AlbumManager::findPAlbum(int id) const
324 {
325 if (!d->rootPAlbum)
326 {
327 return nullptr;
328 }
329
330 int gid = d->rootPAlbum->globalID() + id;
331
332 return static_cast<PAlbum*>((d->allAlbumsIdHash.value(gid)));
333 }
334
335
createPAlbum(const QString & albumRootPath,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)336 PAlbum* AlbumManager::createPAlbum(const QString& albumRootPath, const QString& name,
337 const QString& caption, const QDate& date,
338 const QString& category,
339 QString& errMsg)
340 {
341 CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRootPath);
342
343 return createPAlbum(location, name, caption, date, category, errMsg);
344 }
345
createPAlbum(const CollectionLocation & location,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)346 PAlbum* AlbumManager::createPAlbum(const CollectionLocation& location, const QString& name,
347 const QString& caption, const QDate& date,
348 const QString& category,
349 QString& errMsg)
350 {
351 if (location.isNull() || !location.isAvailable())
352 {
353 errMsg = i18n("The collection location supplied is invalid or currently not available.");
354 return nullptr;
355 }
356
357 PAlbum* const album = d->albumRootAlbumHash.value(location.id());
358
359 if (!album)
360 {
361 errMsg = i18n("No album for collection location: Internal error");
362 return nullptr;
363 }
364
365 return createPAlbum(album, name, caption, date, category, errMsg);
366 }
367
createPAlbum(PAlbum * parent,const QString & name,const QString & caption,const QDate & date,const QString & category,QString & errMsg)368 PAlbum* AlbumManager::createPAlbum(PAlbum* parent,
369 const QString& name,
370 const QString& caption,
371 const QDate& date,
372 const QString& category,
373 QString& errMsg)
374 {
375 if (!parent)
376 {
377 errMsg = i18n("No parent found for album.");
378 return nullptr;
379 }
380
381 // sanity checks
382
383 if (name.isEmpty())
384 {
385 errMsg = i18n("Album name cannot be empty.");
386 return nullptr;
387 }
388
389 if (name.contains(QLatin1Char('/')))
390 {
391 errMsg = i18n("Album name cannot contain '/'.");
392 return nullptr;
393 }
394
395 if (parent->isRoot())
396 {
397 errMsg = i18n("createPAlbum does not accept the root album as parent.");
398 return nullptr;
399 }
400
401 QString albumPath = parent->isAlbumRoot() ? QString(QLatin1Char('/') + name) : QString(parent->albumPath() + QLatin1Char('/') + name);
402 int albumRootId = parent->albumRootId();
403
404 // first check if we have a sibling album with the same name
405
406 PAlbum* child = static_cast<PAlbum*>(parent->firstChild());
407
408 while (child)
409 {
410 if ((child->albumRootId() == albumRootId) && (child->albumPath() == albumPath))
411 {
412 errMsg = i18n("An existing album has the same name.");
413 return nullptr;
414 }
415
416 child = static_cast<PAlbum*>(child->next());
417 }
418
419 CoreDbUrl url = parent->databaseUrl();
420 url = url.adjusted(QUrl::StripTrailingSlash);
421 url.setPath(url.path() + QLatin1Char('/') + name);
422 QUrl fileUrl = url.fileUrl();
423
424 bool ret = QDir().mkpath(fileUrl.toLocalFile());
425
426 if (!ret)
427 {
428 errMsg = i18n("Failed to create directory '%1'", fileUrl.toString()); // TODO add tags?
429 return nullptr;
430 }
431
432 ChangingDB changing(d);
433 int id = CoreDbAccess().db()->addAlbum(albumRootId, albumPath, caption, date, category);
434
435 if (id == -1)
436 {
437 errMsg = i18n("Failed to add album to database");
438 return nullptr;
439 }
440
441 QString parentPath;
442
443 if (!parent->isAlbumRoot())
444 {
445 parentPath = parent->albumPath();
446 }
447
448 PAlbum* const album = new PAlbum(albumRootId, parentPath, name, id);
449 album->m_caption = caption;
450 album->m_category = category;
451 album->m_date = date;
452
453 insertPAlbum(album, parent);
454 emit signalAlbumsUpdated(Album::PHYSICAL);
455
456 return album;
457 }
458
renamePAlbum(PAlbum * album,const QString & newName,QString & errMsg)459 bool AlbumManager::renamePAlbum(PAlbum* album, const QString& newName,
460 QString& errMsg)
461 {
462 if (!album)
463 {
464 errMsg = i18n("No such album");
465 return false;
466 }
467
468 if (album == d->rootPAlbum)
469 {
470 errMsg = i18n("Cannot rename root album");
471 return false;
472 }
473
474 if (album->isAlbumRoot())
475 {
476 errMsg = i18n("Cannot rename album root album");
477 return false;
478 }
479
480 if (newName.contains(QLatin1Char('/')))
481 {
482 errMsg = i18n("Album name cannot contain '/'");
483 return false;
484 }
485
486 // first check if we have another sibling with the same name
487
488 if (hasDirectChildAlbumWithTitle(album->m_parent, newName))
489 {
490 errMsg = i18n("Another album with the same name already exists.\n"
491 "Please choose another name.");
492 return false;
493 }
494
495 d->albumWatch->removeWatchedPAlbums(album);
496
497 // We use a private shortcut around collection scanner noticing our changes,
498 // we rename them directly. Faster.
499
500 ScanController::instance()->suspendCollectionScan();
501
502 QDir dir(album->albumRootPath() + album->m_parentPath);
503 bool ret = dir.rename(album->title(), newName);
504
505 if (!ret)
506 {
507 ScanController::instance()->resumeCollectionScan();
508
509 errMsg = i18n("Failed to rename Album");
510 return false;
511 }
512
513 QString oldAlbumPath = album->albumPath();
514 album->setTitle(newName);
515 album->m_path = newName;
516 QString newAlbumPath = album->albumPath();
517
518 // now rename the album and subalbums in the database
519 {
520 CoreDbAccess access;
521 ChangingDB changing(d);
522 access.db()->renameAlbum(album->id(), album->albumRootId(), album->albumPath());
523
524 PAlbum* subAlbum = nullptr;
525 AlbumIterator it(album);
526
527 while ((subAlbum = static_cast<PAlbum*>(it.current())) != nullptr)
528 {
529 subAlbum->m_parentPath = newAlbumPath + subAlbum->m_parentPath.mid(oldAlbumPath.length());
530 access.db()->renameAlbum(subAlbum->id(), album->albumRootId(), subAlbum->albumPath());
531 emit signalAlbumNewPath(subAlbum);
532 ++it;
533 }
534 }
535
536 updateAlbumPathHash();
537 emit signalAlbumRenamed(album);
538
539 ScanController::instance()->resumeCollectionScan();
540
541 return true;
542 }
543
updatePAlbumIcon(PAlbum * album,qlonglong iconID,QString & errMsg)544 bool AlbumManager::updatePAlbumIcon(PAlbum* album, qlonglong iconID, QString& errMsg)
545 {
546 if (!album)
547 {
548 errMsg = i18n("No such album");
549 return false;
550 }
551
552 if (album == d->rootPAlbum)
553 {
554 errMsg = i18n("Cannot edit root album");
555 return false;
556 }
557
558 {
559 CoreDbAccess access;
560 ChangingDB changing(d);
561 access.db()->setAlbumIcon(album->id(), iconID);
562 album->m_iconId = iconID;
563 }
564
565 emit signalAlbumIconChanged(album);
566
567 return true;
568 }
569
getPAlbumsCount() const570 QMap<int, int> AlbumManager::getPAlbumsCount() const
571 {
572 return d->pAlbumsCount;
573 }
574
insertPAlbum(PAlbum * album,PAlbum * parent)575 void AlbumManager::insertPAlbum(PAlbum* album, PAlbum* parent)
576 {
577 if (!album)
578 {
579 return;
580 }
581
582 emit signalAlbumAboutToBeAdded(album, parent, parent ? parent->lastChild() : nullptr);
583
584 if (parent)
585 {
586 album->setParent(parent);
587 }
588
589 d->albumPathHash[PAlbumPath(album)] = album;
590 d->allAlbumsIdHash[album->globalID()] = album;
591
592 emit signalAlbumAdded(album);
593 }
594
removePAlbum(PAlbum * album)595 void AlbumManager::removePAlbum(PAlbum* album)
596 {
597 if (!album)
598 {
599 return;
600 }
601
602 // remove all children of this album
603
604 Album* child = album->firstChild();
605 PAlbum* toBeRemoved = nullptr;
606
607 while (child)
608 {
609 Album* const next = child->next();
610 toBeRemoved = dynamic_cast<PAlbum*>(child);
611
612 if (toBeRemoved)
613 {
614 removePAlbum(toBeRemoved);
615 toBeRemoved = nullptr;
616 }
617
618 child = next;
619 }
620
621 emit signalAlbumAboutToBeDeleted(album);
622 d->albumPathHash.remove(PAlbumPath(album));
623 d->allAlbumsIdHash.remove(album->globalID());
624
625 CoreDbUrl url = album->databaseUrl();
626
627 if (!d->currentAlbums.isEmpty())
628 {
629 if (album == d->currentAlbums.first())
630 {
631 d->currentAlbums.clear();
632 emit signalAlbumCurrentChanged(d->currentAlbums);
633 }
634 }
635
636 if (album->isAlbumRoot())
637 {
638 d->albumRootAlbumHash.remove(album->albumRootId());
639 }
640
641 emit signalAlbumDeleted(album);
642 quintptr deletedAlbum = reinterpret_cast<quintptr>(album);
643 delete album;
644
645 emit signalAlbumHasBeenDeleted(deletedAlbum);
646 }
647
removeWatchedPAlbums(const PAlbum * const album)648 void AlbumManager::removeWatchedPAlbums(const PAlbum* const album)
649 {
650 d->albumWatch->removeWatchedPAlbums(album);
651 }
652
653 } // namespace Digikam
654