File indexing completed on 2024-11-10 05:02:44

0001 /*
0002     SPDX-FileCopyrightText: 2022 Fushan Wen <qydwhotmail@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "imageproxymodel.h"
0008 
0009 #include <QDir>
0010 
0011 #include <KConfigGroup>
0012 #include <KIO/OpenFileManagerWindowJob>
0013 #include <KSharedConfig>
0014 
0015 #include "../finder/suffixcheck.h"
0016 #include "imagelistmodel.h"
0017 #include "packagelistmodel.h"
0018 
0019 namespace
0020 {
0021 inline bool isChildItem(const QStringList &customPathsInKDirWatch, const QString &childPath)
0022 {
0023     return std::any_of(customPathsInKDirWatch.cbegin(), customPathsInKDirWatch.cend(), [&childPath](const QString &customPath) {
0024         if (customPath.endsWith(QDir::separator())) {
0025             return childPath.startsWith(customPath);
0026         } else {
0027             return childPath.startsWith(customPath + QDir::separator());
0028         }
0029     });
0030 }
0031 }
0032 
0033 ImageProxyModel::ImageProxyModel(const QStringList &customPaths,
0034                                  const QBindable<QSize> &bindableTargetSize,
0035                                  const QBindable<bool> &bindableUsedInConfig,
0036                                  QObject *parent)
0037     : QConcatenateTablesProxyModel(parent)
0038     , m_imageModel(new ImageListModel(bindableTargetSize, bindableUsedInConfig, this))
0039     , m_packageModel(new PackageListModel(bindableTargetSize, bindableUsedInConfig, this))
0040 {
0041     connect(this, &ImageProxyModel::rowsInserted, this, &ImageProxyModel::countChanged);
0042     connect(this, &ImageProxyModel::rowsRemoved, this, &ImageProxyModel::countChanged);
0043     connect(this, &ImageProxyModel::modelReset, this, &ImageProxyModel::countChanged);
0044 
0045     m_customPaths = customPaths;
0046     if (customPaths.empty()) {
0047         KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers"));
0048         m_customPaths = cfg.readEntry("usersWallpapers", QStringList{});
0049         m_imageModel->m_removableWallpapers = m_customPaths;
0050         m_packageModel->m_removableWallpapers = m_customPaths;
0051 
0052         m_customPaths += QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("wallpapers/"), QStandardPaths::LocateDirectory);
0053     }
0054 
0055     connect(m_imageModel, &AbstractImageListModel::loaded, this, &ImageProxyModel::slotHandleLoaded);
0056     connect(m_packageModel, &AbstractImageListModel::loaded, this, &ImageProxyModel::slotHandleLoaded);
0057 
0058     m_loading.setBinding([this] {
0059         return m_loaded.value() != 2;
0060     });
0061     m_loaded = 0;
0062 
0063     m_imageModel->load(m_customPaths);
0064     m_packageModel->load(m_customPaths);
0065 }
0066 
0067 QHash<int, QByteArray> ImageProxyModel::roleNames() const
0068 {
0069     if (const auto models = sourceModels(); !models.empty()) {
0070         return models.at(0)->roleNames();
0071     }
0072 
0073     return QConcatenateTablesProxyModel::roleNames();
0074 }
0075 
0076 int ImageProxyModel::count() const
0077 {
0078     return rowCount();
0079 }
0080 
0081 int ImageProxyModel::indexOf(const QString &packagePath) const
0082 {
0083     int idx = -1;
0084 
0085     for (const auto models{sourceModels()}; auto m : models) {
0086         idx = static_cast<const AbstractImageListModel *>(m)->indexOf(packagePath);
0087 
0088         if (idx >= 0) {
0089             return mapFromSource(m->index(idx, 0)).row();
0090         }
0091     }
0092 
0093     return idx;
0094 }
0095 
0096 QBindable<bool> ImageProxyModel::loading() const
0097 {
0098     return &m_loading;
0099 }
0100 
0101 void ImageProxyModel::reload()
0102 {
0103     for (const auto models{sourceModels()}; auto m : models) {
0104         static_cast<AbstractImageListModel *>(m)->reload();
0105     }
0106 
0107     m_loaded = 0;
0108 }
0109 
0110 QStringList ImageProxyModel::addBackground(const QString &_path)
0111 {
0112     QString path = _path;
0113 
0114     if (constexpr QLatin1String prefix{"file://"}; path.startsWith(prefix)) {
0115         path.remove(0, prefix.size());
0116     }
0117 
0118     const QFileInfo info(path);
0119 
0120     QStringList results;
0121 
0122     if (info.isDir()) {
0123         if (!path.endsWith(QDir::separator())) {
0124             path += QDir::separator();
0125         }
0126 
0127         results = m_packageModel->addBackground(path);
0128     } else if (info.isFile()) {
0129         results = m_imageModel->addBackground(path);
0130     }
0131 
0132     if (!results.empty()) {
0133         m_pendingAddition.append(results);
0134 
0135         for (const QString &path : std::as_const(results)) {
0136             if (m_dirWatch.contains(path) || isChildItem(m_customPaths, path) /* KDirWatch already monitors the parent folder */) {
0137                 continue;
0138             }
0139 
0140             const QFileInfo info(path);
0141             if (info.isFile()) {
0142                 m_dirWatch.addFile(path);
0143             } else if (info.isDir()) {
0144                 m_dirWatch.addDir(path);
0145             }
0146         }
0147     }
0148 
0149     return results;
0150 }
0151 
0152 void ImageProxyModel::removeBackground(const QString &_packagePath)
0153 {
0154     QString packagePath = _packagePath;
0155 
0156     if (constexpr QLatin1String prefix{"file://"}; packagePath.startsWith(prefix)) {
0157         packagePath.remove(0, prefix.size());
0158     }
0159 
0160     QStringList results;
0161 
0162     // The file may be already deleted, so isFile/isDir won't work.
0163     if (const QFileInfo info(packagePath); isAcceptableSuffix(info.suffix())) {
0164         results = m_imageModel->removeBackground(packagePath);
0165 
0166         if (!results.empty() && !isChildItem(m_customPaths, results.at(0))) {
0167             // Don't remove the file if its parent folder is in KDirWatch, otherwise KDirWatchPrivate::removeEntry will also remove the parent folder
0168             m_dirWatch.removeFile(results.at(0));
0169         }
0170     } else {
0171         results = m_packageModel->removeBackground(packagePath);
0172 
0173         if (!results.empty()) {
0174             // Because of KDirWatch::WatchSubDirs, some folders will still be added to KDirWatch
0175             m_dirWatch.removeDir(results.at(0));
0176         }
0177     }
0178 
0179     // The user may add a wallpaper and delete it later.
0180     for (const QString &path : std::as_const(results)) {
0181         m_pendingAddition.removeOne(path);
0182     }
0183 }
0184 
0185 void ImageProxyModel::commitAddition()
0186 {
0187     if (m_pendingAddition.empty()) {
0188         return;
0189     }
0190 
0191     KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers"));
0192     QStringList list = cfg.readEntry("usersWallpapers", QStringList{});
0193 
0194     list += m_pendingAddition;
0195     list.removeDuplicates();
0196 
0197     cfg.writeEntry("usersWallpapers", list);
0198 
0199     m_pendingAddition.clear();
0200 }
0201 
0202 void ImageProxyModel::commitDeletion()
0203 {
0204     QStringList pendingList;
0205 
0206     for (int row = 0; row < rowCount(); row++) {
0207         QModelIndex idx = index(row, 0);
0208 
0209         if (idx.data(PendingDeletionRole).toBool()) {
0210             pendingList.append(idx.data(PackageNameRole).toString());
0211         }
0212     }
0213 
0214     for (const QString &p : std::as_const(pendingList)) {
0215         removeBackground(p);
0216     }
0217 
0218     // Update the config
0219     KConfigGroup cfg = KConfigGroup(KSharedConfig::openConfig(QStringLiteral("plasmarc")), QStringLiteral("Wallpapers"));
0220     const QStringList list = cfg.readEntry("usersWallpapers", QStringList{});
0221     QStringList updatedList;
0222 
0223     // Check if the file still exists
0224     std::copy_if(list.cbegin(), list.cend(), std::back_inserter(updatedList), [&pendingList](const QString &_p) {
0225         QString p = _p;
0226         if (constexpr QLatin1String prefix{"file://"}; p.startsWith(prefix)) {
0227             p.remove(0, prefix.size());
0228         }
0229 
0230         return !pendingList.contains(p) && QFileInfo::exists(p);
0231     });
0232 
0233     cfg.writeEntry("usersWallpapers", updatedList);
0234     cfg.sync();
0235 }
0236 
0237 void ImageProxyModel::openContainingFolder(int row) const
0238 {
0239     KIO::highlightInFileManager({index(row, 0).data(PathRole).toUrl()});
0240 }
0241 
0242 void ImageProxyModel::slotHandleLoaded(AbstractImageListModel *model)
0243 {
0244     disconnect(model, &AbstractImageListModel::loaded, this, 0);
0245     if (m_loaded + 1 == 2) {
0246         // All models are loaded, now add them.
0247         addSourceModel(m_imageModel);
0248         addSourceModel(m_packageModel);
0249 
0250         setupDirWatch();
0251     }
0252     m_loaded = m_loaded + 1;
0253 }
0254 
0255 void ImageProxyModel::slotDirWatchCreated(const QString &_path)
0256 {
0257     QString path = _path;
0258 
0259     if (int idx = path.indexOf(QLatin1String("contents/images/")); idx > 0) {
0260         path = path.mid(0, idx);
0261     }
0262 
0263     addBackground(path);
0264 }
0265 
0266 void ImageProxyModel::slotDirWatchDeleted(const QString &path)
0267 {
0268     removeBackground(path);
0269 }
0270 
0271 void ImageProxyModel::setupDirWatch()
0272 {
0273     // Monitor file changes in the custom directories for the slideshow backend.
0274     for (const QString &path : std::as_const(m_customPaths)) {
0275         if (QFileInfo(path).isDir()) {
0276             m_dirWatch.addDir(path, KDirWatch::WatchFiles | KDirWatch::WatchSubDirs);
0277         }
0278     }
0279 
0280     connect(&m_dirWatch, &KDirWatch::created, this, &ImageProxyModel::slotDirWatchCreated);
0281     connect(&m_dirWatch, &KDirWatch::deleted, this, &ImageProxyModel::slotDirWatchDeleted);
0282 }