feat: skip two-way copy for cuda devices when decoding video wallpapers + cache wallpapers as blobs in new database
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
C++ / fmt (pull_request) Failing after 11s
C++ / build (pull_request) Failing after 14s
C++ / clang-tidy (pull_request) Failing after 9s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Failing after 1m20s
Rust / fmt (pull_request) Successful in 28s
Rust / clippy (pull_request) Successful in 59s
This commit is contained in:
@@ -2,7 +2,12 @@ qml_module(ZShell-models
|
||||
URI ZShell.Models
|
||||
SOURCES
|
||||
filesystemmodel.hpp filesystemmodel.cpp
|
||||
wallpaperdb.hpp wallpaperdb.cpp
|
||||
wallpaperutil.hpp
|
||||
LIBRARIES
|
||||
Qt::Gui
|
||||
Qt::Quick
|
||||
Qt::Qml
|
||||
Qt::Concurrent
|
||||
Qt::Sql
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <qtconcurrentrun.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <mutex>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
@@ -39,136 +40,43 @@ bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
QString videoThumbnailPath(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
QString key = QStringLiteral("%1_%2_%3")
|
||||
.arg(info.baseName())
|
||||
.arg(info.size())
|
||||
.arg(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||
key.replace(QRegularExpression("[^A-Za-z0-9._-]"), "_");
|
||||
|
||||
return QDir::homePath() + "/.cache/zshell/imagecache/video-previews/" +
|
||||
key + "_preview.jpg";
|
||||
}
|
||||
|
||||
bool generateVideoThumbnail(const QString& src, const QString& cachePath) {
|
||||
if (QFile::exists(cachePath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString scale = QStringLiteral(
|
||||
"scale=1920:1080:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2");
|
||||
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
process.waitForFinished(15000);
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
double duration = 0.0;
|
||||
const auto probe = runProcess(
|
||||
bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
src,
|
||||
path,
|
||||
});
|
||||
if (probe.first == 0) {
|
||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
||||
}
|
||||
process.waitForFinished(5000);
|
||||
|
||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
||||
: 8;
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int bestIndex = -1;
|
||||
double bestChroma = -1.0;
|
||||
double bestLuma = -1.0;
|
||||
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
|
||||
const QString seek = duration > 0
|
||||
? QString::number(frac * duration, 'f', 3)
|
||||
: QStringLiteral("0");
|
||||
|
||||
const QString framePath =
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
seek,
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QImage frame(framePath);
|
||||
if (frame.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = frame.convertToFormat(QImage::Format_RGB888);
|
||||
|
||||
double chroma = 0.0;
|
||||
double luma = 0.0;
|
||||
int count = 0;
|
||||
for (int y = 0; y < frame.height(); ++y) {
|
||||
const auto* line = frame.constScanLine(y);
|
||||
for (int x = 0; x < frame.width(); ++x) {
|
||||
const int r = line[x * 3];
|
||||
const int g = line[x * 3 + 1];
|
||||
const int b = line[x * 3 + 2];
|
||||
chroma += std::max({r, g, b}) - std::min({r, g, b});
|
||||
luma += r + g + b;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = std::max(1, count);
|
||||
chroma /= n;
|
||||
luma /= 3.0 * n;
|
||||
|
||||
if (chroma > bestChroma + 0.5 ||
|
||||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
|
||||
bestChroma = chroma;
|
||||
bestLuma = luma;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(cachePath).absolutePath());
|
||||
return QFile::copy(
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)),
|
||||
cachePath);
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
FileSystemEntry::FileSystemEntry(
|
||||
@@ -177,7 +85,6 @@ FileSystemEntry::FileSystemEntry(
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath)
|
||||
, m_thumbnailPath(path)
|
||||
, m_isImageInitialised(false)
|
||||
, m_mimeTypeInitialised(false) {}
|
||||
|
||||
@@ -227,16 +134,7 @@ bool FileSystemEntry::isVideo() const {
|
||||
}
|
||||
|
||||
QString FileSystemEntry::thumbnailPath() const {
|
||||
return m_thumbnailPath;
|
||||
}
|
||||
|
||||
void FileSystemEntry::setThumbnailPath(const QString& path) {
|
||||
if (m_thumbnailPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_thumbnailPath = path;
|
||||
emit thumbnailPathChanged();
|
||||
return m_path;
|
||||
}
|
||||
|
||||
QString FileSystemEntry::mimeType() const {
|
||||
@@ -404,6 +302,46 @@ void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::requireHardwareCodec() const {
|
||||
return m_requireHardwareCodec;
|
||||
}
|
||||
|
||||
void FileSystemModel::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_requireHardwareCodec = requireHardwareCodec;
|
||||
emit requireHardwareCodecChanged();
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
bool FileSystemModel::isHardwareVideo(const QString& path) {
|
||||
const QFileInfo info(path);
|
||||
const QString key =
|
||||
path + '|' + QByteArray::number(info.size()) + '|' +
|
||||
QByteArray::number(info.lastModified().toMSecsSinceEpoch() / 1000);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
|
||||
const auto it = m_codecCache.constFind(key);
|
||||
if (it != m_codecCache.constEnd()) {
|
||||
return it.value();
|
||||
}
|
||||
}
|
||||
|
||||
const bool hardware = probeHardwareCodec(path);
|
||||
|
||||
{
|
||||
std::lock_guard<QMutex> lock(m_codecMutex);
|
||||
m_codecCache.insert(key, hardware);
|
||||
}
|
||||
|
||||
return hardware;
|
||||
}
|
||||
|
||||
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
|
||||
return QQmlListProperty<FileSystemEntry>(this, &m_entries);
|
||||
}
|
||||
@@ -434,14 +372,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) {
|
||||
const auto paths = watcher->result();
|
||||
if (currentDir == m_dir && showHidden == m_showHidden &&
|
||||
!paths.isEmpty()) {
|
||||
// Ignore if dir or showHidden has changed
|
||||
m_watcher.addPaths(paths);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
}
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::update() {
|
||||
@@ -488,6 +424,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
const auto showHidden = m_showHidden;
|
||||
const auto filter = m_filter;
|
||||
const auto nameFilters = m_nameFilters;
|
||||
const auto requireHardware = m_requireHardwareCodec;
|
||||
|
||||
QSet<QString> oldPaths;
|
||||
for (const auto& entry : std::as_const(m_entries)) {
|
||||
@@ -495,7 +432,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
}
|
||||
|
||||
const auto future = QtConcurrent::run(
|
||||
[=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
[=, this](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) {
|
||||
const auto flags = recursive ? QDirIterator::Subdirectories
|
||||
: QDirIterator::NoIteratorFlags;
|
||||
|
||||
@@ -553,18 +490,15 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
|
||||
QString path = iter->next();
|
||||
|
||||
if (filter == Images) {
|
||||
if (hasVideoExtension(path)) {
|
||||
if (requireHardware && !isHardwareVideo(path)) {
|
||||
continue;
|
||||
}
|
||||
} else if (filter == Images || filter == Media) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
} else if (filter == Media) {
|
||||
if (!hasVideoExtension(path)) {
|
||||
QImageReader reader(path);
|
||||
if (!reader.canRead()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newPaths.insert(path);
|
||||
@@ -605,31 +539,6 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) {
|
||||
});
|
||||
|
||||
watcher->setFuture(future);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::startThumbnailGeneration(FileSystemEntry* entry) {
|
||||
const QPointer<FileSystemEntry> guard(entry);
|
||||
const QString source = entry->path();
|
||||
const QString cachePath = videoThumbnailPath(source);
|
||||
|
||||
const auto future = QtConcurrent::run([source, cachePath]() {
|
||||
return generateVideoThumbnail(source, cachePath);
|
||||
});
|
||||
|
||||
const auto watcher = new QFutureWatcher<bool>(this);
|
||||
connect(
|
||||
watcher,
|
||||
&QFutureWatcher<bool>::finished,
|
||||
this,
|
||||
[guard, cachePath, watcher]() {
|
||||
if (guard && watcher->result()) {
|
||||
guard->setThumbnailPath(cachePath);
|
||||
}
|
||||
watcher->deleteLater();
|
||||
});
|
||||
watcher->setFuture(future);
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
void FileSystemModel::applyChanges(
|
||||
@@ -642,7 +551,6 @@ void FileSystemModel::applyChanges(
|
||||
}
|
||||
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
|
||||
|
||||
// Batch remove old entries
|
||||
int start = -1;
|
||||
int end = -1;
|
||||
for (int idx : std::as_const(removedIndices)) {
|
||||
@@ -670,14 +578,10 @@ void FileSystemModel::applyChanges(
|
||||
endRemoveRows();
|
||||
}
|
||||
|
||||
// Create new entries
|
||||
QList<FileSystemEntry*> newEntries;
|
||||
for (const auto& path : addedPaths) {
|
||||
auto* entry =
|
||||
new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
|
||||
if (entry->isVideo()) {
|
||||
startThumbnailGeneration(entry);
|
||||
}
|
||||
newEntries << entry;
|
||||
}
|
||||
std::sort(
|
||||
@@ -687,7 +591,6 @@ void FileSystemModel::applyChanges(
|
||||
return compareEntries(a, b);
|
||||
});
|
||||
|
||||
// Batch insert new entries
|
||||
int insertStart = -1;
|
||||
QList<FileSystemEntry*> batchItems;
|
||||
for (const auto& entry : std::as_const(newEntries)) {
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <qdir.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qfuture.h>
|
||||
#include <qhash.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qmutex.h>
|
||||
#include <qmimedatabase.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
@@ -29,8 +31,7 @@ class FileSystemEntry : public QObject {
|
||||
Q_PROPERTY(bool isDir READ isDir CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||
Q_PROPERTY(
|
||||
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
||||
Q_PROPERTY(QString thumbnailPath READ thumbnailPath CONSTANT)
|
||||
Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
|
||||
|
||||
public:
|
||||
@@ -52,20 +53,16 @@ class FileSystemEntry : public QObject {
|
||||
[[nodiscard]] QString thumbnailPath() const;
|
||||
[[nodiscard]] QString mimeType() const;
|
||||
|
||||
void setThumbnailPath(const QString& path);
|
||||
|
||||
void updateRelativePath(const QDir& dir);
|
||||
|
||||
signals:
|
||||
void relativePathChanged();
|
||||
void thumbnailPathChanged();
|
||||
|
||||
private:
|
||||
const QFileInfo m_fileInfo;
|
||||
|
||||
const QString m_path;
|
||||
QString m_relativePath;
|
||||
QString m_thumbnailPath;
|
||||
|
||||
mutable bool m_isImage;
|
||||
mutable bool m_isImageInitialised;
|
||||
@@ -94,6 +91,9 @@ class FileSystemModel : public QAbstractListModel {
|
||||
Q_PROPERTY(
|
||||
QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY
|
||||
nameFiltersChanged)
|
||||
Q_PROPERTY(
|
||||
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries
|
||||
@@ -132,6 +132,9 @@ class FileSystemModel : public QAbstractListModel {
|
||||
[[nodiscard]] QStringList nameFilters() const;
|
||||
void setNameFilters(const QStringList& nameFilters);
|
||||
|
||||
[[nodiscard]] bool requireHardwareCodec() const;
|
||||
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
|
||||
|
||||
signals:
|
||||
@@ -142,6 +145,7 @@ class FileSystemModel : public QAbstractListModel {
|
||||
void sortReverseChanged();
|
||||
void filterChanged();
|
||||
void nameFiltersChanged();
|
||||
void requireHardwareCodecChanged();
|
||||
void entriesChanged();
|
||||
|
||||
private:
|
||||
@@ -157,6 +161,10 @@ class FileSystemModel : public QAbstractListModel {
|
||||
bool m_sortReverse;
|
||||
Filter m_filter;
|
||||
QStringList m_nameFilters;
|
||||
bool m_requireHardwareCodec = false;
|
||||
|
||||
QHash<QString, bool> m_codecCache;
|
||||
QMutex m_codecMutex;
|
||||
|
||||
void watchDirIfRecursive(const QString& path);
|
||||
void update();
|
||||
@@ -165,7 +173,7 @@ class FileSystemModel : public QAbstractListModel {
|
||||
void updateEntriesForDir(const QString& dir);
|
||||
void applyChanges(
|
||||
const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
|
||||
void startThumbnailGeneration(FileSystemEntry* entry);
|
||||
[[nodiscard]] bool isHardwareVideo(const QString& path);
|
||||
[[nodiscard]] bool compareEntries(
|
||||
const FileSystemEntry* a, const FileSystemEntry* b) const;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,746 @@
|
||||
#include "wallpaperdb.hpp"
|
||||
#include "wallpaperutil.hpp"
|
||||
|
||||
#include <QtQuick/qquickimageprovider.h>
|
||||
#include <qbytearray.h>
|
||||
#include <qdiriterator.h>
|
||||
#include <qelapsedtimer.h>
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qlogging.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qsqldatabase.h>
|
||||
#include <qsqlerror.h>
|
||||
#include <qsqlquery.h>
|
||||
#include <qtconcurrentrun.h>
|
||||
#include <quuid.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
Q_LOGGING_CATEGORY(dbLog, "ZShell.Internal.wallpaper.db", QtWarningMsg)
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
class WallpaperImageProvider : public QQuickImageProvider {
|
||||
public:
|
||||
explicit WallpaperImageProvider(const QString& dbPath)
|
||||
: QQuickImageProvider(QQuickImageProvider::Image), m_dbPath(dbPath) {}
|
||||
|
||||
QImage requestImage(
|
||||
const QString& id, QSize* size, const QSize& requestedSize) override {
|
||||
const QString path =
|
||||
QString::fromUtf8(QByteArray::fromHex(id.toUtf8()));
|
||||
if (path.isEmpty() || !openThreadDb()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QSqlQuery query(QSqlDatabase::database(m_threadUuid));
|
||||
query.prepare("SELECT cache FROM wallpapers WHERE path = ?");
|
||||
query.addBindValue(path);
|
||||
|
||||
if (!query.exec() || !query.next()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const QByteArray bytes = query.value(0).toByteArray();
|
||||
if (bytes.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage image = QImage::fromData(bytes, "JPEG");
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (size) {
|
||||
*size = image.size();
|
||||
}
|
||||
|
||||
if (requestedSize.isValid() && !requestedSize.isEmpty() &&
|
||||
(image.width() > requestedSize.width() ||
|
||||
image.height() > requestedSize.height())) {
|
||||
image = image.scaled(
|
||||
requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private:
|
||||
bool openThreadDb() {
|
||||
if (!m_threadUuid.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (m_dbPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", uuid);
|
||||
db.setConnectOptions("QSQLITE_OPEN_READONLY");
|
||||
db.setDatabaseName(m_dbPath);
|
||||
|
||||
if (!db.open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("PRAGMA journal_mode=WAL");
|
||||
|
||||
m_threadUuid = uuid;
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString m_dbPath;
|
||||
thread_local static QString m_threadUuid;
|
||||
};
|
||||
|
||||
thread_local QString WallpaperImageProvider::m_threadUuid;
|
||||
|
||||
WallpaperEntry::WallpaperEntry(
|
||||
const QString& path, const QString& relativePath, QObject* parent)
|
||||
: QObject(parent)
|
||||
, m_fileInfo(path)
|
||||
, m_path(path)
|
||||
, m_relativePath(relativePath) {}
|
||||
|
||||
QString WallpaperEntry::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
QString WallpaperEntry::relativePath() const {
|
||||
return m_relativePath;
|
||||
}
|
||||
|
||||
QString WallpaperEntry::name() const {
|
||||
return m_fileInfo.fileName();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::baseName() const {
|
||||
return m_fileInfo.baseName();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::parentDir() const {
|
||||
return m_fileInfo.absolutePath();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::suffix() const {
|
||||
return m_fileInfo.completeSuffix();
|
||||
}
|
||||
|
||||
qint64 WallpaperEntry::size() const {
|
||||
return m_fileInfo.size();
|
||||
}
|
||||
|
||||
bool WallpaperEntry::isImage() const {
|
||||
return !hasVideoExtension(m_path);
|
||||
}
|
||||
|
||||
bool WallpaperEntry::isVideo() const {
|
||||
return hasVideoExtension(m_path);
|
||||
}
|
||||
|
||||
bool WallpaperEntry::hardware() const {
|
||||
return m_hardware;
|
||||
}
|
||||
|
||||
bool WallpaperEntry::cached() const {
|
||||
return !m_thumbnailPath.isEmpty();
|
||||
}
|
||||
|
||||
QString WallpaperEntry::thumbnailPath() const {
|
||||
if (!m_thumbnailPath.isEmpty()) {
|
||||
return m_thumbnailPath;
|
||||
}
|
||||
|
||||
return isImage() ? m_path : QString();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setRelativePath(const QString& path) {
|
||||
if (m_relativePath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_relativePath = path;
|
||||
emit relativePathChanged();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setHardware(bool hardware) {
|
||||
if (m_hardware == hardware) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_hardware = hardware;
|
||||
emit hardwareChanged();
|
||||
}
|
||||
|
||||
void WallpaperEntry::setThumbnailPath(const QString& path) {
|
||||
if (m_thumbnailPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_thumbnailPath = path;
|
||||
emit thumbnailPathChanged();
|
||||
}
|
||||
|
||||
WallpaperDb::WallpaperDb(QObject* parent) : QObject(parent) {
|
||||
m_uuid = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
m_dbPath = QDir::homePath() + "/.local/state/zshell/wallpapers.sqlite";
|
||||
|
||||
m_queueTimer.setInterval(300);
|
||||
m_queueTimer.setTimerType(Qt::CoarseTimer);
|
||||
QObject::connect(
|
||||
&m_queueTimer, &QTimer::timeout, this, &WallpaperDb::processQueue);
|
||||
|
||||
m_watchTimer.setInterval(400);
|
||||
m_watchTimer.setSingleShot(true);
|
||||
QObject::connect(
|
||||
&m_watcher, &QFileSystemWatcher::directoryChanged, this, [this]() {
|
||||
m_watchTimer.start();
|
||||
});
|
||||
QObject::connect(
|
||||
&m_watchTimer, &QTimer::timeout, this, &WallpaperDb::scanDir);
|
||||
}
|
||||
|
||||
WallpaperDb::~WallpaperDb() {
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
{
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isValid()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
QSqlDatabase::removeDatabase(m_uuid);
|
||||
}
|
||||
}
|
||||
|
||||
QString WallpaperDb::path() const {
|
||||
return m_path;
|
||||
}
|
||||
|
||||
void WallpaperDb::setPath(const QString& path) {
|
||||
if (m_path == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_path = path;
|
||||
m_dir = QDir(path);
|
||||
emit pathChanged();
|
||||
|
||||
for (const auto& dir : m_watcher.directories()) {
|
||||
m_watcher.removePath(dir);
|
||||
}
|
||||
|
||||
scanDir();
|
||||
}
|
||||
|
||||
QString WallpaperDb::dbPath() const {
|
||||
return m_dbPath;
|
||||
}
|
||||
|
||||
void WallpaperDb::setDbPath(const QString& path) {
|
||||
if (m_dbPath == path) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_dbPath = path;
|
||||
emit dbPathChanged();
|
||||
|
||||
if (!m_path.isEmpty()) {
|
||||
scanDir();
|
||||
}
|
||||
}
|
||||
|
||||
bool WallpaperDb::requireHardwareCodec() const {
|
||||
return m_requireHardwareCodec;
|
||||
}
|
||||
|
||||
void WallpaperDb::setRequireHardwareCodec(bool requireHardwareCodec) {
|
||||
if (m_requireHardwareCodec == requireHardwareCodec) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_requireHardwareCodec = requireHardwareCodec;
|
||||
emit requireHardwareCodecChanged();
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
QQmlListProperty<WallpaperEntry> WallpaperDb::entries() {
|
||||
return QQmlListProperty<WallpaperEntry>(this, &m_entries);
|
||||
}
|
||||
|
||||
QString WallpaperDb::thumbnailUrl(const QString& path) {
|
||||
return QStringLiteral("image:
|
||||
+ QString::fromUtf8(path.toUtf8().toHex());
|
||||
}
|
||||
|
||||
QSize WallpaperDb::cacheSize() const {
|
||||
return m_cacheSize;
|
||||
}
|
||||
|
||||
void WallpaperDb::setCacheSize(const QSize& cacheSize) {
|
||||
if (m_cacheSize == cacheSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QSize prev = m_cacheSize;
|
||||
const bool hadPrevious = !prev.isEmpty();
|
||||
|
||||
m_cacheSize = cacheSize;
|
||||
emit cacheSizeChanged();
|
||||
|
||||
if (hadPrevious && qint64(cacheSize.width()) * cacheSize.height() >
|
||||
qint64(prev.width()) * prev.height()) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
|
||||
for (auto* entry : std::as_const(m_byPath)) {
|
||||
if (!entry->cached()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL"
|
||||
" WHERE path = ?");
|
||||
query.addBindValue(entry->path());
|
||||
query.exec();
|
||||
}
|
||||
|
||||
entry->setThumbnailPath(QString());
|
||||
queueMetadata(entry, entry->isVideo());
|
||||
}
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
if (!m_queue.isEmpty() && !m_queueTimer.isActive()) {
|
||||
m_queueTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperDb::registerProvider() {
|
||||
if (m_provider != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto* engine = qmlEngine(this);
|
||||
if (engine == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_provider = new WallpaperImageProvider(m_dbPath);
|
||||
engine->addImageProvider(QStringLiteral("wallpapers"), m_provider);
|
||||
}
|
||||
|
||||
void WallpaperDb::openDb() {
|
||||
if (m_dbPath.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
{
|
||||
auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isOpen() && db.databaseName() == m_dbPath) {
|
||||
registerProvider();
|
||||
return;
|
||||
}
|
||||
|
||||
if (db.isValid()) {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
QSqlDatabase::removeDatabase(m_uuid);
|
||||
}
|
||||
|
||||
QDir().mkpath(QFileInfo(m_dbPath).absolutePath());
|
||||
|
||||
auto db = QSqlDatabase::addDatabase("QSQLITE", m_uuid);
|
||||
db.setDatabaseName(m_dbPath);
|
||||
|
||||
if (!db.open()) {
|
||||
qWarning(dbLog) << "WallpaperDb: could not open database" << m_dbPath
|
||||
<< db.lastError();
|
||||
return;
|
||||
}
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.exec("PRAGMA journal_mode=WAL");
|
||||
query.exec(
|
||||
"CREATE TABLE IF NOT EXISTS wallpapers("
|
||||
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
|
||||
|
||||
query.exec("SELECT cache FROM wallpapers LIMIT 1");
|
||||
if (query.lastError().isValid()) {
|
||||
query.exec("DROP TABLE IF EXISTS wallpapers");
|
||||
query.exec(
|
||||
"CREATE TABLE wallpapers("
|
||||
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
|
||||
} else {
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
|
||||
}
|
||||
|
||||
registerProvider();
|
||||
}
|
||||
|
||||
void WallpaperDb::scanDir() {
|
||||
openDb();
|
||||
|
||||
watchDir(m_dir.absolutePath());
|
||||
|
||||
if (!m_dir.exists()) {
|
||||
if (!m_entries.isEmpty()) {
|
||||
m_entries.clear();
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
auto* watcher = new QFutureWatcher<ScanResult>(this);
|
||||
QObject::connect(
|
||||
watcher, &QFutureWatcher<ScanResult>::finished, this, [this, watcher]() {
|
||||
applyScan(watcher->result());
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
const QDir dir = m_dir;
|
||||
|
||||
QHash<QString, QPair<qint64, bool>> knownCodecs;
|
||||
for (const auto* entry : std::as_const(m_byPath)) {
|
||||
if (entry->isVideo()) {
|
||||
knownCodecs.insert(
|
||||
entry->path(), qMakePair(entry->size(), entry->hardware()));
|
||||
}
|
||||
}
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([dir, knownCodecs]() {
|
||||
ScanResult result;
|
||||
result.dir = dir;
|
||||
|
||||
QDirIterator it(dir.absolutePath(), QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
const QString path = it.next();
|
||||
const QFileInfo info(path);
|
||||
|
||||
if (info.isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasVideoExtension(path)) {
|
||||
FileEntry file;
|
||||
file.info = info;
|
||||
|
||||
const auto known = knownCodecs.constFind(path);
|
||||
if (known != knownCodecs.constEnd() &&
|
||||
known.value().first == info.size()) {
|
||||
file.hardware = known.value().second;
|
||||
} else {
|
||||
file.hardware = probeHardwareCodec(path);
|
||||
}
|
||||
|
||||
result.files.append(file);
|
||||
} else {
|
||||
QImageReader reader(path);
|
||||
if (reader.canRead()) {
|
||||
FileEntry file;
|
||||
file.info = info;
|
||||
result.files.append(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void WallpaperDb::watchDir(const QString& dir) {
|
||||
if (dir.isEmpty() || m_watcher.directories().contains(dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!QDir(dir).exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_watcher.addPath(dir);
|
||||
}
|
||||
|
||||
void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: scan of %s found %d files (cache target %dx%d)",
|
||||
m_dir.absolutePath().toUtf8().constData(),
|
||||
int(result.files.size()),
|
||||
m_cacheSize.width(),
|
||||
m_cacheSize.height());
|
||||
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
|
||||
QSet<QString> seen;
|
||||
for (const auto& file : result.files) {
|
||||
seen.insert(file.info.absoluteFilePath());
|
||||
}
|
||||
|
||||
for (auto it = m_byPath.begin(); it != m_byPath.end();) {
|
||||
if (!seen.contains(it.key())) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare("DELETE FROM wallpapers WHERE path = ?");
|
||||
query.addBindValue(it.key());
|
||||
query.exec();
|
||||
}
|
||||
|
||||
delete it.value();
|
||||
it = m_byPath.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto& file : result.files) {
|
||||
const QFileInfo& info = file.info;
|
||||
const QString path = info.absoluteFilePath();
|
||||
|
||||
const qint64 size = info.size();
|
||||
const qint64 mtime = info.lastModified().toMSecsSinceEpoch() / 1000;
|
||||
const bool isVideo = hasVideoExtension(path);
|
||||
|
||||
watchDir(info.absolutePath());
|
||||
|
||||
const auto existing = m_byPath.constFind(path);
|
||||
if (existing != m_byPath.constEnd()) {
|
||||
if (existing.value()->size() == size) {
|
||||
existing.value()->setHardware(file.hardware);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperEntry* entry = m_byPath.value(path);
|
||||
if (!entry) {
|
||||
entry =
|
||||
new WallpaperEntry(path, m_dir.relativeFilePath(path), this);
|
||||
m_byPath.insert(path, entry);
|
||||
}
|
||||
|
||||
entry->setHardware(file.hardware);
|
||||
|
||||
bool reused = false;
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"SELECT cache, scheme_thumb, size, mtime FROM wallpapers"
|
||||
" WHERE path = ?");
|
||||
query.addBindValue(path);
|
||||
query.exec();
|
||||
|
||||
if (query.next()) {
|
||||
const auto cache = query.value(0).toByteArray();
|
||||
const auto scheme = query.value(1).toByteArray();
|
||||
const auto storedSize = query.value(2).toLongLong();
|
||||
const auto storedMtime = query.value(3).toLongLong();
|
||||
|
||||
if (storedSize == size && storedMtime == mtime &&
|
||||
!cache.isEmpty()) {
|
||||
reused = true;
|
||||
entry->setThumbnailPath(thumbnailUrl(path));
|
||||
|
||||
if (scheme.isEmpty()) {
|
||||
queueMetadata(entry, isVideo, cache);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!reused) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"INSERT OR REPLACE INTO wallpapers"
|
||||
" (path, kind, hardware, size, mtime, cache)"
|
||||
" VALUES (?, ?, ?, ?, ?, NULL)");
|
||||
query.addBindValue(path);
|
||||
query.addBindValue(isVideo ? "video" : "image");
|
||||
query.addBindValue(
|
||||
isVideo ? (file.hardware ? 1 : 0) : QVariant());
|
||||
query.addBindValue(size);
|
||||
query.addBindValue(mtime);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: could not store the row for" << path
|
||||
<< query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
queueMetadata(entry, isVideo);
|
||||
}
|
||||
}
|
||||
|
||||
rebuildEntries();
|
||||
}
|
||||
|
||||
void WallpaperDb::queueMetadata(
|
||||
WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) {
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: queued cache job for %s (%s%s)",
|
||||
entry->path().toUtf8().constData(),
|
||||
isVideo ? "video" : "image",
|
||||
existingCache.isEmpty() ? "" : ", existing blob");
|
||||
|
||||
m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache});
|
||||
|
||||
if (!m_queueTimer.isActive()) {
|
||||
m_queueTimer.start();
|
||||
}
|
||||
}
|
||||
|
||||
void WallpaperDb::processQueue() {
|
||||
if (m_jobRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_queue.isEmpty()) {
|
||||
m_queueTimer.stop();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_cacheSize.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MetadataJob job = m_queue.takeFirst();
|
||||
m_jobRunning = true;
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: caching %s (%s, %d jobs left in queue)",
|
||||
job.path.toUtf8().constData(),
|
||||
job.isVideo ? "video" : "image",
|
||||
int(m_queue.size()));
|
||||
|
||||
runMetadataJob(job);
|
||||
}
|
||||
|
||||
void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
struct CacheResult {
|
||||
QString path;
|
||||
QByteArray cache;
|
||||
QByteArray scheme;
|
||||
};
|
||||
|
||||
const QSize cacheSize = m_cacheSize;
|
||||
|
||||
QElapsedTimer clock;
|
||||
clock.start();
|
||||
|
||||
auto* watcher = new QFutureWatcher<CacheResult>(this);
|
||||
QObject::connect(
|
||||
watcher,
|
||||
&QFutureWatcher<CacheResult>::finished,
|
||||
this,
|
||||
[this, watcher, clock]() {
|
||||
const auto result = watcher->result();
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: %s produced %d bytes of cache in %dms",
|
||||
result.path.toUtf8().constData(),
|
||||
int(result.cache.size()),
|
||||
int(clock.elapsed()));
|
||||
|
||||
if (QSqlDatabase::contains(m_uuid)) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?"
|
||||
" WHERE path = ?");
|
||||
query.addBindValue(result.cache);
|
||||
query.addBindValue(result.scheme);
|
||||
query.addBindValue(result.path);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: could not store the cache for"
|
||||
<< result.path << query.lastError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.cache.isEmpty()) {
|
||||
if (auto* entry = m_byPath.value(result.path)) {
|
||||
entry->setThumbnailPath(thumbnailUrl(result.path));
|
||||
}
|
||||
|
||||
emit cacheStored(result.path);
|
||||
}
|
||||
|
||||
m_jobRunning = false;
|
||||
rebuildEntries();
|
||||
watcher->deleteLater();
|
||||
});
|
||||
|
||||
watcher->setFuture(QtConcurrent::run([job, cacheSize]() {
|
||||
CacheResult result;
|
||||
result.path = job.path;
|
||||
|
||||
QByteArray cache = job.existingCache;
|
||||
if (cache.isEmpty()) {
|
||||
cache = job.isVideo ? videoCacheBytes(job.path, cacheSize)
|
||||
: imageCacheBytes(job.path, cacheSize);
|
||||
}
|
||||
|
||||
result.cache = cache;
|
||||
result.scheme = cache.isEmpty() ? QByteArray()
|
||||
: schemeThumbBytes(cache, 128);
|
||||
|
||||
return result;
|
||||
}));
|
||||
}
|
||||
|
||||
void WallpaperDb::rebuildEntries() {
|
||||
QList<WallpaperEntry*> next;
|
||||
next.reserve(m_byPath.size());
|
||||
|
||||
for (auto* entry : std::as_const(m_byPath)) {
|
||||
if (entryAllowed(entry)) {
|
||||
next.append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::sort(
|
||||
next.begin(),
|
||||
next.end(),
|
||||
[](const WallpaperEntry* a, const WallpaperEntry* b) {
|
||||
return a->relativePath() < b->relativePath();
|
||||
});
|
||||
|
||||
if (next == m_entries) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_entries = next;
|
||||
emit entriesChanged();
|
||||
}
|
||||
|
||||
bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
|
||||
if (!entry->isVideo()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_requireHardwareCodec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return entry->hardware();
|
||||
}
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
|
||||
#include <qbytearray.h>
|
||||
#include <qdir.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qfilesystemwatcher.h>
|
||||
#include <qhash.h>
|
||||
#include <qlist.h>
|
||||
#include <qobject.h>
|
||||
#include <qqmlintegration.h>
|
||||
#include <qqmllist.h>
|
||||
#include <qsize.h>
|
||||
#include <qtimer.h>
|
||||
#include <qvector.h>
|
||||
|
||||
namespace ZShell::models {
|
||||
|
||||
class WallpaperImageProvider;
|
||||
|
||||
class WallpaperEntry : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
QML_UNCREATABLE(
|
||||
"WallpaperEntry instances can only be retrieved from a WallpaperDb")
|
||||
|
||||
Q_PROPERTY(QString path READ path CONSTANT)
|
||||
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
|
||||
Q_PROPERTY(QString name READ name CONSTANT)
|
||||
Q_PROPERTY(QString baseName READ baseName CONSTANT)
|
||||
Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
|
||||
Q_PROPERTY(QString suffix READ suffix CONSTANT)
|
||||
Q_PROPERTY(qint64 size READ size CONSTANT)
|
||||
Q_PROPERTY(bool isImage READ isImage CONSTANT)
|
||||
Q_PROPERTY(bool isVideo READ isVideo CONSTANT)
|
||||
Q_PROPERTY(bool hardware READ hardware NOTIFY hardwareChanged)
|
||||
Q_PROPERTY(
|
||||
QString thumbnailPath READ thumbnailPath NOTIFY thumbnailPathChanged)
|
||||
|
||||
public:
|
||||
explicit WallpaperEntry(
|
||||
const QString& path,
|
||||
const QString& relativePath,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
[[nodiscard]] QString relativePath() const;
|
||||
[[nodiscard]] QString name() const;
|
||||
[[nodiscard]] QString baseName() const;
|
||||
[[nodiscard]] QString parentDir() const;
|
||||
[[nodiscard]] QString suffix() const;
|
||||
[[nodiscard]] qint64 size() const;
|
||||
[[nodiscard]] bool isImage() const;
|
||||
[[nodiscard]] bool isVideo() const;
|
||||
[[nodiscard]] bool hardware() const;
|
||||
[[nodiscard]] bool cached() const;
|
||||
[[nodiscard]] QString thumbnailPath() const;
|
||||
|
||||
void setRelativePath(const QString& path);
|
||||
void setHardware(bool hardware);
|
||||
void setThumbnailPath(const QString& path);
|
||||
|
||||
signals:
|
||||
void relativePathChanged();
|
||||
void hardwareChanged();
|
||||
void thumbnailPathChanged();
|
||||
|
||||
private:
|
||||
const QFileInfo m_fileInfo;
|
||||
|
||||
const QString m_path;
|
||||
QString m_relativePath;
|
||||
QString m_thumbnailPath;
|
||||
bool m_hardware = false;
|
||||
};
|
||||
|
||||
class WallpaperDb : public QObject {
|
||||
Q_OBJECT
|
||||
QML_ELEMENT
|
||||
|
||||
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
|
||||
Q_PROPERTY(QString dbPath READ dbPath WRITE setDbPath NOTIFY dbPathChanged)
|
||||
Q_PROPERTY(
|
||||
bool requireHardwareCodec READ requireHardwareCodec WRITE
|
||||
setRequireHardwareCodec NOTIFY requireHardwareCodecChanged)
|
||||
Q_PROPERTY(
|
||||
QQmlListProperty<ZShell::models::WallpaperEntry> entries READ entries
|
||||
NOTIFY entriesChanged)
|
||||
Q_PROPERTY(
|
||||
QSize cacheSize READ cacheSize WRITE setCacheSize NOTIFY
|
||||
cacheSizeChanged)
|
||||
|
||||
public:
|
||||
explicit WallpaperDb(QObject* parent = nullptr);
|
||||
~WallpaperDb() override;
|
||||
|
||||
[[nodiscard]] QString path() const;
|
||||
void setPath(const QString& path);
|
||||
|
||||
[[nodiscard]] QString dbPath() const;
|
||||
void setDbPath(const QString& path);
|
||||
|
||||
[[nodiscard]] bool requireHardwareCodec() const;
|
||||
void setRequireHardwareCodec(bool requireHardwareCodec);
|
||||
|
||||
[[nodiscard]] QQmlListProperty<WallpaperEntry> entries();
|
||||
|
||||
[[nodiscard]] QSize cacheSize() const;
|
||||
void setCacheSize(const QSize& cacheSize);
|
||||
|
||||
[[nodiscard]] static QString thumbnailUrl(const QString& path);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void dbPathChanged();
|
||||
void requireHardwareCodecChanged();
|
||||
void entriesChanged();
|
||||
void cacheSizeChanged();
|
||||
|
||||
void cacheStored(const QString& path);
|
||||
|
||||
private:
|
||||
struct FileEntry {
|
||||
QFileInfo info;
|
||||
bool hardware = false;
|
||||
};
|
||||
|
||||
struct ScanResult {
|
||||
QVector<FileEntry> files;
|
||||
QDir dir;
|
||||
};
|
||||
|
||||
struct MetadataJob {
|
||||
WallpaperEntry* entry;
|
||||
QString path;
|
||||
bool isVideo;
|
||||
QByteArray existingCache;
|
||||
};
|
||||
|
||||
void openDb();
|
||||
void registerProvider();
|
||||
void scanDir();
|
||||
void watchDir(const QString& dir);
|
||||
void applyScan(const ScanResult& result);
|
||||
void queueMetadata(
|
||||
WallpaperEntry* entry,
|
||||
bool isVideo,
|
||||
const QByteArray& existingCache = {});
|
||||
void processQueue();
|
||||
void runMetadataJob(const MetadataJob& job);
|
||||
void rebuildEntries();
|
||||
|
||||
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
|
||||
|
||||
QDir m_dir;
|
||||
QString m_path;
|
||||
QString m_dbPath;
|
||||
QString m_uuid;
|
||||
WallpaperImageProvider* m_provider = nullptr;
|
||||
|
||||
bool m_requireHardwareCodec = false;
|
||||
QSize m_cacheSize;
|
||||
|
||||
QList<WallpaperEntry*> m_entries;
|
||||
QHash<QString, WallpaperEntry*> m_byPath;
|
||||
|
||||
QVector<MetadataJob> m_queue;
|
||||
QTimer m_queueTimer;
|
||||
bool m_jobRunning = false;
|
||||
|
||||
QFileSystemWatcher m_watcher;
|
||||
QTimer m_watchTimer;
|
||||
};
|
||||
|
||||
} // namespace ZShell::models
|
||||
@@ -0,0 +1,313 @@
|
||||
#pragma once
|
||||
|
||||
#include <qbuffer.h>
|
||||
#include <qfile.h>
|
||||
#include <qfileinfo.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qprocess.h>
|
||||
#include <qstring.h>
|
||||
#include <qstringlist.h>
|
||||
#include <qtemporarydir.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
|
||||
namespace ZShell::models {
|
||||
namespace {
|
||||
|
||||
inline const QStringList kVideoExtensions = {
|
||||
"mp4",
|
||||
"mkv",
|
||||
"webm",
|
||||
"avi",
|
||||
"mov",
|
||||
"wmv",
|
||||
"flv",
|
||||
"m4v",
|
||||
"mpg",
|
||||
"mpeg",
|
||||
"3gp",
|
||||
"ogv",
|
||||
"ts",
|
||||
"vob",
|
||||
};
|
||||
|
||||
inline bool hasVideoExtension(const QString& path) {
|
||||
return kVideoExtensions.contains(QFileInfo(path).suffix().toLower());
|
||||
}
|
||||
|
||||
inline const QStringList kHardwareCodecs = {
|
||||
"av1",
|
||||
"hevc",
|
||||
"h264",
|
||||
"vp9",
|
||||
"vp8",
|
||||
"vc1",
|
||||
"mpeg2",
|
||||
"mpeg2video",
|
||||
"mpeg4",
|
||||
"mjpeg",
|
||||
};
|
||||
|
||||
inline bool probeHardwareCodec(const QString& path) {
|
||||
QProcess process;
|
||||
process.start(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=codec_name",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
path,
|
||||
});
|
||||
process.waitForFinished(5000);
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QString codec =
|
||||
QString::fromUtf8(process.readAllStandardOutput()).trimmed().toLower();
|
||||
return kHardwareCodecs.contains(codec);
|
||||
}
|
||||
|
||||
inline QSize coverSizeNoUpscale(const QSize& source, const QSize& fit) {
|
||||
if (!source.isValid() || source.isEmpty() || !fit.isValid() ||
|
||||
fit.isEmpty()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const QSize cover = source.scaled(fit, Qt::KeepAspectRatioByExpanding);
|
||||
if (cover.width() <= source.width() && cover.height() <= source.height()) {
|
||||
return cover;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
inline QImage cleanImage(const QImage& src) {
|
||||
QImage out(src.size(), QImage::Format_RGB888);
|
||||
if (out.isNull()) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const auto rowBytes =
|
||||
static_cast<size_t>(std::min(src.bytesPerLine(), out.bytesPerLine()));
|
||||
for (int y = 0; y < src.height(); ++y) {
|
||||
std::memcpy(out.scanLine(y), src.constScanLine(y), rowBytes);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
inline QByteArray imageCacheBytes(const QString& path, const QSize& fit) {
|
||||
QImageReader reader(path);
|
||||
reader.setAutoTransform(true);
|
||||
|
||||
const QSize source = reader.size();
|
||||
const QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target != source && !target.isEmpty()) {
|
||||
reader.setScaledSize(target);
|
||||
}
|
||||
|
||||
QImage image = reader.read();
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 90)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
|
||||
auto runProcess = [](const QString& program, const QStringList& args) {
|
||||
QProcess process;
|
||||
process.start(program, args);
|
||||
if (!process.waitForFinished(15000)) {
|
||||
process.kill();
|
||||
process.waitForFinished(1000);
|
||||
}
|
||||
return qMakePair(process.exitCode(), process.readAllStandardOutput());
|
||||
};
|
||||
|
||||
double duration = 0.0;
|
||||
const auto probe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
"format=duration",
|
||||
"-of",
|
||||
"default=nw=1:nk=1",
|
||||
src,
|
||||
});
|
||||
if (probe.first == 0) {
|
||||
duration = QString::fromUtf8(probe.second).trimmed().toDouble();
|
||||
}
|
||||
|
||||
QSize source;
|
||||
const auto resProbe = runProcess(
|
||||
"ffprobe",
|
||||
{
|
||||
"-v",
|
||||
"error",
|
||||
"-select_streams",
|
||||
"v:0",
|
||||
"-show_entries",
|
||||
"stream=width,height",
|
||||
"-of",
|
||||
"csv=s=x:p=0",
|
||||
src,
|
||||
});
|
||||
if (resProbe.first == 0) {
|
||||
const auto parts =
|
||||
QString::fromUtf8(resProbe.second).trimmed().split('x');
|
||||
if (parts.size() == 2) {
|
||||
source = QSize(parts[0].toInt(), parts[1].toInt());
|
||||
}
|
||||
}
|
||||
|
||||
const int samples = duration > 0 ? std::min(10, std::max(4, int(duration)))
|
||||
: 8;
|
||||
|
||||
QSize target = coverSizeNoUpscale(source, fit);
|
||||
if (target.isEmpty()) {
|
||||
target = fit;
|
||||
}
|
||||
|
||||
target.setWidth(target.width() & ~1);
|
||||
target.setHeight(target.height() & ~1);
|
||||
|
||||
const QString scale =
|
||||
QStringLiteral(
|
||||
"scale=%1:%2:force_original_aspect_ratio=decrease,"
|
||||
"scale=trunc(iw/2)*2:trunc(ih/2)*2")
|
||||
.arg(target.width())
|
||||
.arg(target.height());
|
||||
|
||||
QTemporaryDir tmpDir;
|
||||
if (!tmpDir.isValid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
int bestIndex = -1;
|
||||
double bestChroma = -1.0;
|
||||
double bestLuma = -1.0;
|
||||
|
||||
for (int i = 0; i < samples; ++i) {
|
||||
const double frac = duration > 0 ? (i + 0.5) / samples : 0.0;
|
||||
const QString seek = duration > 0
|
||||
? QString::number(frac * duration, 'f', 3)
|
||||
: QStringLiteral("0");
|
||||
|
||||
const QString framePath =
|
||||
tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(i));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
seek,
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QImage frame(framePath);
|
||||
if (frame.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
frame = frame.convertToFormat(QImage::Format_RGB888);
|
||||
|
||||
double chroma = 0.0;
|
||||
double luma = 0.0;
|
||||
int count = 0;
|
||||
for (int y = 0; y < frame.height(); ++y) {
|
||||
const auto* line = frame.constScanLine(y);
|
||||
for (int x = 0; x < frame.width(); ++x) {
|
||||
const int r = line[x * 3];
|
||||
const int g = line[x * 3 + 1];
|
||||
const int b = line[x * 3 + 2];
|
||||
chroma += std::max({r, g, b}) - std::min({r, g, b});
|
||||
luma += r + g + b;
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
const int n = std::max(1, count);
|
||||
chroma /= n;
|
||||
luma /= 3.0 * n;
|
||||
|
||||
if (chroma > bestChroma + 0.5 ||
|
||||
(chroma > bestChroma - 0.5 && luma > bestLuma)) {
|
||||
bestChroma = chroma;
|
||||
bestLuma = luma;
|
||||
bestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex < 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QFile frame(tmpDir.filePath(QStringLiteral("frame_%1.jpg").arg(bestIndex)));
|
||||
if (!frame.open(QIODevice::ReadOnly)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return frame.readAll();
|
||||
}
|
||||
|
||||
inline QByteArray schemeThumbBytes(const QByteArray& source, int maxSide) {
|
||||
QImage image = QImage::fromData(source, "JPEG");
|
||||
if (image.isNull()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (image.width() > maxSide || image.height() > maxSide) {
|
||||
image = image.scaled(
|
||||
maxSide, maxSide, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
}
|
||||
|
||||
image = cleanImage(image.convertToFormat(QImage::Format_RGB888));
|
||||
|
||||
QByteArray bytes;
|
||||
QBuffer buffer(&bytes);
|
||||
if (!buffer.open(QIODevice::WriteOnly) ||
|
||||
!image.save(&buffer, "JPEG", 85)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ZShell::models
|
||||
Reference in New Issue
Block a user