Merge pull request #1431 from caelestia-dots/feat/cache-image-provider

Use QQuickAsyncImageProvider for image caching
This commit is contained in:
2 * r + 2 * t 2026-04-30 02:08:16 +10:00 committed by GitHub
commit a2b7ce6d40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 466 additions and 323 deletions

View file

@ -1,28 +1,17 @@
import QtQuick
import Quickshell
import Caelestia.Internal
import qs.utils
import Caelestia.Images
Image {
id: root
property alias path: manager.path
property string path
asynchronous: true
fillMode: Image.PreserveAspectCrop
Connections {
function onDevicePixelRatioChanged(): void {
manager.updateSource();
}
target: QsWindow.window
}
CachingImageManager {
id: manager
item: root
cacheDir: Qt.resolvedUrl(Paths.imagecache)
source: IUtils.urlForPath(path, fillMode)
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
}

View file

@ -184,8 +184,10 @@ Item {
source: Players.getArtUrl(Players.active)
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
MouseArea {
anchors.fill: parent

View file

@ -1,5 +1,6 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
import Caelestia.Config
import Caelestia.Services
import qs.components
@ -109,8 +110,10 @@ Item {
source: Players.getArtUrl(Players.active)
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
}
}

View file

@ -1,4 +1,5 @@
import QtQuick
import Quickshell
import Caelestia.Config
import Caelestia.Models
import qs.components
@ -63,11 +64,13 @@ Item {
}
CachingImage {
anchors.fill: parent
path: root.modelData.path
smooth: !root.PathView.view.moving
cache: true
anchors.fill: parent
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr);
}
}
}

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Caelestia.Config
import qs.components
import qs.components.effects
@ -22,8 +23,10 @@ Item {
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: width
sourceSize.height: height
sourceSize: {
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
return Qt.size(width * dpr, height * dpr);
}
layer.enabled: true
layer.effect: OpacityMask {

View file

@ -52,7 +52,7 @@ ColumnLayout {
asynchronous: true
source: Paths.absolutePath(Config.paths.lockNoNotifsPic)
fillMode: Image.PreserveAspectFit
sourceSize.width: clipRect.width * 0.8
sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
layer.enabled: true
layer.effect: Colouriser {

View file

@ -73,8 +73,10 @@ StyledRect {
Image {
source: Qt.resolvedUrl(root.image)
fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image
sourceSize.height: TokenConfig.sizes.notifs.image
sourceSize: {
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false
asynchronous: true
width: TokenConfig.sizes.notifs.image

View file

@ -126,8 +126,10 @@ StyledRect {
anchors.fill: parent
source: Qt.resolvedUrl(root.modelData.image)
fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image
sourceSize.height: TokenConfig.sizes.notifs.image
sourceSize: {
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false
asynchronous: true
}

View file

@ -48,7 +48,7 @@ Column {
AnimatedImage {
width: Tokens.sizes.session.button
height: Tokens.sizes.session.button
sourceSize.width: width
sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
playing: visible
asynchronous: true

View file

@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Caelestia.Config
import qs.components
@ -98,7 +99,7 @@ Item {
asynchronous: true
source: Paths.absolutePath(Config.paths.noNotifsPic)
fillMode: Image.PreserveAspectFit
sourceSize.width: clipRect.width * 0.8
sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
layer.enabled: true
layer.effect: Colouriser {

View file

@ -87,8 +87,10 @@ StyledRect {
Image {
source: Qt.resolvedUrl(root.image)
fillMode: Image.PreserveAspectCrop
sourceSize.width: TokenConfig.sizes.notifs.image
sourceSize.height: TokenConfig.sizes.notifs.image
sourceSize: {
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
return Qt.size(size, size);
}
cache: false
asynchronous: true
width: TokenConfig.sizes.notifs.image

View file

@ -79,3 +79,4 @@ add_subdirectory(Internal)
add_subdirectory(Models)
add_subdirectory(Services)
add_subdirectory(Blobs)
add_subdirectory(Images)

View file

@ -0,0 +1,11 @@
qml_module(caelestia-images
URI Caelestia.Images
SOURCES
cachingimageprovider.cpp
imagecacher.cpp
iutils.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
)

View file

@ -0,0 +1,120 @@
#include "cachingimageprovider.hpp"
#include "imagecacher.hpp"
#include <qfileinfo.h>
#include <qimage.h>
#include <qimagereader.h>
#include <qloggingcategory.h>
#include <qrunnable.h>
#include <qthreadpool.h>
Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg)
namespace caelestia::images {
namespace {
class CachingImageResponse final : public QQuickImageResponse, public QRunnable {
public:
CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::FillMode fillMode)
: m_id(id)
, m_requestedSize(requestedSize)
, m_fillMode(fillMode) {
setAutoDelete(false);
}
[[nodiscard]] QQuickTextureFactory* textureFactory() const override {
return QQuickTextureFactory::textureFactoryForImage(m_image);
}
[[nodiscard]] QString errorString() const override { return m_error; }
void run() override {
process();
emit finished();
}
private:
void process() {
QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded());
if (!path.startsWith(QLatin1Char('/')))
path.prepend(QLatin1Char('/'));
if (!QFileInfo::exists(path)) {
m_error = QStringLiteral("Source file does not exist: ") + path;
qCWarning(lcCProv).noquote() << m_error;
return;
}
QSize size = m_requestedSize;
const bool needsW = size.width() <= 0;
const bool needsH = size.height() <= 0;
// If both dimensions are missing, return the original directly
if (needsW && needsH) {
qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path;
m_image = QImage(path);
if (m_image.isNull()) {
m_error = QStringLiteral("Failed to decode source: ") + path;
qCWarning(lcCProv).noquote() << m_error;
}
return;
}
// If one dimension is missing, derive it from the source aspect ratio
if (needsW || needsH) {
const QImageReader sourceReader(path);
const QSize sourceSize = sourceReader.size();
if (!sourceSize.isValid() || sourceSize.isEmpty()) {
m_error = QStringLiteral("Could not determine source size for: ") + path;
qCWarning(lcCProv).noquote() << m_error;
return;
}
if (needsW)
size.setWidth(qRound(size.height() * sourceSize.width() / static_cast<qreal>(sourceSize.height())));
else
size.setHeight(qRound(size.width() * sourceSize.height() / static_cast<qreal>(sourceSize.width())));
}
// Try to use cached image
const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode);
if (!cachePath.isEmpty()) {
QImageReader cacheReader(cachePath);
if (cacheReader.canRead()) {
m_image = cacheReader.read();
if (!m_image.isNull())
return;
}
}
// Schedule cache job (this call will return the original image, but later ones will use cache)
ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode);
m_image = QImage(path);
if (m_image.isNull()) {
m_error = QStringLiteral("Failed to decode source: ") + path;
qCWarning(lcCProv).noquote() << m_error;
}
}
QString m_id;
QSize m_requestedSize;
ImageCacher::FillMode m_fillMode;
QImage m_image;
QString m_error;
};
} // namespace
CachingImageProvider::CachingImageProvider(FillMode fillMode)
: m_fillMode(fillMode) {}
QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) {
auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode);
QThreadPool::globalInstance()->start(response);
return response;
}
} // namespace caelestia::images

View file

@ -0,0 +1,21 @@
#pragma once
#include "imagecacher.hpp"
#include <qquickimageprovider.h>
namespace caelestia::images {
class CachingImageProvider : public QQuickAsyncImageProvider {
public:
using FillMode = ImageCacher::FillMode;
explicit CachingImageProvider(FillMode fillMode);
QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override;
private:
FillMode m_fillMode;
};
} // namespace caelestia::images

View file

@ -0,0 +1,159 @@
#include "imagecacher.hpp"
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfile.h>
#include <qfileinfo.h>
#include <qimage.h>
#include <qloggingcategory.h>
#include <qmutex.h>
#include <qpainter.h>
#include <qsavefile.h>
#include <qthreadpool.h>
Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg)
namespace caelestia::images {
namespace {
QString sha256sum(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path;
return {};
}
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file);
file.close();
return hash.result().toHex();
}
QString fillSuffix(ImageCacher::FillMode fillMode) {
switch (fillMode) {
case ImageCacher::FillMode::Crop:
return QStringLiteral("crop");
case ImageCacher::FillMode::Fit:
return QStringLiteral("fit");
default:
return QStringLiteral("stretch");
}
}
} // namespace
const QString& ImageCacher::cacheDir() {
static const QString s_dir = [] {
QString cache = qEnvironmentVariable("XDG_CACHE_HOME");
if (cache.isEmpty())
cache = QDir::homePath() + QStringLiteral("/.cache");
return cache + QStringLiteral("/caelestia/imagecache");
}();
return s_dir;
}
QString ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) {
const QString sha = sha256sum(sourcePath);
if (sha.isEmpty())
return {};
const QString filename =
QStringLiteral("%1@%2x%3-%4.png")
.arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode));
return cacheDir() + QLatin1Char('/') + filename;
}
ImageCacher* ImageCacher::instance() {
static ImageCacher s_instance;
return &s_instance;
}
ImageCacher::ImageCacher(QObject* parent)
: QObject(parent) {}
void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) {
schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode);
}
void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) {
if (cachePath.isEmpty())
return;
{
QMutexLocker locker(&m_mutex);
if (m_inflight.contains(cachePath))
return;
m_inflight.insert(cachePath);
}
QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() {
runJob(sourcePath, cachePath, size, fillMode);
QMutexLocker locker(&m_mutex);
m_inflight.remove(cachePath);
});
}
void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) {
if (QFile::exists(cachePath)) {
return;
}
QImage image(sourcePath);
if (image.isNull()) {
qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath;
return;
}
Qt::AspectRatioMode scaleMode;
switch (fillMode) {
case FillMode::Crop:
scaleMode = Qt::KeepAspectRatioByExpanding;
break;
case FillMode::Fit:
scaleMode = Qt::KeepAspectRatio;
break;
case FillMode::Stretch:
scaleMode = Qt::IgnoreAspectRatio;
break;
}
image.convertTo(QImage::Format_ARGB32);
image = image.scaled(size, scaleMode, Qt::SmoothTransformation);
if (image.isNull()) {
qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath;
return;
}
QImage canvas;
if (fillMode == FillMode::Stretch) {
canvas = image;
} else {
canvas = QImage(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QPainter painter(&canvas);
painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image);
painter.end();
}
const QString parent = QFileInfo(cachePath).absolutePath();
if (!QDir().mkpath(parent)) {
qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent;
return;
}
QSaveFile saveFile(cachePath);
if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) {
qCWarning(
lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString()));
return;
}
qCDebug(lcCacher).noquote() << "Saved to" << cachePath;
}
} // namespace caelestia::images

