File indexing completed on 2024-05-12 04:06:26

0001 /*
0002     SPDX-FileCopyrightText: 2009-2011 Stefan Majewsky <majewsky@gmx.net>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "collection.h"
0008 #include "collection_p.h"
0009 #include "components.h"
0010 #include "puzzle.h"
0011 
0012 #include <QDir>
0013 #include <QFile>
0014 #include <QFileInfo>
0015 #include <QUuid>
0016 #include <QStandardPaths>
0017 #include <QProgressDialog>
0018 #include <KConfig>
0019 #include <KConfigGroup>
0020 #include <KLocalizedString>
0021 #include <QRegularExpression>
0022 
0023 //BEGIN Palapeli::Collection::Item
0024 
0025 Palapeli::Collection::Item::Item(Palapeli::Puzzle* puzzle)
0026     : m_puzzle(puzzle)
0027 {
0028     //NOTE: Previously, the metadata.modifyProtection flag was used to decide
0029     //whether the puzzle is deletable. The current implementation uses the
0030     //identifier: Puzzles that have been imported by the user have UUID
0031     //identifiers. QUuid::createUuid().toString() always encloses the UUID in
0032     //curly braces.
0033     const QString id = puzzle->identifier();
0034     setData(id, Palapeli::Collection::IdentifierRole);
0035     setData(id.startsWith(QLatin1Char('{')), Palapeli::Collection::IsDeleteableRole);
0036     setData(i18n("Loading puzzle..."), Qt::DisplayRole);
0037     setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable);
0038     //request metadata
0039     puzzle->get(Palapeli::PuzzleComponent::Metadata);
0040     populate ();
0041     //take ownership of puzzle
0042     m_puzzle->QObject::setParent(this);
0043 }
0044 
0045 void Palapeli::Collection::Item::populate()
0046 {
0047     const Palapeli::MetadataComponent* cmp = m_puzzle->component<Palapeli::MetadataComponent>();
0048     if (!cmp)
0049         return;
0050     const Palapeli::PuzzleMetadata metadata = cmp->metadata;
0051     setData(metadata.name, Palapeli::Collection::NameRole);
0052     setData(metadata.comment, Palapeli::Collection::CommentRole);
0053     setData(metadata.author, Palapeli::Collection::AuthorRole);
0054     setData(metadata.pieceCount, Palapeli::Collection::PieceCountRole);
0055     setData(metadata.thumbnail, Palapeli::Collection::ThumbnailRole);
0056 }
0057 
0058 //END Palapeli::Collection::Item
0059 
0060 Palapeli::Collection* Palapeli::Collection::instance(QWidget *parent)
0061 {
0062     static Palapeli::Collection instance(parent);
0063     return &instance;
0064 }
0065 
0066 static QString readPseudoUrl(const QString& path_, bool local)
0067 {
0068     static const QLatin1String pseudoUrl("palapeli:/");
0069     if (path_.startsWith(pseudoUrl))
0070     {
0071         const QString path = path_.mid(pseudoUrl.size() + 1);
0072         if (local)
0073         {
0074             // this file can exist, if not, make sure all is ready
0075             // for creating the file
0076             // --> simulate KStandardDirs::locateLocal()
0077             const QString loc =
0078                     QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) +
0079                     path;
0080             QFileInfo fi(loc);
0081             if (fi.exists())
0082                 return loc;
0083             // file does not exist, make sure directory exists
0084             QDir d(fi.absoluteDir());
0085             if (!d.exists())
0086                 d.mkpath(fi.absolutePath());
0087             return loc;
0088         }
0089         else
0090         {
0091             // this file *must* exist
0092             return QStandardPaths::locate(QStandardPaths::AppDataLocation,
0093                                           path,
0094                                           QStandardPaths::LocateFile);
0095         }
0096     }
0097     else
0098         return path_;
0099 }
0100 
0101 Palapeli::Collection::Collection(QWidget *parent)
0102     : m_config(new KConfig(QStringLiteral("palapeli-collectionrc"), KConfig::CascadeConfig))
0103     , m_group(new KConfigGroup(m_config, QStringLiteral("Palapeli Collection")))
0104 {
0105     //read the puzzles
0106     const QStringList puzzleIds = m_group->groupList();
0107     int len = puzzleIds.length ();
0108     int count = 0;
0109     QProgressDialog dlg(i18n("Loading collection"), QString(), 0, len, parent);
0110     dlg.setMinimumDuration(2000);
0111     dlg.setWindowModality(Qt::WindowModal);
0112     for (const QString& puzzleId : puzzleIds)
0113     {
0114         dlg.setValue(count++);
0115         KConfigGroup* puzzleGroup = new KConfigGroup(m_group, puzzleId);
0116         //find involved files
0117         const QString basePath = puzzleGroup->readEntry("Location", QString());
0118         const QString path = readPseudoUrl(basePath, false);
0119         QString baseDesktopPath(basePath);
0120         baseDesktopPath.replace(QRegularExpression(QStringLiteral("\\.puzzle$")), QLatin1String(".desktop"));
0121         const QString desktopPath = readPseudoUrl(baseDesktopPath, false);
0122         //construct puzzle with CollectionStorageComponent
0123         if (!path.isEmpty() && (desktopPath.isEmpty() || QFileInfo(path).lastModified() >= QFileInfo(desktopPath).lastModified()))
0124         {
0125             Palapeli::Puzzle* puzzle = new Palapeli::Puzzle(new Palapeli::CollectionStorageComponent(puzzleGroup), path, puzzleId);
0126             appendRow(new Item(puzzle));
0127             continue;
0128         }
0129         //no success - try to construct with RetailStorageComponent
0130         if (desktopPath.isEmpty())
0131             continue;
0132         const QString puzzlePath = readPseudoUrl(basePath, true);
0133         Palapeli::Puzzle* puzzle = new Palapeli::Puzzle(new Palapeli::RetailStorageComponent(desktopPath), puzzlePath, puzzleId);
0134         appendRow(new Item(puzzle));
0135         delete puzzleGroup;
0136         //make sure puzzle gets converted to archive format
0137         puzzle->get(Palapeli::PuzzleComponent::ArchiveStorage);
0138     }
0139     /* Moved out of CollectionStorageComponent, where we'd potentially call fdatasync on every
0140        puzzle.  */
0141     m_config->sync();
0142 }
0143 
0144 Palapeli::Collection::~Collection()
0145 {
0146     delete m_config;
0147     delete m_group;
0148 }
0149 
0150 Palapeli::Puzzle* Palapeli::Collection::puzzleFromIndex(const QModelIndex& index) const
0151 {
0152     //a simple lookup like dynamic_cast<Item*>(itemFromIndex(index))->puzzle()
0153     //does not work because of proxy models in the CollectionView
0154     const QString identifier = index.data(IdentifierRole).toString();
0155     const int count = rowCount();
0156     for (int row = 0; row < count; ++row)
0157     {
0158         Item* item = dynamic_cast<Item*>(this->item(row));
0159         if (item && item->puzzle()->identifier() == identifier)
0160             return item->puzzle();
0161     }
0162     return nullptr;
0163 }
0164 
0165 void Palapeli::Collection::importPuzzle(Palapeli::Puzzle* puzzle)
0166 {
0167     //determine new location
0168     const QString id = puzzle->identifier();
0169     const QString palapeliUrl = QStringLiteral("palapeli:///collection/%1.puzzle").arg(id);
0170     puzzle->setLocation(readPseudoUrl(palapeliUrl, true));
0171     //store puzzle
0172     puzzle->get(Palapeli::PuzzleComponent::ArchiveStorage);
0173     //create the config group for this puzzle (use pseudo-URL to avoid problems
0174     //when the configuration directory is moved)
0175     KConfigGroup puzzleGroup(m_group, id);
0176     puzzleGroup.writeEntry("Location", palapeliUrl);
0177     m_config->sync();
0178     //add to the model
0179     appendRow(new Item(puzzle));
0180 }
0181 
0182 Palapeli::Puzzle* Palapeli::Collection::importPuzzle(const QString& path)
0183 {
0184     const QString id = Palapeli::Puzzle::fsIdentifier(path);
0185     Palapeli::Puzzle* puzzle = new Palapeli::Puzzle(new Palapeli::ArchiveStorageComponent, path, id);
0186     //insert a copy of this puzzle into the collection
0187     const QString newId = QUuid::createUuid().toString();
0188     Palapeli::Puzzle* newPuzzle = new Palapeli::Puzzle(new Palapeli::CopyComponent(puzzle), path, newId);
0189     importPuzzle(newPuzzle);
0190     //cleanup
0191     puzzle->QObject::setParent(newPuzzle);
0192     return newPuzzle;
0193 }
0194 
0195 void Palapeli::Collection::exportPuzzle(const QModelIndex& index, const QString& path)
0196 {
0197     //find existing puzzle
0198     Palapeli::Puzzle* puzzle = puzzleFromIndex(index);
0199     if (!puzzle)
0200         return;
0201     //create a copy of the given puzzle, and relocate it to the new location
0202     const QString identifier = Palapeli::Puzzle::fsIdentifier(path);
0203     Palapeli::Puzzle* newPuzzle = new Palapeli::Puzzle(new Palapeli::CopyComponent(puzzle), path, identifier);
0204     newPuzzle->get(Palapeli::PuzzleComponent::ArchiveStorage);
0205 }
0206 
0207 bool Palapeli::Collection::deletePuzzle(const QModelIndex& index)
0208 {
0209     Palapeli::Puzzle* puzzle = puzzleFromIndex(index);
0210     if (!puzzle)
0211         return false;
0212     //check whether that particular file can be removed
0213     if (!QFile(puzzle->location()).remove())
0214         return false;
0215     //remove puzzle from config
0216     KConfigGroup(m_group, puzzle->identifier()).deleteGroup();
0217     m_config->sync();
0218     //remove puzzle from model and delete it
0219     const int count = rowCount();
0220     for (int row = 0; row < count; ++row)
0221     {
0222         Item* item = dynamic_cast<Item*>(this->item(row));
0223         if (item && item->puzzle() == puzzle)
0224             qDeleteAll(this->takeRow(row));
0225     }
0226     return true;
0227 }
0228 
0229 
0230 //
0231 
0232 #include "moc_collection.cpp"
0233 #include "moc_collection_p.cpp"