Skip to content

Commit

Permalink
Add thumbnails (#22)
Browse files Browse the repository at this point in the history
* Add AsyncImage

* Add CacheThumbnailImageProvider

* Add AbstractCache

* Add and use DiskCache

* Add cacheUpdated signal

* Fix hanging on close

* Fix crash

* cleanup includes
  • Loading branch information
zhulik authored Jul 9, 2023
1 parent 63585f2 commit cba28eb
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 28 deletions.
28 changes: 28 additions & 0 deletions resources/qml/AsyncImage.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import QtQuick 2.15
import QtQuick.Controls 2.15

Item {
id: root

property bool cached: false

property string source

Image {
id: image
anchors.fill: parent

fillMode: Image.PreserveAspectFit
asynchronous: true

sourceSize.width: image.width
sourceSize.height: image.height

source: root.cached ? `image://cache_thumbnail/${root.source}` : root.source
}

BusyIndicator {
anchors.fill: parent
visible: image.status === Image.Loading
}
}
38 changes: 20 additions & 18 deletions resources/qml/DirectoryView/FileDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ ItemDelegate {
anchors.margins: 5
spacing: 5

MDI.Icon {
name: fileIcon
FileIcon {
Layout.fillHeight: parent
Layout.preferredWidth: 50

name: fileIcon
mime: fileMime
path: filePath
}

ColumnLayout {
Expand All @@ -32,23 +36,21 @@ ItemDelegate {

RowLayout {

Label {
text: fileSizeString
Layout.fillWidth: parent
color: Material.accent
font.pointSize: 8
}

Label {
text: fileMime
elide: Text.ElideMiddle
color: Material.accent
Layout.alignment: Qt.AlignVCenter
font.pointSize: 8
}
Label {
text: fileSizeString
Layout.fillWidth: parent
color: Material.accent
font.pointSize: 8
}

Label {
text: fileMime
elide: Text.ElideMiddle
color: Material.accent
Layout.alignment: Qt.AlignVCenter
font.pointSize: 8
}
}
}


}
}
29 changes: 29 additions & 0 deletions resources/qml/DirectoryView/FileIcon.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import QtQuick 2.15

import "../MDI" as MDI
import ".." as Core

Item {
id: root

property string name
property string mime
property string path

readonly property bool isImage: root.mime.startsWith("image/")

MDI.Icon {
id: icon
anchors.fill: parent

name: root.name
visible: !root.isImage
}

Core.AsyncImage {
anchors.fill: parent
source: path
cached: true
visible: root.isImage
}
}
11 changes: 11 additions & 0 deletions src/abstractcache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#include "abstractcache.h"

AbstractCache::AbstractCache(QObject *parent) : QObject{parent} {}

QByteArray AbstractCache::withCache(const QString &key, std::function<QByteArray()> f) {
if (!exists(key)) {
set(key, f());
}

return get(key);
}
19 changes: 19 additions & 0 deletions src/abstractcache.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#pragma once

#include <QObject>
#include <functional>

class AbstractCache : public QObject {
Q_OBJECT
public:
explicit AbstractCache(QObject *parent = nullptr);

virtual QByteArray get(const QString &) const = 0;
virtual void set(const QString &, const QByteArray &) = 0;
virtual bool exists(const QString &) const = 0;

QByteArray withCache(const QString &, std::function<QByteArray()>);

signals:
void cacheUpdated();
};
20 changes: 12 additions & 8 deletions src/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <QtQuickControls2/QQuickStyle>

#include "application.h"
#include "cachethumbnailimageprovider.h"
#include "diskcache.h"
#include "folderlistmodel.h"
#include "fshelpers.h"

Expand All @@ -18,9 +20,10 @@ Application::Application(int &argc, char **argv) : QGuiApplication{argc, argv} {

QFontDatabase::addApplicationFont("resources/fonts/materialdesignicons-webfont.ttf");
QQuickStyle::setStyle("Material");
m_engine = new QQmlApplicationEngine();

m_engine = new QQmlApplicationEngine(this);
m_engine = new QQmlApplicationEngine();
auto cacheThumbnailImageProvider = new CacheThumbnailImageProvider();
cacheThumbnailImageProvider->setCache(new DiskCache(m_engine));

qmlRegisterType<FolderListModel>("DeckFM", 1, 0, "FolderListModel");

Expand All @@ -30,29 +33,28 @@ Application::Application(int &argc, char **argv) : QGuiApplication{argc, argv} {
qmlRegisterSingletonInstance("DeckFM", 1, 0, "FSHelpers", new FSHelpers());

try {
m_steamworks = new QSteamworks::SteamAPI(this);
m_steamworks = new QSteamworks::SteamAPI(m_engine);
} catch (QSteamworks::InitializationFailed &e) {
qDebug() << "\n" << e.what() << "\n";
}

m_engine->rootContext()->setContextProperty("qApp", this);
m_engine->rootContext()->setContextProperty("qmlEngine", m_engine);
m_engine->rootContext()->setContextProperty("steamAPI", m_steamworks);

m_engine->addImageProvider("cache_thumbnail", cacheThumbnailImageProvider);

if (m_steamworks != nullptr) {
auto callbackTimer = new QTimer(this);
auto callbackTimer = new QTimer(m_engine);
connect(callbackTimer, &QTimer::timeout, m_steamworks, &QSteamworks::SteamAPI::runCallbacks);
callbackTimer->start(16);
}

connect(m_engine, &QQmlApplicationEngine::objectCreated, [this](auto obj) {
connect(m_engine, &QQmlApplicationEngine::objectCreated, m_engine, [this](auto obj) {
if (obj == nullptr) {
throw std::runtime_error("Cannot load qml.");
}

auto mainWindow = (QQuickWindow *)m_engine->rootObjects().at(0);
connect(mainWindow, &QQuickWindow::activeFocusItemChanged,
[mainWindow, this]() { m_activeFocusItem = mainWindow->activeFocusItem(); });

if (arguments().count() > 1) {
mainWindow->setProperty("openFile", arguments().at(1));
Expand All @@ -61,3 +63,5 @@ Application::Application(int &argc, char **argv) : QGuiApplication{argc, argv} {

m_engine->load("resources/qml/MainWindow.qml");
}

Application::~Application() { delete m_engine; }
6 changes: 4 additions & 2 deletions src/application.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ namespace QSteamworks {
class SteamAPI;
}

class CacheThumbnailImageProvider;

class Application : public QGuiApplication {

public:
explicit Application(int &argc, char **argv);
virtual ~Application() override;

private:
QQmlApplicationEngine *m_engine;
QQuickItem *m_activeFocusItem = nullptr;
QQmlApplicationEngine *m_engine = nullptr;
QSteamworks::SteamAPI *m_steamworks = nullptr;
};
75 changes: 75 additions & 0 deletions src/cachethumbnailimageprovider.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#include "cachethumbnailimageprovider.h"

#include <QFutureWatcher>
#include <QQuickImageResponse>
#include <QtConcurrent/QtConcurrent>

#include "abstractcache.h"

class AsyncImageResponse : public QQuickImageResponse {
public:
AsyncImageResponse(const QString &id, const QSize &requestedSize, AbstractCache *cache) {
auto watcher = new QFutureWatcher<QImage>();

auto f = QtConcurrent::run([id, requestedSize, this, cache]() {
QImage result;

if (requestedSize.isEmpty()) {
return result;
}

if (cache != nullptr) {
auto data = cache->withCache(imageCacheId(id, requestedSize), [id, requestedSize]() {
QByteArray ba;
QImage image(id); // TODO: add support for network sources
if (image.isNull()) {
return ba;
}
if (!image.isNull()) {
image = image.scaled(requestedSize, Qt::KeepAspectRatio);
}
QBuffer buffer(&ba);
buffer.open(QIODevice::WriteOnly);
Q_ASSERT(image.save(&buffer, "png"));
qDebug() << ba.size();
return ba;
});

result.loadFromData(data);
} else {
QImage image(id); // TODO: add support for network sources
if (!image.isNull()) {
result = image.scaled(requestedSize, Qt::KeepAspectRatio);
}
}
return result;
});

connect(watcher, &QFutureWatcher<QImage>::finished, watcher, [this, f, watcher]() {
m_image = f.result();
emit finished();
watcher->deleteLater();
});

watcher->setFuture(f);
}

QQuickTextureFactory *textureFactory() const override {
return QQuickTextureFactory::textureFactoryForImage(m_image);
}

private:
QImage m_image;

QString imageCacheId(const QString &id, const QSize &size) const {
return QString("%1//%2x%3").arg(id).arg(size.width()).arg(size.height());
}
};

QQuickImageResponse *CacheThumbnailImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize) {
return new AsyncImageResponse(id, requestedSize, m_cache);
}

AbstractCache *CacheThumbnailImageProvider::cache() const { return m_cache; }

void CacheThumbnailImageProvider::setCache(AbstractCache *newCache) { m_cache = newCache; }
17 changes: 17 additions & 0 deletions src/cachethumbnailimageprovider.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include <QQuickAsyncImageProvider>
#include <QThreadPool>

class AbstractCache;

class CacheThumbnailImageProvider : public QQuickAsyncImageProvider {
public:
virtual QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize);

AbstractCache *cache() const;
void setCache(AbstractCache *newCache);

private:
AbstractCache *m_cache = nullptr;
};
63 changes: 63 additions & 0 deletions src/diskcache.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include "diskcache.h"

#include <QCryptographicHash>
#include <QStandardPaths>

DiskCache::DiskCache(QObject *parent) : AbstractCache{parent} {
m_root = QDir(QStandardPaths::standardLocations(QStandardPaths::CacheLocation)[0]);
m_root.mkpath("thumbnails");
m_root.cd("thumbnails");

Q_ASSERT(m_root.exists());
}

QByteArray DiskCache::get(const QString &key) const {
auto info = keyToPath(key);
if (m_root.exists(info.filePath())) {
QFile f(m_root.absoluteFilePath(info.filePath()));
f.open(QIODevice::ReadOnly);
auto data = f.readAll();
f.close();
return data;
} else {
return QByteArray();
}
}

void DiskCache::set(const QString &key, const QByteArray &data) {
auto info = keyToPath(key);
Q_ASSERT(m_root.mkpath(info.path()));

QFile file(m_root.absoluteFilePath(info.filePath()));

Q_ASSERT(file.open(QIODevice::WriteOnly));

file.write(data);
file.close();
emit cacheUpdated();
}

bool DiskCache::exists(const QString &key) const { return m_root.exists(keyToPath(key).filePath()); }

QFileInfo DiskCache::keyToPath(const QString &key) const {
auto parts = key.split("//");
QFileInfo info(parts[0]);
Q_ASSERT(info.exists());

auto size = parts[1].split("x");
Q_ASSERT(size.length() == 2);

QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(info.absoluteFilePath().toLocal8Bit());

auto hashStr = hash.result().toHex();

return QFileInfo(QString("%1/%2/%3/%4_%5x%6.%7")
.arg(hashStr[0])
.arg(hashStr[1])
.arg(hashStr[2])
.arg(QString(hashStr))
.arg(size[0])
.arg(size[1])
.arg(info.suffix()));
}
Loading

0 comments on commit cba28eb

Please sign in to comment.