File indexing completed on 2025-01-19 03:53:20

0001 /* ============================================================
0002  *
0003  * This file is a part of digiKam project
0004  * https://www.digikam.org
0005  *
0006  * Date        : 2004-06-15
0007  * Description : Albums manager interface - Physical Album helpers.
0008  *
0009  * SPDX-FileCopyrightText: 2006-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
0010  * SPDX-FileCopyrightText: 2006-2011 by Marcel Wiesweg <marcel dot wiesweg at gmx dot de>
0011  * SPDX-FileCopyrightText: 2015      by Mohamed_Anwer <m_dot_anwer at gmx dot com>
0012  *
0013  * SPDX-License-Identifier: GPL-2.0-or-later
0014  *
0015  * ============================================================ */
0016 
0017 #include "albummanager_p.h"
0018 
0019 namespace Digikam
0020 {
0021 
0022 void AlbumManager::scanPAlbums()
0023 {
0024     d->scanPAlbumsTimer->stop();
0025 
0026     // first insert all the current normal PAlbums into a map for quick lookup
0027 
0028     QHash<int, PAlbum*> oldAlbums;
0029     AlbumIterator it(d->rootPAlbum);
0030 
0031     while (it.current())
0032     {
0033         PAlbum* const a    = (PAlbum*)(*it);
0034         oldAlbums[a->id()] = a;
0035         ++it;
0036     }
0037 
0038     // scan db and get a list of all albums
0039 
0040     QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
0041 
0042     // sort by relative path so that parents are created before children
0043 
0044     std::sort(currentAlbums.begin(), currentAlbums.end());
0045 
0046     QList<AlbumInfo> newAlbums;
0047 
0048     // go through all the Albums and see which ones are already present
0049 
0050     Q_FOREACH (const AlbumInfo& info, currentAlbums)
0051     {
0052         // check that location of album is available
0053 
0054         if (d->showOnlyAvailableAlbums &&
0055             !CollectionManager::instance()->locationForAlbumRootId(info.albumRootId).isAvailable())
0056         {
0057             continue;
0058         }
0059 
0060         if (oldAlbums.contains(info.id))
0061         {
0062             oldAlbums.remove(info.id);
0063         }
0064         else
0065         {
0066             newAlbums << info;
0067         }
0068     }
0069 
0070     // now oldAlbums contains all the deleted albums and
0071     // newAlbums contains all the new albums
0072 
0073     // delete old albums, informing all frontends
0074 
0075     // The albums have to be removed with children being removed first,
0076     // removePAlbum takes care of that.
0077     // So we only feed it the albums from oldAlbums topmost in hierarchy.
0078 
0079     QSet<PAlbum*> topMostOldAlbums;
0080 
0081     Q_FOREACH (PAlbum* const album, oldAlbums)
0082     {
0083         if (album->isTrashAlbum())
0084         {
0085             continue;
0086         }
0087 
0088         if (!album->parent() || !oldAlbums.contains(album->parent()->id()))
0089         {
0090             topMostOldAlbums << album;
0091         }
0092     }
0093 
0094     Q_FOREACH (PAlbum* const album, topMostOldAlbums)
0095     {
0096         // recursively removes all children and the album
0097 
0098         removePAlbum(album);
0099     }
0100 
0101     // sort by relative path so that parents are created before children
0102 
0103     std::sort(newAlbums.begin(), newAlbums.end());
0104 
0105     // create all new albums
0106 
0107     Q_FOREACH (const AlbumInfo& info, newAlbums)
0108     {
0109         if (info.relativePath.isEmpty())
0110         {
0111             continue;
0112         }
0113 
0114         PAlbum* album  = nullptr;
0115         PAlbum* parent = nullptr;
0116 
0117         if (info.relativePath == QLatin1String("/"))
0118         {
0119             // Albums that represent the root directory of an album root
0120             // We have them as here new albums first time after their creation
0121 
0122             parent = d->rootPAlbum;
0123             album  = d->albumRootAlbumHash.value(info.albumRootId);
0124 
0125             if (!album)
0126             {
0127                 qCDebug(DIGIKAM_GENERAL_LOG) << "Did not find album root album in hash";
0128                 continue;
0129             }
0130 
0131             // it has been created from the collection location
0132             // with album root id, parentPath "/" and a name, but no album id yet.
0133 
0134             album->m_id = info.id;
0135         }
0136         else
0137         {
0138             // last section, no slash
0139 
0140             QString name = info.relativePath.section(QLatin1Char('/'), -1, -1);
0141 
0142             // all but last sections, leading slash, no trailing slash
0143 
0144             QString parentPath = info.relativePath.section(QLatin1Char('/'), 0, -2);
0145 
0146             if (parentPath.isEmpty())
0147             {
0148                 parent = d->albumRootAlbumHash.value(info.albumRootId);
0149             }
0150             else
0151             {
0152                 parent = d->albumPathHash.value(PAlbumPath(info.albumRootId, parentPath));
0153             }
0154 
0155             if (!parent)
0156             {
0157                 qCDebug(DIGIKAM_GENERAL_LOG) << "Could not find parent with url: "
0158                                              << parentPath << " for: "
0159                                              << info.relativePath;
0160                 continue;
0161             }
0162 
0163             // Create the new album
0164 
0165             album = new PAlbum(info.albumRootId, parentPath, name, info.id);
0166         }
0167 
0168         album->m_caption  = info.caption;
0169         album->m_category = info.category;
0170         album->m_date     = info.date;
0171         album->m_iconId   = info.iconId;
0172 
0173         insertPAlbum(album, parent);
0174 
0175         if (album->isAlbumRoot())
0176         {
0177             // Inserting virtual Trash PAlbum for AlbumsRootAlbum using special constructor
0178 
0179             PAlbum* trashAlbum = new PAlbum(album->title(), album->id());
0180             insertPAlbum(trashAlbum, album);
0181         }
0182     }
0183 
0184     if (!topMostOldAlbums.isEmpty() || !newAlbums.isEmpty())
0185     {
0186         Q_EMIT signalAlbumsUpdated(Album::PHYSICAL);
0187     }
0188 
0189     getAlbumItemsCount();
0190 }
0191 
0192 void AlbumManager::updateChangedPAlbums()
0193 {
0194     d->updatePAlbumsTimer->stop();
0195 
0196     // scan db and get a list of all albums
0197 
0198     QList<AlbumInfo> currentAlbums = CoreDbAccess().db()->scanAlbums();
0199     bool needScanPAlbums           = false;
0200 
0201     // Find the AlbumInfo for each id in changedPAlbums
0202 
0203     Q_FOREACH (int id, d->changedPAlbums)
0204     {
0205         Q_FOREACH (const AlbumInfo& info, currentAlbums)
0206         {
0207             if (info.id == id)
0208             {
0209                 d->changedPAlbums.remove(info.id);
0210 
0211                 PAlbum* album = findPAlbum(info.id);
0212 
0213                 if (album)
0214                 {
0215                     // Renamed?
0216 
0217                     if (info.relativePath != QLatin1String("/"))
0218                     {
0219                         // Handle rename of album name
0220                         // last section, no slash
0221 
0222                         QString name       = info.relativePath.section(QLatin1Char('/'), -1, -1);
0223                         QString parentPath = info.relativePath;
0224                         parentPath.chop(name.length());
0225 
0226                         if (parentPath != album->m_parentPath || info.albumRootId != album->albumRootId())
0227                         {
0228                             // Handle actual move operations: trigger ScanPAlbums
0229 
0230                             needScanPAlbums = true;
0231                             removePAlbum(album);
0232                             break;
0233                         }
0234                         else if (name != album->title())
0235                         {
0236                             album->setTitle(name);
0237                             updateAlbumPathHash();
0238                             Q_EMIT signalAlbumRenamed(album);
0239                         }
0240                     }
0241 
0242                     // Update caption, collection, date
0243 
0244                     album->m_caption  = info.caption;
0245                     album->m_category = info.category;
0246                     album->m_date     = info.date;
0247 
0248                     // Icon changed?
0249 
0250                     if (album->m_iconId != info.iconId)
0251                     {
0252                         album->m_iconId = info.iconId;
0253                         Q_EMIT signalAlbumIconChanged(album);
0254                     }
0255                 }
0256             }
0257         }
0258     }
0259 
0260     if (needScanPAlbums)
0261     {
0262         scanPAlbums();
0263     }
0264 }
0265 
0266 AlbumList AlbumManager::allPAlbums() const
0267 {
0268     AlbumList list;
0269 
0270     if (d->rootPAlbum)
0271     {
0272         list.append(d->rootPAlbum);
0273     }
0274 
0275     AlbumIterator it(d->rootPAlbum);
0276 
0277     while (it.current())
0278     {
0279         list.append(*it);
0280         ++it;
0281     }
0282 
0283     return list;
0284 }
0285 
0286 PAlbum* AlbumManager::currentPAlbum() const
0287 {
0288     /**
0289      * Temporary fix, to return multiple items,
0290      * iterate and cast each element
0291      */
0292     if (!d->currentAlbums.isEmpty())
0293     {
0294         return dynamic_cast<PAlbum*>(d->currentAlbums.first());
0295     }
0296     else
0297     {
0298         return nullptr;
0299     }
0300 }
0301 
0302 PAlbum* AlbumManager::findPAlbum(const QUrl& url) const
0303 {
0304     CollectionLocation location = CollectionManager::instance()->locationForUrl(url);
0305 
0306     if (location.isNull())
0307     {
0308         return nullptr;
0309     }
0310 
0311     return d->albumPathHash.value(PAlbumPath(location.id(), CollectionManager::instance()->album(location, url)));
0312 }
0313 
0314 PAlbum* AlbumManager::findPAlbum(int id) const
0315 {
0316     if (!d->rootPAlbum)
0317     {
0318         return nullptr;
0319     }
0320 
0321     int gid = d->rootPAlbum->globalID() + id;
0322 
0323     return static_cast<PAlbum*>((d->allAlbumsIdHash.value(gid)));
0324 }
0325 
0326 
0327 PAlbum* AlbumManager::createPAlbum(const QString& albumRootPath, const QString& name,
0328                                    const QString& caption, const QDate& date,
0329                                    const QString& category,
0330                                    QString& errMsg)
0331 {
0332     CollectionLocation location = CollectionManager::instance()->locationForAlbumRootPath(albumRootPath);
0333 
0334     return createPAlbum(location, name, caption, date, category, errMsg);
0335 }
0336 
0337 PAlbum* AlbumManager::createPAlbum(const CollectionLocation& location, const QString& name,
0338                                    const QString& caption, const QDate& date,
0339                                    const QString& category,
0340                                    QString& errMsg)
0341 {
0342     if (location.isNull() || !location.isAvailable())
0343     {
0344         errMsg = i18n("The collection location supplied is invalid or currently not available.");
0345         return nullptr;
0346     }
0347 
0348     PAlbum* const album = d->albumRootAlbumHash.value(location.id());
0349 
0350     if (!album)
0351     {
0352         errMsg = i18n("No album for collection location: Internal error");
0353         return nullptr;
0354     }
0355 
0356     return createPAlbum(album, name, caption, date, category, errMsg);
0357 }
0358 
0359 PAlbum* AlbumManager::createPAlbum(PAlbum*        parent,
0360                                    const QString& name,
0361                                    const QString& caption,
0362                                    const QDate&   date,
0363                                    const QString& category,
0364                                    QString&       errMsg)
0365 {
0366     if (!parent)
0367     {
0368         errMsg = i18n("No parent found for album.");
0369         return nullptr;
0370     }
0371 
0372     // sanity checks
0373 
0374     if (name.isEmpty())
0375     {
0376         errMsg = i18n("Album name cannot be empty.");
0377         return nullptr;
0378     }
0379 
0380     if (name.contains(QLatin1Char('/')))
0381     {
0382         errMsg = i18n("Album name cannot contain '/'.");
0383         return nullptr;
0384     }
0385 
0386     if (parent->isRoot())
0387     {
0388         errMsg = i18n("createPAlbum does not accept the root album as parent.");
0389         return nullptr;
0390     }
0391 
0392     QString albumPath = parent->isAlbumRoot() ? QString(QLatin1Char('/') + name) : QString(parent->albumPath() + QLatin1Char('/') + name);
0393     int albumRootId   = parent->albumRootId();
0394 
0395     // first check if we have a sibling album with the same name
0396 
0397     PAlbum* child = static_cast<PAlbum*>(parent->firstChild());
0398 
0399     while (child)
0400     {
0401         if ((child->albumRootId() == albumRootId) && (child->albumPath() == albumPath))
0402         {
0403             errMsg = i18n("An existing album has the same name.");
0404             return nullptr;
0405         }
0406 
0407         child = static_cast<PAlbum*>(child->next());
0408     }
0409 
0410     CoreDbUrl url   = parent->databaseUrl();
0411     url             = url.adjusted(QUrl::StripTrailingSlash);
0412     url.setPath(url.path() + QLatin1Char('/') + name);
0413     QUrl fileUrl    = url.fileUrl();
0414 
0415     bool ret        = QDir().mkpath(fileUrl.toLocalFile());
0416 
0417     if (!ret)
0418     {
0419         errMsg = i18n("Failed to create directory '%1'", fileUrl.toString()); // TODO add tags?
0420         return nullptr;
0421     }
0422 
0423     ChangingDB changing(d);
0424     int        id = CoreDbAccess().db()->addAlbum(albumRootId, albumPath, caption, date, category);
0425 
0426     if (id == -1)
0427     {
0428         errMsg = i18n("Failed to add album to database");
0429         return nullptr;
0430     }
0431 
0432     QString parentPath;
0433 
0434     if (!parent->isAlbumRoot())
0435     {
0436         parentPath = parent->albumPath();
0437     }
0438 
0439     PAlbum* const album = new PAlbum(albumRootId, parentPath, name, id);
0440     album->m_caption    = caption;
0441     album->m_category   = category;
0442     album->m_date       = date;
0443 
0444     insertPAlbum(album, parent);
0445     Q_EMIT signalAlbumsUpdated(Album::PHYSICAL);
0446 
0447     return album;
0448 }
0449 
0450 bool AlbumManager::renamePAlbum(PAlbum* album, const QString& newName,
0451                                 QString& errMsg)
0452 {
0453     if (!album)
0454     {
0455         errMsg = i18n("No such album");
0456         return false;
0457     }
0458 
0459     if (album == d->rootPAlbum)
0460     {
0461         errMsg = i18n("Cannot rename root album");
0462         return false;
0463     }
0464 
0465     if (album->isAlbumRoot())
0466     {
0467         errMsg = i18n("Cannot rename album root album");
0468         return false;
0469     }
0470 
0471     if (newName.contains(QLatin1Char('/')))
0472     {
0473         errMsg = i18n("Album name cannot contain '/'");
0474         return false;
0475     }
0476 
0477     // first check if we have another sibling with the same name
0478 
0479     if (hasDirectChildAlbumWithTitle(album->m_parent, newName))
0480     {
0481         errMsg = i18n("Another album with the same name already exists.\n"
0482                       "Please choose another name.");
0483         return false;
0484     }
0485 
0486     d->albumWatch->removeWatchedPAlbums(album);
0487 
0488     // We use a private shortcut around collection scanner noticing our changes,
0489     // we rename them directly. Faster.
0490 
0491     ScanController::instance()->suspendCollectionScan();
0492 
0493     QDir dir(album->albumRootPath() + album->m_parentPath);
0494     bool ret = dir.rename(album->title(), newName);
0495 
0496     if (!ret)
0497     {
0498         ScanController::instance()->resumeCollectionScan();
0499 
0500         errMsg = i18n("Failed to rename Album");
0501         return false;
0502     }
0503 
0504     QString oldAlbumPath = album->albumPath();
0505     album->setTitle(newName);
0506     album->m_path        = newName;
0507     QString newAlbumPath = album->albumPath();
0508 
0509     // now rename the album and subalbums in the database
0510     {
0511         CoreDbAccess access;
0512         ChangingDB changing(d);
0513         access.db()->renameAlbum(album->id(), album->albumRootId(), album->albumPath());
0514 
0515         PAlbum* subAlbum = nullptr;
0516         AlbumIterator it(album);
0517 
0518         while ((subAlbum = static_cast<PAlbum*>(it.current())) != nullptr)
0519         {
0520             subAlbum->m_parentPath = newAlbumPath + subAlbum->m_parentPath.mid(oldAlbumPath.length());
0521             access.db()->renameAlbum(subAlbum->id(), album->albumRootId(), subAlbum->albumPath());
0522             Q_EMIT signalAlbumNewPath(subAlbum);
0523             ++it;
0524         }
0525     }
0526 
0527     updateAlbumPathHash();
0528     Q_EMIT signalAlbumRenamed(album);
0529 
0530     ScanController::instance()->resumeCollectionScan();
0531 
0532     return true;
0533 }
0534 
0535 bool AlbumManager::updatePAlbumIcon(PAlbum* album, qlonglong iconID, QString& errMsg)
0536 {
0537     if (!album)
0538     {
0539         errMsg = i18n("No such album");
0540         return false;
0541     }
0542 
0543     if (album == d->rootPAlbum)
0544     {
0545         errMsg = i18n("Cannot edit root album");
0546         return false;
0547     }
0548 
0549     {
0550         CoreDbAccess access;
0551         ChangingDB changing(d);
0552         access.db()->setAlbumIcon(album->id(), iconID);
0553         album->m_iconId = iconID;
0554     }
0555 
0556     Q_EMIT signalAlbumIconChanged(album);
0557 
0558     return true;
0559 }
0560 
0561 QHash<int, int> AlbumManager::getPAlbumsCount() const
0562 {
0563     return d->pAlbumsCount;
0564 }
0565 
0566 void AlbumManager::insertPAlbum(PAlbum* album, PAlbum* parent)
0567 {
0568     if (!album)
0569     {
0570         return;
0571     }
0572 
0573     Q_EMIT signalAlbumAboutToBeAdded(album, parent, parent ? parent->lastChild() : nullptr);
0574 
0575     if (parent)
0576     {
0577         album->setParent(parent);
0578     }
0579 
0580     d->albumPathHash[PAlbumPath(album)]   = album;
0581     d->allAlbumsIdHash[album->globalID()] = album;
0582 
0583     Q_EMIT signalAlbumAdded(album);
0584 }
0585 
0586 void AlbumManager::removePAlbum(PAlbum* album)
0587 {
0588     if (!album)
0589     {
0590         return;
0591     }
0592 
0593     // remove all children of this album
0594 
0595     Album* child        = album->firstChild();
0596     PAlbum* toBeRemoved = nullptr;
0597 
0598     while (child)
0599     {
0600         Album* const next = child->next();
0601         toBeRemoved       = dynamic_cast<PAlbum*>(child);
0602 
0603         if (toBeRemoved)
0604         {
0605             removePAlbum(toBeRemoved);
0606             toBeRemoved = nullptr;
0607         }
0608 
0609         child             = next;
0610     }
0611 
0612     Q_EMIT signalAlbumAboutToBeDeleted(album);
0613     d->albumPathHash.remove(PAlbumPath(album));
0614     d->allAlbumsIdHash.remove(album->globalID());
0615 
0616     CoreDbUrl url = album->databaseUrl();
0617 
0618     if (!d->currentAlbums.isEmpty())
0619     {
0620         if (album == d->currentAlbums.first())
0621         {
0622             d->currentAlbums.clear();
0623             Q_EMIT signalAlbumCurrentChanged(d->currentAlbums);
0624         }
0625     }
0626 
0627     if (album->isAlbumRoot())
0628     {
0629         d->albumRootAlbumHash.remove(album->albumRootId());
0630     }
0631 
0632     Q_EMIT signalAlbumDeleted(album);
0633     quintptr deletedAlbum = reinterpret_cast<quintptr>(album);
0634     delete album;
0635 
0636     Q_EMIT signalAlbumHasBeenDeleted(deletedAlbum);
0637 }
0638 
0639 void AlbumManager::removeWatchedPAlbums(const PAlbum* const album)
0640 {
0641     d->albumWatch->removeWatchedPAlbums(album);
0642 }
0643 
0644 } // namespace Digikam