Merge branch 'main' into feat/m3-revamp
This commit is contained in:
commit
91b09253d8
22 changed files with 459 additions and 315 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import M3Shapes
|
||||
import Caelestia.Config
|
||||
import Caelestia.Services
|
||||
|
|
@ -150,8 +151,11 @@ 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);
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: Mask {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Caelestia.Config
|
||||
import Caelestia.Models
|
||||
import qs.components
|
||||
|
|
@ -64,11 +65,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -51,7 +51,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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -124,8 +124,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
|||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Caelestia.Config
|
||||
import qs.components
|
||||
|
|
@ -96,7 +97,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 {
|
||||
|
|
|
|||
|
|
@ -85,8 +85,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
|
||||
|
|
|
|||
|
|
@ -89,3 +89,4 @@ add_subdirectory(Internal)
|
|||
add_subdirectory(Models)
|
||||
add_subdirectory(Services)
|
||||
add_subdirectory(Blobs)
|
||||
add_subdirectory(Images)
|
||||
|
|
|
|||
11
plugin/src/Caelestia/Images/CMakeLists.txt
Normal file
11
plugin/src/Caelestia/Images/CMakeLists.txt
Normal 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
|
||||
)
|
||||
120
plugin/src/Caelestia/Images/cachingimageprovider.cpp
Normal file
120
plugin/src/Caelestia/Images/cachingimageprovider.cpp
Normal 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
|
||||
21
plugin/src/Caelestia/Images/cachingimageprovider.hpp
Normal file
21
plugin/src/Caelestia/Images/cachingimageprovider.hpp
Normal 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
|
||||
159
plugin/src/Caelestia/Images/imagecacher.cpp
Normal file
159
plugin/src/Caelestia/Images/imagecacher.cpp
Normal 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
|
||||
38
plugin/src/Caelestia/Images/imagecacher.hpp
Normal file
38
plugin/src/Caelestia/Images/imagecacher.hpp
Normal 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
|
||||
42
plugin/src/Caelestia/Images/iutils.cpp
Normal file
42
plugin/src/Caelestia/Images/iutils.cpp
Normal 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
|
||||
24
plugin/src/Caelestia/Images/iutils.hpp
Normal file
24
plugin/src/Caelestia/Images/iutils.hpp
Normal 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
|
||||
|
|
@ -2,7 +2,6 @@ qml_module(caelestia-internal
|
|||
URI Caelestia.Internal
|
||||
SOURCES
|
||||
arcgauge.cpp
|
||||
cachingimagemanager.cpp
|
||||
circularbuffer.cpp
|
||||
circularindicatormanager.cpp
|
||||
linearindicatormanager.cpp
|
||||
|
|
@ -14,7 +13,6 @@ qml_module(caelestia-internal
|
|||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Concurrent
|
||||
Qt::Network
|
||||
Qt::DBus
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue