...
 
Commits (6)
......@@ -79,6 +79,7 @@ WizardDialog {
Importer {
id: importer
property var importFolder
property bool loading: false
property bool importTags: false
property bool recursive: true // TODO have a setting for this
property int lastRollId: -1
......@@ -94,7 +95,11 @@ WizardDialog {
function updateFiles() {
clear();
addItems(Utils.findFiles(importFolder, recursive), [], importTags)
loading = true
Utils.findFiles(importFolder, recursive, function(files) {
addItems(files, [], importTags)
loading = false
})
}
}
......
......@@ -11,13 +11,15 @@ ColumnLayout {
property var importer: null
property var tagModel: null
enabled: !importer.loading
GroupBox {
id: toolBar
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.minimumWidth: 400
title: qsTr("Import from:")
Column {
ColumnLayout {
anchors.fill: parent
spacing: 10
......@@ -35,6 +37,13 @@ ColumnLayout {
onClicked: importer.importFolder = "file://" + model.path
}
}
BusyIndicator {
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
running: importer.loading
visible: running
}
}
}
......
......@@ -38,6 +38,15 @@ ColumnLayout {
}
}
Label {
Layout.fillWidth: true
text: qsTr("%n file(s) are not supported and therefore are not going to be imported!",
"", importer.unsupportedCount)
font.bold: true
wrapMode: Text.Wrap
visible: importer.unsupportedCount > 0
}
TagInput {
id: tagInput
Layout.fillWidth: true
......
......@@ -29,6 +29,8 @@
#include <QFileInfo>
#include <QFutureWatcher>
#include <QMetaObject>
#include <QMimeDatabase>
#include <QMimeType>
#include <QtConcurrent>
#include <climits>
......@@ -97,7 +99,8 @@ public:
int modelIndexToIndex(int index) const;
void setStatus(int index, Importer::ItemStatus status);
inline bool mustClear(Importer::ItemStatus status);
QList<QUrl> filterItems(const QList<QUrl> &items) const;
bool fileIsSupported(const QString &fileName) const;
QList<QUrl> filterItems(const QList<QUrl> &items);
private Q_SLOTS:
void doSetStatus(int index, Importer::ItemStatus status);
......@@ -117,10 +120,12 @@ private:
int m_importedCount;
int m_failedCount;
int m_ignoredCount;
int m_unsupportedCount;
Importer::ClearStatuses m_autoClear;
Importer::RawPolicy m_rawPolicy;
QFuture<void> m_execFuture;
QFutureWatcher<void> m_watcher;
QMimeDatabase m_mimeDatabase;
mutable Importer *q_ptr;
};
......@@ -142,6 +147,7 @@ ImporterPrivate::ImporterPrivate(Importer *q):
m_importedCount(0),
m_failedCount(0),
m_ignoredCount(0),
m_unsupportedCount(0),
m_autoClear(0),
m_rawPolicy(Importer::EditedIsMaster),
q_ptr(q)
......@@ -431,23 +437,34 @@ bool ImporterPrivate::mustClear(Importer::ItemStatus status)
}
}
QList<QUrl> ImporterPrivate::filterItems(const QList<QUrl> &items) const
bool ImporterPrivate::fileIsSupported(const QString &fileName) const
{
/* We let through the most common extensions; for the rest, we use
* QMimeDatabase and accept only image types. */
int index = fileName.lastIndexOf('.');
if (index > 0) {
QByteArray extension = fileName.mid(index + 1).toUtf8().toLower();
if (extension == "jpg" ||
extension == "jpeg" ||
extension == "png") {
return true;
}
}
QMimeType mimeType = m_mimeDatabase.mimeTypeForFile(fileName);
return mimeType.name().startsWith("image/");
}
QList<QUrl> ImporterPrivate::filterItems(const QList<QUrl> &items)
{
/* This function will take the list and remove all the items
* that we don't want to import. */
QList<QUrl> ret;
for (const QUrl &url: items) {
QFileInfo fileInfo(url.toLocalFile());
QString extension = fileInfo.suffix().toLower();
if (extension == "xmp" || // metadata sidecar
/* Other files often found in local image galleries */
extension == "txt" ||
extension == "html") {
continue;
if (fileIsSupported(url.toLocalFile())) {
ret.append(url);
} else {
m_unsupportedCount++;
}
ret.append(url);
}
return ret;
}
......@@ -591,6 +608,12 @@ int Importer::ignoredCount() const
return d->m_ignoredCount;
}
int Importer::unsupportedCount() const
{
Q_D(const Importer);
return d->m_unsupportedCount;
}
void Importer::setAutoClear(ClearStatuses statuses)
{
Q_D(Importer);
......@@ -636,6 +659,8 @@ void Importer::clear()
Q_EMIT failedCountChanged();
d->m_ignoredCount = 0;
Q_EMIT ignoredCountChanged();
d->m_unsupportedCount = 0;
Q_EMIT unsupportedCountChanged();
beginResetModel();
d->m_lastRollId = 0;
......@@ -663,6 +688,7 @@ void Importer::addItems(const QList<QUrl> &items, const QStringList &tagNames,
d->m_items.append(ImporterPrivate::Item(pair, data, tagNames, importTags));
}
endInsertRows();
Q_EMIT unsupportedCountChanged();
}
#define RETURN_IF_ROW_INVALID(row) \
......
......@@ -54,6 +54,8 @@ class Importer: public QAbstractListModel
NOTIFY failedCountChanged)
Q_PROPERTY(int ignoredCount READ ignoredCount \
NOTIFY ignoredCountChanged)
Q_PROPERTY(int unsupportedCount READ unsupportedCount \
NOTIFY unsupportedCountChanged)
Q_PROPERTY(ClearStatuses autoClear READ autoClear WRITE setAutoClear \
NOTIFY autoClearChanged)
Q_PROPERTY(RawPolicy rawPolicy READ rawPolicy WRITE setRawPolicy \
......@@ -125,6 +127,7 @@ public:
int importedCount() const;
int failedCount() const;
int ignoredCount() const;
int unsupportedCount() const;
void setAutoClear(ClearStatuses statuses);
ClearStatuses autoClear() const;
......@@ -163,6 +166,7 @@ Q_SIGNALS:
void importedCountChanged();
void failedCountChanged();
void ignoredCountChanged();
void unsupportedCountChanged();
void autoClearChanged();
void rawPolicyChanged();
void finished(int rollId);
......
......@@ -25,9 +25,12 @@
#include <QDebug>
#include <QDir>
#include <QFileInfo>
#include <QFutureWatcher>
#include <QJSEngine>
#include <QJSValue>
#include <QRegularExpression>
#include <QSequentialIterable>
#include <QtConcurrent>
using namespace Imaginario;
......@@ -217,6 +220,22 @@ QList<QUrl> Utils::findFiles(const QUrl &dirUrl, bool recursive) const
return files;
}
void Utils::findFiles(const QUrl &dirUrl, bool recursive,
const QJSValue &callback)
{
auto *watcher = new QFutureWatcher<QList<QUrl>>(this);
QObject::connect(watcher, &QFutureWatcher<QList<QUrl>>::finished,
this, [this,watcher,callback]() {
QList<QUrl> files = watcher->result();
QJSValue cbCopy(callback); // needed as callback is captured as const
QJSEngine *engine = qjsEngine(this);
cbCopy.call(QJSValueList { engine->toScriptValue(files) });
watcher->deleteLater();
});
watcher->setFuture(QtConcurrent::run(this, &Utils::findFiles,
dirUrl, recursive));
}
template QList<int> parseList(const QVariant &variant, bool *ok);
template QList<Tag> parseList(const QVariant &variant, bool *ok);
template QList<QList<Tag>> parseList(const QVariant &variant, bool *ok);
......
......@@ -51,6 +51,8 @@ public:
Q_INVOKABLE QString fileName(const QUrl &url) const;
Q_INVOKABLE QList<QUrl> findFiles(const QUrl &dirUrl,
bool recursive) const;
Q_INVOKABLE void findFiles(const QUrl &dirUrl, bool recursive,
const QJSValue &callback);
Q_INVOKABLE QMimeType invalidMimeType() const { return QMimeType(); }
};
......
......@@ -5,6 +5,7 @@ CONFIG += \
no_keywords
QT += \
concurrent \
positioning \
testlib
......
......@@ -107,6 +107,8 @@ void ImporterTest::testProperties()
QCOMPARE(importer.property("running").toBool(), false);
QCOMPARE(importer.property("unsupportedCount").toInt(), 0);
QCOMPARE(importer.property("embed").toBool(), false);
importer.setProperty("embed", true);
QCOMPARE(importer.property("embed").toBool(), true);
......@@ -172,9 +174,6 @@ void ImporterTest::testRoles_data()
{
QTest::addColumn<QUrl>("url");
QTest::newRow("invalid URL") <<
QUrl();
QTest::newRow("local URL") <<
QUrl("file:///tmp/foo.png");
}
......@@ -215,10 +214,6 @@ void ImporterTest::testCopy_data()
QTest::newRow("with missing file") <<
(QStringList() << "image1.jpg" << "imageNaN.png") <<
(QStringList() << "imageNaN.png");
QTest::newRow("with non local file") <<
(QStringList() << "image1.jpg" << "http://myhost.com/imageNaN.png") <<
(QStringList() << "http://myhost.com/imageNaN.png");
}
void ImporterTest::testCopy()
......@@ -856,10 +851,12 @@ void ImporterTest::testAutoClear_data()
(QStringList() << "image0.png");
QTest::newRow("index mapping") <<
(QStringList() << "m1" << "m2" << "m3" << "m4" << "m5" << "m6") <<
(QStringList() << "m3" << "m5") <<
QStringList { "m1.jpg", "m2.jpg", "m3.jpg", "m4.jpg", "m5.jpg",
"m6.jpg"
} <<
QStringList { "m3.jpg", "m5.jpg" } <<
Importer::ClearStatuses(Importer::ClearIgnored) <<
(QStringList() << "m1" << "m2" << "m4" << "m6");
QStringList{ "m1.jpg", "m2.jpg", "m4.jpg", "m6.jpg" };
}
void ImporterTest::testAutoClear()
......@@ -921,7 +918,7 @@ void ImporterTest::testItemAdding()
SIGNAL(rowsInserted(const QModelIndex&,int,int)));
QList<QUrl> items;
items << QUrl("file:///a");
items << QUrl("file:///a.jpg");
importer.addItems(items, QStringList());
QCOMPARE(rowsInserted.count(), 1);
......@@ -930,7 +927,7 @@ void ImporterTest::testItemAdding()
rowsInserted.clear();
items.clear();
items << QUrl("file:///b") << QUrl("file:///c");
items << QUrl("file:///b.jpg") << QUrl("file:///c.jpg");
importer.addItems(items, QStringList());
QCOMPARE(rowsInserted.count(), 1);
......@@ -1195,6 +1192,16 @@ void ImporterTest::testFiltering_data()
"image0.png", "image1.jpg", "image2.jpg", "image3.png",
"image4.jpg",
};
QTest::newRow("mix, with raw files") <<
QStringList {
"image0.png", "image1.jpg", "image3.xmp", "image2.arw",
"image3.nef", "doc.txt", "image4.cr2", "im3.dcr",
} <<
QStringList {
"image0.png", "image1.jpg", "image2.arw", "image3.nef",
"image4.cr2", "im3.dcr",
};
}
void ImporterTest::testFiltering()
......@@ -1221,6 +1228,9 @@ void ImporterTest::testFiltering()
filteredNames.insert(fileInfo.fileName());
}
QCOMPARE(filteredNames, expectedNames.toSet());
int expectedUnsupportedCount = fileNames.count() - expectedNames.count();
QCOMPARE(importer.unsupportedCount(), expectedUnsupportedCount);
}
void ImporterTest::testRollCreation_data()
......
......@@ -19,6 +19,10 @@
#include "utils.h"
#include <QQmlComponent>
#include <QQmlEngine>
#include <QScopedPointer>
#include <QSignalSpy>
#include <QTemporaryDir>
#include <QTest>
......@@ -34,6 +38,7 @@ public:
private Q_SLOTS:
void testFindFiles_data();
void testFindFiles();
void testFindFilesAsync();
void testParseListTag_data();
void testParseListTag();
......@@ -95,6 +100,65 @@ void UtilsTest::testFindFiles()
QCOMPARE(files, expectedFiles);
}
void UtilsTest::testFindFilesAsync()
{
QTemporaryDir tmpDir;
QDir dest(tmpDir.path());
QString fileName = m_dataDir.filePath("image0.png");
QFile::copy(fileName, dest.filePath("a.jpg"));
QFile::copy(fileName, dest.filePath("b.jpg"));
QFile::copy(fileName, dest.filePath("c.jpg"));
QFile::copy(fileName, dest.filePath("z.jpg"));
dest.mkdir("subdir");
QFile::copy(fileName, dest.filePath("subdir/1.jpg"));
QFile::copy(fileName, dest.filePath("subdir/2.jpg"));
QFile::copy(fileName, dest.filePath("subdir/3.jpg"));
QStringList expectedFiles {
"a.jpg", "b.jpg", "c.jpg",
"subdir/1.jpg",
"subdir/2.jpg",
"subdir/3.jpg",
"z.jpg",
};
QQmlEngine engine;
qmlRegisterType<Utils>("MyTest", 1, 0, "Utils");
QQmlComponent component(&engine);
component.setData(R"(
import MyTest 1.0
Utils {
id: root
signal done(var files)
function run(folder) {
findFiles(folder, true, function(files) {
root.done(files)
})
}
})",
QUrl());
QScopedPointer<QObject> object(component.create());
QVERIFY(object != 0);
QSignalSpy done(object.data(), SIGNAL(done(QVariant)));
QVariant folder(QUrl::fromLocalFile(tmpDir.path()));
bool ok = QMetaObject::invokeMethod(object.data(), "run",
Q_ARG(QVariant, folder));
QVERIFY(ok);
QTRY_COMPARE(done.count(), 1);
QStringList files;
QList<QUrl> urlList = done.at(0).at(0).value<QList<QUrl>>();
for (const QUrl &url: urlList) {
files.append(dest.relativeFilePath(url.toLocalFile()));
}
QCOMPARE(files, expectedFiles);
QTest::qWait(10); // Wait for QFuture destruction
}
void UtilsTest::testParseListTag_data()
{
typedef QList<int> TagList;
......