View file

@ -0,0 +1,38 @@
#pragma once
#include <qmutex.h>
#include <qobject.h>
#include <qset.h>
#include <qsize.h>
#include <qstring.h>
namespace caelestia::images {
class ImageCacher : public QObject {
Q_OBJECT
public:
enum class FillMode {
Crop,
Fit,
Stretch,
};
static ImageCacher* instance();
static const QString& cacheDir();
static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode);
void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode);
void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode);
private:
explicit ImageCacher(QObject* parent = nullptr);
static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode);
QMutex m_mutex;
QSet<QString> m_inflight;
};
} // namespace caelestia::images

View file

@ -0,0 +1,42 @@
#include "iutils.hpp"
#include "cachingimageprovider.hpp"
namespace caelestia::images {
IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) {
Q_UNUSED(jsEngine);
engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop));
engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit));
engine->addImageProvider(
QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch));
return new IUtils(engine);
}
QUrl IUtils::urlForPath(const QString& path, int fillMode) {
if (path.isEmpty())
return QUrl();
QString prefix;
switch (fillMode) {
case 1: // Image.PreserveAspectFit
prefix = QStringLiteral("fcache");
break;
case 2: // Image.PreserveAspectCrop
prefix = QStringLiteral("ccache");
break;
default: // Image.Stretch or any other ones
prefix = QStringLiteral("scache");
break;
}
QUrl url;
url.setScheme(QStringLiteral("image"));
url.setHost(prefix);
url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path);
return url;
}
} // namespace caelestia::images

