|
@@ -26,6 +26,26 @@
|
|
#include <QImageReader>
|
|
#include <QImageReader>
|
|
#include <QMimeDatabase>
|
|
#include <QMimeDatabase>
|
|
#include <QElapsedTimer>
|
|
#include <QElapsedTimer>
|
|
|
|
+#include <QStandardPaths>
|
|
|
|
+
|
|
|
|
+#include <QtSql/QSqlDatabase>
|
|
|
|
+#include <QtSql/QSqlError>
|
|
|
|
+#include <QtSql/QSqlQuery>
|
|
|
|
+
|
|
|
|
+namespace {
|
|
|
|
+ QString stripDbHostileCharacters(QString path) {
|
|
|
|
+ return path.replace(QString("/"), QString(""));
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ inline int offsetHash(int hash) { return hash + 1; }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+struct ArtPiece {
|
|
|
|
+ ArtPiece() : refCount(0) { /**/ }
|
|
|
|
+ QString path;
|
|
|
|
+ QSize size;
|
|
|
|
+ int refCount;
|
|
|
|
+};
|
|
|
|
|
|
struct FSNode {
|
|
struct FSNode {
|
|
FSNode(const QString& rname, const FSNode *pparent = nullptr);
|
|
FSNode(const QString& rname, const FSNode *pparent = nullptr);
|
|
@@ -63,27 +83,29 @@ class FSNodeTree : public QObject
|
|
{
|
|
{
|
|
Q_OBJECT
|
|
Q_OBJECT
|
|
public:
|
|
public:
|
|
- FSNodeTree(PictureModel *p);
|
|
|
|
|
|
+ FSNodeTree(const QString& path);
|
|
|
|
+ virtual ~FSNodeTree();
|
|
|
|
|
|
void addModelNode(const FSNode* parentNode);
|
|
void addModelNode(const FSNode* parentNode);
|
|
- void setModelRoot(const QString& rootDir) { this->rootDir = rootDir; }
|
|
|
|
|
|
|
|
int fileCount() const { return files.length(); }
|
|
int fileCount() const { return files.length(); }
|
|
QVector<FSLeafNode*> files;
|
|
QVector<FSLeafNode*> files;
|
|
public slots:
|
|
public slots:
|
|
- void populate();
|
|
|
|
|
|
+ void populate(bool useDatabaseBackend);
|
|
signals:
|
|
signals:
|
|
void countChanged();
|
|
void countChanged();
|
|
private:
|
|
private:
|
|
|
|
+ QSqlError initDb();
|
|
|
|
+ QSqlError dumpTreeToDb();
|
|
|
|
+
|
|
QStringList extensions;
|
|
QStringList extensions;
|
|
QString rootDir;
|
|
QString rootDir;
|
|
};
|
|
};
|
|
|
|
|
|
-FSNodeTree::FSNodeTree(PictureModel *p)
|
|
|
|
- : QObject(nullptr)
|
|
|
|
|
|
+FSNodeTree::FSNodeTree(const QString& path)
|
|
|
|
+ : QObject(nullptr),
|
|
|
|
+ rootDir(path)
|
|
{
|
|
{
|
|
- connect(this, SIGNAL(countChanged()), p, SIGNAL(countChanged()));
|
|
|
|
-
|
|
|
|
QMimeDatabase mimeDatabase;
|
|
QMimeDatabase mimeDatabase;
|
|
foreach(const QByteArray &m, QImageReader::supportedMimeTypes()) {
|
|
foreach(const QByteArray &m, QImageReader::supportedMimeTypes()) {
|
|
foreach(const QString &suffix, mimeDatabase.mimeTypeForName(m).suffixes())
|
|
foreach(const QString &suffix, mimeDatabase.mimeTypeForName(m).suffixes())
|
|
@@ -95,6 +117,18 @@ FSNodeTree::FSNodeTree(PictureModel *p)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+FSNodeTree::~FSNodeTree()
|
|
|
|
+{
|
|
|
|
+ QSet<const FSNode*> nodes;
|
|
|
|
+ foreach(const FSNode *node, files) {
|
|
|
|
+ while(node) {
|
|
|
|
+ nodes << node;
|
|
|
|
+ node = node->parent;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ qDeleteAll(nodes.toList());
|
|
|
|
+}
|
|
|
|
+
|
|
void FSNodeTree::addModelNode(const FSNode* parentNode)
|
|
void FSNodeTree::addModelNode(const FSNode* parentNode)
|
|
{
|
|
{
|
|
// TODO: Check for symlink recursion
|
|
// TODO: Check for symlink recursion
|
|
@@ -137,16 +171,70 @@ void FSNodeTree::addModelNode(const FSNode* parentNode)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-void FSNodeTree::populate()
|
|
|
|
|
|
+void FSNodeTree::populate(bool useDatabaseBackend)
|
|
{
|
|
{
|
|
QElapsedTimer timer;
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
timer.start();
|
|
QDir currentDir(rootDir);
|
|
QDir currentDir(rootDir);
|
|
if (!currentDir.exists()) {
|
|
if (!currentDir.exists()) {
|
|
- qDebug() << "Being told to watch a non existent directory";
|
|
|
|
|
|
+ qDebug() << "Being told to watch a non existent directory:" << rootDir;
|
|
}
|
|
}
|
|
addModelNode(new FSNode(rootDir));
|
|
addModelNode(new FSNode(rootDir));
|
|
qDebug() << "Completed building file tree after:" << timer.elapsed();
|
|
qDebug() << "Completed building file tree after:" << timer.elapsed();
|
|
|
|
+
|
|
|
|
+ if (useDatabaseBackend) {
|
|
|
|
+ timer.restart();
|
|
|
|
+ QSqlError err = dumpTreeToDb();
|
|
|
|
+ qDebug() << "Completed database dump after:" << timer.elapsed();
|
|
|
|
+
|
|
|
|
+ if (err.type() != QSqlError::NoError) {
|
|
|
|
+ qDebug() << "Database dump of content tree failed with" << err.text();
|
|
|
|
+ } else {
|
|
|
|
+ qDebug() << "Successfully finished populating DB:" << rootDir;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+QSqlError FSNodeTree::dumpTreeToDb()
|
|
|
|
+{
|
|
|
|
+ QSqlError err = initDb();
|
|
|
|
+ if (err.type() != QSqlError::NoError) {
|
|
|
|
+ return err;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ QString insertQuery = QString("INSERT INTO %1 (path, width, height) VALUES ").arg(::stripDbHostileCharacters(rootDir));
|
|
|
|
+ QString insertQueryValues("(?, ?, ?),");
|
|
|
|
+
|
|
|
|
+ insertQuery.reserve(insertQuery.size() + insertQueryValues.size()*files.length());
|
|
|
|
+ for(int i = 0; i < files.length(); i++) {
|
|
|
|
+ insertQuery.append(insertQueryValues);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ insertQuery = insertQuery.replace(insertQuery.length()-1, 1, ";");
|
|
|
|
+
|
|
|
|
+ QSqlQuery q;
|
|
|
|
+
|
|
|
|
+ if (!q.prepare(insertQuery))
|
|
|
|
+ return q.lastError();
|
|
|
|
+
|
|
|
|
+ foreach(const FSLeafNode *node, files) {
|
|
|
|
+ q.addBindValue(node->qualifyNode(node));
|
|
|
|
+ q.addBindValue(node->size.width());
|
|
|
|
+ q.addBindValue(node->size.height());
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ q.exec();
|
|
|
|
+
|
|
|
|
+ return q.lastError();
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+QSqlError FSNodeTree::initDb()
|
|
|
|
+{
|
|
|
|
+ QSqlQuery q;
|
|
|
|
+ if (!q.exec(QString("create table %1 (path varchar, width integer, height integer)").arg(::stripDbHostileCharacters(rootDir))))
|
|
|
|
+ return q.lastError();
|
|
|
|
+
|
|
|
|
+ return QSqlError();
|
|
}
|
|
}
|
|
|
|
|
|
class PictureModel::PictureModelPrivate {
|
|
class PictureModel::PictureModelPrivate {
|
|
@@ -155,36 +243,117 @@ public:
|
|
~PictureModelPrivate();
|
|
~PictureModelPrivate();
|
|
|
|
|
|
FSNodeTree *fsTree;
|
|
FSNodeTree *fsTree;
|
|
|
|
+ bool useDatabaseBackend;
|
|
|
|
+
|
|
|
|
+ void cacheIndex(int index);
|
|
|
|
+ void retireCachedIndex(int index);
|
|
|
|
+ int itemCount();
|
|
|
|
+
|
|
|
|
+ QHash<int, ArtPiece*> artwork;
|
|
private:
|
|
private:
|
|
|
|
+ PictureModel *parent;
|
|
|
|
+ int collectionSize;
|
|
|
|
+ QString artPath;
|
|
|
|
+ void createFSTree(const QString &path);
|
|
QThread scanningThread;
|
|
QThread scanningThread;
|
|
};
|
|
};
|
|
|
|
|
|
PictureModel::PictureModelPrivate::PictureModelPrivate(PictureModel* p)
|
|
PictureModel::PictureModelPrivate::PictureModelPrivate(PictureModel* p)
|
|
|
|
+ : fsTree(nullptr),
|
|
|
|
+ parent(p)
|
|
{
|
|
{
|
|
QSettings settings;
|
|
QSettings settings;
|
|
- QString artPath = settings.value("artPath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString();
|
|
|
|
|
|
+ useDatabaseBackend = settings.value("useDatabaseBackend", true).toBool();
|
|
|
|
+ settings.setValue("useDatabaseBackend", useDatabaseBackend);
|
|
|
|
|
|
|
|
+ artPath = settings.value("artPath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString();
|
|
settings.setValue("artPath", artPath);
|
|
settings.setValue("artPath", artPath);
|
|
|
|
|
|
- fsTree = new FSNodeTree(p);
|
|
|
|
-
|
|
|
|
- fsTree->setModelRoot(artPath);
|
|
|
|
|
|
+ if (useDatabaseBackend) {
|
|
|
|
+ QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
|
|
|
|
+ QFileInfo dbFile(QStandardPaths::standardLocations(QStandardPaths::DataLocation).first() + "/" + qApp->applicationName() + ".db");
|
|
|
|
+ QDir().mkpath(dbFile.absolutePath());
|
|
|
|
+ db.setDatabaseName(dbFile.absoluteFilePath());
|
|
|
|
+
|
|
|
|
+ if (db.open()) {
|
|
|
|
+ QStringList tables = db.tables();
|
|
|
|
+ if (tables.contains(::stripDbHostileCharacters(artPath), Qt::CaseInsensitive)) {
|
|
|
|
+ QString queryString = "SELECT COUNT(*) FROM " % ::stripDbHostileCharacters(artPath) % ";";
|
|
|
|
+ QSqlQuery query(queryString);
|
|
|
|
+ query.next();
|
|
|
|
+
|
|
|
|
+ collectionSize = query.value(0).toInt();
|
|
|
|
+ QMetaObject::invokeMethod(parent, "countChanged");
|
|
|
|
+ } else {
|
|
|
|
+ createFSTree(artPath);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ qDebug() << "Failed to open the database:" << dbFile.absoluteFilePath();
|
|
|
|
+ qDebug() << "Error:" << db.lastError().text();
|
|
|
|
+ qApp->exit(-1);
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ createFSTree(artPath);
|
|
|
|
+ }
|
|
|
|
+};
|
|
|
|
|
|
|
|
+void PictureModel::PictureModelPrivate::createFSTree(const QString &path)
|
|
|
|
+{
|
|
|
|
+ fsTree = new FSNodeTree(path);
|
|
|
|
+ connect(fsTree, &FSNodeTree::countChanged, parent, &PictureModel::countChanged);
|
|
fsTree->moveToThread(&scanningThread);
|
|
fsTree->moveToThread(&scanningThread);
|
|
scanningThread.start();
|
|
scanningThread.start();
|
|
-
|
|
|
|
- QMetaObject::invokeMethod(fsTree, "populate", Qt::QueuedConnection);
|
|
|
|
-};
|
|
|
|
|
|
+ QMetaObject::invokeMethod(fsTree, "populate", Qt::QueuedConnection, Q_ARG(bool, useDatabaseBackend));
|
|
|
|
+}
|
|
|
|
|
|
PictureModel::PictureModelPrivate::~PictureModelPrivate()
|
|
PictureModel::PictureModelPrivate::~PictureModelPrivate()
|
|
{
|
|
{
|
|
- scanningThread.quit();
|
|
|
|
- scanningThread.wait(5000);
|
|
|
|
|
|
+ if (fsTree) {
|
|
|
|
+ scanningThread.quit();
|
|
|
|
+ scanningThread.wait(5000);
|
|
|
|
+
|
|
|
|
+ delete fsTree;
|
|
|
|
+ fsTree = nullptr;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
|
|
- delete fsTree;
|
|
|
|
- fsTree = nullptr;
|
|
|
|
|
|
+int PictureModel::PictureModelPrivate::itemCount() {
|
|
|
|
+ return fsTree ? fsTree->fileCount() : collectionSize;
|
|
};
|
|
};
|
|
|
|
|
|
|
|
+void PictureModel::PictureModelPrivate::cacheIndex(int index)
|
|
|
|
+{
|
|
|
|
+ int hashIndex = ::offsetHash(index);
|
|
|
|
+
|
|
|
|
+ if (artwork.contains(hashIndex)) {
|
|
|
|
+ artwork[hashIndex]->refCount++;
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ QString queryString = "SELECT path, width, height FROM " % ::stripDbHostileCharacters(artPath) % " LIMIT 1 OFFSET " % QString::number(index) % ";";
|
|
|
|
+
|
|
|
|
+ QSqlQuery query(queryString);
|
|
|
|
+
|
|
|
|
+ query.next();
|
|
|
|
+
|
|
|
|
+ ArtPiece *art = new ArtPiece;
|
|
|
|
+ art->path = query.value(0).toString();
|
|
|
|
+ art->size = QSize(query.value(1).toInt(), query.value(2).toInt());
|
|
|
|
+ art->refCount++;
|
|
|
|
+
|
|
|
|
+ artwork[hashIndex] = art;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void PictureModel::PictureModelPrivate::retireCachedIndex(int index)
|
|
|
|
+{
|
|
|
|
+ int hashIndex = ::offsetHash(index);
|
|
|
|
+ artwork[hashIndex]->refCount--;
|
|
|
|
+ if (artwork[hashIndex]->refCount < 1) {
|
|
|
|
+ delete artwork[hashIndex];
|
|
|
|
+ artwork.remove(hashIndex);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
PictureModel::PictureModel(QObject *parent)
|
|
PictureModel::PictureModel(QObject *parent)
|
|
: QAbstractListModel(parent),
|
|
: QAbstractListModel(parent),
|
|
d(new PictureModelPrivate(this)) { /**/ }
|
|
d(new PictureModelPrivate(this)) { /**/ }
|
|
@@ -198,12 +367,14 @@ PictureModel::~PictureModel()
|
|
int PictureModel::rowCount(const QModelIndex &parent) const
|
|
int PictureModel::rowCount(const QModelIndex &parent) const
|
|
{
|
|
{
|
|
Q_UNUSED(parent)
|
|
Q_UNUSED(parent)
|
|
- return d->fsTree->fileCount();
|
|
|
|
|
|
+ return d->itemCount();
|
|
}
|
|
}
|
|
|
|
|
|
QVariant PictureModel::data(const QModelIndex &index, int role) const
|
|
QVariant PictureModel::data(const QModelIndex &index, int role) const
|
|
{
|
|
{
|
|
- if (index.row() < 0 || index.row() >= d->fsTree->fileCount()) {
|
|
|
|
|
|
+ // What the fuck; Qt queries item 0 before we substantiate it
|
|
|
|
+ // I get to offset my hash by 1 or loss a piece of art
|
|
|
|
+ if (index.row() <= 0 || index.row() >= d->itemCount()) {
|
|
switch (role) {
|
|
switch (role) {
|
|
case SizeRole:
|
|
case SizeRole:
|
|
return QSize(1222,900);
|
|
return QSize(1222,900);
|
|
@@ -215,20 +386,51 @@ QVariant PictureModel::data(const QModelIndex &index, int role) const
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
-
|
|
|
|
- switch (role) {
|
|
|
|
- case SizeRole:
|
|
|
|
- return d->fsTree->files.at(index.row())->size;
|
|
|
|
- case NameRole:
|
|
|
|
- return d->fsTree->files.at(index.row())->name;
|
|
|
|
- case PathRole:
|
|
|
|
- default:
|
|
|
|
- return QUrl::fromLocalFile(FSNode::qualifyNode(d->fsTree->files.at(index.row())));
|
|
|
|
|
|
+ if (d->fsTree) {
|
|
|
|
+ switch (role) {
|
|
|
|
+ case SizeRole:
|
|
|
|
+ return d->fsTree->files.at(index.row())->size;
|
|
|
|
+ case NameRole:
|
|
|
|
+ return d->fsTree->files.at(index.row())->name;
|
|
|
|
+ case PathRole:
|
|
|
|
+ default:
|
|
|
|
+ return QUrl::fromLocalFile(FSNode::qualifyNode(d->fsTree->files.at(index.row())));
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ int hashIndex = ::offsetHash(index.row());
|
|
|
|
+ switch (role) {
|
|
|
|
+ case SizeRole: {
|
|
|
|
+ return d->artwork[hashIndex]->size;
|
|
|
|
+ }
|
|
|
|
+ case NameRole:
|
|
|
|
+ return d->artwork[hashIndex]->path;
|
|
|
|
+ case PathRole:
|
|
|
|
+ default:
|
|
|
|
+ return QUrl::fromLocalFile(d->artwork[hashIndex]->path);
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
return QVariant();
|
|
return QVariant();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+int PictureModel::requestIndex()
|
|
|
|
+{
|
|
|
|
+ int index = d->itemCount() == 0 ? 0 : qrand() % d->itemCount();
|
|
|
|
+
|
|
|
|
+ if (!d->fsTree) {
|
|
|
|
+ d->cacheIndex(index);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ return index;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+void PictureModel::retireIndex(int index)
|
|
|
|
+{
|
|
|
|
+ if (!d->fsTree) {
|
|
|
|
+ d->retireCachedIndex(index);
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
QHash<int, QByteArray> PictureModel::roleNames() const
|
|
QHash<int, QByteArray> PictureModel::roleNames() const
|
|
{
|
|
{
|
|
QHash<int, QByteArray> roles;
|
|
QHash<int, QByteArray> roles;
|