plugin: create caching image manager

No need for external proc
This commit is contained in:
2 * r + 2 * t 2025-08-27 20:32:51 +10:00
parent 8311eed7a6
commit 4f60c07e05
7 changed files with 221 additions and 45 deletions

View file

@ -148,7 +148,6 @@ Item {
anchors.top: parent.top
anchors.topMargin: Appearance.padding.normal
asynchronous: true
implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2
source: {
if (!item.fileIsDir)

View file

@ -1,31 +1,42 @@
pragma ComponentBehavior: Bound
import qs.utils
import Quickshell.Widgets
import QtQuick
Item {
property alias asynchronous: image.asynchronous
property alias status: image.status
property alias mipmap: image.mipmap
property alias backer: image
id: root
property real implicitSize
readonly property int status: loader.item?.status ?? Image.Null
readonly property real actualSize: Math.min(width, height)
property real implicitSize
property url source
onSourceChanged: {
if (source?.toString().startsWith("image://icon/"))
// Directly skip the path prop and treat like a normal Image component
image.source = source;
else if (source)
image.path = source;
}
implicitWidth: implicitSize
implicitHeight: implicitSize
CachingImage {
id: image
Loader {
id: loader
anchors.fill: parent
fillMode: Image.PreserveAspectFit
sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null
}
Component {
id: cachingImage
CachingImage {
path: Paths.strip(root.source)
fillMode: Image.PreserveAspectFit
}
}
Component {
id: iconImage
IconImage {
source: root.source
asynchronous: true
}
}
}

View file

@ -1,44 +1,29 @@
import qs.utils
import Caelestia
import Quickshell
import Quickshell.Io
import QtQuick
Image {
id: root
property string path
property string hash
readonly property url cachePath: `${Paths.imagecache}/${hash}@${effectiveWidth}x${effectiveHeight}.png`
property alias path: manager.path
readonly property real effectiveScale: QsWindow.window?.devicePixelRatio ?? 1
readonly property int effectiveWidth: Math.ceil(width * effectiveScale)
readonly property int effectiveHeight: Math.ceil(height * effectiveScale)
property int sourceWidth
property int sourceHeight
asynchronous: true
fillMode: Image.PreserveAspectCrop
sourceSize.width: effectiveWidth
sourceSize.height: effectiveHeight
onPathChanged: shaProc.exec(["sha256sum", Paths.strip(path)])
onCachePathChanged: {
if (hash)
source = cachePath;
}
sourceSize.width: sourceWidth
sourceSize.height: sourceHeight
onStatusChanged: {
if (source == cachePath && status === Image.Error)
source = path;
else if (source == path && status === Image.Ready)
CUtils.saveItem(this, cachePath);
if (!manager.usingCache && status === Image.Ready)
CUtils.saveItem(this, manager.cachePath);
}
Process {
id: shaProc
CachingImageManager {
id: manager
stdout: StdioCollector {
onStreamFinished: root.hash = text.split(" ")[0]
}
item: root
cacheDir: Paths.imagecache
}
}

View file

@ -3,6 +3,7 @@ qt_add_qml_module(caelestia
VERSION 0.1
SOURCES
cutils.hpp cutils.cpp
cachingimagemanager.hpp cachingimagemanager.cpp
)
qt_query_qml_module(caelestia

View file

@ -0,0 +1,122 @@
#include "cachingimagemanager.hpp"
#include <qobject.h>
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickWindow>
#include <QCryptographicHash>
#include <QThreadPool>
#include <QFile>
qreal CachingImageManager::effectiveScale() const {
if (m_item->window() && m_item->window()->screen()) {
return m_item->window()->screen()->devicePixelRatio();
}
return 1.0;
}
int CachingImageManager::effectiveWidth() const {
int width = std::ceil(m_item->width() * effectiveScale());
m_item->setProperty("sourceWidth", width);
return width;
}
int CachingImageManager::effectiveHeight() const {
int height = std::ceil(m_item->height() * effectiveScale());
m_item->setProperty("sourceHeight", height);
return height;
}
QQuickItem* CachingImageManager::item() const {
return m_item;
}
void CachingImageManager::setItem(QQuickItem* item) {
if (m_item == item) {
return;
}
m_item = item;
emit itemChanged();
}
QUrl CachingImageManager::cacheDir() const {
return m_cacheDir;
}
void CachingImageManager::setCacheDir(const QUrl& cacheDir) {
if (m_cacheDir == cacheDir) {
return;
}
m_cacheDir = cacheDir;
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()) {
QThreadPool::globalInstance()->start([path, this] {
const QString filename = QString("%1@%2x%3.png")
.arg(sha256sum(path))
.arg(effectiveWidth())
.arg(effectiveHeight());
m_cachePath = m_cacheDir.resolved(QUrl(filename));
emit cachePathChanged();
if (!m_cachePath.isLocalFile()) {
qWarning() << "CachingImageManager::setPath: cachePath" << m_cachePath << "is not a local file";
return;
}
if (QFile::exists(m_cachePath.toLocalFile())) {
QMetaObject::invokeMethod(m_item, [this]() {
m_item->setProperty("source", m_cachePath);
}, Qt::QueuedConnection);
m_usingCache = true;
emit usingCacheChanged();
} else {
QMetaObject::invokeMethod(m_item, [path, this]() {
m_item->setProperty("source", QUrl::fromLocalFile(path));
}, Qt::QueuedConnection);
m_usingCache = false;
emit usingCacheChanged();
}
});
}
}
QUrl CachingImageManager::cachePath() const {
return m_cachePath;
}
bool CachingImageManager::usingCache() const {
return m_usingCache;
}
QString CachingImageManager::sha256sum(const QString& path) const {
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "CachingImageManager::sha256sum: failed to open" << path;
return "";
}
QCryptographicHash hash(QCryptographicHash::Sha256);
hash.addData(&file);
file.close();
return hash.result().toHex();
}

View file

@ -0,0 +1,57 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <QtQuick/QQuickItem>
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);
Q_PROPERTY(bool usingCache READ usingCache NOTIFY usingCacheChanged);
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;
[[nodiscard]] bool usingCache() const;
signals:
void itemChanged();
void cacheDirChanged();
void pathChanged();
void cachePathChanged();
void usingCacheChanged();
private slots:
void handleStatusChanged();
private:
QQuickItem* m_item;
QUrl m_cacheDir;
QString m_path;
QUrl m_cachePath;
bool m_usingCache;
[[nodiscard]] qreal effectiveScale() const;
int effectiveWidth() const;
int effectiveHeight() const;
[[nodiscard]] QString sha256sum(const QString& path) const;
};

View file

@ -1,11 +1,12 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <QtQuick/QQuickItem>
class CUtils : public QObject {
Q_OBJECT;
QML_NAMED_ELEMENT(CUtils);
QML_ELEMENT;
QML_SINGLETON;
public: