feat: add wallpaper transitions, store crop in database per-wallpaper, move CUDA context creation to async thread
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 9s
C++ / clang-tidy (pull_request) Failing after 10s
JS/TS / fmt (pull_request) Successful in 10s
JS/TS / lint (pull_request) Successful in 8s
Python / static (pull_request) Failing after 28s
Python / verify (pull_request) Failing after 1m29s
Rust / build (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 42s
Rust / clippy (pull_request) Successful in 1m4s
This commit is contained in:
@@ -8,6 +8,8 @@
|
||||
#include <qfuturewatcher.h>
|
||||
#include <qimage.h>
|
||||
#include <qimagereader.h>
|
||||
#include <qjsondocument.h>
|
||||
#include <qjsonobject.h>
|
||||
#include <qlogging.h>
|
||||
#include <qqmlengine.h>
|
||||
#include <qsqldatabase.h>
|
||||
@@ -343,8 +345,8 @@ void WallpaperDb::setCacheSize(const QSize& cacheSize) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL"
|
||||
" WHERE path = ?");
|
||||
"UPDATE wallpapers SET cache = NULL, scheme_thumb = NULL,"
|
||||
" poster = NULL WHERE path = ?");
|
||||
query.addBindValue(entry->path());
|
||||
query.exec();
|
||||
}
|
||||
@@ -413,7 +415,8 @@ void WallpaperDb::openDb() {
|
||||
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)");
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||
" poster BLOB, crop TEXT)");
|
||||
|
||||
query.exec("SELECT cache FROM wallpapers LIMIT 1");
|
||||
if (query.lastError().isValid()) {
|
||||
@@ -421,9 +424,12 @@ void WallpaperDb::openDb() {
|
||||
query.exec(
|
||||
"CREATE TABLE wallpapers("
|
||||
"path TEXT PRIMARY KEY, kind TEXT, codec TEXT, hardware INTEGER,"
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB)");
|
||||
" size INTEGER, mtime INTEGER, cache BLOB, scheme_thumb BLOB,"
|
||||
" poster BLOB, crop TEXT)");
|
||||
} else {
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN scheme_thumb BLOB");
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN poster BLOB");
|
||||
query.exec("ALTER TABLE wallpapers ADD COLUMN crop TEXT");
|
||||
}
|
||||
|
||||
registerProvider();
|
||||
@@ -538,6 +544,7 @@ void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
}
|
||||
|
||||
delete it.value();
|
||||
m_crops.remove(it.key());
|
||||
it = m_byPath.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -575,24 +582,28 @@ void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"SELECT cache, scheme_thumb, size, mtime FROM wallpapers"
|
||||
" WHERE path = ?");
|
||||
"SELECT cache, scheme_thumb, poster, size, mtime, crop"
|
||||
" 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();
|
||||
const auto poster = query.value(2).toByteArray();
|
||||
const auto storedSize = query.value(3).toLongLong();
|
||||
const auto storedMtime = query.value(4).toLongLong();
|
||||
loadCrop(path, query.value(5).toString());
|
||||
|
||||
const bool posterOk = !isVideo || !poster.isEmpty();
|
||||
|
||||
if (storedSize == size && storedMtime == mtime &&
|
||||
!cache.isEmpty()) {
|
||||
reused = true;
|
||||
entry->setThumbnailPath(thumbnailUrl(path));
|
||||
|
||||
if (scheme.isEmpty()) {
|
||||
queueMetadata(entry, isVideo, cache);
|
||||
if (scheme.isEmpty() || !posterOk) {
|
||||
queueMetadata(entry, isVideo, cache, poster);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -602,9 +613,13 @@ void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(
|
||||
"INSERT OR REPLACE INTO wallpapers"
|
||||
"INSERT INTO wallpapers"
|
||||
" (path, kind, hardware, size, mtime, cache)"
|
||||
" VALUES (?, ?, ?, ?, ?, NULL)");
|
||||
" VALUES (?, ?, ?, ?, ?, NULL)"
|
||||
" ON CONFLICT(path) DO UPDATE SET kind = excluded.kind,"
|
||||
" hardware = excluded.hardware, size = excluded.size,"
|
||||
" mtime = excluded.mtime, cache = NULL,"
|
||||
" scheme_thumb = NULL, poster = NULL");
|
||||
query.addBindValue(path);
|
||||
query.addBindValue(isVideo ? "video" : "image");
|
||||
query.addBindValue(
|
||||
@@ -626,7 +641,10 @@ void WallpaperDb::applyScan(const ScanResult& result) {
|
||||
}
|
||||
|
||||
void WallpaperDb::queueMetadata(
|
||||
WallpaperEntry* entry, bool isVideo, const QByteArray& existingCache) {
|
||||
WallpaperEntry* entry,
|
||||
bool isVideo,
|
||||
const QByteArray& existingCache,
|
||||
const QByteArray& existingPoster) {
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: queued cache job for %s (%s%s)",
|
||||
@@ -634,7 +652,8 @@ void WallpaperDb::queueMetadata(
|
||||
isVideo ? "video" : "image",
|
||||
existingCache.isEmpty() ? "" : ", existing blob");
|
||||
|
||||
m_queue.append(MetadataJob{entry, entry->path(), isVideo, existingCache});
|
||||
m_queue.append(
|
||||
MetadataJob{entry, entry->path(), isVideo, existingCache, existingPoster});
|
||||
|
||||
if (!m_queueTimer.isActive()) {
|
||||
m_queueTimer.start();
|
||||
@@ -672,10 +691,12 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
struct CacheResult {
|
||||
QString path;
|
||||
QByteArray cache;
|
||||
QByteArray poster;
|
||||
QByteArray scheme;
|
||||
};
|
||||
|
||||
const QSize cacheSize = m_cacheSize;
|
||||
const bool isVideoJob = job.isVideo;
|
||||
|
||||
QElapsedTimer clock;
|
||||
clock.start();
|
||||
@@ -685,7 +706,7 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
watcher,
|
||||
&QFutureWatcher<CacheResult>::finished,
|
||||
this,
|
||||
[this, watcher, clock]() {
|
||||
[this, watcher, clock, isVideoJob]() {
|
||||
const auto result = watcher->result();
|
||||
|
||||
qCInfo(
|
||||
@@ -695,15 +716,27 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
int(result.cache.size()),
|
||||
int(clock.elapsed()));
|
||||
|
||||
qCInfo(
|
||||
dbLog,
|
||||
"WallpaperDb: %s poster %d bytes",
|
||||
result.path.toUtf8().constData(),
|
||||
int(result.poster.size()));
|
||||
|
||||
if (isVideoJob && result.poster.isEmpty()) {
|
||||
qWarning(dbLog)
|
||||
<< "WallpaperDb: no poster produced for" << result.path;
|
||||
}
|
||||
|
||||
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 = ?");
|
||||
"UPDATE wallpapers SET cache = ?, scheme_thumb = ?,"
|
||||
" poster = ? WHERE path = ?");
|
||||
query.addBindValue(result.cache);
|
||||
query.addBindValue(result.scheme);
|
||||
query.addBindValue(result.poster);
|
||||
query.addBindValue(result.path);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog)
|
||||
@@ -736,7 +769,14 @@ void WallpaperDb::runMetadataJob(const MetadataJob& job) {
|
||||
: imageCacheBytes(job.path, cacheSize);
|
||||
}
|
||||
|
||||
QByteArray poster = job.existingPoster;
|
||||
if (poster.isEmpty()) {
|
||||
poster =
|
||||
job.isVideo ? videoPosterBytes(job.path, cacheSize) : cache;
|
||||
}
|
||||
|
||||
result.cache = cache;
|
||||
result.poster = poster;
|
||||
result.scheme = cache.isEmpty() ? QByteArray()
|
||||
: schemeThumbBytes(cache, 128);
|
||||
|
||||
@@ -781,4 +821,56 @@ bool WallpaperDb::entryAllowed(const WallpaperEntry* entry) const {
|
||||
return entry->hardware();
|
||||
}
|
||||
|
||||
QVariantMap WallpaperDb::crop(const QString& path, const QString& screen) const {
|
||||
const auto pathCrops = m_crops.constFind(path);
|
||||
if (pathCrops == m_crops.constEnd()) return {};
|
||||
return pathCrops.value().value(screen).toMap();
|
||||
}
|
||||
|
||||
void WallpaperDb::setCrop(
|
||||
const QString& path, const QString& screen, const QVariantMap& crop) {
|
||||
if (path.isEmpty() || screen.isEmpty()) return;
|
||||
|
||||
QVariantMap& pathCrops = m_crops[path];
|
||||
pathCrops[screen] = crop;
|
||||
storeCropJson(path);
|
||||
emit cropsChanged(path);
|
||||
}
|
||||
|
||||
void WallpaperDb::loadCrop(const QString& path, const QString& json) {
|
||||
if (json.isEmpty()) {
|
||||
m_crops.remove(path);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto doc = QJsonDocument::fromJson(json.toUtf8());
|
||||
if (!doc.isObject()) {
|
||||
m_crops.remove(path);
|
||||
return;
|
||||
}
|
||||
|
||||
m_crops[path] = doc.object().toVariantMap();
|
||||
}
|
||||
|
||||
void WallpaperDb::storeCropJson(const QString& path) {
|
||||
const auto db = QSqlDatabase::database(m_uuid, false);
|
||||
if (!db.isOpen()) return;
|
||||
|
||||
const auto pathCrops = m_crops.value(path);
|
||||
const QByteArray json =
|
||||
pathCrops.isEmpty()
|
||||
? QByteArray()
|
||||
: QJsonDocument(QJsonObject::fromVariantMap(pathCrops))
|
||||
.toJson(QJsonDocument::Compact);
|
||||
|
||||
QSqlQuery query(db);
|
||||
query.prepare("UPDATE wallpapers SET crop = ? WHERE path = ?");
|
||||
query.addBindValue(json.isEmpty() ? QVariant() : QVariant(json));
|
||||
query.addBindValue(path);
|
||||
if (!query.exec()) {
|
||||
qWarning(dbLog) << "WallpaperDb: could not store the crop for" << path
|
||||
<< query.lastError();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ZShell::models
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <qqmllist.h>
|
||||
#include <qsize.h>
|
||||
#include <qtimer.h>
|
||||
#include <qvariant.h>
|
||||
#include <qvector.h>
|
||||
|
||||
namespace ZShell::models {
|
||||
@@ -109,6 +110,11 @@ class WallpaperDb : public QObject {
|
||||
|
||||
[[nodiscard]] static QString thumbnailUrl(const QString& path);
|
||||
|
||||
Q_INVOKABLE QVariantMap crop(const QString& path, const QString& screen)
|
||||
const;
|
||||
Q_INVOKABLE void setCrop(
|
||||
const QString& path, const QString& screen, const QVariantMap& crop);
|
||||
|
||||
signals:
|
||||
void pathChanged();
|
||||
void dbPathChanged();
|
||||
@@ -117,6 +123,7 @@ class WallpaperDb : public QObject {
|
||||
void cacheSizeChanged();
|
||||
|
||||
void cacheStored(const QString& path);
|
||||
void cropsChanged(const QString& path);
|
||||
|
||||
private:
|
||||
struct FileEntry {
|
||||
@@ -134,6 +141,7 @@ class WallpaperDb : public QObject {
|
||||
QString path;
|
||||
bool isVideo;
|
||||
QByteArray existingCache;
|
||||
QByteArray existingPoster;
|
||||
};
|
||||
|
||||
void openDb();
|
||||
@@ -144,11 +152,15 @@ class WallpaperDb : public QObject {
|
||||
void queueMetadata(
|
||||
WallpaperEntry* entry,
|
||||
bool isVideo,
|
||||
const QByteArray& existingCache = {});
|
||||
const QByteArray& existingCache = {},
|
||||
const QByteArray& existingPoster = {});
|
||||
void processQueue();
|
||||
void runMetadataJob(const MetadataJob& job);
|
||||
void rebuildEntries();
|
||||
|
||||
void loadCrop(const QString& path, const QString& json);
|
||||
void storeCropJson(const QString& path);
|
||||
|
||||
[[nodiscard]] bool entryAllowed(const WallpaperEntry* entry) const;
|
||||
|
||||
QDir m_dir;
|
||||
@@ -163,6 +175,8 @@ class WallpaperDb : public QObject {
|
||||
QList<WallpaperEntry*> m_entries;
|
||||
QHash<QString, WallpaperEntry*> m_byPath;
|
||||
|
||||
QHash<QString, QVariantMap> m_crops;
|
||||
|
||||
QVector<MetadataJob> m_queue;
|
||||
QTimer m_queueTimer;
|
||||
bool m_jobRunning = false;
|
||||
|
||||
@@ -285,6 +285,91 @@ inline QByteArray videoCacheBytes(const QString& src, const QSize& fit) {
|
||||
return frame.readAll();
|
||||
}
|
||||
|
||||
inline QByteArray videoPosterBytes(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());
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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 {};
|
||||
}
|
||||
|
||||
const QString framePath = tmpDir.filePath(QStringLiteral("poster.jpg"));
|
||||
|
||||
runProcess(
|
||||
"ffmpeg",
|
||||
{
|
||||
"-hide_banner",
|
||||
"-loglevel",
|
||||
"error",
|
||||
"-ss",
|
||||
"0",
|
||||
"-i",
|
||||
src,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-vf",
|
||||
scale,
|
||||
"-pix_fmt",
|
||||
"yuvj420p",
|
||||
"-q:v",
|
||||
"2",
|
||||
"-y",
|
||||
framePath,
|
||||
});
|
||||
|
||||
QFile frame(framePath);
|
||||
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()) {
|
||||
|
||||
Reference in New Issue
Block a user