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 QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import Caelestia.Internal
|
import Caelestia.Images
|
||||||
import qs.utils
|
|
||||||
|
|
||||||
Image {
|
Image {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property alias path: manager.path
|
property string path
|
||||||
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
source: IUtils.urlForPath(path, fillMode)
|
||||||
Connections {
|
sourceSize: {
|
||||||
function onDevicePixelRatioChanged(): void {
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
manager.updateSource();
|
return Qt.size(width * dpr, height * dpr);
|
||||||
}
|
|
||||||
|
|
||||||
target: QsWindow.window
|
|
||||||
}
|
|
||||||
|
|
||||||
CachingImageManager {
|
|
||||||
id: manager
|
|
||||||
|
|
||||||
item: root
|
|
||||||
cacheDir: Qt.resolvedUrl(Paths.imagecache)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -184,8 +184,10 @@ Item {
|
||||||
source: Players.getArtUrl(Players.active)
|
source: Players.getArtUrl(Players.active)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: width
|
sourceSize: {
|
||||||
sourceSize.height: height
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
|
return Qt.size(width * dpr, height * dpr);
|
||||||
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Effects
|
import QtQuick.Effects
|
||||||
|
import Quickshell
|
||||||
import M3Shapes
|
import M3Shapes
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import Caelestia.Services
|
import Caelestia.Services
|
||||||
|
|
@ -150,8 +151,11 @@ Item {
|
||||||
source: Players.getArtUrl(Players.active)
|
source: Players.getArtUrl(Players.active)
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
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.enabled: true
|
||||||
layer.effect: Mask {
|
layer.effect: Mask {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import Caelestia.Models
|
import Caelestia.Models
|
||||||
import qs.components
|
import qs.components
|
||||||
|
|
@ -64,11 +65,13 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
CachingImage {
|
CachingImage {
|
||||||
|
anchors.fill: parent
|
||||||
path: root.modelData.path
|
path: root.modelData.path
|
||||||
smooth: !root.PathView.view.moving
|
smooth: !root.PathView.view.moving
|
||||||
cache: true
|
sourceSize: {
|
||||||
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
anchors.fill: parent
|
return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
import qs.components.effects
|
import qs.components.effects
|
||||||
|
|
@ -22,8 +23,10 @@ Item {
|
||||||
|
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: width
|
sourceSize: {
|
||||||
sourceSize.height: height
|
const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1;
|
||||||
|
return Qt.size(width * dpr, height * dpr);
|
||||||
|
}
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: OpacityMask {
|
layer.effect: OpacityMask {
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ ColumnLayout {
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: Paths.absolutePath(Config.paths.lockNoNotifsPic)
|
source: Paths.absolutePath(Config.paths.lockNoNotifsPic)
|
||||||
fillMode: Image.PreserveAspectFit
|
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.enabled: true
|
||||||
layer.effect: Colouriser {
|
layer.effect: Colouriser {
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,10 @@ StyledRect {
|
||||||
Image {
|
Image {
|
||||||
source: Qt.resolvedUrl(root.image)
|
source: Qt.resolvedUrl(root.image)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: TokenConfig.sizes.notifs.image
|
sourceSize: {
|
||||||
sourceSize.height: TokenConfig.sizes.notifs.image
|
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||||
|
return Qt.size(size, size);
|
||||||
|
}
|
||||||
cache: false
|
cache: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
width: TokenConfig.sizes.notifs.image
|
width: TokenConfig.sizes.notifs.image
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,10 @@ StyledRect {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: Qt.resolvedUrl(root.modelData.image)
|
source: Qt.resolvedUrl(root.modelData.image)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: TokenConfig.sizes.notifs.image
|
sourceSize: {
|
||||||
sourceSize.height: TokenConfig.sizes.notifs.image
|
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||||
|
return Qt.size(size, size);
|
||||||
|
}
|
||||||
cache: false
|
cache: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ Column {
|
||||||
AnimatedImage {
|
AnimatedImage {
|
||||||
width: Tokens.sizes.session.button
|
width: Tokens.sizes.session.button
|
||||||
height: Tokens.sizes.session.button
|
height: Tokens.sizes.session.button
|
||||||
sourceSize.width: width
|
sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
|
||||||
|
|
||||||
playing: visible
|
playing: visible
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
|
import Quickshell
|
||||||
import Quickshell.Widgets
|
import Quickshell.Widgets
|
||||||
import Caelestia.Config
|
import Caelestia.Config
|
||||||
import qs.components
|
import qs.components
|
||||||
|
|
@ -96,7 +97,7 @@ Item {
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
source: Paths.absolutePath(Config.paths.noNotifsPic)
|
source: Paths.absolutePath(Config.paths.noNotifsPic)
|
||||||
fillMode: Image.PreserveAspectFit
|
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.enabled: true
|
||||||
layer.effect: Colouriser {
|
layer.effect: Colouriser {
|
||||||
|
|
|
||||||
|
|
@ -85,8 +85,10 @@ StyledRect {
|
||||||
Image {
|
Image {
|
||||||
source: Qt.resolvedUrl(root.image)
|
source: Qt.resolvedUrl(root.image)
|
||||||
fillMode: Image.PreserveAspectCrop
|
fillMode: Image.PreserveAspectCrop
|
||||||
sourceSize.width: TokenConfig.sizes.notifs.image
|
sourceSize: {
|
||||||
sourceSize.height: TokenConfig.sizes.notifs.image
|
const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1);
|
||||||
|
return Qt.size(size, size);
|
||||||
|
}
|
||||||
cache: false
|
cache: false
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
width: TokenConfig.sizes.notifs.image
|
width: TokenConfig.sizes.notifs.image
|
||||||
|
|
|
||||||
|
|
@ -89,3 +89,4 @@ add_subdirectory(Internal)
|
||||||
add_subdirectory(Models)
|
add_subdirectory(Models)
|
||||||
add_subdirectory(Services)
|
add_subdirectory(Services)
|
||||||
add_subdirectory(Blobs)
|
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
|
URI Caelestia.Internal
|
||||||
SOURCES
|
SOURCES
|
||||||
arcgauge.cpp
|
arcgauge.cpp
|
||||||
cachingimagemanager.cpp
|
|
||||||
circularbuffer.cpp
|
circularbuffer.cpp
|
||||||
circularindicatormanager.cpp
|
circularindicatormanager.cpp
|
||||||
linearindicatormanager.cpp
|
linearindicatormanager.cpp
|
||||||
|
|
@ -14,7 +13,6 @@ qml_module(caelestia-internal
|
||||||
LIBRARIES
|
LIBRARIES
|
||||||
Qt::Gui
|
Qt::Gui
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::Concurrent
|
|
||||||
Qt::Network
|
Qt::Network
|
||||||
Qt::DBus
|
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