Compare commits
10 Commits
66db6a9d5b
...
3156e46c62
| Author | SHA1 | Date | |
|---|---|---|---|
|
3156e46c62
|
|||
|
23c80d30e9
|
|||
|
11c9c2f88d
|
|||
|
b06d27cecf
|
|||
|
5df0b53df0
|
|||
|
bf2f3d57c7
|
|||
|
1e9c175dd5
|
|||
|
da515566cb
|
|||
|
a6caa0c950
|
|||
|
807278d748
|
@@ -81,3 +81,5 @@ CMakeLists.txt.user*
|
||||
.uic/
|
||||
/build*/
|
||||
.cache
|
||||
|
||||
.vscode
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
It might not be that worthy to build a Qt application from ground for such a small feature, but I kind of enjoy the pain... So here it is.
|
||||
|
||||
<img src="https://io.uyani.de/s/s6t5JDMEfqZmADB/preview"/>
|
||||
<img src="https://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/misc/screenshot.webp"/>
|
||||
|
||||
## How to build
|
||||
|
||||
@@ -50,14 +50,16 @@ It might not be that worthy to build a Qt application from ground for such a sma
|
||||
Install it to the previously specified prefix. This step may require root permissions if the install prefix is set to a system directory like `/usr/local`.
|
||||
|
||||
```bash
|
||||
cmake --install build
|
||||
cmake --install build --strip
|
||||
```
|
||||
|
||||
`--strip` option is used to reduce the binary size by removing symbol information, which is generally not needed for normal usage.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
Refer to [config.schema.json](config.schema.json) for a complete reference of the configuration file schema. Below is a summary of the available options.
|
||||
|
||||
The configuration file is divided into five main sections: `wallpaper`, `theme`, `action`, `style`, and `sort`.
|
||||
The configuration file is divided into five main sections: `wallpaper`, `theme`, `action`, `style`, and `cache`.
|
||||
|
||||
### Wallpaper (`wallpaper`)
|
||||
|
||||
@@ -78,8 +80,7 @@ By default, a **dominant color** will be extracted from each wallpaper. If a pal
|
||||
There are a few embeded palettes available in the application, including "Catppuccin Frappe", "Catppuccin Latte", "Catppuccin Macchiato", and "Catppuccin Mocha". You can also define your own palettes or override the embeded ones by providing a custom configuration.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| :--------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `defaultPalette` | String | `""` | Name of the default palette to use. |
|
||||
| :--------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `palettes` | Array of Objects | `[]` | List of defined palettes. Each contains a `name` (string) and an array of `colors` (each with a `name` and a hex `value` like `"#ff0000"`). |
|
||||
|
||||
### Action (`action`)
|
||||
@@ -87,13 +88,13 @@ There are a few embeded palettes available in the application, including "Catppu
|
||||
Configures system commands to execute on specific events mapping to your window manager or wallpaper utility (e.g., `swaybg`, `feh`).
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. |
|
||||
| `printSelected` | Boolean | `true` | Print selected wallpaper path to stdout on confirm. |
|
||||
| `printPreview` | Boolean | `false` | Print previewed wallpaper path to stdout on preview. |
|
||||
| `onSelected` | String | `""` | Command to execute when a wallpaper is confirmed. |
|
||||
| `onPreview` | String | `""` | Command to execute when a wallpaper is previewed. |
|
||||
| `saveState` | Array of Objects | `[]` | Commands to fetch system states before changing wallpapers. Each object defines: `key`, `default` (fallback value), `command` (stdout mapping), and `timeout` (ms). |
|
||||
| `saveState` | Array of Objects | `[]` | Commands to fetch system states before changing wallpapers. Each object defines: `key`, `fallback` (fallback value), `command` (stdout mapping), and `timeout` (ms). |
|
||||
| `onRestore` | String | `""` | Command to execute on restore. Extracted states from `saveState` can be injected using `{{ key }}`. |
|
||||
| `quitOnSelected` | Boolean | `false` | Quit the application after a selection is made. |
|
||||
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
|
||||
@@ -109,7 +110,7 @@ Available placeholders for `onSelected`, `onPreview` commands:
|
||||
| `{{ colorName }}` | Name of the currently determined primary color. ("null" if none) |
|
||||
| `{{ colorHex }}` | Hex code (starting with "#") of the currently determined primary color. ("null" if none) |
|
||||
| `{{ domColorHex }}` | Hex code (starting with "#") of the dominant color in the selected or previewed wallpaper. |
|
||||
| `{{ key }}` | Value of the saved state with the specified key. |
|
||||
| `{{ <key> }}` | Value of the saved state with the specified key. |
|
||||
|
||||
### Style (`style`)
|
||||
|
||||
@@ -123,14 +124,15 @@ Controls the layout and dimensions of the application window and image items.
|
||||
| `window_width` | Integer | `750` | Initial application window width. |
|
||||
| `window_height` | Integer | `500` | Initial application window height. |
|
||||
|
||||
### Sort (`sort`)
|
||||
### Cache (`cache`)
|
||||
|
||||
Initial sorting behavior for loaded images.
|
||||
Controls what UI state is persisted between sessions.
|
||||
|
||||
| Property | Type | Default | Description |
|
||||
| :----------- | :------ | :------- | :------------------------------------------------------------------------------- |
|
||||
| `type` | String | `"date"` | Defines sorting criteria. Acceptable values: `"name"`, `"date"`, `"size"`. |
|
||||
| `descending` | Boolean | `true` | If true, sorts in descending order (e.g. newer dates first, larger files first). |
|
||||
| :---------------- | :------ | :------ | :---------------------------------------------------------------------------- |
|
||||
| `saveSortMethod` | Boolean | `true` | Whether to persist the sort type and order. |
|
||||
| `savePalette` | Boolean | `true` | Whether to persist the selected palette. |
|
||||
| `maxImageEntries` | Integer | `1000` | Maximum number of entries in the image cache (older entries will be evicted). |
|
||||
|
||||
---
|
||||
|
||||
@@ -150,7 +152,6 @@ Initial sorting behavior for loaded images.
|
||||
"excludes": ["\\.gif$"]
|
||||
},
|
||||
"theme": {
|
||||
"defaultPalette": "Dark",
|
||||
"palettes": [
|
||||
{
|
||||
"name": "Dark",
|
||||
@@ -169,7 +170,7 @@ Initial sorting behavior for loaded images.
|
||||
"saveState": [
|
||||
{
|
||||
"key": "current_wp",
|
||||
"default": "/home/user/Pictures/default.jpg",
|
||||
"fallback": "/home/user/Pictures/default.jpg",
|
||||
"command": "find ~/.config/wallpaper/current -type f | head -n 1",
|
||||
"timeout": 1000
|
||||
}
|
||||
@@ -183,9 +184,10 @@ Initial sorting behavior for loaded images.
|
||||
"window_width": 1280,
|
||||
"window_height": 720
|
||||
},
|
||||
"sort": {
|
||||
"type": "date",
|
||||
"descending": true
|
||||
"cache": {
|
||||
"saveSortMethod": true,
|
||||
"savePalette": true,
|
||||
"maxImageEntries": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -203,6 +205,7 @@ Options:
|
||||
-q, --quiet Suppress all log output
|
||||
-d, --append-dir <dir> Append an additional wallpaper search directory
|
||||
-c, --config-file <file> Specify a custom configuration file
|
||||
-a, --apply <file> Apply the specified image as wallpaper and exit
|
||||
```
|
||||
|
||||
A few things to notice:
|
||||
@@ -212,3 +215,5 @@ A few things to notice:
|
||||
- The `--append-dir` option can be used multiple times to add multiple directories.
|
||||
|
||||
- It is quite obvious that some options conflicts with each other (e.g. `--verbose` and `--quiet`). Case mutually exclusive options are provided together, the behavior is un.. just please, don't do that.
|
||||
|
||||
- Given `--apply`, the config file (default or specified with `--config-file`) will be parsed, and the `onSelected` action will be executed with the properties of the specified image available for placeholders. If `savePalette` is enabled and a palette is selected in the last session, `palette`, `colorName` and `colorHex` placeholders will also be available. `saveState` commands will also be executed to fetch states for placeholders. After the action is executed, the application will exit immediately without showing the UI. This allows you to use WallReel as a command-line wallpaper setter that also supports palette-based theming and state management.
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
qt_add_resources(${EXECUTABLE_NAME} "app_icons"
|
||||
PREFIX "/"
|
||||
FILES
|
||||
${EXECUTABLE_NAME}.svg
|
||||
FILES icon.svg
|
||||
)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.desktop
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/app.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
||||
RENAME ${EXECUTABLE_NAME}.desktop
|
||||
)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.svg
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/apply.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
||||
RENAME ${EXECUTABLE_NAME}-apply.desktop
|
||||
)
|
||||
|
||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/icon.svg
|
||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
|
||||
RENAME ${EXECUTABLE_NAME}.svg
|
||||
)
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Apply with WallReel
|
||||
Icon=wallreel
|
||||
NoDisplay=true
|
||||
GenericName=Animated wallpaper selector
|
||||
TryExec=wallreel
|
||||
Exec=wallreel -a %f
|
||||
Comment=A small wallpaper utility made with Qt
|
||||
Terminal=false
|
||||
Categories=Application;Utility;DesktopSettings;
|
||||
StartupNotify=true
|
||||
Keywords=wallpaper;animated;utility;qt;
|
||||
MimeType=image/jpeg;image/png;image/webp;image/bmp;image/gif;image/heic;image/heif;
|
||||
|
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 326 B |
@@ -15,7 +15,7 @@ qt_add_qml_module(${CORELIB_NAME}
|
||||
Config/data.hpp
|
||||
Config/manager.hpp Config/manager.cpp
|
||||
logger.hpp logger.cpp
|
||||
Service/manager.hpp
|
||||
Service/manager.hpp Service/manager.cpp
|
||||
Service/wallpaper.hpp Service/wallpaper.cpp
|
||||
appoptions.hpp appoptions.cpp
|
||||
)
|
||||
|
||||
+289
-28
@@ -7,6 +7,7 @@
|
||||
#include <QSqlError>
|
||||
#include <QSqlQuery>
|
||||
#include <QThread>
|
||||
#include <QtConcurrent>
|
||||
|
||||
#include "logger.hpp"
|
||||
|
||||
@@ -16,21 +17,51 @@ using namespace Qt::StringLiterals;
|
||||
|
||||
namespace WallReel::Core::Cache {
|
||||
|
||||
static QLatin1StringView settingKey(SettingsType type) {
|
||||
switch (type) {
|
||||
case SettingsType::LastSelectedPalette: return "last_selected_palette"_L1;
|
||||
case SettingsType::LastSortType: return "last_sort_type"_L1;
|
||||
case SettingsType::LastSortDescending: return "last_sort_descending"_L1;
|
||||
}
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
|
||||
QString Manager::cacheKey(const QFileInfo& fileInfo, const QSize& imageSize) {
|
||||
const QString raw = fileInfo.absoluteFilePath() + QString::number(fileInfo.lastModified().toMSecsSinceEpoch()) + u'x' + QString::number(imageSize.width()) + u'x' + QString::number(imageSize.height());
|
||||
const QString raw = fileInfo.absoluteFilePath() +
|
||||
QString::number(fileInfo.lastModified().toMSecsSinceEpoch()) +
|
||||
u'x' + QString::number(imageSize.width()) +
|
||||
u'x' + QString::number(imageSize.height());
|
||||
return QString::fromLatin1(
|
||||
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
|
||||
}
|
||||
|
||||
Manager::Manager(const QDir& cacheDir)
|
||||
: m_cacheDir(cacheDir), m_dbPath(cacheDir.filePath(u"cache.db"_s)), m_connectionPrefix(u"WallReelCache:"_s + QString::fromLatin1(QCryptographicHash::hash(m_dbPath.toUtf8(), QCryptographicHash::Md5).toHex())) {
|
||||
Manager::Manager(const QDir& cacheDir, int maxEntries)
|
||||
: m_cacheDir(cacheDir),
|
||||
m_maxEntries(maxEntries),
|
||||
m_dbPath(cacheDir.filePath(u"cache.db"_s)),
|
||||
m_connectionPrefix(u"WallReelCache:"_s +
|
||||
QString::fromLatin1(QCryptographicHash::hash(
|
||||
m_dbPath.toUtf8(),
|
||||
QCryptographicHash::Md5)
|
||||
.toHex())) {
|
||||
WR_DEBUG(u"Initializing cache db: %1"_s.arg(m_dbPath));
|
||||
// Open a connection on the constructing thread so the schema is
|
||||
// guaranteed to exist before any worker thread first calls _db().
|
||||
_db();
|
||||
}
|
||||
|
||||
void Manager::evictOldEntries() {
|
||||
if (m_maxEntries > 0)
|
||||
m_cleanupFuture = QtConcurrent::run([this] { _runCleanup(); });
|
||||
}
|
||||
|
||||
Manager::~Manager() {
|
||||
// Wait for the background cleanup to finish before tearing down DB connections.
|
||||
if (m_cleanupFuture.isValid() && !m_cleanupFuture.isFinished()) {
|
||||
WR_DEBUG(u"Waiting for cache cleanup to finish..."_s);
|
||||
m_cleanupFuture.waitForFinished();
|
||||
}
|
||||
|
||||
QSet<QString> names;
|
||||
{
|
||||
QMutexLocker lock(&m_connectionsMutex);
|
||||
@@ -50,41 +81,59 @@ void Manager::clearCache(Type type) {
|
||||
if ((type & Type::Image) != Type::None) {
|
||||
int removed = 0;
|
||||
QSqlQuery selectQuery(db);
|
||||
if (selectQuery.exec(QStringLiteral("SELECT file_name FROM image_cache"))) {
|
||||
if (selectQuery.exec(u"SELECT file_name FROM image_cache"_s)) {
|
||||
while (selectQuery.next()) {
|
||||
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
|
||||
++removed;
|
||||
}
|
||||
}
|
||||
QSqlQuery(db).exec(QStringLiteral("DELETE FROM image_cache"));
|
||||
QSqlQuery(db).exec(u"DELETE FROM image_cache"_s);
|
||||
WR_INFO(u"Cleared %1 image cache file(s)"_s.arg(removed));
|
||||
}
|
||||
|
||||
if ((type & Type::Color) != Type::None) {
|
||||
QSqlQuery(db).exec(QStringLiteral("DELETE FROM color_cache"));
|
||||
QSqlQuery(db).exec(u"DELETE FROM color_cache"_s);
|
||||
WR_INFO(u"Cleared color cache"_s);
|
||||
}
|
||||
|
||||
if ((type & Type::Settings) != Type::None) {
|
||||
QSqlQuery(db).exec(u"DELETE FROM settings_cache"_s);
|
||||
WR_INFO(u"Cleared settings cache"_s);
|
||||
}
|
||||
}
|
||||
|
||||
QColor Manager::getColor(const QString& key, const std::function<QColor()>& computeFunc) {
|
||||
QSqlDatabase db = _db();
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT r, g, b, a FROM color_cache WHERE key = :key"));
|
||||
query.prepare(u"SELECT r, g, b, a FROM color_cache WHERE key = :key"_s);
|
||||
query.bindValue(u":key"_s, key);
|
||||
|
||||
if (query.exec() && query.next()) {
|
||||
WR_DEBUG(u"Color cache hit [%1]"_s.arg(key));
|
||||
return QColor(
|
||||
QColor result(
|
||||
query.value(0).toInt(),
|
||||
query.value(1).toInt(),
|
||||
query.value(2).toInt(),
|
||||
query.value(3).toInt());
|
||||
{
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
m_hotColorKeys.insert(key);
|
||||
}
|
||||
QSqlQuery touchQuery(db);
|
||||
touchQuery.prepare(u"UPDATE color_cache SET last_accessed = CURRENT_TIMESTAMP WHERE key = :key"_s);
|
||||
touchQuery.bindValue(u":key"_s, key);
|
||||
touchQuery.exec();
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
WR_DEBUG(u"Color cache miss [%1], computing"_s.arg(key));
|
||||
if (!computeFunc) {
|
||||
WR_WARN(u"No compute function provided for color cache miss [%1]"_s.arg(key));
|
||||
return QColor();
|
||||
}
|
||||
|
||||
const QColor color = computeFunc();
|
||||
|
||||
if (!color.isValid()) {
|
||||
@@ -94,9 +143,9 @@ QColor Manager::getColor(const QString& key, const std::function<QColor()>& comp
|
||||
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery insertQuery(db);
|
||||
insertQuery.prepare(QStringLiteral(
|
||||
"INSERT OR REPLACE INTO color_cache (key, r, g, b, a) "
|
||||
"VALUES (:key, :r, :g, :b, :a)"));
|
||||
insertQuery.prepare(
|
||||
u"INSERT OR REPLACE INTO color_cache (key, r, g, b, a, last_accessed) "
|
||||
"VALUES (:key, :r, :g, :b, :a, CURRENT_TIMESTAMP)"_s);
|
||||
insertQuery.bindValue(u":key"_s, key);
|
||||
insertQuery.bindValue(u":r"_s, color.red());
|
||||
insertQuery.bindValue(u":g"_s, color.green());
|
||||
@@ -116,8 +165,7 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
|
||||
QSqlDatabase db = _db();
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(QStringLiteral(
|
||||
"SELECT file_name FROM image_cache WHERE key = :key"));
|
||||
query.prepare(u"SELECT file_name FROM image_cache WHERE key = :key"_s);
|
||||
query.bindValue(u":key"_s, key);
|
||||
|
||||
if (query.exec() && query.next()) {
|
||||
@@ -125,29 +173,42 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
|
||||
if (cached.exists()) {
|
||||
WR_DEBUG(u"Image cache hit [%1] -> %2"_s
|
||||
.arg(key, cached.absoluteFilePath()));
|
||||
{
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
m_hotImageKeys.insert(key);
|
||||
}
|
||||
QSqlQuery touchQuery(db);
|
||||
touchQuery.prepare(u"UPDATE image_cache SET last_accessed = CURRENT_TIMESTAMP WHERE key = :key"_s);
|
||||
touchQuery.bindValue(u":key"_s, key);
|
||||
touchQuery.exec();
|
||||
return cached;
|
||||
}
|
||||
|
||||
// File was deleted externally — evict the stale DB record.
|
||||
WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
|
||||
QSqlQuery evict(db);
|
||||
evict.prepare(QStringLiteral("DELETE FROM image_cache WHERE key = :key"));
|
||||
evict.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
|
||||
evict.bindValue(u":key"_s, key);
|
||||
evict.exec();
|
||||
}
|
||||
}
|
||||
|
||||
WR_DEBUG(u"Image cache miss [%1], computing"_s.arg(key));
|
||||
if (!computeFunc) {
|
||||
WR_WARN(u"No compute function provided for image cache miss [%1]"_s.arg(key));
|
||||
return QFileInfo{};
|
||||
}
|
||||
|
||||
const QImage image = computeFunc();
|
||||
if (image.isNull()) {
|
||||
WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
|
||||
return QFileInfo{};
|
||||
}
|
||||
|
||||
const QString fileName = key + u".png"_s;
|
||||
const QString fileName = key + u".jpg"_s;
|
||||
const QString filePath = m_cacheDir.filePath(fileName);
|
||||
|
||||
if (!image.save(filePath, "PNG")) {
|
||||
if (!image.save(filePath, "JPEG", 85)) {
|
||||
WR_WARN(u"Failed to save image to %1"_s.arg(filePath));
|
||||
return QFileInfo{};
|
||||
}
|
||||
@@ -155,19 +216,93 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
|
||||
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery insertQuery(db);
|
||||
insertQuery.prepare(QStringLiteral(
|
||||
"INSERT OR REPLACE INTO image_cache (key, file_name) "
|
||||
"VALUES (:key, :file_name)"));
|
||||
insertQuery.prepare(
|
||||
u"INSERT OR REPLACE INTO image_cache (key, file_name, last_accessed) "
|
||||
"VALUES (:key, :file_name, CURRENT_TIMESTAMP)"_s);
|
||||
insertQuery.bindValue(u":key"_s, key);
|
||||
insertQuery.bindValue(u":file_name"_s, fileName);
|
||||
if (!insertQuery.exec())
|
||||
WR_WARN(u"Failed to record image in db [%1]: %2"_s
|
||||
.arg(key, insertQuery.lastError().text()));
|
||||
else {
|
||||
QMutexLocker lock(&m_hotKeysMutex);
|
||||
m_hotImageKeys.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
return QFileInfo(filePath);
|
||||
}
|
||||
|
||||
QString Manager::getSetting(SettingsType key, const std::function<QString()>& computeFunc) {
|
||||
QSqlDatabase db = _db();
|
||||
const QLatin1StringView keyStr = settingKey(key);
|
||||
|
||||
if (db.isOpen()) {
|
||||
QSqlQuery query(db);
|
||||
query.prepare(u"SELECT value FROM settings_cache WHERE key = :key"_s);
|
||||
query.bindValue(u":key"_s, keyStr);
|
||||
|
||||
if (query.exec() && query.next()) {
|
||||
WR_DEBUG(u"Settings cache hit [%1]"_s.arg(keyStr));
|
||||
return query.value(0).toString();
|
||||
}
|
||||
}
|
||||
|
||||
WR_DEBUG(u"Settings cache miss [%1], computing"_s.arg(keyStr));
|
||||
if (!computeFunc) {
|
||||
WR_WARN(u"No compute function provided for settings cache miss [%1]"_s.arg(keyStr));
|
||||
return QString{};
|
||||
}
|
||||
|
||||
const QString value = computeFunc();
|
||||
|
||||
if (db.isOpen() && !value.isNull()) {
|
||||
QSqlQuery insertQuery(db);
|
||||
insertQuery.prepare(
|
||||
u"INSERT OR REPLACE INTO settings_cache (key, value) "
|
||||
"VALUES (:key, :value)"_s);
|
||||
insertQuery.bindValue(u":key"_s, keyStr);
|
||||
insertQuery.bindValue(u":value"_s, value);
|
||||
if (!insertQuery.exec())
|
||||
WR_WARN(u"Failed to cache setting [%1]: %2"_s
|
||||
.arg(keyStr, insertQuery.lastError().text()));
|
||||
else
|
||||
WR_DEBUG(u"Setting cached [%1]"_s.arg(keyStr));
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
void Manager::storeSetting(SettingsType key, const QString& value) {
|
||||
QSqlDatabase db = _db();
|
||||
const QLatin1StringView keyStr = settingKey(key);
|
||||
|
||||
if (db.isOpen()) {
|
||||
if (value.isNull()) {
|
||||
QSqlQuery deleteQuery(db);
|
||||
deleteQuery.prepare(u"DELETE FROM settings_cache WHERE key = :key"_s);
|
||||
deleteQuery.bindValue(u":key"_s, keyStr);
|
||||
if (!deleteQuery.exec())
|
||||
WR_WARN(u"Failed to delete setting [%1]: %2"_s
|
||||
.arg(keyStr, deleteQuery.lastError().text()));
|
||||
else
|
||||
WR_DEBUG(u"Setting deleted [%1]"_s.arg(keyStr));
|
||||
} else {
|
||||
QSqlQuery insertQuery(db);
|
||||
insertQuery.prepare(
|
||||
u"INSERT OR REPLACE INTO settings_cache (key, value) "
|
||||
"VALUES (:key, :value)"_s);
|
||||
insertQuery.bindValue(u":key"_s, keyStr);
|
||||
insertQuery.bindValue(u":value"_s, value);
|
||||
if (!insertQuery.exec())
|
||||
WR_WARN(u"Failed to store setting [%1]: %2"_s
|
||||
.arg(keyStr, insertQuery.lastError().text()));
|
||||
else
|
||||
WR_DEBUG(u"Setting stored [%1]"_s.arg(keyStr));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an open QSqlDatabase for the calling thread, creating it on first use.
|
||||
QSqlDatabase Manager::_db() const {
|
||||
// thread_local: one slot per OS thread, initialized on first call in that thread.
|
||||
@@ -225,19 +360,145 @@ QSqlDatabase Manager::_db() const {
|
||||
|
||||
void Manager::_setupTables(QSqlDatabase& db) const {
|
||||
QSqlQuery q(db);
|
||||
q.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS color_cache ("
|
||||
q.exec(
|
||||
u"CREATE TABLE IF NOT EXISTS color_cache ("
|
||||
" key TEXT PRIMARY KEY NOT NULL,"
|
||||
" r INTEGER NOT NULL,"
|
||||
" g INTEGER NOT NULL,"
|
||||
" b INTEGER NOT NULL,"
|
||||
" a INTEGER NOT NULL"
|
||||
")"));
|
||||
q.exec(QStringLiteral(
|
||||
"CREATE TABLE IF NOT EXISTS image_cache ("
|
||||
" a INTEGER NOT NULL,"
|
||||
" last_accessed TEXT"
|
||||
")"_s);
|
||||
q.exec(
|
||||
u"CREATE TABLE IF NOT EXISTS image_cache ("
|
||||
" key TEXT PRIMARY KEY NOT NULL,"
|
||||
" file_name TEXT NOT NULL"
|
||||
")"));
|
||||
" file_name TEXT NOT NULL,"
|
||||
" last_accessed TEXT"
|
||||
")"_s);
|
||||
q.exec(
|
||||
u"CREATE TABLE IF NOT EXISTS settings_cache ("
|
||||
" key TEXT PRIMARY KEY NOT NULL,"
|
||||
" value TEXT NOT NULL"
|
||||
");"_s);
|
||||
// Migrate existing databases that predate the last_accessed column.
|
||||
q.exec(u"ALTER TABLE color_cache ADD COLUMN last_accessed TEXT"_s);
|
||||
q.exec(u"ALTER TABLE image_cache ADD COLUMN last_accessed TEXT"_s);
|
||||
}
|
||||
|
||||
void Manager::_runCleanup() {
|
||||
WR_DEBUG(u"Cache cleanup started (maxEntries=%1)"_s.arg(m_maxEntries));
|
||||
|
||||
QSqlDatabase db = _db();
|
||||
if (!db.isOpen())
|
||||
return;
|
||||
|
||||
// Evict image_cache rows whose backing file no longer exists
|
||||
{
|
||||
QSqlQuery sel(db);
|
||||
if (sel.exec(u"SELECT key, file_name FROM image_cache"_s)) {
|
||||
struct Stale {
|
||||
QString key, fileName;
|
||||
};
|
||||
|
||||
QList<Stale> stale;
|
||||
while (sel.next()) {
|
||||
const QString k = sel.value(0).toString();
|
||||
const QString file = sel.value(1).toString();
|
||||
if (!QFileInfo::exists(m_cacheDir.filePath(file)))
|
||||
stale.push_back({k, file});
|
||||
}
|
||||
int evicted = 0;
|
||||
for (const auto& s : std::as_const(stale)) {
|
||||
{
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
if (m_hotImageKeys.contains(s.key))
|
||||
continue;
|
||||
}
|
||||
QSqlQuery del(db);
|
||||
del.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
|
||||
del.bindValue(u":key"_s, s.key);
|
||||
if (del.exec())
|
||||
++evicted;
|
||||
}
|
||||
if (evicted)
|
||||
WR_INFO(u"Cleanup evicted %1 stale image cache row(s)"_s.arg(evicted));
|
||||
}
|
||||
}
|
||||
|
||||
// Trim image_cache to m_maxEntries (oldest last_accessed first)
|
||||
{
|
||||
QSqlQuery countQ(db);
|
||||
if (countQ.exec(u"SELECT COUNT(*) FROM image_cache"_s) && countQ.next()) {
|
||||
int excess = countQ.value(0).toInt() - m_maxEntries;
|
||||
if (excess > 0) {
|
||||
QSqlQuery sel(db);
|
||||
sel.exec(u"SELECT key, file_name FROM image_cache ORDER BY last_accessed ASC"_s);
|
||||
QList<QPair<QString, QString>> toDelete;
|
||||
while (sel.next() && excess > 0) {
|
||||
const QString k = sel.value(0).toString();
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
if (!m_hotImageKeys.contains(k)) {
|
||||
toDelete.push_back({k, sel.value(1).toString()});
|
||||
--excess;
|
||||
}
|
||||
}
|
||||
int removed = 0;
|
||||
for (const auto& [k, fileName] : std::as_const(toDelete)) {
|
||||
{
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
if (m_hotImageKeys.contains(k))
|
||||
continue;
|
||||
}
|
||||
QFile::remove(m_cacheDir.filePath(fileName));
|
||||
QSqlQuery del(db);
|
||||
del.prepare(u"DELETE FROM image_cache WHERE key = :key"_s);
|
||||
del.bindValue(u":key"_s, k);
|
||||
if (del.exec())
|
||||
++removed;
|
||||
}
|
||||
if (removed)
|
||||
WR_INFO(u"Cleanup trimmed %1 image cache entry(ies)"_s.arg(removed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Trim color_cache to m_maxEntries (oldest last_accessed first)
|
||||
{
|
||||
QSqlQuery countQ(db);
|
||||
if (countQ.exec(u"SELECT COUNT(*) FROM color_cache"_s) && countQ.next()) {
|
||||
int excess = countQ.value(0).toInt() - m_maxEntries;
|
||||
if (excess > 0) {
|
||||
QSqlQuery sel(db);
|
||||
sel.exec(u"SELECT key FROM color_cache ORDER BY last_accessed ASC"_s);
|
||||
QStringList toDelete;
|
||||
while (sel.next() && excess > 0) {
|
||||
const QString k = sel.value(0).toString();
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
if (!m_hotColorKeys.contains(k)) {
|
||||
toDelete << k;
|
||||
--excess;
|
||||
}
|
||||
}
|
||||
int removed = 0;
|
||||
for (const QString& k : std::as_const(toDelete)) {
|
||||
{
|
||||
QMutexLocker lk(&m_hotKeysMutex);
|
||||
if (m_hotColorKeys.contains(k))
|
||||
continue;
|
||||
}
|
||||
QSqlQuery del(db);
|
||||
del.prepare(u"DELETE FROM color_cache WHERE key = :key"_s);
|
||||
del.bindValue(u":key"_s, k);
|
||||
if (del.exec())
|
||||
++removed;
|
||||
}
|
||||
if (removed)
|
||||
WR_INFO(u"Cleanup trimmed %1 color cache entry(ies)"_s.arg(removed));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WR_DEBUG(u"Cache cleanup complete"_s);
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core::Cache
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QFuture>
|
||||
#include <QMutex>
|
||||
#include <QSet>
|
||||
#include <QtSql>
|
||||
@@ -15,26 +16,40 @@ class Manager {
|
||||
public:
|
||||
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
|
||||
|
||||
Manager(const QDir& cacheDir);
|
||||
Manager(const QDir& cacheDir, int maxEntries = 1000);
|
||||
|
||||
~Manager();
|
||||
|
||||
void evictOldEntries();
|
||||
|
||||
void clearCache(Type type = Type::Image | Type::Color);
|
||||
|
||||
QColor getColor(const QString& key, const std::function<QColor()>& computeFunc);
|
||||
QColor getColor(const QString& key, const std::function<QColor()>& computeFunc = nullptr);
|
||||
|
||||
QFileInfo getImage(const QString& key, const std::function<QImage()>& computeFunc);
|
||||
QFileInfo getImage(const QString& key, const std::function<QImage()>& computeFunc = nullptr);
|
||||
|
||||
QString getSetting(SettingsType key, const std::function<QString()>& computeFunc = nullptr);
|
||||
|
||||
void storeSetting(SettingsType key, const QString& value);
|
||||
|
||||
private:
|
||||
QDir m_cacheDir;
|
||||
int m_maxEntries;
|
||||
QString m_dbPath;
|
||||
QString m_connectionPrefix;
|
||||
|
||||
mutable QMutex m_connectionsMutex;
|
||||
mutable QSet<QString> m_connectionNames;
|
||||
|
||||
mutable QMutex m_hotKeysMutex;
|
||||
mutable QSet<QString> m_hotColorKeys;
|
||||
mutable QSet<QString> m_hotImageKeys;
|
||||
|
||||
QFuture<void> m_cleanupFuture;
|
||||
|
||||
QSqlDatabase _db() const;
|
||||
void _setupTables(QSqlDatabase& db) const;
|
||||
void _runCleanup();
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Cache
|
||||
|
||||
@@ -12,7 +12,8 @@ namespace WallReel::Core::Cache {
|
||||
enum class Type : uint32_t {
|
||||
None = 0,
|
||||
Image = 1, ///< Cache for processed images
|
||||
Color = 1 << 1, ///< Cache for palette color matching results
|
||||
Color = 1 << 1, ///< Cache for dominant colors
|
||||
Settings = 1 << 2, ///< Cache for settings (simple key-value pairs)
|
||||
All = ~0u
|
||||
};
|
||||
|
||||
@@ -28,6 +29,12 @@ inline constexpr Type operator&(Type a, Type b) {
|
||||
|
||||
using Data = std::variant<std::monostate, QFileInfo, QColor>;
|
||||
|
||||
enum class SettingsType : uint32_t {
|
||||
LastSelectedPalette = 0,
|
||||
LastSortType,
|
||||
LastSortDescending,
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Cache
|
||||
|
||||
#endif // WALLREEL_CACHE_TYPES_HPP
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
||||
// wallpaper.excludes array [] Exclude patterns (regex)
|
||||
//
|
||||
// theme.defaultPalette string "" Name of the default palette to use
|
||||
// theme.palettes array []
|
||||
// theme.palettes[].name string "" Name of the palette
|
||||
// theme.palettes[].colors array [] List of colors in the palette
|
||||
@@ -33,9 +32,9 @@
|
||||
// action.onPreview string "" Command to execute on preview
|
||||
// action.saveState array [] Useful for restore command
|
||||
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
|
||||
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
|
||||
// action.saveState[].fallback string "" Value to save, used when "command" is not set or command execution fails or output is empty
|
||||
// action.saveState[].command string "" Command that outputs(to stdout) the value to save when executed
|
||||
// action.saveState[].timeout number 3000 Timeout for executing "cmd" in milliseconds. 0 or negative means no timeout
|
||||
// action.saveState[].timeout number 3000 Timeout for executing "command" in milliseconds. 0 or negative means no timeout
|
||||
// action.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
|
||||
// action.quitOnSelected boolean false Whether to quit the application after confirming a wallpaper
|
||||
// action.restoreOnClose boolean true Whether to run the restore command after closing the application without confirming a wallpaper
|
||||
@@ -46,11 +45,9 @@
|
||||
// style.window_width number 750 Initial window width
|
||||
// style.window_height number 500 Initial window height
|
||||
//
|
||||
// sort.type string "date" Initial sorting type: "name", "date", "size"
|
||||
// sort.descending boolean true Initial sorting order
|
||||
// Ascending: name: lexicographical, e.g. "a.jpg" before "b.jpg"
|
||||
// date: older before newer
|
||||
// size: smaller before larger
|
||||
// cache.saveSortMethod boolean true Whether to persist the sort type and order
|
||||
// cache.savePalette bool true Whether to persist the selected palette
|
||||
// cache.maxImageEntries number 1000 Maximum number of entries in the image cache (older entries will be evicted)
|
||||
|
||||
namespace WallReel::Core::Config {
|
||||
|
||||
@@ -64,7 +61,7 @@ enum class SortType : int {
|
||||
|
||||
inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"};
|
||||
|
||||
inline QString sortTypeToString(SortType type) {
|
||||
inline QString sortTypeToString(const SortType& type) {
|
||||
switch (type) {
|
||||
case SortType::Name:
|
||||
return "Name";
|
||||
@@ -112,7 +109,6 @@ struct ThemeConfigItems {
|
||||
};
|
||||
|
||||
QList<PaletteConfigItem> palettes;
|
||||
QString defaultPalette;
|
||||
};
|
||||
|
||||
struct ActionConfigItems {
|
||||
@@ -143,9 +139,14 @@ struct StyleConfigItems {
|
||||
int windowHeight = 500;
|
||||
};
|
||||
|
||||
struct SortConfigItems {
|
||||
SortType type = SortType::Date;
|
||||
bool descending = true;
|
||||
struct CacheConfigItems {
|
||||
bool saveSortMethod = true;
|
||||
bool savePalette = true;
|
||||
int maxImageEntries = 1000;
|
||||
|
||||
static const QString defaultSortType;
|
||||
static const QString defaultSortDescending;
|
||||
static const QString defaultSelectedPalette;
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Config
|
||||
|
||||
@@ -15,13 +15,24 @@
|
||||
|
||||
WALLREEL_DECLARE_SENDER("ConfigManager")
|
||||
|
||||
WallReel::Core::Config::Manager::Manager(
|
||||
namespace WallReel::Core::Config {
|
||||
|
||||
const QString CacheConfigItems::defaultSortType = "Date";
|
||||
const QString CacheConfigItems::defaultSortDescending = "true";
|
||||
const QString CacheConfigItems::defaultSelectedPalette = "";
|
||||
|
||||
Manager::Manager(
|
||||
const QDir& configDir,
|
||||
const QDir& picturesDir,
|
||||
const QStringList& searchDirs,
|
||||
const QString& configPath,
|
||||
QObject* parent)
|
||||
: QObject(parent), m_configDir(configDir) {
|
||||
connect(this, &Manager::stateCaptured, this, [this]() {
|
||||
m_stateCaptured = true;
|
||||
WR_INFO("State capture completed");
|
||||
});
|
||||
|
||||
// Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName)
|
||||
if (configPath.isEmpty()) {
|
||||
WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
|
||||
@@ -47,10 +58,10 @@ WallReel::Core::Config::Manager::Manager(
|
||||
_loadWallpapers();
|
||||
}
|
||||
|
||||
WallReel::Core::Config::Manager::~Manager() {
|
||||
Manager::~Manager() {
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) {
|
||||
void Manager::_loadConfig(const QString& configPath) {
|
||||
WR_INFO(QString("Loading configuration from: %1").arg(configPath));
|
||||
QFile configFile(configPath);
|
||||
if (!configFile.open(QIODevice::ReadOnly)) {
|
||||
@@ -72,10 +83,10 @@ void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) {
|
||||
_loadThemeConfig(jsonObj);
|
||||
_loadActionConfig(jsonObj);
|
||||
_loadStyleConfig(jsonObj);
|
||||
_loadSortConfig(jsonObj);
|
||||
_loadCacheConfig(jsonObj);
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& root) {
|
||||
void Manager::_loadWallpaperConfig(const QJsonObject& root) {
|
||||
if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) {
|
||||
return;
|
||||
}
|
||||
@@ -121,14 +132,11 @@ void WallReel::Core::Config::Manager::_loadWallpaperConfig(const QJsonObject& ro
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root) {
|
||||
void Manager::_loadThemeConfig(const QJsonObject& root) {
|
||||
if (!root.contains("theme") || !root["theme"].isObject()) {
|
||||
return;
|
||||
}
|
||||
const QJsonObject& theme = root["theme"].toObject();
|
||||
if (theme.contains("defaultPalette") && theme["defaultPalette"].isString()) {
|
||||
m_themeConfig.defaultPalette = theme["defaultPalette"].toString();
|
||||
}
|
||||
|
||||
if (!theme.contains("palettes") || !theme["palettes"].isArray()) {
|
||||
return;
|
||||
@@ -176,7 +184,7 @@ void WallReel::Core::Config::Manager::_loadThemeConfig(const QJsonObject& root)
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root) {
|
||||
void Manager::_loadActionConfig(const QJsonObject& root) {
|
||||
if (!root.contains("action") || !root["action"].isObject()) {
|
||||
return;
|
||||
}
|
||||
@@ -209,8 +217,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
||||
if (obj.contains("key") && obj["key"].isString()) {
|
||||
sItem.key = obj["key"].toString();
|
||||
}
|
||||
if (obj.contains("default") && obj["default"].isString()) {
|
||||
sItem.defaultVal = obj["default"].toString();
|
||||
if (obj.contains("fallback") && obj["fallback"].isString()) {
|
||||
sItem.defaultVal = obj["fallback"].toString();
|
||||
}
|
||||
if (obj.contains("command") && obj["command"].isString()) {
|
||||
sItem.command = obj["command"].toString();
|
||||
@@ -257,7 +265,7 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root) {
|
||||
void Manager::_loadStyleConfig(const QJsonObject& root) {
|
||||
if (!root.contains("style") || !root["style"].isObject()) {
|
||||
return;
|
||||
}
|
||||
@@ -295,36 +303,33 @@ void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root)
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) {
|
||||
if (!root.contains("sort") || !root["sort"].isObject()) {
|
||||
void Manager::_loadCacheConfig(const QJsonObject& root) {
|
||||
if (!root.contains("cache") || !root["cache"].isObject()) {
|
||||
return;
|
||||
}
|
||||
const QJsonObject& config = root["sort"].toObject();
|
||||
const QJsonObject& config = root["cache"].toObject();
|
||||
|
||||
if (config.contains("type")) {
|
||||
const auto& val = config["type"];
|
||||
if (val.isString()) {
|
||||
QString type = val.toString().toLower();
|
||||
if (type == "name") {
|
||||
m_sortConfig.type = SortType::Name;
|
||||
} else if (type == "date") {
|
||||
m_sortConfig.type = SortType::Date;
|
||||
} else if (type == "size") {
|
||||
m_sortConfig.type = SortType::Size;
|
||||
} else {
|
||||
WR_WARN(QString("Unknown sort type: %1").arg(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config.contains("descending")) {
|
||||
const auto& val = config["descending"];
|
||||
if (config.contains("saveSortMethod")) {
|
||||
const auto& val = config["saveSortMethod"];
|
||||
if (val.isBool()) {
|
||||
m_sortConfig.descending = val.toBool();
|
||||
m_cacheConfig.saveSortMethod = val.toBool();
|
||||
}
|
||||
}
|
||||
if (config.contains("savePalette")) {
|
||||
const auto& val = config["savePalette"];
|
||||
if (val.isBool()) {
|
||||
m_cacheConfig.savePalette = val.toBool();
|
||||
}
|
||||
}
|
||||
if (config.contains("maxImageEntries")) {
|
||||
const auto& val = config["maxImageEntries"];
|
||||
if (val.isDouble() && val.toDouble() > 0) {
|
||||
m_cacheConfig.maxImageEntries = val.toInt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_loadWallpapers() {
|
||||
void Manager::_loadWallpapers() {
|
||||
m_wallpapers.clear();
|
||||
|
||||
// Add paths first using a set to avoid duplicates
|
||||
@@ -389,7 +394,12 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
|
||||
WR_INFO(QString("Found %1 images").arg(paths.size()));
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::captureState() {
|
||||
void Manager::captureState() {
|
||||
if (m_stateCaptured) {
|
||||
WR_DEBUG("State already captured, skipping capture");
|
||||
emit stateCaptured();
|
||||
}
|
||||
|
||||
if (m_pendingCaptures > 0) {
|
||||
WR_WARN("State capture already in progress, ignoring new capture request");
|
||||
return;
|
||||
@@ -481,7 +491,7 @@ void WallReel::Core::Config::Manager::captureState() {
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const QString& value) {
|
||||
void Manager::_onCaptureResult(const QString& key, const QString& value) {
|
||||
// This is all in main thread, so no lock needed
|
||||
m_actionConfig.savedState[key] = value;
|
||||
m_pendingCaptures--;
|
||||
@@ -489,3 +499,5 @@ void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const
|
||||
emit stateCaptured();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core::Config
|
||||
|
||||
@@ -57,7 +57,9 @@ class Manager : public QObject {
|
||||
|
||||
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
|
||||
|
||||
const SortConfigItems& getSortConfig() const { return m_sortConfig; }
|
||||
const CacheConfigItems& getCacheConfig() const { return m_cacheConfig; }
|
||||
|
||||
bool isStateCaptured() const { return m_stateCaptured; }
|
||||
|
||||
QSize getFocusImageSize() const {
|
||||
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
||||
@@ -78,7 +80,7 @@ class Manager : public QObject {
|
||||
void _loadThemeConfig(const QJsonObject& config);
|
||||
void _loadActionConfig(const QJsonObject& config);
|
||||
void _loadStyleConfig(const QJsonObject& config);
|
||||
void _loadSortConfig(const QJsonObject& config);
|
||||
void _loadCacheConfig(const QJsonObject& config);
|
||||
// Load wallpapers
|
||||
void _loadWallpapers();
|
||||
// Callback for state capture results
|
||||
@@ -90,11 +92,12 @@ class Manager : public QObject {
|
||||
ThemeConfigItems m_themeConfig;
|
||||
ActionConfigItems m_actionConfig;
|
||||
StyleConfigItems m_styleConfig;
|
||||
SortConfigItems m_sortConfig;
|
||||
CacheConfigItems m_cacheConfig;
|
||||
|
||||
QStringList m_wallpapers;
|
||||
|
||||
int m_pendingCaptures = 0;
|
||||
bool m_stateCaptured = false; // changed and accessed in main thread, no lock needed
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Config
|
||||
|
||||
@@ -24,11 +24,13 @@ WallReel::Core::Image::Data::Data(const QString& path, const QSize& targetSize,
|
||||
: m_cacheMgr(cacheMgr), m_file(path), m_targetSize(targetSize) {
|
||||
m_id = cacheMgr.cacheKey(m_file, m_targetSize);
|
||||
m_cachedFile = cacheMgr.getImage(m_id, [this]() { return computeImage(); });
|
||||
m_dominantColor = cacheMgr.getColor(m_id, [this]() { return computeDominantColor(loadImage()); });
|
||||
m_dominantColor = cacheMgr.getColor(m_id, [this]() { return computeDominantColor(loadImageFromCache()); });
|
||||
m_isValid = m_cachedFile.isFile() && m_dominantColor.isValid();
|
||||
}
|
||||
|
||||
QImage WallReel::Core::Image::Data::loadImage() const {
|
||||
QImage WallReel::Core::Image::Data::loadImageFromCache() const {
|
||||
QImageReader reader(m_cachedFile.absoluteFilePath());
|
||||
|
||||
if (!reader.canRead()) {
|
||||
WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath());
|
||||
return QImage();
|
||||
|
||||
@@ -54,8 +54,11 @@ class Data {
|
||||
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
|
||||
QHash<QString, QString> m_colorCache; ///< Cache for palette color matching results, key is palette name, value is matched color name
|
||||
|
||||
bool m_isValid = false;
|
||||
|
||||
QImage computeImage() const;
|
||||
QColor computeDominantColor(const QImage& image) const;
|
||||
QImage loadImageFromCache() const;
|
||||
|
||||
Data(const QString& path, const QSize& size, Cache::Manager& cacheMgr);
|
||||
|
||||
@@ -75,7 +78,7 @@ class Data {
|
||||
|
||||
QUrl getUrl() const { return QUrl::fromLocalFile(m_cachedFile.absoluteFilePath()); }
|
||||
|
||||
bool isValid() const { return m_cachedFile.exists(); }
|
||||
bool isValid() const { return m_isValid; }
|
||||
|
||||
QString getFullPath() const { return m_file.absoluteFilePath(); }
|
||||
|
||||
@@ -87,8 +90,6 @@ class Data {
|
||||
|
||||
const QFileInfo& getFileInfo() const { return m_file; }
|
||||
|
||||
QImage loadImage() const;
|
||||
|
||||
const QColor& getDominantColor() const { return m_dominantColor; }
|
||||
|
||||
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
||||
|
||||
@@ -49,19 +49,6 @@ WallReel::Core::Palette::Manager::Manager(
|
||||
m_palettes.append(newP);
|
||||
}
|
||||
}
|
||||
|
||||
// Set default palette if specified
|
||||
if (!config.defaultPalette.isEmpty()) {
|
||||
for (const auto& p : m_palettes) {
|
||||
if (p.name == config.defaultPalette) {
|
||||
m_selectedColor = std::nullopt;
|
||||
m_selectedPalette = p;
|
||||
emit selectedColorChanged();
|
||||
emit selectedPaletteChanged();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WallReel::Core::Palette::Manager::updateColor(const QString& imageId) {
|
||||
|
||||
@@ -42,8 +42,23 @@ class Manager : public QObject {
|
||||
void setSelectedPalette(const QVariant& paletteVar) {
|
||||
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
||||
m_selectedPalette = std::nullopt;
|
||||
} else {
|
||||
} else if (paletteVar.canConvert<PaletteItem>()) {
|
||||
m_selectedPalette = paletteVar.value<PaletteItem>();
|
||||
} else if (paletteVar.canConvert<QString>()) {
|
||||
QString paletteName = paletteVar.toString();
|
||||
auto it =
|
||||
std::find_if(m_palettes.begin(),
|
||||
m_palettes.end(),
|
||||
[&paletteName](const PaletteItem& item) {
|
||||
return item.name == paletteName;
|
||||
});
|
||||
if (it != m_palettes.end()) {
|
||||
m_selectedPalette = *it;
|
||||
} else {
|
||||
m_selectedPalette = std::nullopt;
|
||||
}
|
||||
} else {
|
||||
m_selectedPalette = std::nullopt;
|
||||
}
|
||||
m_selectedColor = std::nullopt;
|
||||
emit selectedPaletteChanged();
|
||||
|
||||
@@ -10,7 +10,7 @@ WALLREEL_DECLARE_SENDER("PaletteMatchColor")
|
||||
|
||||
namespace WallReel::Core::Palette {
|
||||
|
||||
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates) {
|
||||
ColorItem bestMatch(const QColor& target, const QList<ColorItem>& candidates) {
|
||||
if (candidates.isEmpty() || !target.isValid()) {
|
||||
WR_WARN("No candidates or invalid target color for palette matching");
|
||||
static ColorItem emptyItem;
|
||||
|
||||
@@ -10,9 +10,9 @@ namespace WallReel::Core::Palette {
|
||||
*
|
||||
* @param target
|
||||
* @param candidates
|
||||
* @return const ColorItem& The best matching color item, or an empty ColorItem if no candidates are provided
|
||||
* @return ColorItem The best matching color item, or an empty ColorItem if no candidates are provided
|
||||
*/
|
||||
const ColorItem& bestMatch(const QColor& target, const QList<ColorItem>& candidates);
|
||||
ColorItem bestMatch(const QColor& target, const QList<ColorItem>& candidates);
|
||||
|
||||
} // namespace WallReel::Core::Palette
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "Service/manager.hpp"
|
||||
#include "Utils/misc.hpp"
|
||||
#include "appoptions.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
namespace WallReel::Core::Provider {
|
||||
|
||||
@@ -18,18 +19,21 @@ class Bootstrap {
|
||||
|
||||
public:
|
||||
Bootstrap(const AppOptions& options) {
|
||||
cacheMgr = new Cache::Manager(Utils::getCacheDir());
|
||||
|
||||
if (options.clearCache) {
|
||||
cacheMgr->clearCache();
|
||||
return;
|
||||
}
|
||||
configMgr = new Config::Manager(
|
||||
Utils::getConfigDir(),
|
||||
Utils::getPicturesDir(),
|
||||
options.appendDirs,
|
||||
options.configPath);
|
||||
|
||||
cacheMgr = new Cache::Manager(
|
||||
Utils::getCacheDir(),
|
||||
configMgr->getCacheConfig().maxImageEntries);
|
||||
|
||||
if (options.clearCache) {
|
||||
cacheMgr->clearCache();
|
||||
return;
|
||||
}
|
||||
|
||||
imageMgr = new Image::Manager(
|
||||
*cacheMgr,
|
||||
configMgr->getFocusImageSize());
|
||||
@@ -40,19 +44,77 @@ class Bootstrap {
|
||||
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
|
||||
qRegisterMetaType<Palette::ColorItem>("ColorItem");
|
||||
|
||||
ServiceMgr = new Service::Manager(
|
||||
serviceMgr = new Service::Manager(
|
||||
configMgr->getActionConfig(),
|
||||
*imageMgr,
|
||||
*paletteMgr);
|
||||
*paletteMgr,
|
||||
options.disableActions);
|
||||
}
|
||||
|
||||
void start() {
|
||||
cacheMgr->evictOldEntries();
|
||||
configMgr->captureState();
|
||||
imageMgr->loadAndProcess(configMgr->getWallpapers());
|
||||
}
|
||||
|
||||
bool apply(const QString& path) {
|
||||
QEventLoop loop;
|
||||
bool successFlag = false;
|
||||
|
||||
paletteMgr->setSelectedPalette(cacheMgr->getSetting(
|
||||
Cache::SettingsType::LastSelectedPalette,
|
||||
[]() { return Config::CacheConfigItems::defaultSelectedPalette; }));
|
||||
|
||||
QObject::connect(
|
||||
configMgr,
|
||||
&Config::Manager::stateCaptured,
|
||||
&loop,
|
||||
[&]() {
|
||||
loop.quit();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
configMgr->captureState();
|
||||
loop.exec();
|
||||
|
||||
QMetaObject::Connection connection;
|
||||
|
||||
connection = QObject::connect(
|
||||
imageMgr,
|
||||
&Image::Manager::isLoadingChanged,
|
||||
&loop,
|
||||
[&]() {
|
||||
if (!imageMgr->isLoading()) {
|
||||
QObject::disconnect(connection);
|
||||
QVariant idVar = imageMgr->model()->data(
|
||||
imageMgr->model()->index(0, 0),
|
||||
Image::Model::IdRole);
|
||||
if (idVar.isValid()) {
|
||||
auto id = idVar.toString();
|
||||
paletteMgr->updateColor(id);
|
||||
QObject::connect(
|
||||
serviceMgr,
|
||||
&Service::Manager::selectCompleted,
|
||||
&loop,
|
||||
[&](bool success) {
|
||||
successFlag = success;
|
||||
loop.quit();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
serviceMgr->selectWallpaper(id);
|
||||
} else {
|
||||
Logger::critical("Bootstrap", "No images loaded, cannot apply wallpaper");
|
||||
loop.quit();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
imageMgr->loadAndProcess({Utils::expandPath(path)});
|
||||
loop.exec();
|
||||
return successFlag;
|
||||
}
|
||||
|
||||
~Bootstrap() {
|
||||
delete ServiceMgr;
|
||||
delete serviceMgr;
|
||||
delete paletteMgr;
|
||||
delete imageMgr;
|
||||
delete configMgr;
|
||||
@@ -64,7 +126,7 @@ class Bootstrap {
|
||||
Config::Manager* configMgr{};
|
||||
Image::Manager* imageMgr{};
|
||||
Palette::Manager* paletteMgr{};
|
||||
Service::Manager* ServiceMgr{};
|
||||
Service::Manager* serviceMgr{};
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Provider
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
|
||||
#define WALLREEL_PROVIDER_CAROUSEL_HPP
|
||||
|
||||
#include <qapplication.h>
|
||||
|
||||
#include <QApplication>
|
||||
|
||||
#include "Cache/manager.hpp"
|
||||
#include "Cache/types.hpp"
|
||||
#include "Config/data.hpp"
|
||||
#include "Config/manager.hpp"
|
||||
#include "Image/manager.hpp"
|
||||
@@ -146,10 +146,6 @@ class Carousel : public QObject {
|
||||
|
||||
signals:
|
||||
void isProcessingChanged();
|
||||
void selectCompleted();
|
||||
void previewCompleted();
|
||||
void restoreCompleted();
|
||||
void cancelCompleted();
|
||||
|
||||
// Other states
|
||||
|
||||
@@ -190,10 +186,11 @@ class Carousel : public QObject {
|
||||
Bootstrap& bootstrap,
|
||||
QObject* parent = nullptr)
|
||||
: QObject(parent),
|
||||
m_cacheMgr(bootstrap.cacheMgr),
|
||||
m_configMgr(bootstrap.configMgr),
|
||||
m_imageMgr(bootstrap.imageMgr),
|
||||
m_paletteMgr(bootstrap.paletteMgr),
|
||||
m_serviceMgr(bootstrap.ServiceMgr) {
|
||||
m_serviceMgr(bootstrap.serviceMgr) {
|
||||
// Simply forward signals
|
||||
connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged);
|
||||
connect(m_imageMgr, &Image::Manager::processedCountChanged, this, &Carousel::processedCountChanged);
|
||||
@@ -203,10 +200,6 @@ class Carousel : public QObject {
|
||||
connect(m_paletteMgr, &Palette::Manager::colorChanged, this, &Carousel::colorChanged);
|
||||
connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged);
|
||||
connect(m_serviceMgr, &Service::Manager::isProcessingChanged, this, &Carousel::isProcessingChanged);
|
||||
connect(m_serviceMgr, &Service::Manager::selectCompleted, this, &Carousel::selectCompleted);
|
||||
connect(m_serviceMgr, &Service::Manager::previewCompleted, this, &Carousel::previewCompleted);
|
||||
connect(m_serviceMgr, &Service::Manager::restoreCompleted, this, &Carousel::restoreCompleted);
|
||||
connect(m_serviceMgr, &Service::Manager::cancelCompleted, this, &Carousel::cancelCompleted);
|
||||
|
||||
// "Preview" is costly, but is (usually) protected by a debounce timer, so it seems fine
|
||||
// to call it multiple times in a short period, and it simplifies the code a lot :)
|
||||
@@ -248,19 +241,25 @@ class Carousel : public QObject {
|
||||
}
|
||||
});
|
||||
|
||||
// Defer preview until state captured
|
||||
connect(m_configMgr,
|
||||
&Config::Manager::stateCaptured,
|
||||
m_serviceMgr,
|
||||
&Service::Manager::onStateCaptured);
|
||||
|
||||
// Quit on selected
|
||||
if (m_configMgr->getActionConfig().quitOnSelected) {
|
||||
QObject::connect(
|
||||
this,
|
||||
&Provider::Carousel::selectCompleted,
|
||||
m_serviceMgr,
|
||||
&Service::Manager::selectCompleted,
|
||||
app,
|
||||
&QApplication::quit,
|
||||
Qt::QueuedConnection);
|
||||
}
|
||||
// Quit on cancel
|
||||
QObject::connect(
|
||||
this,
|
||||
&Provider::Carousel::cancelCompleted,
|
||||
m_serviceMgr,
|
||||
&Service::Manager::cancelCompleted,
|
||||
app,
|
||||
&QApplication::quit,
|
||||
Qt::QueuedConnection);
|
||||
@@ -273,12 +272,41 @@ class Carousel : public QObject {
|
||||
&Service::Manager::restoreOnQuit);
|
||||
}
|
||||
|
||||
// Initial value of sort method
|
||||
setSortType(m_configMgr->getSortConfig().type);
|
||||
setSortDescending(m_configMgr->getSortConfig().descending);
|
||||
// Restore last state if configured
|
||||
// and store state on change if configured
|
||||
// Note: connect after restoring state to avoid storing the restored state again
|
||||
if (m_configMgr->getCacheConfig().saveSortMethod) {
|
||||
setSortType(m_cacheMgr->getSetting(
|
||||
Cache::SettingsType::LastSortType,
|
||||
[]() { return Config::CacheConfigItems::defaultSortType; }));
|
||||
setSortDescending(m_cacheMgr->getSetting(
|
||||
Cache::SettingsType::LastSortDescending,
|
||||
[]() { return Config::CacheConfigItems::defaultSortDescending; }) == "true");
|
||||
connect(app, &QApplication::aboutToQuit, this, [this]() {
|
||||
m_cacheMgr->storeSetting(
|
||||
Cache::SettingsType::LastSortType,
|
||||
Config::sortTypeToString(m_imageMgr->sortType()));
|
||||
});
|
||||
connect(app, &QApplication::aboutToQuit, this, [this]() {
|
||||
m_cacheMgr->storeSetting(
|
||||
Cache::SettingsType::LastSortDescending,
|
||||
m_imageMgr->sortDescending() ? "true" : "false");
|
||||
});
|
||||
}
|
||||
if (m_configMgr->getCacheConfig().savePalette) {
|
||||
requestSelectPalette(m_cacheMgr->getSetting(
|
||||
Cache::SettingsType::LastSelectedPalette,
|
||||
[]() { return Config::CacheConfigItems::defaultSelectedPalette; }));
|
||||
connect(app, &QApplication::aboutToQuit, this, [this]() {
|
||||
m_cacheMgr->storeSetting(
|
||||
Cache::SettingsType::LastSelectedPalette,
|
||||
m_paletteMgr->getSelectedPaletteName());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Cache::Manager* m_cacheMgr;
|
||||
Config::Manager* m_configMgr;
|
||||
Image::Manager* m_imageMgr;
|
||||
Palette::Manager* m_paletteMgr;
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
#include "manager.hpp"
|
||||
|
||||
#include "Utils/texttemplate.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
WALLREEL_DECLARE_SENDER("ServiceManager")
|
||||
|
||||
namespace WallReel::Core::Service {
|
||||
|
||||
Manager::Manager(
|
||||
const Config::ActionConfigItems& actionConfig,
|
||||
Image::Manager& imageManager,
|
||||
Palette::Manager& paletteManager,
|
||||
bool disableActions,
|
||||
QObject* parent)
|
||||
: m_actionConfig(actionConfig),
|
||||
m_imageManager(imageManager),
|
||||
m_paletteManager(paletteManager),
|
||||
m_disableActions(disableActions) {
|
||||
m_wallpaperService = new WallpaperService(m_actionConfig.previewDebounceTime, this);
|
||||
|
||||
// Forward signals
|
||||
// Direct signal 2 signal connection
|
||||
connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted);
|
||||
// Signal 2 slot connection to handle processing state
|
||||
connect(m_wallpaperService, &WallpaperService::selectCompleted, this, &Manager::_onSelectCompleted);
|
||||
connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, &Manager::_onRestoreCompleted);
|
||||
}
|
||||
|
||||
void Manager::onStateCaptured() {
|
||||
m_stateCaptured = true;
|
||||
|
||||
if (!m_pendingPreviewId.isEmpty()) {
|
||||
WR_DEBUG("State captured, executing pending preview for id " + m_pendingPreviewId);
|
||||
const QString pending = m_pendingPreviewId;
|
||||
m_pendingPreviewId.clear();
|
||||
previewWallpaper(pending);
|
||||
}
|
||||
}
|
||||
|
||||
void Manager::selectWallpaper(const QString& id) {
|
||||
WR_DEBUG("Select action triggered for id " + id);
|
||||
if (m_disableActions) {
|
||||
WR_DEBUG("Actions are disabled, skipping select for id " + id);
|
||||
emit selectCompleted(true);
|
||||
return;
|
||||
}
|
||||
if (m_isProcessing) {
|
||||
WR_DEBUG("Already processing an select action, ignoring new request");
|
||||
return;
|
||||
}
|
||||
m_isProcessing = true;
|
||||
emit isProcessingChanged();
|
||||
const auto* data = m_imageManager.imageAt(id);
|
||||
|
||||
if (!data || !data->isValid()) {
|
||||
WR_WARN(QString("No valid image data at id %1. Skipping select action.").arg(id));
|
||||
m_isProcessing = false;
|
||||
emit isProcessingChanged();
|
||||
emit selectCompleted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto command = _renderCommand(m_actionConfig.onSelected, _generateVariables(*data));
|
||||
m_wallpaperService->select(command);
|
||||
}
|
||||
|
||||
void Manager::restore() {
|
||||
WR_DEBUG("Restore action triggered");
|
||||
if (m_disableActions) {
|
||||
WR_DEBUG("Actions are disabled, skipping restore");
|
||||
emit restoreCompleted(true);
|
||||
return;
|
||||
}
|
||||
if (m_isProcessing) {
|
||||
WR_DEBUG("Already processing an restore action, ignoring new request");
|
||||
return;
|
||||
}
|
||||
if (!m_stateCaptured) {
|
||||
WR_DEBUG("State not captured yet, skipping restore action");
|
||||
emit restoreCompleted(false);
|
||||
return;
|
||||
}
|
||||
m_isProcessing = true;
|
||||
emit isProcessingChanged();
|
||||
|
||||
m_wallpaperService->restore(_renderCommand(m_actionConfig.onRestore, m_actionConfig.savedState));
|
||||
}
|
||||
|
||||
void Manager::cancel() {
|
||||
WR_DEBUG("Cancel action triggered");
|
||||
if (m_disableActions) {
|
||||
WR_DEBUG("Actions are disabled, skipping cancel");
|
||||
emit cancelCompleted();
|
||||
return;
|
||||
}
|
||||
m_wallpaperService->stopAll();
|
||||
emit cancelCompleted();
|
||||
}
|
||||
|
||||
void Manager::previewWallpaper(const QString& id) {
|
||||
if (m_disableActions) {
|
||||
WR_DEBUG("Actions are disabled, skipping preview for id " + id);
|
||||
emit previewCompleted(true);
|
||||
return;
|
||||
}
|
||||
if (!m_stateCaptured) {
|
||||
WR_DEBUG("State not captured yet, deferring preview for id " + id);
|
||||
m_pendingPreviewId = id;
|
||||
emit previewCompleted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
WR_DEBUG("Preview action triggered for id " + id);
|
||||
|
||||
const auto* data = m_imageManager.imageAt(id);
|
||||
|
||||
if (!data || !data->isValid()) {
|
||||
WR_WARN(QString("No valid image data at id %1. Skipping preview action.").arg(id));
|
||||
emit previewCompleted(false);
|
||||
return;
|
||||
}
|
||||
|
||||
m_wallpaperService->preview(_renderCommand(m_actionConfig.onPreview, _generateVariables(*data)));
|
||||
}
|
||||
|
||||
void Manager::restoreOnQuit() {
|
||||
if (m_hasSelected) {
|
||||
WR_DEBUG("Quit with selected wallpaper, no need to restore");
|
||||
return;
|
||||
}
|
||||
WR_DEBUG("Restore on quit");
|
||||
m_wallpaperService->stopAll();
|
||||
if (m_disableActions) {
|
||||
WR_DEBUG("Actions are disabled, skipping restore on quit");
|
||||
return;
|
||||
}
|
||||
QEventLoop loop;
|
||||
connect(this, &Manager::restoreCompleted, &loop, &QEventLoop::quit);
|
||||
// Call restore after the event loop starts
|
||||
QTimer::singleShot(0, this, &Manager::restore);
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
void Manager::_onSelectCompleted(bool success) {
|
||||
WR_DEBUG("Select completed");
|
||||
_onProcessCompleted();
|
||||
m_hasSelected = m_hasSelected || success;
|
||||
emit selectCompleted(success);
|
||||
}
|
||||
|
||||
void Manager::_onRestoreCompleted(bool success) {
|
||||
WR_DEBUG("Restore completed");
|
||||
_onProcessCompleted();
|
||||
emit restoreCompleted(success);
|
||||
}
|
||||
|
||||
void Manager::_onProcessCompleted() {
|
||||
m_isProcessing = false;
|
||||
emit isProcessingChanged();
|
||||
}
|
||||
|
||||
QString Manager::_renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const {
|
||||
return Utils::renderTemplate(templateStr, variables);
|
||||
}
|
||||
|
||||
QHash<QString, QString> Manager::_generateVariables(const Image::Data& imageData) const {
|
||||
auto palette = m_paletteManager.getSelectedPaletteName();
|
||||
if (palette.isEmpty()) {
|
||||
palette = "null";
|
||||
}
|
||||
auto color = m_paletteManager.getCurrentColorName();
|
||||
if (color.isEmpty()) {
|
||||
color = "null";
|
||||
}
|
||||
auto hex = m_paletteManager.getCurrentColorHex();
|
||||
if (hex.isEmpty()) {
|
||||
hex = "null";
|
||||
}
|
||||
QHash<QString, QString> ret{
|
||||
{"path", imageData.getFullPath()},
|
||||
{"name", imageData.getFileName()},
|
||||
{"size", QString::number(imageData.getSize())},
|
||||
{"palette", palette},
|
||||
{"colorName", color},
|
||||
{"colorHex", hex},
|
||||
{"domColorHex", imageData.getDominantColor().name()},
|
||||
};
|
||||
|
||||
ret.insert(m_actionConfig.savedState);
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core::Service
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "Image/manager.hpp"
|
||||
#include "Palette/manager.hpp"
|
||||
#include "Service/wallpaper.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
namespace WallReel::Core::Service {
|
||||
|
||||
@@ -22,16 +21,8 @@ class Manager : public QObject {
|
||||
const Config::ActionConfigItems& actionConfig,
|
||||
Image::Manager& imageManager,
|
||||
Palette::Manager& paletteManager,
|
||||
QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageManager(imageManager), m_paletteManager(paletteManager) {
|
||||
m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, this);
|
||||
|
||||
// Forward signals
|
||||
// Direct signal 2 signal connection
|
||||
connect(m_wallpaperService, &WallpaperService::previewCompleted, this, &Manager::previewCompleted);
|
||||
// Signal 2 slot connection to handle processing state
|
||||
connect(m_wallpaperService, &WallpaperService::selectCompleted, this, &Manager::_onSelectCompleted);
|
||||
connect(m_wallpaperService, &WallpaperService::restoreCompleted, this, &Manager::_onRestoreCompleted);
|
||||
}
|
||||
bool disableActions = false,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
bool isProcessing() const { return m_isProcessing; }
|
||||
|
||||
@@ -39,102 +30,49 @@ class Manager : public QObject {
|
||||
|
||||
public slots:
|
||||
|
||||
void selectWallpaper(const QString& id) {
|
||||
Logger::debug("ServiceManager", QString("Select wallpaper with id %1").arg(id));
|
||||
if (m_isProcessing) {
|
||||
Logger::debug("ServiceManager", "Already processing an select action, ignoring new request");
|
||||
return;
|
||||
}
|
||||
m_isProcessing = true;
|
||||
emit isProcessingChanged();
|
||||
const auto* data = m_imageManager.imageAt(id);
|
||||
if (data) {
|
||||
m_wallpaperService->select(*data);
|
||||
} else {
|
||||
Logger::warn("ServiceManager", QString("No image data at id %1. Skipping select action.").arg(id));
|
||||
m_isProcessing = false;
|
||||
emit isProcessingChanged();
|
||||
emit selectCompleted();
|
||||
}
|
||||
}
|
||||
void onStateCaptured();
|
||||
|
||||
void restore() {
|
||||
Logger::debug("ServiceManager", "Restore states");
|
||||
if (m_isProcessing) {
|
||||
Logger::debug("ServiceManager", "Already processing an restore action, ignoring new request");
|
||||
return;
|
||||
}
|
||||
m_isProcessing = true;
|
||||
emit isProcessingChanged();
|
||||
m_wallpaperService->restore();
|
||||
}
|
||||
void selectWallpaper(const QString& id);
|
||||
|
||||
void cancel() {
|
||||
Logger::debug("ServiceManager", "Cancel action");
|
||||
m_wallpaperService->stopAll();
|
||||
emit cancelCompleted();
|
||||
}
|
||||
void restore();
|
||||
|
||||
void previewWallpaper(const QString& id) {
|
||||
Logger::debug("ServiceManager", "Preview wallpaper");
|
||||
const auto* data = m_imageManager.imageAt(id);
|
||||
if (data) {
|
||||
m_wallpaperService->preview(*data);
|
||||
} else {
|
||||
Logger::warn("ServiceManager", "No image data at id " + id + ". Skipping preview action.");
|
||||
emit previewCompleted();
|
||||
}
|
||||
}
|
||||
void cancel();
|
||||
|
||||
void restoreOnQuit() {
|
||||
if (m_hasSelected) {
|
||||
Logger::debug("ServiceManager", "Quit with selected wallpaper, no need to restore");
|
||||
return;
|
||||
}
|
||||
Logger::debug("ServiceManager", "Restore on quit");
|
||||
m_wallpaperService->stopAll();
|
||||
QEventLoop loop;
|
||||
connect(m_wallpaperService, &WallpaperService::restoreCompleted, &loop, &QEventLoop::quit);
|
||||
// Call restore after the event loop starts
|
||||
QTimer::singleShot(0, m_wallpaperService, &WallpaperService::restore);
|
||||
loop.exec();
|
||||
}
|
||||
void previewWallpaper(const QString& id);
|
||||
|
||||
void restoreOnQuit();
|
||||
|
||||
private slots:
|
||||
|
||||
void _onSelectCompleted() {
|
||||
Logger::debug("ServiceManager", "Select completed");
|
||||
_onProcessCompleted();
|
||||
m_hasSelected = true;
|
||||
emit selectCompleted();
|
||||
}
|
||||
void _onSelectCompleted(bool success);
|
||||
|
||||
void _onRestoreCompleted() {
|
||||
Logger::debug("ServiceManager", "Restore completed");
|
||||
_onProcessCompleted();
|
||||
emit restoreCompleted();
|
||||
}
|
||||
void _onRestoreCompleted(bool success);
|
||||
|
||||
void _onProcessCompleted() {
|
||||
m_isProcessing = false;
|
||||
emit isProcessingChanged();
|
||||
}
|
||||
void _onProcessCompleted();
|
||||
|
||||
signals:
|
||||
void isProcessingChanged();
|
||||
void selectCompleted();
|
||||
void previewCompleted();
|
||||
void restoreCompleted();
|
||||
void selectCompleted(bool success);
|
||||
void previewCompleted(bool success);
|
||||
void restoreCompleted(bool success);
|
||||
void cancelCompleted();
|
||||
|
||||
private:
|
||||
QString _renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const;
|
||||
QHash<QString, QString> _generateVariables(const Image::Data& imageData) const;
|
||||
|
||||
private:
|
||||
WallpaperService* m_wallpaperService;
|
||||
const Config::ActionConfigItems& m_actionConfig;
|
||||
Image::Manager& m_imageManager;
|
||||
Palette::Manager& m_paletteManager;
|
||||
bool m_disableActions;
|
||||
|
||||
bool m_isProcessing = false;
|
||||
bool m_hasSelected = false;
|
||||
|
||||
bool m_stateCaptured = false;
|
||||
QString m_pendingPreviewId;
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Service
|
||||
|
||||
@@ -1,54 +1,103 @@
|
||||
#include "Service/wallpaper.hpp"
|
||||
|
||||
#include <QColor>
|
||||
#include <iostream>
|
||||
#include <qprocess.h>
|
||||
|
||||
#include <QColor>
|
||||
|
||||
#include "Utils/texttemplate.hpp"
|
||||
#include "logger.hpp"
|
||||
|
||||
WALLREEL_DECLARE_SENDER("WallpaperService")
|
||||
|
||||
WallReel::Core::Service::WallpaperService::WallpaperService(
|
||||
const Config::ActionConfigItems& actionConfig,
|
||||
const Palette::Manager& paletteManager,
|
||||
QObject* parent)
|
||||
: QObject(parent), m_actionConfig(actionConfig), m_paletteManager(paletteManager) {
|
||||
namespace WallReel::Core::Service {
|
||||
|
||||
WallpaperService::WallpaperService(int previewDebounceTime, QObject* parent)
|
||||
: QObject(parent) {
|
||||
m_previewDebounceTimer = new QTimer(this);
|
||||
m_previewDebounceTimer->setSingleShot(true);
|
||||
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime);
|
||||
m_previewDebounceTimer->setInterval(previewDebounceTime);
|
||||
connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() {
|
||||
_doPreview(*m_pendingImageData);
|
||||
_doPreview(m_pendingPreviewCommand);
|
||||
});
|
||||
|
||||
// There is a chance that a QProcess fails to start, changing its state from Starting to NotRunning without emitting finished signal,
|
||||
// so we need to handle errorOccurred signal to catch that case and emit previewCompleted/selectCompleted/restoreCompleted with
|
||||
// false to indicate failure.
|
||||
// However, this is probably impossible since we use "sh" "-c" to execute commands and "sh" should always be available.
|
||||
|
||||
m_previewProcess = new QProcess(this);
|
||||
connect(m_previewProcess,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[this](QProcess::ProcessError error) {
|
||||
WR_WARN(QString("Preview command process error: %1").arg(error));
|
||||
if (error == QProcess::FailedToStart) {
|
||||
WR_WARN("Failed to start preview command process.");
|
||||
emit previewCompleted(false);
|
||||
}
|
||||
});
|
||||
connect(m_previewProcess,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
WR_DEBUG(QString("Preview process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
emit previewCompleted();
|
||||
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||
if (!success) {
|
||||
WR_WARN(QString("Preview command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
} else {
|
||||
WR_DEBUG("Preview command executed successfully");
|
||||
}
|
||||
emit previewCompleted(success);
|
||||
});
|
||||
|
||||
m_selectProcess = new QProcess(this);
|
||||
connect(m_selectProcess,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[this](QProcess::ProcessError error) {
|
||||
WR_WARN(QString("Select command process error: %1").arg(error));
|
||||
if (error == QProcess::FailedToStart) {
|
||||
WR_WARN("Failed to start select command process.");
|
||||
emit selectCompleted(false);
|
||||
}
|
||||
});
|
||||
connect(m_selectProcess,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
WR_DEBUG(QString("Select process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
emit selectCompleted();
|
||||
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||
if (!success) {
|
||||
WR_WARN(QString("Select command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
} else {
|
||||
WR_DEBUG("Select command executed successfully");
|
||||
}
|
||||
emit selectCompleted(success);
|
||||
});
|
||||
|
||||
m_restoreProcess = new QProcess(this);
|
||||
connect(m_restoreProcess,
|
||||
&QProcess::errorOccurred,
|
||||
this,
|
||||
[this](QProcess::ProcessError error) {
|
||||
WR_WARN(QString("Restore command process error: %1").arg(error));
|
||||
if (error == QProcess::FailedToStart) {
|
||||
WR_WARN("Failed to start restore command process.");
|
||||
emit restoreCompleted(false);
|
||||
}
|
||||
});
|
||||
connect(m_restoreProcess,
|
||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this,
|
||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||
WR_DEBUG(QString("Restore process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
emit restoreCompleted();
|
||||
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||
if (!success) {
|
||||
WR_WARN(QString("Restore command failed with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
||||
} else {
|
||||
WR_DEBUG("Restore command executed successfully");
|
||||
}
|
||||
emit restoreCompleted(success);
|
||||
});
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::stopAll() {
|
||||
void WallpaperService::stopAll() {
|
||||
WR_DEBUG("Stopping all wallpaper service processes");
|
||||
if (m_previewProcess->state() != QProcess::NotRunning) {
|
||||
m_previewProcess->kill();
|
||||
@@ -65,74 +114,32 @@ void WallReel::Core::Service::WallpaperService::stopAll() {
|
||||
m_previewDebounceTimer->stop();
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) {
|
||||
m_pendingImageData = &imageData;
|
||||
void WallpaperService::preview(const QString& command) {
|
||||
m_pendingPreviewCommand = command;
|
||||
m_previewDebounceTimer->start();
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::select(const Image::Data& imageData) {
|
||||
void WallpaperService::select(const QString& command) {
|
||||
if (m_selectProcess->state() != QProcess::NotRunning) {
|
||||
WR_WARN("Previous select command is still running. Ignoring new command.");
|
||||
return;
|
||||
}
|
||||
WR_DEBUG(QString("Select wallpaper: %1").arg(imageData.getFullPath()));
|
||||
_doSelect(imageData);
|
||||
_doSelect(command);
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::restore() {
|
||||
void WallpaperService::restore(const QString& command) {
|
||||
if (m_restoreProcess->state() != QProcess::NotRunning) {
|
||||
WR_WARN("Previous restore command is still running. Ignoring new command.");
|
||||
return;
|
||||
}
|
||||
WR_DEBUG("Restore state");
|
||||
_doRestore();
|
||||
_doRestore(command);
|
||||
}
|
||||
|
||||
QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) {
|
||||
auto palette = m_paletteManager.getSelectedPaletteName();
|
||||
if (palette.isEmpty()) {
|
||||
palette = "null";
|
||||
}
|
||||
auto color = m_paletteManager.getCurrentColorName();
|
||||
if (color.isEmpty()) {
|
||||
color = "null";
|
||||
}
|
||||
auto hex = m_paletteManager.getCurrentColorHex();
|
||||
if (hex.isEmpty()) {
|
||||
hex = "null";
|
||||
}
|
||||
QHash<QString, QString> ret{
|
||||
{"path", imageData.getFullPath()},
|
||||
{"name", imageData.getFileName()},
|
||||
{"size", QString::number(imageData.getSize())},
|
||||
{"palette", palette},
|
||||
{"colorName", color},
|
||||
{"colorHex", hex},
|
||||
{"domColorHex", imageData.getDominantColor().name()},
|
||||
};
|
||||
|
||||
ret.insert(m_actionConfig.savedState);
|
||||
return ret;
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& imageData) {
|
||||
QString path = imageData.getFullPath();
|
||||
|
||||
if (path.isEmpty()) {
|
||||
WR_WARN("No valid image path for preview. Skipping preview action.");
|
||||
emit previewCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_actionConfig.printPreview) {
|
||||
std::cout << path.toStdString() << std::endl;
|
||||
}
|
||||
|
||||
const auto variables = _generateVariables(imageData);
|
||||
auto command = Utils::renderTemplate(m_actionConfig.onPreview, variables);
|
||||
void WallpaperService::_doPreview(const QString& command) {
|
||||
if (command.isEmpty()) {
|
||||
WR_DEBUG("No preview command configured. Skipping preview action.");
|
||||
emit previewCompleted();
|
||||
emit previewCompleted(true);
|
||||
return;
|
||||
}
|
||||
WR_DEBUG(QString("Executing preview command: %1").arg(command));
|
||||
@@ -144,43 +151,24 @@ void WallReel::Core::Service::WallpaperService::_doPreview(const Image::Data& im
|
||||
m_previewProcess->start("sh", QStringList() << "-c" << command);
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& imageData) {
|
||||
QString path = imageData.getFullPath();
|
||||
|
||||
if (path.isEmpty()) {
|
||||
WR_WARN("No valid image path for select. Skipping select action.");
|
||||
emit selectCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_actionConfig.printSelected) {
|
||||
std::cout << path.toStdString() << std::endl;
|
||||
}
|
||||
|
||||
const auto variables = _generateVariables(imageData);
|
||||
auto command = Utils::renderTemplate(m_actionConfig.onSelected, variables);
|
||||
void WallpaperService::_doSelect(const QString& command) {
|
||||
if (command.isEmpty()) {
|
||||
WR_DEBUG("No select command configured. Skipping select action.");
|
||||
emit selectCompleted();
|
||||
emit selectCompleted(true);
|
||||
return;
|
||||
}
|
||||
WR_DEBUG(QString("Executing select command: %1").arg(command));
|
||||
m_selectProcess->start("sh", QStringList() << "-c" << command);
|
||||
}
|
||||
|
||||
void WallReel::Core::Service::WallpaperService::_doRestore() {
|
||||
if (m_actionConfig.onRestore.isEmpty()) {
|
||||
WR_DEBUG("No restore command configured. Skipping restore action.");
|
||||
emit restoreCompleted();
|
||||
return;
|
||||
}
|
||||
|
||||
const QString command = Utils::renderTemplate(m_actionConfig.onRestore, m_actionConfig.savedState);
|
||||
void WallpaperService::_doRestore(const QString& command) {
|
||||
if (command.isEmpty()) {
|
||||
WR_DEBUG("Restore command is empty after rendering. Skipping restore action.");
|
||||
emit restoreCompleted();
|
||||
WR_DEBUG("Restore command is empty. Skipping restore action.");
|
||||
emit restoreCompleted(true);
|
||||
return;
|
||||
}
|
||||
WR_DEBUG(QString("Executing restore command: %1").arg(command));
|
||||
m_restoreProcess->start("sh", QStringList() << "-c" << command);
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core::Service
|
||||
|
||||
@@ -4,43 +4,33 @@
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
||||
#include "Config/data.hpp"
|
||||
#include "Image/data.hpp"
|
||||
#include "Palette/manager.hpp"
|
||||
|
||||
namespace WallReel::Core::Service {
|
||||
|
||||
class WallpaperService : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
WallpaperService(
|
||||
const Config::ActionConfigItems& actionConfig,
|
||||
const Palette::Manager& paletteManager,
|
||||
QObject* parent = nullptr);
|
||||
WallpaperService(int previewDebounceTime, QObject* parent = nullptr);
|
||||
|
||||
void stopAll();
|
||||
|
||||
public slots:
|
||||
void preview(const Image::Data& imageData); // execute after 500ms of inactivity
|
||||
void select(const Image::Data& imageData); // execute immediately, ignore if already running
|
||||
void restore(); // execute immediately, ignore if already running
|
||||
void preview(const QString& command); // execute after 500ms of inactivity
|
||||
void select(const QString& command); // execute immediately, ignore if already running
|
||||
void restore(const QString& command); // execute immediately, ignore if already running
|
||||
|
||||
signals:
|
||||
void previewCompleted();
|
||||
void selectCompleted();
|
||||
void restoreCompleted();
|
||||
void previewCompleted(bool success);
|
||||
void selectCompleted(bool success);
|
||||
void restoreCompleted(bool success);
|
||||
|
||||
private:
|
||||
void _doPreview(const Image::Data& imageData);
|
||||
void _doSelect(const Image::Data& imageData);
|
||||
void _doRestore();
|
||||
QHash<QString, QString> _generateVariables(const Image::Data& imageData);
|
||||
void _doPreview(const QString& command);
|
||||
void _doSelect(const QString& command);
|
||||
void _doRestore(const QString& command);
|
||||
|
||||
const Config::ActionConfigItems& m_actionConfig;
|
||||
const Palette::Manager& m_paletteManager;
|
||||
QTimer* m_previewDebounceTimer;
|
||||
const Image::Data* m_pendingImageData;
|
||||
QString m_pendingPreviewCommand;
|
||||
QProcess* m_previewProcess;
|
||||
QProcess* m_selectProcess;
|
||||
QProcess* m_restoreProcess;
|
||||
|
||||
@@ -67,6 +67,12 @@ void AppOptions::parseArgs(QApplication& app) {
|
||||
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
|
||||
parser.addOption(configFileOption);
|
||||
|
||||
QCommandLineOption disableActionsOption(QStringList() << "D" << "disable-actions", "Disable actions set in configuration file");
|
||||
parser.addOption(disableActionsOption);
|
||||
|
||||
QCommandLineOption applyOption(QStringList() << "a" << "apply", "Apply the specified image as wallpaper and exit", "file");
|
||||
parser.addOption(applyOption);
|
||||
|
||||
// Not parser.process(a->arguments()) because we want to handle exit logics ourselves.
|
||||
// parser.process(...) will do something like exit(...) that will terminate
|
||||
// the application brutally and produce unwanted warnings.
|
||||
@@ -111,7 +117,7 @@ void AppOptions::parseArgs(QApplication& app) {
|
||||
}
|
||||
|
||||
if (parser.isSet(configFileOption)) {
|
||||
QString path = parser.value(configFileOption);
|
||||
QString path = Utils::expandPath(parser.value(configFileOption));
|
||||
if (Utils::checkFile(path)) {
|
||||
configPath = path;
|
||||
} else {
|
||||
@@ -120,6 +126,21 @@ void AppOptions::parseArgs(QApplication& app) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (parser.isSet(disableActionsOption)) {
|
||||
disableActions = true;
|
||||
}
|
||||
|
||||
if (parser.isSet(applyOption)) {
|
||||
QString path = Utils::expandPath(parser.value(applyOption));
|
||||
if (Utils::checkImageFile(path)) {
|
||||
applyPath = path;
|
||||
} else {
|
||||
errorText = QString("Error: Image file does not exist, is not accessible, or has an unsupported format: %1").arg(path);
|
||||
printError();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace WallReel::Core
|
||||
|
||||
@@ -26,7 +26,9 @@ class AppOptions {
|
||||
QString configPath;
|
||||
QStringList appendDirs;
|
||||
QString errorText;
|
||||
QString applyPath; // -a --apply
|
||||
bool clearCache = false; // -C --clear-cache
|
||||
bool disableActions = false; // -D --disable-actions
|
||||
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
|
||||
|
||||
AppOptions();
|
||||
|
||||
+52
-8
@@ -1,9 +1,15 @@
|
||||
#include <qapplication.h>
|
||||
#include <qobject.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QSocketNotifier>
|
||||
|
||||
extern "C" {
|
||||
#include <signal.h>
|
||||
#include <sys/signalfd.h>
|
||||
#include <unistd.h>
|
||||
}
|
||||
|
||||
#include "Core/Provider/bootstrap.hpp"
|
||||
#include "Core/Provider/carousel.hpp"
|
||||
@@ -20,14 +26,29 @@ int main(int argc, char* argv[]) {
|
||||
// 1. QQmlApplicationEngine (with all QML objects)
|
||||
// 2. provider (manages states and connections)
|
||||
// 3. bootstrap (manages lifecycle of all managers)
|
||||
// 4. QApplication
|
||||
// 4. QSocketNotifier (receives signals for graceful shutdown)
|
||||
// 5. QApplication
|
||||
|
||||
// Mask signals for graceful shutdown
|
||||
sigset_t mask;
|
||||
sigemptyset(&mask);
|
||||
sigaddset(&mask, SIGINT);
|
||||
sigaddset(&mask, SIGHUP);
|
||||
sigaddset(&mask, SIGTERM);
|
||||
sigaddset(&mask, SIGUSR1);
|
||||
sigaddset(&mask, SIGUSR2);
|
||||
if (pthread_sigmask(SIG_BLOCK, &mask, nullptr) == -1) {
|
||||
// Logger is yet to be initialized, but is still usable with default behavior
|
||||
WR_CRITICAL(QString("Failed to block signals: %1").arg(strerror(errno)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setApplicationName(APP_NAME);
|
||||
a.setApplicationVersion(APP_VERSION);
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 4, 0)
|
||||
using namespace Qt::StringLiterals;
|
||||
a.setWindowIcon(QIcon(u":/%1.svg"_s.arg(APP_NAME)));
|
||||
a.setWindowIcon(QIcon(u":/icon.svg"_s));
|
||||
#else
|
||||
a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME)));
|
||||
#endif
|
||||
@@ -35,6 +56,27 @@ int main(int argc, char* argv[]) {
|
||||
{
|
||||
Logger::init();
|
||||
|
||||
// Create signalfd to receive signals in the Qt event loop
|
||||
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
|
||||
if (sfd == -1) {
|
||||
WR_CRITICAL(QString("Failed to create signalfd: %1").arg(strerror(errno)));
|
||||
return 1;
|
||||
}
|
||||
QSocketNotifier notifier(sfd, QSocketNotifier::Read, &a);
|
||||
|
||||
QObject::connect(
|
||||
¬ifier,
|
||||
&QSocketNotifier::activated,
|
||||
&a,
|
||||
[sfd, &a]() {
|
||||
struct signalfd_siginfo fdsi;
|
||||
ssize_t s = read(sfd, &fdsi, sizeof(struct signalfd_siginfo));
|
||||
if (s == sizeof(struct signalfd_siginfo)) {
|
||||
WR_DEBUG(QString("Received signal: %1").arg(fdsi.ssi_signo));
|
||||
a.quit();
|
||||
}
|
||||
});
|
||||
|
||||
AppOptions options;
|
||||
options.parseArgs(a);
|
||||
|
||||
@@ -48,6 +90,10 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!options.applyPath.isEmpty()) {
|
||||
return bootstrap.apply(options.applyPath) ? 0 : 1;
|
||||
}
|
||||
|
||||
{
|
||||
Provider::Carousel provider(&a, bootstrap);
|
||||
qmlRegisterSingletonInstance(
|
||||
@@ -66,13 +112,11 @@ int main(int argc, char* argv[]) {
|
||||
[]() { QCoreApplication::exit(-1); },
|
||||
Qt::QueuedConnection);
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
using namespace Qt::StringLiterals;
|
||||
engine.loadFromModule(UIMODULE_URI, u"Main"_s);
|
||||
#elif QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||
engine.loadFromModule(UIMODULE_URI, u"Main"_qs);
|
||||
#else
|
||||
engine.addImportPath(u"qrc:/"_qs));
|
||||
engine.addImportPath(u"qrc:/"_qs);
|
||||
engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs));
|
||||
#endif
|
||||
|
||||
|
||||
+16
-20
@@ -47,11 +47,6 @@
|
||||
"theme": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"defaultPalette": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Name of the default palette to use"
|
||||
},
|
||||
"palettes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -126,10 +121,10 @@
|
||||
"default": "",
|
||||
"description": "Key of value to save, used as {{ key }} in onRestore command"
|
||||
},
|
||||
"default": {
|
||||
"fallback": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Value to save, used when \"cmd\" is not set or command execution fails or output is empty"
|
||||
"description": "Value to save, used when \"command\" is not set or command execution fails or output is empty"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
@@ -179,6 +174,7 @@
|
||||
"image_focus_scale": {
|
||||
"type": "number",
|
||||
"default": 1.5,
|
||||
"minimum": 1.0,
|
||||
"description": "Scale of the focused image (relative to unfocused image)"
|
||||
},
|
||||
"window_width": {
|
||||
@@ -193,23 +189,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sort": {
|
||||
"cache": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"name",
|
||||
"date",
|
||||
"size"
|
||||
],
|
||||
"default": "date",
|
||||
"description": "Initial sorting type"
|
||||
},
|
||||
"descending": {
|
||||
"saveSortMethod": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Initial sorting order. Ascending: name: lexicographical, date: older before newer, size: smaller before larger"
|
||||
"description": "Whether to persist the sort type and order"
|
||||
},
|
||||
"savePalette": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Whether to persist the selected palette"
|
||||
},
|
||||
"maxImageEntries": {
|
||||
"type": "integer",
|
||||
"default": 1000,
|
||||
"description": "Maximum number of entries in the image cache (older entries will be evicted)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user