View file

@ -0,0 +1,24 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qurl.h>
namespace caelestia::images {
class IUtils : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine);
Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode);
private:
explicit IUtils(QObject* parent = nullptr)
: QObject(parent) {};
};
} // namespace caelestia::images

View file

@ -1,19 +1,17 @@
qml_module(caelestia-internal
URI Caelestia.Internal
SOURCES
arcgauge.hpp arcgauge.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
circularbuffer.hpp circularbuffer.cpp
circularindicatormanager.hpp circularindicatormanager.cpp
hyprdevices.hpp hyprdevices.cpp
hyprextras.hpp hyprextras.cpp
logindmanager.hpp logindmanager.cpp
sparklineitem.hpp sparklineitem.cpp
visualiserbars.hpp visualiserbars.cpp
arcgauge.cpp
circularbuffer.cpp
circularindicatormanager.cpp
hyprdevices.cpp
hyprextras.cpp
logindmanager.cpp
sparklineitem.cpp
visualiserbars.cpp
LIBRARIES
Qt::Gui
Qt::Quick
Qt::Concurrent
Qt::Network
Qt::DBus
)

View file

@ -1,213 +0,0 @@
#include "cachingimagemanager.hpp"
#include <QtQuick/qquickwindow.h>
#include <qcryptographichash.h>
#include <qdir.h>
#include <qfileinfo.h>
#include <qfuturewatcher.h>
#include <qimagereader.h>
#include <qloggingcategory.h>
#include <qpainter.h>
#include <qtconcurrentrun.h>
Q_LOGGING_CATEGORY(lcCim, "caelestia.internal.cim", QtInfoMsg)
namespace caelestia::internal {
qreal CachingImageManager::effectiveScale() const {
if (m_item && m_item->window()) {
return m_item->window()->devicePixelRatio();
}
return 1.0;
}
QSize CachingImageManager::effectiveSize() const {
if (!m_item) {
return QSize();
}
const qreal scale = effectiveScale();
const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize();
m_item->setProperty("sourceSize", size);
return size;
}
QQuickItem* CachingImageManager::item() const {
return m_item;
}
void CachingImageManager::setItem(QQuickItem* item) {
if (m_item == item) {
return;
}
if (m_widthConn) {
disconnect(m_widthConn);
}
if (m_heightConn) {
disconnect(m_heightConn);
}
m_item = item;
emit itemChanged();
if (item) {
m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() {
updateSource();
});
m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() {
updateSource();
});
updateSource();
}
}
QUrl CachingImageManager::cacheDir() const {
return m_cacheDir;
}
void CachingImageManager::setCacheDir(const QUrl& cacheDir) {
if (m_cacheDir == cacheDir) {
return;
}
m_cacheDir = cacheDir;
if (!m_cacheDir.path().endsWith("/")) {
m_cacheDir.setPath(m_cacheDir.path() + "/");
}
emit cacheDirChanged();
}
QString CachingImageManager::path() const {
return m_path;
}
void CachingImageManager::setPath(const QString& path) {
if (m_path == path) {
return;
}
m_path = path;
emit pathChanged();
if (!path.isEmpty()) {
updateSource(path);
}
}
void CachingImageManager::updateSource() {
updateSource(m_path);
}
void CachingImageManager::updateSource(const QString& path) {
if (path.isEmpty() || path == m_shaPath) {
// Path is empty or already calculating sha for path
return;
}
m_shaPath = path;
QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) {
if (m_path != path) {
return;
}
const QSize size = effectiveSize();
if (!m_item || !size.width() || !size.height()) {
return;
}
const QString fillMode = m_item->property("fillMode").toString();
// clang-format off
const QString filename = QString("%1@%2x%3-%4.png")
.arg(sha).arg(size.width()).arg(size.height())
.arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch");
// clang-format on
const QUrl cache = m_cacheDir.resolved(QUrl(filename));
if (m_cachePath == cache) {
return;
}
m_cachePath = cache;
emit cachePathChanged();
if (!cache.isLocalFile()) {
qCWarning(lcCim) << "updateSource: cachePath" << cache << "is not a local file";
return;
}
const QImageReader reader(cache.toLocalFile());
if (reader.canRead()) {
m_item->setProperty("source", cache);
} else {
m_item->setProperty("source", QUrl::fromLocalFile(path));
createCache(path, cache.toLocalFile(), fillMode, size);
}
// Clear current running sha if same
if (m_shaPath == path) {
m_shaPath = QString();
}
});
}
QUrl CachingImageManager::cachePath() const {
return m_cachePath;
}
void CachingImageManager::createCache(
const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const {
QThreadPool::globalInstance()->start([path, cache, fillMode, size] {
QImage image(path);
if (image.isNull()) {
qCWarning(lcCim) << "createCache: failed to read" << path;
return;
}
image.convertTo(QImage::Format_ARGB32);
if (fillMode == "PreserveAspectCrop") {
image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation);
} else if (fillMode == "PreserveAspectFit") {
image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation);
} else {
image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
}
if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") {
QImage canvas(size, QImage::Format_ARGB32);
canvas.fill(Qt::transparent);
QPainter painter(&canvas);
painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image);
painter.end();
image = canvas;
}
const QString parent = QFileInfo(cache).absolutePath();
if (!QDir().mkpath(parent) || !image.save(cache)) {
qCWarning(lcCim) << "createCache: failed to save to" << cache;
}
});
}
QString CachingImageManager::sha256sum(const QString& path) {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(lcCim) << "sha256sum: failed to open" << path;
return "";
}
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file);
file.close();
return hash.result().toHex();
}
} // namespace caelestia::internal

View file

@ -1,65 +0,0 @@
#pragma once
#include <QtQuick/qquickitem.h>
#include <qobject.h>
#include <qpointer.h>
#include <qqmlintegration.h>
namespace caelestia::internal {
class CachingImageManager : public QObject {
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED)
Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED)
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged)
public:
explicit CachingImageManager(QObject* parent = nullptr)
: QObject(parent) {}
[[nodiscard]] QQuickItem* item() const;
void setItem(QQuickItem* item);
[[nodiscard]] QUrl cacheDir() const;
void setCacheDir(const QUrl& cacheDir);
[[nodiscard]] QString path() const;
void setPath(const QString& path);
[[nodiscard]] QUrl cachePath() const;
Q_INVOKABLE void updateSource();
Q_INVOKABLE void updateSource(const QString& path);
signals:
void itemChanged();
void cacheDirChanged();
void pathChanged();
void cachePathChanged();
void usingCacheChanged();
private:
QString m_shaPath;
QPointer<QQuickItem> m_item;
QUrl m_cacheDir;
QString m_path;
QUrl m_cachePath;
QMetaObject::Connection m_widthConn;
QMetaObject::Connection m_heightConn;
[[nodiscard]] qreal effectiveScale() const;
[[nodiscard]] QSize effectiveSize() const;
void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const;
[[nodiscard]] static QString sha256sum(const QString& path);
};
} // namespace caelestia::internal