Compare commits
18 Commits
66db6a9d5b
..
v2.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| e59fba0689 | |||
| fe174ba2e0 | |||
| d8ab530fa8 | |||
| 07142eb19e | |||
| da3c0d6896 | |||
| 07d281d9f1 | |||
| 7fb0de38c9 | |||
|
5d4b50ebad
|
|||
|
3156e46c62
|
|||
|
23c80d30e9
|
|||
|
11c9c2f88d
|
|||
|
b06d27cecf
|
|||
|
5df0b53df0
|
|||
|
bf2f3d57c7
|
|||
|
1e9c175dd5
|
|||
|
da515566cb
|
|||
|
a6caa0c950
|
|||
|
807278d748
|
@@ -0,0 +1,138 @@
|
|||||||
|
name: CI/CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Package
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Extract Version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Generate PKGBUILD
|
||||||
|
run: |
|
||||||
|
TAR_URL="https://git.uyani.de/Uyanide/WallReel/archive/v${{ env.VERSION }}.tar.gz"
|
||||||
|
|
||||||
|
wget -qO source.tar.gz "$TAR_URL"
|
||||||
|
SHA256=$(sha256sum source.tar.gz | awk '{print $1}')
|
||||||
|
|
||||||
|
cat << 'EOF' > PKGBUILD
|
||||||
|
# Maintainer: Uyanide <me@uyani.de>
|
||||||
|
pkgname=wallreel
|
||||||
|
pkgver=${{ env.VERSION }}
|
||||||
|
pkgrel=1
|
||||||
|
pkgdesc="Choose and set desktop wallpapers with customizable themes and actions"
|
||||||
|
arch=('x86_64')
|
||||||
|
url="https://git.uyani.de/Uyanide/WallReel"
|
||||||
|
license=('MIT')
|
||||||
|
depends=('qt6-base' 'qt6-declarative' 'gcc-libs' 'glibc')
|
||||||
|
makedepends=('cmake' 'ninja')
|
||||||
|
source=("${pkgname}-${pkgver}.tar.gz::https://git.uyani.de/Uyanide/WallReel/archive/v${pkgver}.tar.gz")
|
||||||
|
sha256sums=('INSERT_SHA256_HERE')
|
||||||
|
|
||||||
|
build() {
|
||||||
|
cd "wallreel"
|
||||||
|
cmake -B build -S . -G Ninja \
|
||||||
|
-DCMAKE_BUILD_TYPE='Release' \
|
||||||
|
-DCMAKE_INSTALL_PREFIX='/usr' \
|
||||||
|
-Wno-dev
|
||||||
|
cmake --build build
|
||||||
|
}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "wallreel"
|
||||||
|
DESTDIR="$pkgdir" cmake --install build
|
||||||
|
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sed -i "s/INSERT_SHA256_HERE/$SHA256/" PKGBUILD
|
||||||
|
|
||||||
|
- name: Build and Generate AUR Meta
|
||||||
|
run: |
|
||||||
|
tar -cf - . | docker run --rm -i archlinux:latest /bin/bash -e -c "
|
||||||
|
mkdir -p /workspace && cd /workspace
|
||||||
|
tar -xf -
|
||||||
|
exec 3>&1 1>&2
|
||||||
|
|
||||||
|
pacman-key --init && pacman-key --populate
|
||||||
|
pacman -Sy --noconfirm archlinux-keyring
|
||||||
|
pacman -Su --noconfirm base-devel cmake ninja qt6-base qt6-declarative sudo
|
||||||
|
|
||||||
|
useradd -m builduser
|
||||||
|
chown -R builduser:builduser /workspace
|
||||||
|
echo 'builduser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
|
||||||
|
|
||||||
|
su - builduser -c 'cd /workspace && makepkg -sf --noconfirm'
|
||||||
|
su - builduser -c 'cd /workspace && makepkg --printsrcinfo' > SRCINFO.txt
|
||||||
|
|
||||||
|
tar -cf - *.pkg.tar.zst PKGBUILD LICENSE SRCINFO.txt >&3
|
||||||
|
" | tar -xf -
|
||||||
|
|
||||||
|
- name: Upload Artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: |
|
||||||
|
*.pkg.tar.zst
|
||||||
|
PKGBUILD
|
||||||
|
LICENSE
|
||||||
|
SRCINFO.txt
|
||||||
|
|
||||||
|
release:
|
||||||
|
name: Publish
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract Version
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Download Artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: release-artifacts
|
||||||
|
path: .
|
||||||
|
|
||||||
|
- name: Publish to Gitea Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
name: WallReel ${{ env.VERSION }}
|
||||||
|
draft: false
|
||||||
|
prerelease: false
|
||||||
|
files: "*.pkg.tar.zst"
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Publish to AUR
|
||||||
|
env:
|
||||||
|
AUR_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "$AUR_KEY" > ~/.ssh/aur
|
||||||
|
chmod 600 ~/.ssh/aur
|
||||||
|
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
cat <<EOF > ~/.ssh/config
|
||||||
|
Host aur.archlinux.org
|
||||||
|
IdentityFile ~/.ssh/aur
|
||||||
|
User aur
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git config --global user.name "Uyanide"
|
||||||
|
git config --global user.email "me@uyani.de"
|
||||||
|
|
||||||
|
git clone ssh://aur@aur.archlinux.org/wallreel.git aur-repo
|
||||||
|
cp PKGBUILD LICENSE aur-repo/
|
||||||
|
cp SRCINFO.txt aur-repo/.SRCINFO
|
||||||
|
cd aur-repo
|
||||||
|
|
||||||
|
git add PKGBUILD LICENSE .SRCINFO
|
||||||
|
git commit -m "Release v${{ env.VERSION }}"
|
||||||
|
git push origin master
|
||||||
@@ -81,3 +81,5 @@ CMakeLists.txt.user*
|
|||||||
.uic/
|
.uic/
|
||||||
/build*/
|
/build*/
|
||||||
.cache
|
.cache
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
## What this is
|
## What this is
|
||||||
|
|
||||||
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.
|
It might be a bit overkill 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"/>
|

|
||||||
|
|
||||||
## How to build
|
## How to build
|
||||||
|
|
||||||
1. Make sure you have Qt6 libraries, CMake and a C++ compiler installed.
|
1. Make sure Qt6 libraries, CMake, and a C++ compiler are installed.
|
||||||
|
|
||||||
e.g. On Arch-based systems:
|
On Arch-based systems:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pacman -S --needed qt6-base qt6-declarative cmake gcc
|
sudo pacman -S --needed qt6-base qt6-declarative cmake gcc
|
||||||
```
|
```
|
||||||
|
|
||||||
on Debian-based systems:
|
On Debian-based systems:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo apt install --no-install-recommends qt6-base-dev qt6-declarative-dev qml6-module-qtquick qml6-module-qtquick-controls2 qml6-module-qtquick-layouts qml6-module-qtquick-templates qml6-qtqml-workerscript cmake g++
|
sudo apt install --no-install-recommends qt6-base-dev qt6-declarative-dev qml6-module-qtquick qml6-module-qtquick-controls2 qml6-module-qtquick-layouts qml6-module-qtquick-templates qml6-qtqml-workerscript cmake g++
|
||||||
@@ -29,35 +29,63 @@ It might not be that worthy to build a Qt application from ground for such a sma
|
|||||||
|
|
||||||
3. Build and install:
|
3. Build and install:
|
||||||
|
|
||||||
This is a standard CMake managed project, so the build process is pretty normal and straightforward. First, configure the project:
|
This is a standard CMake project. First, configure it. Adjust the install prefix as needed.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
|
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/usr/local
|
||||||
```
|
```
|
||||||
|
|
||||||
Adjust install prefix to your needs. Start building:
|
Then build.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake --build build -- -j$(nproc)
|
cmake --build build -- -j$(nproc)
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary will be located at `build/wallreel` and can be run directly for testing:
|
The binary will be located at `build/wallreel` and can be run directly for testing.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
build/wallreel
|
build/wallreel
|
||||||
```
|
```
|
||||||
|
|
||||||
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`.
|
Install to the configured prefix. This step may require root permissions if the prefix is set to a system path such as `/usr/local`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cmake --install build
|
cmake --install build --strip
|
||||||
|
```
|
||||||
|
|
||||||
|
`--strip` reduces binary size by removing symbol information that is usually unnecessary for normal usage.
|
||||||
|
|
||||||
|
## Man Pages
|
||||||
|
|
||||||
|
This project ships man pages and installs them through `cmake --install`.
|
||||||
|
|
||||||
|
- `wallreel(1)` for CLI usage
|
||||||
|
- `wallreel(5)` for configuration
|
||||||
|
|
||||||
|
The source files are maintained in Markdown for easier editing:
|
||||||
|
|
||||||
|
- `docs/man/man.1.md`
|
||||||
|
- `docs/man/man.5.md`
|
||||||
|
|
||||||
|
Generated man files are committed for packaging and normal installation without extra tool dependencies:
|
||||||
|
|
||||||
|
- `WallReel/Assets/man/man.1`
|
||||||
|
- `WallReel/Assets/man/man.5`
|
||||||
|
|
||||||
|
To regenerate them, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for src in docs/man/man.*.md; do
|
||||||
|
dst="WallReel/Assets/man/$(basename "${src%.md}")"
|
||||||
|
pandoc --from gfm --to man --standalone -o "$dst" "$src"
|
||||||
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
## Configuration Reference
|
## 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.
|
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`)
|
### Wallpaper (`wallpaper`)
|
||||||
|
|
||||||
@@ -73,13 +101,12 @@ Defines where WallReel looks for images and what to exclude. If none of the `pat
|
|||||||
|
|
||||||
Configures the color palettes.
|
Configures the color palettes.
|
||||||
|
|
||||||
By default, a **dominant color** will be extracted from each wallpaper. If a palette is **selected**, the color that matches the dominant color the best will be selected as the **primary color**. This might be convinient if you prefer to set your desktop theme to match the wallpaper using a predefined palette (e.g. Catppuccin, Tokyo Night) instead of generating a custom one (e.g. using matugen).
|
By default, a **dominant color** is extracted from each wallpaper. If a palette is **selected**, the closest palette color is used as the **primary color**. This is useful when you want your desktop theme to follow a predefined palette (for example Catppuccin or Tokyo Night) instead of generating a custom one (for example with matugen).
|
||||||
|
|
||||||
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.
|
Several embedded palettes are available, including "Catppuccin Frappe", "Catppuccin Latte", "Catppuccin Macchiato", and "Catppuccin Mocha". You can also define custom palettes or override embedded ones via configuration.
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
| 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"`). |
|
| `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`)
|
### Action (`action`)
|
||||||
@@ -87,13 +114,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`).
|
Configures system commands to execute on specific events mapping to your window manager or wallpaper utility (e.g., `swaybg`, `feh`).
|
||||||
|
|
||||||
| Property | Type | Default | Description |
|
| Property | Type | Default | Description |
|
||||||
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| :-------------------- | :--------------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. |
|
| `previewDebounceTime` | Integer | `300` | Debounce time (ms) for triggering the preview action. |
|
||||||
| `printSelected` | Boolean | `true` | Print selected wallpaper path to stdout on confirm. |
|
| `printSelected` | Boolean | `true` | Print selected wallpaper path to stdout on confirm. |
|
||||||
| `printPreview` | Boolean | `false` | Print previewed wallpaper path to stdout on preview. |
|
| `printPreview` | Boolean | `false` | Print previewed wallpaper path to stdout on preview. |
|
||||||
| `onSelected` | String | `""` | Command to execute when a wallpaper is confirmed. |
|
| `onSelected` | String | `""` | Command to execute when a wallpaper is confirmed. |
|
||||||
| `onPreview` | String | `""` | Command to execute when a wallpaper is previewed. |
|
| `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 }}`. |
|
| `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. |
|
| `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. |
|
| `restoreOnClose` | Boolean | `true` | Run `onRestore` command if the application is closed without making a final selection. |
|
||||||
@@ -109,7 +136,7 @@ Available placeholders for `onSelected`, `onPreview` commands:
|
|||||||
| `{{ colorName }}` | Name of the currently determined primary color. ("null" if none) |
|
| `{{ 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) |
|
| `{{ 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. |
|
| `{{ 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`)
|
### Style (`style`)
|
||||||
|
|
||||||
@@ -123,14 +150,15 @@ Controls the layout and dimensions of the application window and image items.
|
|||||||
| `window_width` | Integer | `750` | Initial application window width. |
|
| `window_width` | Integer | `750` | Initial application window width. |
|
||||||
| `window_height` | Integer | `500` | Initial application window height. |
|
| `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 |
|
| Property | Type | Default | Description |
|
||||||
| :----------- | :------ | :------- | :------------------------------------------------------------------------------- |
|
| :---------------- | :------ | :------ | :---------------------------------------------------------------------------- |
|
||||||
| `type` | String | `"date"` | Defines sorting criteria. Acceptable values: `"name"`, `"date"`, `"size"`. |
|
| `saveSortMethod` | Boolean | `true` | Whether to persist the sort type and order. |
|
||||||
| `descending` | Boolean | `true` | If true, sorts in descending order (e.g. newer dates first, larger files first). |
|
| `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 +178,6 @@ Initial sorting behavior for loaded images.
|
|||||||
"excludes": ["\\.gif$"]
|
"excludes": ["\\.gif$"]
|
||||||
},
|
},
|
||||||
"theme": {
|
"theme": {
|
||||||
"defaultPalette": "Dark",
|
|
||||||
"palettes": [
|
"palettes": [
|
||||||
{
|
{
|
||||||
"name": "Dark",
|
"name": "Dark",
|
||||||
@@ -169,7 +196,7 @@ Initial sorting behavior for loaded images.
|
|||||||
"saveState": [
|
"saveState": [
|
||||||
{
|
{
|
||||||
"key": "current_wp",
|
"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",
|
"command": "find ~/.config/wallpaper/current -type f | head -n 1",
|
||||||
"timeout": 1000
|
"timeout": 1000
|
||||||
}
|
}
|
||||||
@@ -183,32 +210,37 @@ Initial sorting behavior for loaded images.
|
|||||||
"window_width": 1280,
|
"window_width": 1280,
|
||||||
"window_height": 720
|
"window_height": 720
|
||||||
},
|
},
|
||||||
"sort": {
|
"cache": {
|
||||||
"type": "date",
|
"saveSortMethod": true,
|
||||||
"descending": true
|
"savePalette": true,
|
||||||
|
"maxImageEntries": 300
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
|
|
||||||
```
|
```text
|
||||||
Usage: wallreel [options]
|
Usage: wallreel [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Displays help on commandline options.
|
-h, --help Displays help on commandline options.
|
||||||
-v, --version Displays version information.
|
-v, --version Displays version information.
|
||||||
-V, --verbose Set log level to DEBUG (default is INFO)
|
-V, --verbose Set log level to DEBUG (default is INFO)
|
||||||
-C, --clear-cache Clear the cache and exit
|
-C, --clear-cache Clear the image cache and exit
|
||||||
-q, --quiet Suppress all log output
|
-q, --quiet Suppress all log output
|
||||||
-d, --append-dir <dir> Append an additional wallpaper search directory
|
-d, --append-dir <dir> Append an additional wallpaper search directory
|
||||||
-c, --config-file <file> Specify a custom configuration file
|
-c, --config-file <file> Specify a custom configuration file
|
||||||
|
-D, --disable-actions Disable actions set in configuration file
|
||||||
|
-a, --apply <file> Apply the specified image as wallpaper and exit
|
||||||
```
|
```
|
||||||
|
|
||||||
A few things to notice:
|
A few things to notice:
|
||||||
|
|
||||||
- It's generally not necessary to provide any CLI arguments, I would recommend using the config file to customize the behavior instead. However, it is still possible to control some essential options via CLI.
|
- In most cases you do not need CLI arguments; configuration is usually the better place to customize behavior. CLI flags are still useful for quick overrides and one-shot runs though.
|
||||||
|
|
||||||
- The `--append-dir` option can be used multiple times to add multiple directories.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
- With `--apply`, WallReel still parses the configuration (default path or `--config-file`) and executes `onSelected` with placeholders resolved from the specified image. If `savePalette` is enabled and a palette was selected in the last session, `palette`, `colorName`, and `colorHex` placeholders are also available. `saveState` commands are executed as well. The application exits immediately after executing the action, without opening the UI. This mode allows WallReel to be used as a command-line wallpaper setter with palette-aware theming and state placeholders.
|
||||||
|
|||||||
@@ -1,13 +1,29 @@
|
|||||||
qt_add_resources(${EXECUTABLE_NAME} "app_icons"
|
qt_add_resources(${EXECUTABLE_NAME} "app_icons"
|
||||||
PREFIX "/"
|
PREFIX "/"
|
||||||
FILES
|
FILES icon.svg
|
||||||
${EXECUTABLE_NAME}.svg
|
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.desktop
|
install(FILES ${CMAKE_CURRENT_LIST_DIR}/app.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/applications
|
||||||
|
RENAME ${EXECUTABLE_NAME}.desktop
|
||||||
)
|
)
|
||||||
|
|
||||||
install(FILES ${CMAKE_CURRENT_LIST_DIR}/${EXECUTABLE_NAME}.svg
|
install(FILES ${CMAKE_CURRENT_LIST_DIR}/apply.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/icons/hicolor/scalable/apps
|
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
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES ${CMAKE_CURRENT_LIST_DIR}/man/man.1
|
||||||
|
DESTINATION ${CMAKE_INSTALL_MANDIR}/man1
|
||||||
|
RENAME ${EXECUTABLE_NAME}.1
|
||||||
|
)
|
||||||
|
|
||||||
|
install(FILES ${CMAKE_CURRENT_LIST_DIR}/man/man.5
|
||||||
|
DESTINATION ${CMAKE_INSTALL_MANDIR}/man5
|
||||||
|
RENAME ${EXECUTABLE_NAME}.5
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ Version=1.0
|
|||||||
Type=Application
|
Type=Application
|
||||||
Name=WallReel
|
Name=WallReel
|
||||||
Icon=wallreel
|
Icon=wallreel
|
||||||
GenericName=Animated wallpaper selector
|
GenericName=Wallpaper Selector
|
||||||
TryExec=wallreel
|
TryExec=wallreel
|
||||||
Exec=wallreel
|
Exec=wallreel
|
||||||
Comment=A small wallpaper utility made with Qt
|
Comment=Choose and set desktop wallpapers with customizable themes and actions
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Categories=Application;Utility;DesktopSettings;
|
Categories=Application;Utility;DesktopSettings;
|
||||||
StartupNotify=true
|
StartupNotify=true
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Version=1.0
|
||||||
|
Type=Application
|
||||||
|
Name=Apply with WallReel
|
||||||
|
Icon=wallreel
|
||||||
|
NoDisplay=true
|
||||||
|
GenericName=Wallpaper Selector
|
||||||
|
TryExec=wallreel
|
||||||
|
Exec=wallreel -a %f
|
||||||
|
Comment=Choose and set desktop wallpapers with customizable themes and actions
|
||||||
|
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 |
@@ -0,0 +1,89 @@
|
|||||||
|
.\" Automatically generated by Pandoc 3.5
|
||||||
|
.\"
|
||||||
|
.TH "WALLREEL" "1" "2026\-03\-24" "WallReel 2.0.0" "User Commands"
|
||||||
|
.SH NAME
|
||||||
|
wallreel \- Choose and set desktop wallpapers with customizable themes
|
||||||
|
and actions
|
||||||
|
.SH SYNOPSIS
|
||||||
|
\f[B]wallreel\f[R] [\f[I]options\f[R]]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
\f[B]wallreel\f[R] is a Qt6 application for browsing wallpaper images,
|
||||||
|
previewing candidates, and applying a selected image.
|
||||||
|
.PP
|
||||||
|
Configuration is loaded from a JSON file.
|
||||||
|
CLI options are available for logging, one\-shot operations, and runtime
|
||||||
|
overrides.
|
||||||
|
.SH OPTIONS
|
||||||
|
\f[B]\-h, \-\-help\f[R] : Display help for command\-line options.
|
||||||
|
.PP
|
||||||
|
\f[B]\-v, \-\-version\f[R] : Display version information.
|
||||||
|
.PP
|
||||||
|
\f[B]\-V, \-\-verbose\f[R] : Set log level to DEBUG (default is INFO).
|
||||||
|
.PP
|
||||||
|
\f[B]\-C, \-\-clear\-cache\f[R] : Clear image cache and exit.
|
||||||
|
.PP
|
||||||
|
\f[B]\-q, \-\-quiet\f[R] : Suppress log output.
|
||||||
|
.PP
|
||||||
|
\f[B]\-d, \-\-append\-dir\f[R] \f[I]dir\f[R] : Append an additional
|
||||||
|
wallpaper search directory.
|
||||||
|
.PP
|
||||||
|
This option can be provided multiple times.
|
||||||
|
.PP
|
||||||
|
\f[B]\-c, \-\-config\-file\f[R] \f[I]file\f[R] : Use a custom
|
||||||
|
configuration file.
|
||||||
|
.PP
|
||||||
|
\f[B]\-D, \-\-disable\-actions\f[R] : Disable actions defined in the
|
||||||
|
configuration file.
|
||||||
|
.PP
|
||||||
|
\f[B]\-a, \-\-apply\f[R] \f[I]file\f[R] : Apply the specified image as
|
||||||
|
wallpaper and exit.
|
||||||
|
.PP
|
||||||
|
In this mode, the configuration is still parsed.
|
||||||
|
Action placeholders are resolved from the selected image and any
|
||||||
|
captured state values.
|
||||||
|
.SH BEHAVIOR NOTES
|
||||||
|
.IP \[bu] 2
|
||||||
|
CLI options are generally optional; configuration is the preferred
|
||||||
|
customization path.
|
||||||
|
.IP \[bu] 2
|
||||||
|
Some options are mutually exclusive (for example \f[CR]\-\-verbose\f[R]
|
||||||
|
and \f[CR]\-\-quiet\f[R]).
|
||||||
|
.IP \[bu] 2
|
||||||
|
With \f[CR]\-\-apply\f[R], WallReel executes configured selection
|
||||||
|
actions without opening the UI.
|
||||||
|
.SH FILES
|
||||||
|
\f[CR]\[ti]/.config/wallreel/config.json\f[R] : Default configuration
|
||||||
|
file location.
|
||||||
|
.PP
|
||||||
|
\f[CR]\[ti]/.cache/wallreel/\f[R] : Runtime cache location.
|
||||||
|
.SH EXAMPLES
|
||||||
|
Run with default configuration:
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
wallreel
|
||||||
|
.EE
|
||||||
|
.PP
|
||||||
|
Use a custom configuration file:
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
wallreel \-\-config\-file \[ti]/.config/wallreel/config.json
|
||||||
|
.EE
|
||||||
|
.PP
|
||||||
|
Append additional search directories:
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
wallreel \-\-append\-dir \[ti]/Pictures/Wallpapers \-\-append\-dir \[ti]/Art
|
||||||
|
.EE
|
||||||
|
.PP
|
||||||
|
Apply a wallpaper and exit:
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
wallreel \-\-apply \[ti]/Pictures/wallpaper.jpg
|
||||||
|
.EE
|
||||||
|
.SH EXIT STATUS
|
||||||
|
Returns \f[CR]0\f[R] on success.
|
||||||
|
Returns a non\-zero value on failure.
|
||||||
|
.SH SEE ALSO
|
||||||
|
\f[B]wallreel\f[R](5)
|
||||||
|
.SH AUTHOR
|
||||||
|
Uyanide <github.com/Uyanide>
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
.\" Automatically generated by Pandoc 3.5
|
||||||
|
.\"
|
||||||
|
.TH "WALLREEL" "5" "2026\-03\-24" "WallReel 2.0.0" "File Formats Manual"
|
||||||
|
.SH NAME
|
||||||
|
wallreel\-config \- configuration format for wallreel
|
||||||
|
.SH SYNOPSIS
|
||||||
|
\f[CR]\[ti]/.config/wallreel/config.json\f[R]
|
||||||
|
.SH DESCRIPTION
|
||||||
|
WallReel reads configuration from a JSON document.
|
||||||
|
The root object is divided into five sections:
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]wallpaper\f[R]
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]theme\f[R]
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]action\f[R]
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]style\f[R]
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]cache\f[R]
|
||||||
|
.PP
|
||||||
|
For complete machine\-readable validation details, refer to
|
||||||
|
\f[CR]config.schema.json\f[R].
|
||||||
|
.SH WALLPAPER SECTION
|
||||||
|
Defines where WallReel looks for images and what to exclude.
|
||||||
|
.PP
|
||||||
|
If both \f[CR]paths\f[R] and \f[CR]dirs\f[R] are empty or omitted,
|
||||||
|
WallReel defaults to recursively scanning the user\[aq]s Pictures
|
||||||
|
directory and treating all supported image files as wallpaper
|
||||||
|
candidates.
|
||||||
|
.PP
|
||||||
|
\f[CR]paths\f[R] (array of string, default: \f[CR][]\f[R]) : Exact paths
|
||||||
|
to specific image files.
|
||||||
|
.PP
|
||||||
|
\f[CR]dirs\f[R] (array of object, default: \f[CR][]\f[R]) : Directories
|
||||||
|
to scan for images.
|
||||||
|
.PP
|
||||||
|
Each item has:
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]path\f[R] (string)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]recursive\f[R] (boolean)
|
||||||
|
.PP
|
||||||
|
\f[CR]excludes\f[R] (array of string, default: \f[CR][]\f[R]) : Exclude
|
||||||
|
patterns as regular expressions.
|
||||||
|
.SH THEME SECTION
|
||||||
|
Configures color palettes.
|
||||||
|
.PP
|
||||||
|
A dominant color is extracted from each wallpaper.
|
||||||
|
If a palette is selected, WallReel picks the closest palette color as
|
||||||
|
the primary color.
|
||||||
|
.PP
|
||||||
|
\f[CR]palettes\f[R] (array of object, default: \f[CR][]\f[R]) : Custom
|
||||||
|
palette definitions.
|
||||||
|
.PP
|
||||||
|
Each palette has:
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]name\f[R] (string)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]colors\f[R] (array)
|
||||||
|
.PP
|
||||||
|
Each color item has:
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]name\f[R] (string)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]value\f[R] (hex string, for example \f[CR]\[dq]#89b4fa\[dq]\f[R])
|
||||||
|
.SH ACTION SECTION
|
||||||
|
Configures commands executed for preview, selection, and restore
|
||||||
|
behavior.
|
||||||
|
.PP
|
||||||
|
\f[CR]previewDebounceTime\f[R] (integer, default: \f[CR]300\f[R]) :
|
||||||
|
Debounce interval in milliseconds for preview actions.
|
||||||
|
.PP
|
||||||
|
\f[CR]printSelected\f[R] (boolean, default: \f[CR]true\f[R]) : Print
|
||||||
|
selected wallpaper path to stdout on confirmation.
|
||||||
|
.PP
|
||||||
|
\f[CR]printPreview\f[R] (boolean, default: \f[CR]false\f[R]) : Print
|
||||||
|
previewed wallpaper path to stdout on preview.
|
||||||
|
.PP
|
||||||
|
\f[CR]onSelected\f[R] (string, default: \f[CR]\[dq]\[dq]\f[R]) : Command
|
||||||
|
executed when a wallpaper is confirmed.
|
||||||
|
.PP
|
||||||
|
\f[CR]onPreview\f[R] (string, default: \f[CR]\[dq]\[dq]\f[R]) : Command
|
||||||
|
executed when a wallpaper is previewed.
|
||||||
|
.PP
|
||||||
|
\f[CR]saveState\f[R] (array of object, default: \f[CR][]\f[R]) :
|
||||||
|
Commands for capturing system values before changing wallpaper.
|
||||||
|
.PP
|
||||||
|
Each item has:
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]key\f[R] (placeholder key)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]fallback\f[R] (default value)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]command\f[R] (stdout\-mapped command)
|
||||||
|
.IP \[bu] 2
|
||||||
|
\f[CR]timeout\f[R] (milliseconds)
|
||||||
|
.PP
|
||||||
|
\f[CR]onRestore\f[R] (string, default: \f[CR]\[dq]\[dq]\f[R]) : Command
|
||||||
|
executed on restore.
|
||||||
|
Saved state keys are usable as placeholders.
|
||||||
|
.PP
|
||||||
|
\f[CR]quitOnSelected\f[R] (boolean, default: \f[CR]false\f[R]) : Exit
|
||||||
|
application immediately after confirming a selection.
|
||||||
|
.PP
|
||||||
|
\f[CR]restoreOnClose\f[R] (boolean, default: \f[CR]true\f[R]) : Run
|
||||||
|
\f[CR]onRestore\f[R] when application closes without a final selection.
|
||||||
|
.SS ACTION PLACEHOLDERS
|
||||||
|
The following placeholders are available in \f[CR]onSelected\f[R],
|
||||||
|
\f[CR]onPreview\f[R], and \f[CR]onRestore\f[R] (where applicable):
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ path }}\f[R] : Full path of selected or previewed wallpaper.
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ name }}\f[R] : File name of selected or previewed wallpaper.
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ size }}\f[R] : Size in bytes of selected or previewed
|
||||||
|
wallpaper.
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ palette }}\f[R] : Selected palette name
|
||||||
|
(\f[CR]\[dq]null\[dq]\f[R] if none).
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ colorName }}\f[R] : Chosen primary color name
|
||||||
|
(\f[CR]\[dq]null\[dq]\f[R] if none).
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ colorHex }}\f[R] : Chosen primary color hex
|
||||||
|
(\f[CR]\[dq]null\[dq]\f[R] if none).
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ domColorHex }}\f[R] : Dominant color hex extracted from the
|
||||||
|
wallpaper.
|
||||||
|
.PP
|
||||||
|
\f[CR]{{ <key> }}\f[R] : Value of a saved state item with matching key.
|
||||||
|
.SH STYLE SECTION
|
||||||
|
Controls window layout and thumbnail dimensions.
|
||||||
|
.PP
|
||||||
|
\f[CR]image_width\f[R] (integer, default: \f[CR]320\f[R]) : Width of
|
||||||
|
each thumbnail.
|
||||||
|
.PP
|
||||||
|
\f[CR]image_height\f[R] (integer, default: \f[CR]180\f[R]) : Height of
|
||||||
|
each thumbnail.
|
||||||
|
.PP
|
||||||
|
\f[CR]image_focus_scale\f[R] (number, default: \f[CR]1.5\f[R]) : Focus
|
||||||
|
scale multiplier for highlighted thumbnail.
|
||||||
|
.PP
|
||||||
|
\f[CR]window_width\f[R] (integer, default: \f[CR]750\f[R]) : Initial
|
||||||
|
window width.
|
||||||
|
.PP
|
||||||
|
\f[CR]window_height\f[R] (integer, default: \f[CR]500\f[R]) : Initial
|
||||||
|
window height.
|
||||||
|
.SH CACHE SECTION
|
||||||
|
Controls persisted UI state.
|
||||||
|
.PP
|
||||||
|
\f[CR]saveSortMethod\f[R] (boolean, default: \f[CR]true\f[R]) : Persist
|
||||||
|
sort method and direction.
|
||||||
|
.PP
|
||||||
|
\f[CR]savePalette\f[R] (boolean, default: \f[CR]true\f[R]) : Persist
|
||||||
|
selected palette.
|
||||||
|
.PP
|
||||||
|
\f[CR]maxImageEntries\f[R] (integer, default: \f[CR]1000\f[R]) : Maximum
|
||||||
|
number of image cache entries.
|
||||||
|
Older entries are evicted.
|
||||||
|
.SH EXAMPLE
|
||||||
|
.IP
|
||||||
|
.EX
|
||||||
|
{
|
||||||
|
\[dq]$schema\[dq]: \[dq]https://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/config.schema.json\[dq],
|
||||||
|
\[dq]wallpaper\[dq]: {
|
||||||
|
\[dq]paths\[dq]: [\[dq]/home/user/Pictures/favorite.jpg\[dq]],
|
||||||
|
\[dq]dirs\[dq]: [
|
||||||
|
{
|
||||||
|
\[dq]path\[dq]: \[dq]/home/user/Pictures/Wallpapers\[dq],
|
||||||
|
\[dq]recursive\[dq]: \f[B]true\f[R]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\[dq]excludes\[dq]: [\[dq]\[rs]\[rs].gif$\[dq]]
|
||||||
|
},
|
||||||
|
\[dq]theme\[dq]: {
|
||||||
|
\[dq]palettes\[dq]: [
|
||||||
|
{
|
||||||
|
\[dq]name\[dq]: \[dq]Dark\[dq],
|
||||||
|
\[dq]colors\[dq]: [
|
||||||
|
{ \[dq]name\[dq]: \[dq]blue\[dq], \[dq]value\[dq]: \[dq]#89b4fa\[dq] },
|
||||||
|
{ \[dq]name\[dq]: \[dq]red\[dq], \[dq]value\[dq]: \[dq]#f38ba8\[dq] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
\[dq]action\[dq]: {
|
||||||
|
\[dq]previewDebounceTime\[dq]: 500,
|
||||||
|
\[dq]quitOnSelected\[dq]: \f[B]true\f[R],
|
||||||
|
\[dq]onPreview\[dq]: \[dq]swww img {{ path }}\[dq],
|
||||||
|
\[dq]onSelected\[dq]: \[dq]cp {{ path }} \[ti]/.config/wallpaper/current/ && swww img {{ path }}\[dq],
|
||||||
|
\[dq]saveState\[dq]: [
|
||||||
|
{
|
||||||
|
\[dq]key\[dq]: \[dq]current_wp\[dq],
|
||||||
|
\[dq]fallback\[dq]: \[dq]/home/user/Pictures/default.jpg\[dq],
|
||||||
|
\[dq]command\[dq]: \[dq]find \[ti]/.config/wallpaper/current \-type f | head \-n 1\[dq],
|
||||||
|
\[dq]timeout\[dq]: 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
\[dq]onRestore\[dq]: \[dq]swww img {{ current_wp }}\[dq]
|
||||||
|
},
|
||||||
|
\[dq]style\[dq]: {
|
||||||
|
\[dq]image_width\[dq]: 640,
|
||||||
|
\[dq]image_height\[dq]: 400,
|
||||||
|
\[dq]image_focus_scale\[dq]: 1.2,
|
||||||
|
\[dq]window_width\[dq]: 1280,
|
||||||
|
\[dq]window_height\[dq]: 720
|
||||||
|
},
|
||||||
|
\[dq]cache\[dq]: {
|
||||||
|
\[dq]saveSortMethod\[dq]: \f[B]true\f[R],
|
||||||
|
\[dq]savePalette\[dq]: \f[B]true\f[R],
|
||||||
|
\[dq]maxImageEntries\[dq]: 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.EE
|
||||||
|
.SH SEE ALSO
|
||||||
|
\f[B]wallreel\f[R](1)
|
||||||
|
.SH AUTHOR
|
||||||
|
Uyanide <github.com/Uyanide>
|
||||||
@@ -15,7 +15,7 @@ qt_add_qml_module(${CORELIB_NAME}
|
|||||||
Config/data.hpp
|
Config/data.hpp
|
||||||
Config/manager.hpp Config/manager.cpp
|
Config/manager.hpp Config/manager.cpp
|
||||||
logger.hpp logger.cpp
|
logger.hpp logger.cpp
|
||||||
Service/manager.hpp
|
Service/manager.hpp Service/manager.cpp
|
||||||
Service/wallpaper.hpp Service/wallpaper.cpp
|
Service/wallpaper.hpp Service/wallpaper.cpp
|
||||||
appoptions.hpp appoptions.cpp
|
appoptions.hpp appoptions.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
+289
-28
@@ -7,6 +7,7 @@
|
|||||||
#include <QSqlError>
|
#include <QSqlError>
|
||||||
#include <QSqlQuery>
|
#include <QSqlQuery>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
|
#include <QtConcurrent>
|
||||||
|
|
||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
|
|
||||||
@@ -16,21 +17,51 @@ using namespace Qt::StringLiterals;
|
|||||||
|
|
||||||
namespace WallReel::Core::Cache {
|
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) {
|
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(
|
return QString::fromLatin1(
|
||||||
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
|
QCryptographicHash::hash(raw.toUtf8(), QCryptographicHash::Sha256).toHex());
|
||||||
}
|
}
|
||||||
|
|
||||||
Manager::Manager(const QDir& cacheDir)
|
Manager::Manager(const QDir& cacheDir, int maxEntries)
|
||||||
: 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())) {
|
: 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));
|
WR_DEBUG(u"Initializing cache db: %1"_s.arg(m_dbPath));
|
||||||
// Open a connection on the constructing thread so the schema is
|
// Open a connection on the constructing thread so the schema is
|
||||||
// guaranteed to exist before any worker thread first calls _db().
|
// guaranteed to exist before any worker thread first calls _db().
|
||||||
_db();
|
_db();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Manager::evictOldEntries() {
|
||||||
|
if (m_maxEntries > 0)
|
||||||
|
m_cleanupFuture = QtConcurrent::run([this] { _runCleanup(); });
|
||||||
|
}
|
||||||
|
|
||||||
Manager::~Manager() {
|
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;
|
QSet<QString> names;
|
||||||
{
|
{
|
||||||
QMutexLocker lock(&m_connectionsMutex);
|
QMutexLocker lock(&m_connectionsMutex);
|
||||||
@@ -50,41 +81,59 @@ void Manager::clearCache(Type type) {
|
|||||||
if ((type & Type::Image) != Type::None) {
|
if ((type & Type::Image) != Type::None) {
|
||||||
int removed = 0;
|
int removed = 0;
|
||||||
QSqlQuery selectQuery(db);
|
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()) {
|
while (selectQuery.next()) {
|
||||||
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
|
QFile::remove(m_cacheDir.filePath(selectQuery.value(0).toString()));
|
||||||
++removed;
|
++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));
|
WR_INFO(u"Cleared %1 image cache file(s)"_s.arg(removed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((type & Type::Color) != Type::None) {
|
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);
|
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) {
|
QColor Manager::getColor(const QString& key, const std::function<QColor()>& computeFunc) {
|
||||||
QSqlDatabase db = _db();
|
QSqlDatabase db = _db();
|
||||||
if (db.isOpen()) {
|
if (db.isOpen()) {
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(QStringLiteral(
|
query.prepare(u"SELECT r, g, b, a FROM color_cache WHERE key = :key"_s);
|
||||||
"SELECT r, g, b, a FROM color_cache WHERE key = :key"));
|
|
||||||
query.bindValue(u":key"_s, key);
|
query.bindValue(u":key"_s, key);
|
||||||
|
|
||||||
if (query.exec() && query.next()) {
|
if (query.exec() && query.next()) {
|
||||||
WR_DEBUG(u"Color cache hit [%1]"_s.arg(key));
|
WR_DEBUG(u"Color cache hit [%1]"_s.arg(key));
|
||||||
return QColor(
|
QColor result(
|
||||||
query.value(0).toInt(),
|
query.value(0).toInt(),
|
||||||
query.value(1).toInt(),
|
query.value(1).toInt(),
|
||||||
query.value(2).toInt(),
|
query.value(2).toInt(),
|
||||||
query.value(3).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));
|
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();
|
const QColor color = computeFunc();
|
||||||
|
|
||||||
if (!color.isValid()) {
|
if (!color.isValid()) {
|
||||||
@@ -94,9 +143,9 @@ QColor Manager::getColor(const QString& key, const std::function<QColor()>& comp
|
|||||||
|
|
||||||
if (db.isOpen()) {
|
if (db.isOpen()) {
|
||||||
QSqlQuery insertQuery(db);
|
QSqlQuery insertQuery(db);
|
||||||
insertQuery.prepare(QStringLiteral(
|
insertQuery.prepare(
|
||||||
"INSERT OR REPLACE INTO color_cache (key, r, g, b, a) "
|
u"INSERT OR REPLACE INTO color_cache (key, r, g, b, a, last_accessed) "
|
||||||
"VALUES (:key, :r, :g, :b, :a)"));
|
"VALUES (:key, :r, :g, :b, :a, CURRENT_TIMESTAMP)"_s);
|
||||||
insertQuery.bindValue(u":key"_s, key);
|
insertQuery.bindValue(u":key"_s, key);
|
||||||
insertQuery.bindValue(u":r"_s, color.red());
|
insertQuery.bindValue(u":r"_s, color.red());
|
||||||
insertQuery.bindValue(u":g"_s, color.green());
|
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();
|
QSqlDatabase db = _db();
|
||||||
if (db.isOpen()) {
|
if (db.isOpen()) {
|
||||||
QSqlQuery query(db);
|
QSqlQuery query(db);
|
||||||
query.prepare(QStringLiteral(
|
query.prepare(u"SELECT file_name FROM image_cache WHERE key = :key"_s);
|
||||||
"SELECT file_name FROM image_cache WHERE key = :key"));
|
|
||||||
query.bindValue(u":key"_s, key);
|
query.bindValue(u":key"_s, key);
|
||||||
|
|
||||||
if (query.exec() && query.next()) {
|
if (query.exec() && query.next()) {
|
||||||
@@ -125,29 +173,42 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
|
|||||||
if (cached.exists()) {
|
if (cached.exists()) {
|
||||||
WR_DEBUG(u"Image cache hit [%1] -> %2"_s
|
WR_DEBUG(u"Image cache hit [%1] -> %2"_s
|
||||||
.arg(key, cached.absoluteFilePath()));
|
.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;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
// File was deleted externally — evict the stale DB record.
|
// File was deleted externally — evict the stale DB record.
|
||||||
WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
|
WR_WARN(u"Image cache stale, file missing [%1], evicting"_s.arg(key));
|
||||||
QSqlQuery evict(db);
|
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.bindValue(u":key"_s, key);
|
||||||
evict.exec();
|
evict.exec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
WR_DEBUG(u"Image cache miss [%1], computing"_s.arg(key));
|
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();
|
const QImage image = computeFunc();
|
||||||
if (image.isNull()) {
|
if (image.isNull()) {
|
||||||
WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
|
WR_WARN(u"ComputeFunc returned null image for key [%1]"_s.arg(key));
|
||||||
return QFileInfo{};
|
return QFileInfo{};
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString fileName = key + u".png"_s;
|
const QString fileName = key + u".jpg"_s;
|
||||||
const QString filePath = m_cacheDir.filePath(fileName);
|
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));
|
WR_WARN(u"Failed to save image to %1"_s.arg(filePath));
|
||||||
return QFileInfo{};
|
return QFileInfo{};
|
||||||
}
|
}
|
||||||
@@ -155,19 +216,93 @@ QFileInfo Manager::getImage(const QString& key, const std::function<QImage()>& c
|
|||||||
|
|
||||||
if (db.isOpen()) {
|
if (db.isOpen()) {
|
||||||
QSqlQuery insertQuery(db);
|
QSqlQuery insertQuery(db);
|
||||||
insertQuery.prepare(QStringLiteral(
|
insertQuery.prepare(
|
||||||
"INSERT OR REPLACE INTO image_cache (key, file_name) "
|
u"INSERT OR REPLACE INTO image_cache (key, file_name, last_accessed) "
|
||||||
"VALUES (:key, :file_name)"));
|
"VALUES (:key, :file_name, CURRENT_TIMESTAMP)"_s);
|
||||||
insertQuery.bindValue(u":key"_s, key);
|
insertQuery.bindValue(u":key"_s, key);
|
||||||
insertQuery.bindValue(u":file_name"_s, fileName);
|
insertQuery.bindValue(u":file_name"_s, fileName);
|
||||||
if (!insertQuery.exec())
|
if (!insertQuery.exec())
|
||||||
WR_WARN(u"Failed to record image in db [%1]: %2"_s
|
WR_WARN(u"Failed to record image in db [%1]: %2"_s
|
||||||
.arg(key, insertQuery.lastError().text()));
|
.arg(key, insertQuery.lastError().text()));
|
||||||
|
else {
|
||||||
|
QMutexLocker lock(&m_hotKeysMutex);
|
||||||
|
m_hotImageKeys.insert(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return QFileInfo(filePath);
|
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.
|
/// Returns an open QSqlDatabase for the calling thread, creating it on first use.
|
||||||
QSqlDatabase Manager::_db() const {
|
QSqlDatabase Manager::_db() const {
|
||||||
// thread_local: one slot per OS thread, initialized on first call in that thread.
|
// 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 {
|
void Manager::_setupTables(QSqlDatabase& db) const {
|
||||||
QSqlQuery q(db);
|
QSqlQuery q(db);
|
||||||
q.exec(QStringLiteral(
|
q.exec(
|
||||||
"CREATE TABLE IF NOT EXISTS color_cache ("
|
u"CREATE TABLE IF NOT EXISTS color_cache ("
|
||||||
" key TEXT PRIMARY KEY NOT NULL,"
|
" key TEXT PRIMARY KEY NOT NULL,"
|
||||||
" r INTEGER NOT NULL,"
|
" r INTEGER NOT NULL,"
|
||||||
" g INTEGER NOT NULL,"
|
" g INTEGER NOT NULL,"
|
||||||
" b INTEGER NOT NULL,"
|
" b INTEGER NOT NULL,"
|
||||||
" a INTEGER NOT NULL"
|
" a INTEGER NOT NULL,"
|
||||||
")"));
|
" last_accessed TEXT"
|
||||||
q.exec(QStringLiteral(
|
")"_s);
|
||||||
"CREATE TABLE IF NOT EXISTS image_cache ("
|
q.exec(
|
||||||
|
u"CREATE TABLE IF NOT EXISTS image_cache ("
|
||||||
" key TEXT PRIMARY KEY NOT NULL,"
|
" 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
|
} // namespace WallReel::Core::Cache
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QFuture>
|
||||||
#include <QMutex>
|
#include <QMutex>
|
||||||
#include <QSet>
|
#include <QSet>
|
||||||
#include <QtSql>
|
#include <QtSql>
|
||||||
@@ -15,26 +16,40 @@ class Manager {
|
|||||||
public:
|
public:
|
||||||
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
|
static QString cacheKey(const QFileInfo& fileInfo, const QSize& imageSize);
|
||||||
|
|
||||||
Manager(const QDir& cacheDir);
|
Manager(const QDir& cacheDir, int maxEntries = 1000);
|
||||||
|
|
||||||
~Manager();
|
~Manager();
|
||||||
|
|
||||||
|
void evictOldEntries();
|
||||||
|
|
||||||
void clearCache(Type type = Type::Image | Type::Color);
|
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:
|
private:
|
||||||
QDir m_cacheDir;
|
QDir m_cacheDir;
|
||||||
|
int m_maxEntries;
|
||||||
QString m_dbPath;
|
QString m_dbPath;
|
||||||
QString m_connectionPrefix;
|
QString m_connectionPrefix;
|
||||||
|
|
||||||
mutable QMutex m_connectionsMutex;
|
mutable QMutex m_connectionsMutex;
|
||||||
mutable QSet<QString> m_connectionNames;
|
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;
|
QSqlDatabase _db() const;
|
||||||
void _setupTables(QSqlDatabase& db) const;
|
void _setupTables(QSqlDatabase& db) const;
|
||||||
|
void _runCleanup();
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Cache
|
} // namespace WallReel::Core::Cache
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ namespace WallReel::Core::Cache {
|
|||||||
enum class Type : uint32_t {
|
enum class Type : uint32_t {
|
||||||
None = 0,
|
None = 0,
|
||||||
Image = 1, ///< Cache for processed images
|
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
|
All = ~0u
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +29,12 @@ inline constexpr Type operator&(Type a, Type b) {
|
|||||||
|
|
||||||
using Data = std::variant<std::monostate, QFileInfo, QColor>;
|
using Data = std::variant<std::monostate, QFileInfo, QColor>;
|
||||||
|
|
||||||
|
enum class SettingsType : uint32_t {
|
||||||
|
LastSelectedPalette = 0,
|
||||||
|
LastSortType,
|
||||||
|
LastSortDescending,
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Cache
|
} // namespace WallReel::Core::Cache
|
||||||
|
|
||||||
#endif // WALLREEL_CACHE_TYPES_HPP
|
#endif // WALLREEL_CACHE_TYPES_HPP
|
||||||
|
|||||||
@@ -19,7 +19,6 @@
|
|||||||
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
||||||
// wallpaper.excludes array [] Exclude patterns (regex)
|
// wallpaper.excludes array [] Exclude patterns (regex)
|
||||||
//
|
//
|
||||||
// theme.defaultPalette string "" Name of the default palette to use
|
|
||||||
// theme.palettes array []
|
// theme.palettes array []
|
||||||
// theme.palettes[].name string "" Name of the palette
|
// theme.palettes[].name string "" Name of the palette
|
||||||
// theme.palettes[].colors array [] List of colors in 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.onPreview string "" Command to execute on preview
|
||||||
// action.saveState array [] Useful for restore command
|
// action.saveState array [] Useful for restore command
|
||||||
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore 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[].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.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.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
|
// 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_width number 750 Initial window width
|
||||||
// style.window_height number 500 Initial window height
|
// style.window_height number 500 Initial window height
|
||||||
//
|
//
|
||||||
// sort.type string "date" Initial sorting type: "name", "date", "size"
|
// cache.saveSortMethod boolean true Whether to persist the sort type and order
|
||||||
// sort.descending boolean true Initial sorting order
|
// cache.savePalette bool true Whether to persist the selected palette
|
||||||
// Ascending: name: lexicographical, e.g. "a.jpg" before "b.jpg"
|
// cache.maxImageEntries number 1000 Maximum number of entries in the image cache (older entries will be evicted)
|
||||||
// date: older before newer
|
|
||||||
// size: smaller before larger
|
|
||||||
|
|
||||||
namespace WallReel::Core::Config {
|
namespace WallReel::Core::Config {
|
||||||
|
|
||||||
@@ -64,7 +61,7 @@ enum class SortType : int {
|
|||||||
|
|
||||||
inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"};
|
inline const QStringList s_availableSortTypes = {"Name", "Date", "Size"};
|
||||||
|
|
||||||
inline QString sortTypeToString(SortType type) {
|
inline QString sortTypeToString(const SortType& type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case SortType::Name:
|
case SortType::Name:
|
||||||
return "Name";
|
return "Name";
|
||||||
@@ -112,7 +109,6 @@ struct ThemeConfigItems {
|
|||||||
};
|
};
|
||||||
|
|
||||||
QList<PaletteConfigItem> palettes;
|
QList<PaletteConfigItem> palettes;
|
||||||
QString defaultPalette;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ActionConfigItems {
|
struct ActionConfigItems {
|
||||||
@@ -143,9 +139,14 @@ struct StyleConfigItems {
|
|||||||
int windowHeight = 500;
|
int windowHeight = 500;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct SortConfigItems {
|
struct CacheConfigItems {
|
||||||
SortType type = SortType::Date;
|
bool saveSortMethod = true;
|
||||||
bool descending = true;
|
bool savePalette = true;
|
||||||
|
int maxImageEntries = 1000;
|
||||||
|
|
||||||
|
static const QString defaultSortType;
|
||||||
|
static const QString defaultSortDescending;
|
||||||
|
static const QString defaultSelectedPalette;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Config
|
} // namespace WallReel::Core::Config
|
||||||
|
|||||||
@@ -15,13 +15,24 @@
|
|||||||
|
|
||||||
WALLREEL_DECLARE_SENDER("ConfigManager")
|
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& configDir,
|
||||||
const QDir& picturesDir,
|
const QDir& picturesDir,
|
||||||
const QStringList& searchDirs,
|
const QStringList& searchDirs,
|
||||||
const QString& configPath,
|
const QString& configPath,
|
||||||
QObject* parent)
|
QObject* parent)
|
||||||
: QObject(parent), m_configDir(configDir) {
|
: 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)
|
// Load configPath if not empty, otherwise load from default location (configDir + s_DefaultConfigFileName)
|
||||||
if (configPath.isEmpty()) {
|
if (configPath.isEmpty()) {
|
||||||
WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
|
WR_INFO(QString("Configuration directory: %1").arg(m_configDir.absolutePath()));
|
||||||
@@ -47,10 +58,10 @@ WallReel::Core::Config::Manager::Manager(
|
|||||||
_loadWallpapers();
|
_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));
|
WR_INFO(QString("Loading configuration from: %1").arg(configPath));
|
||||||
QFile configFile(configPath);
|
QFile configFile(configPath);
|
||||||
if (!configFile.open(QIODevice::ReadOnly)) {
|
if (!configFile.open(QIODevice::ReadOnly)) {
|
||||||
@@ -72,10 +83,10 @@ void WallReel::Core::Config::Manager::_loadConfig(const QString& configPath) {
|
|||||||
_loadThemeConfig(jsonObj);
|
_loadThemeConfig(jsonObj);
|
||||||
_loadActionConfig(jsonObj);
|
_loadActionConfig(jsonObj);
|
||||||
_loadStyleConfig(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()) {
|
if (!root.contains("wallpaper") || !root["wallpaper"].isObject()) {
|
||||||
return;
|
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()) {
|
if (!root.contains("theme") || !root["theme"].isObject()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QJsonObject& theme = root["theme"].toObject();
|
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()) {
|
if (!theme.contains("palettes") || !theme["palettes"].isArray()) {
|
||||||
return;
|
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()) {
|
if (!root.contains("action") || !root["action"].isObject()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -209,8 +217,8 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
|||||||
if (obj.contains("key") && obj["key"].isString()) {
|
if (obj.contains("key") && obj["key"].isString()) {
|
||||||
sItem.key = obj["key"].toString();
|
sItem.key = obj["key"].toString();
|
||||||
}
|
}
|
||||||
if (obj.contains("default") && obj["default"].isString()) {
|
if (obj.contains("fallback") && obj["fallback"].isString()) {
|
||||||
sItem.defaultVal = obj["default"].toString();
|
sItem.defaultVal = obj["fallback"].toString();
|
||||||
}
|
}
|
||||||
if (obj.contains("command") && obj["command"].isString()) {
|
if (obj.contains("command") && obj["command"].isString()) {
|
||||||
sItem.command = obj["command"].toString();
|
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()) {
|
if (!root.contains("style") || !root["style"].isObject()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -295,36 +303,33 @@ void WallReel::Core::Config::Manager::_loadStyleConfig(const QJsonObject& root)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Config::Manager::_loadSortConfig(const QJsonObject& root) {
|
void Manager::_loadCacheConfig(const QJsonObject& root) {
|
||||||
if (!root.contains("sort") || !root["sort"].isObject()) {
|
if (!root.contains("cache") || !root["cache"].isObject()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const QJsonObject& config = root["sort"].toObject();
|
const QJsonObject& config = root["cache"].toObject();
|
||||||
|
|
||||||
if (config.contains("type")) {
|
if (config.contains("saveSortMethod")) {
|
||||||
const auto& val = config["type"];
|
const auto& val = config["saveSortMethod"];
|
||||||
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 (val.isBool()) {
|
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();
|
m_wallpapers.clear();
|
||||||
|
|
||||||
// Add paths first using a set to avoid duplicates
|
// 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()));
|
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) {
|
if (m_pendingCaptures > 0) {
|
||||||
WR_WARN("State capture already in progress, ignoring new capture request");
|
WR_WARN("State capture already in progress, ignoring new capture request");
|
||||||
return;
|
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
|
// This is all in main thread, so no lock needed
|
||||||
m_actionConfig.savedState[key] = value;
|
m_actionConfig.savedState[key] = value;
|
||||||
m_pendingCaptures--;
|
m_pendingCaptures--;
|
||||||
@@ -489,3 +499,5 @@ void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const
|
|||||||
emit stateCaptured();
|
emit stateCaptured();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Config
|
||||||
|
|||||||
@@ -57,7 +57,9 @@ class Manager : public QObject {
|
|||||||
|
|
||||||
const StyleConfigItems& getStyleConfig() const { return m_styleConfig; }
|
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 {
|
QSize getFocusImageSize() const {
|
||||||
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
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 _loadThemeConfig(const QJsonObject& config);
|
||||||
void _loadActionConfig(const QJsonObject& config);
|
void _loadActionConfig(const QJsonObject& config);
|
||||||
void _loadStyleConfig(const QJsonObject& config);
|
void _loadStyleConfig(const QJsonObject& config);
|
||||||
void _loadSortConfig(const QJsonObject& config);
|
void _loadCacheConfig(const QJsonObject& config);
|
||||||
// Load wallpapers
|
// Load wallpapers
|
||||||
void _loadWallpapers();
|
void _loadWallpapers();
|
||||||
// Callback for state capture results
|
// Callback for state capture results
|
||||||
@@ -90,11 +92,12 @@ class Manager : public QObject {
|
|||||||
ThemeConfigItems m_themeConfig;
|
ThemeConfigItems m_themeConfig;
|
||||||
ActionConfigItems m_actionConfig;
|
ActionConfigItems m_actionConfig;
|
||||||
StyleConfigItems m_styleConfig;
|
StyleConfigItems m_styleConfig;
|
||||||
SortConfigItems m_sortConfig;
|
CacheConfigItems m_cacheConfig;
|
||||||
|
|
||||||
QStringList m_wallpapers;
|
QStringList m_wallpapers;
|
||||||
|
|
||||||
int m_pendingCaptures = 0;
|
int m_pendingCaptures = 0;
|
||||||
|
bool m_stateCaptured = false; // changed and accessed in main thread, no lock needed
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Config
|
} // 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_cacheMgr(cacheMgr), m_file(path), m_targetSize(targetSize) {
|
||||||
m_id = cacheMgr.cacheKey(m_file, m_targetSize);
|
m_id = cacheMgr.cacheKey(m_file, m_targetSize);
|
||||||
m_cachedFile = cacheMgr.getImage(m_id, [this]() { return computeImage(); });
|
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());
|
QImageReader reader(m_cachedFile.absoluteFilePath());
|
||||||
|
|
||||||
if (!reader.canRead()) {
|
if (!reader.canRead()) {
|
||||||
WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath());
|
WR_WARN("Cannot read cached image: " + m_cachedFile.absoluteFilePath());
|
||||||
return QImage();
|
return QImage();
|
||||||
|
|||||||
@@ -54,8 +54,11 @@ class Data {
|
|||||||
QColor m_dominantColor; ///< Dominant color of the image, used for palette matching
|
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
|
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;
|
QImage computeImage() const;
|
||||||
QColor computeDominantColor(const QImage& image) const;
|
QColor computeDominantColor(const QImage& image) const;
|
||||||
|
QImage loadImageFromCache() const;
|
||||||
|
|
||||||
Data(const QString& path, const QSize& size, Cache::Manager& cacheMgr);
|
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()); }
|
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(); }
|
QString getFullPath() const { return m_file.absoluteFilePath(); }
|
||||||
|
|
||||||
@@ -87,8 +90,6 @@ class Data {
|
|||||||
|
|
||||||
const QFileInfo& getFileInfo() const { return m_file; }
|
const QFileInfo& getFileInfo() const { return m_file; }
|
||||||
|
|
||||||
QImage loadImage() const;
|
|
||||||
|
|
||||||
const QColor& getDominantColor() const { return m_dominantColor; }
|
const QColor& getDominantColor() const { return m_dominantColor; }
|
||||||
|
|
||||||
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
std::optional<QString> getCachedColor(const QString& paletteName) const {
|
||||||
|
|||||||
@@ -49,19 +49,6 @@ WallReel::Core::Palette::Manager::Manager(
|
|||||||
m_palettes.append(newP);
|
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) {
|
void WallReel::Core::Palette::Manager::updateColor(const QString& imageId) {
|
||||||
|
|||||||
@@ -42,8 +42,23 @@ class Manager : public QObject {
|
|||||||
void setSelectedPalette(const QVariant& paletteVar) {
|
void setSelectedPalette(const QVariant& paletteVar) {
|
||||||
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
if (paletteVar.isNull() || !paletteVar.isValid()) {
|
||||||
m_selectedPalette = std::nullopt;
|
m_selectedPalette = std::nullopt;
|
||||||
} else {
|
} else if (paletteVar.canConvert<PaletteItem>()) {
|
||||||
m_selectedPalette = paletteVar.value<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;
|
m_selectedColor = std::nullopt;
|
||||||
emit selectedPaletteChanged();
|
emit selectedPaletteChanged();
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ WALLREEL_DECLARE_SENDER("PaletteMatchColor")
|
|||||||
|
|
||||||
namespace WallReel::Core::Palette {
|
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()) {
|
if (candidates.isEmpty() || !target.isValid()) {
|
||||||
WR_WARN("No candidates or invalid target color for palette matching");
|
WR_WARN("No candidates or invalid target color for palette matching");
|
||||||
static ColorItem emptyItem;
|
static ColorItem emptyItem;
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ namespace WallReel::Core::Palette {
|
|||||||
*
|
*
|
||||||
* @param target
|
* @param target
|
||||||
* @param candidates
|
* @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
|
} // namespace WallReel::Core::Palette
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "Service/manager.hpp"
|
#include "Service/manager.hpp"
|
||||||
#include "Utils/misc.hpp"
|
#include "Utils/misc.hpp"
|
||||||
#include "appoptions.hpp"
|
#include "appoptions.hpp"
|
||||||
|
#include "logger.hpp"
|
||||||
|
|
||||||
namespace WallReel::Core::Provider {
|
namespace WallReel::Core::Provider {
|
||||||
|
|
||||||
@@ -18,18 +19,21 @@ class Bootstrap {
|
|||||||
|
|
||||||
public:
|
public:
|
||||||
Bootstrap(const AppOptions& options) {
|
Bootstrap(const AppOptions& options) {
|
||||||
cacheMgr = new Cache::Manager(Utils::getCacheDir());
|
|
||||||
|
|
||||||
if (options.clearCache) {
|
|
||||||
cacheMgr->clearCache();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
configMgr = new Config::Manager(
|
configMgr = new Config::Manager(
|
||||||
Utils::getConfigDir(),
|
Utils::getConfigDir(),
|
||||||
Utils::getPicturesDir(),
|
Utils::getPicturesDir(),
|
||||||
options.appendDirs,
|
options.appendDirs,
|
||||||
options.configPath);
|
options.configPath);
|
||||||
|
|
||||||
|
cacheMgr = new Cache::Manager(
|
||||||
|
Utils::getCacheDir(),
|
||||||
|
configMgr->getCacheConfig().maxImageEntries);
|
||||||
|
|
||||||
|
if (options.clearCache) {
|
||||||
|
cacheMgr->clearCache();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
imageMgr = new Image::Manager(
|
imageMgr = new Image::Manager(
|
||||||
*cacheMgr,
|
*cacheMgr,
|
||||||
configMgr->getFocusImageSize());
|
configMgr->getFocusImageSize());
|
||||||
@@ -40,19 +44,77 @@ class Bootstrap {
|
|||||||
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
|
qRegisterMetaType<Palette::PaletteItem>("PaletteItem");
|
||||||
qRegisterMetaType<Palette::ColorItem>("ColorItem");
|
qRegisterMetaType<Palette::ColorItem>("ColorItem");
|
||||||
|
|
||||||
ServiceMgr = new Service::Manager(
|
serviceMgr = new Service::Manager(
|
||||||
configMgr->getActionConfig(),
|
configMgr->getActionConfig(),
|
||||||
*imageMgr,
|
*imageMgr,
|
||||||
*paletteMgr);
|
*paletteMgr,
|
||||||
|
options.disableActions);
|
||||||
}
|
}
|
||||||
|
|
||||||
void start() {
|
void start() {
|
||||||
|
cacheMgr->evictOldEntries();
|
||||||
configMgr->captureState();
|
configMgr->captureState();
|
||||||
imageMgr->loadAndProcess(configMgr->getWallpapers());
|
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() {
|
~Bootstrap() {
|
||||||
delete ServiceMgr;
|
delete serviceMgr;
|
||||||
delete paletteMgr;
|
delete paletteMgr;
|
||||||
delete imageMgr;
|
delete imageMgr;
|
||||||
delete configMgr;
|
delete configMgr;
|
||||||
@@ -64,7 +126,7 @@ class Bootstrap {
|
|||||||
Config::Manager* configMgr{};
|
Config::Manager* configMgr{};
|
||||||
Image::Manager* imageMgr{};
|
Image::Manager* imageMgr{};
|
||||||
Palette::Manager* paletteMgr{};
|
Palette::Manager* paletteMgr{};
|
||||||
Service::Manager* ServiceMgr{};
|
Service::Manager* serviceMgr{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Provider
|
} // namespace WallReel::Core::Provider
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
|
#ifndef WALLREEL_PROVIDER_CAROUSEL_HPP
|
||||||
#define WALLREEL_PROVIDER_CAROUSEL_HPP
|
#define WALLREEL_PROVIDER_CAROUSEL_HPP
|
||||||
|
|
||||||
#include <qapplication.h>
|
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
|
|
||||||
|
#include "Cache/manager.hpp"
|
||||||
|
#include "Cache/types.hpp"
|
||||||
#include "Config/data.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "Config/manager.hpp"
|
#include "Config/manager.hpp"
|
||||||
#include "Image/manager.hpp"
|
#include "Image/manager.hpp"
|
||||||
@@ -146,10 +146,6 @@ class Carousel : public QObject {
|
|||||||
|
|
||||||
signals:
|
signals:
|
||||||
void isProcessingChanged();
|
void isProcessingChanged();
|
||||||
void selectCompleted();
|
|
||||||
void previewCompleted();
|
|
||||||
void restoreCompleted();
|
|
||||||
void cancelCompleted();
|
|
||||||
|
|
||||||
// Other states
|
// Other states
|
||||||
|
|
||||||
@@ -190,10 +186,11 @@ class Carousel : public QObject {
|
|||||||
Bootstrap& bootstrap,
|
Bootstrap& bootstrap,
|
||||||
QObject* parent = nullptr)
|
QObject* parent = nullptr)
|
||||||
: QObject(parent),
|
: QObject(parent),
|
||||||
|
m_cacheMgr(bootstrap.cacheMgr),
|
||||||
m_configMgr(bootstrap.configMgr),
|
m_configMgr(bootstrap.configMgr),
|
||||||
m_imageMgr(bootstrap.imageMgr),
|
m_imageMgr(bootstrap.imageMgr),
|
||||||
m_paletteMgr(bootstrap.paletteMgr),
|
m_paletteMgr(bootstrap.paletteMgr),
|
||||||
m_serviceMgr(bootstrap.ServiceMgr) {
|
m_serviceMgr(bootstrap.serviceMgr) {
|
||||||
// Simply forward signals
|
// Simply forward signals
|
||||||
connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged);
|
connect(m_imageMgr, &Image::Manager::isLoadingChanged, this, &Carousel::isLoadingChanged);
|
||||||
connect(m_imageMgr, &Image::Manager::processedCountChanged, this, &Carousel::processedCountChanged);
|
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::colorChanged, this, &Carousel::colorChanged);
|
||||||
connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged);
|
connect(m_paletteMgr, &Palette::Manager::colorNameChanged, this, &Carousel::colorNameChanged);
|
||||||
connect(m_serviceMgr, &Service::Manager::isProcessingChanged, this, &Carousel::isProcessingChanged);
|
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
|
// "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 :)
|
// 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
|
// Quit on selected
|
||||||
if (m_configMgr->getActionConfig().quitOnSelected) {
|
if (m_configMgr->getActionConfig().quitOnSelected) {
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
this,
|
m_serviceMgr,
|
||||||
&Provider::Carousel::selectCompleted,
|
&Service::Manager::selectCompleted,
|
||||||
app,
|
app,
|
||||||
&QApplication::quit,
|
&QApplication::quit,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
}
|
}
|
||||||
// Quit on cancel
|
// Quit on cancel
|
||||||
QObject::connect(
|
QObject::connect(
|
||||||
this,
|
m_serviceMgr,
|
||||||
&Provider::Carousel::cancelCompleted,
|
&Service::Manager::cancelCompleted,
|
||||||
app,
|
app,
|
||||||
&QApplication::quit,
|
&QApplication::quit,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@@ -273,12 +272,41 @@ class Carousel : public QObject {
|
|||||||
&Service::Manager::restoreOnQuit);
|
&Service::Manager::restoreOnQuit);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial value of sort method
|
// Restore last state if configured
|
||||||
setSortType(m_configMgr->getSortConfig().type);
|
// and store state on change if configured
|
||||||
setSortDescending(m_configMgr->getSortConfig().descending);
|
// 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:
|
private:
|
||||||
|
Cache::Manager* m_cacheMgr;
|
||||||
Config::Manager* m_configMgr;
|
Config::Manager* m_configMgr;
|
||||||
Image::Manager* m_imageMgr;
|
Image::Manager* m_imageMgr;
|
||||||
Palette::Manager* m_paletteMgr;
|
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 "Image/manager.hpp"
|
||||||
#include "Palette/manager.hpp"
|
#include "Palette/manager.hpp"
|
||||||
#include "Service/wallpaper.hpp"
|
#include "Service/wallpaper.hpp"
|
||||||
#include "logger.hpp"
|
|
||||||
|
|
||||||
namespace WallReel::Core::Service {
|
namespace WallReel::Core::Service {
|
||||||
|
|
||||||
@@ -22,16 +21,8 @@ class Manager : public QObject {
|
|||||||
const Config::ActionConfigItems& actionConfig,
|
const Config::ActionConfigItems& actionConfig,
|
||||||
Image::Manager& imageManager,
|
Image::Manager& imageManager,
|
||||||
Palette::Manager& paletteManager,
|
Palette::Manager& paletteManager,
|
||||||
QObject* parent = nullptr) : m_actionConfig(actionConfig), m_imageManager(imageManager), m_paletteManager(paletteManager) {
|
bool disableActions = false,
|
||||||
m_wallpaperService = new WallpaperService(m_actionConfig, m_paletteManager, this);
|
QObject* parent = nullptr);
|
||||||
|
|
||||||
// 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 isProcessing() const { return m_isProcessing; }
|
bool isProcessing() const { return m_isProcessing; }
|
||||||
|
|
||||||
@@ -39,102 +30,49 @@ class Manager : public QObject {
|
|||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
|
|
||||||
void selectWallpaper(const QString& id) {
|
void onStateCaptured();
|
||||||
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 restore() {
|
void selectWallpaper(const QString& id);
|
||||||
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 cancel() {
|
void restore();
|
||||||
Logger::debug("ServiceManager", "Cancel action");
|
|
||||||
m_wallpaperService->stopAll();
|
|
||||||
emit cancelCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
void previewWallpaper(const QString& id) {
|
void cancel();
|
||||||
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 restoreOnQuit() {
|
void previewWallpaper(const QString& id);
|
||||||
if (m_hasSelected) {
|
|
||||||
Logger::debug("ServiceManager", "Quit with selected wallpaper, no need to restore");
|
void restoreOnQuit();
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
|
|
||||||
void _onSelectCompleted() {
|
void _onSelectCompleted(bool success);
|
||||||
Logger::debug("ServiceManager", "Select completed");
|
|
||||||
_onProcessCompleted();
|
|
||||||
m_hasSelected = true;
|
|
||||||
emit selectCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onRestoreCompleted() {
|
void _onRestoreCompleted(bool success);
|
||||||
Logger::debug("ServiceManager", "Restore completed");
|
|
||||||
_onProcessCompleted();
|
|
||||||
emit restoreCompleted();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onProcessCompleted() {
|
void _onProcessCompleted();
|
||||||
m_isProcessing = false;
|
|
||||||
emit isProcessingChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void isProcessingChanged();
|
void isProcessingChanged();
|
||||||
void selectCompleted();
|
void selectCompleted(bool success);
|
||||||
void previewCompleted();
|
void previewCompleted(bool success);
|
||||||
void restoreCompleted();
|
void restoreCompleted(bool success);
|
||||||
void cancelCompleted();
|
void cancelCompleted();
|
||||||
|
|
||||||
|
private:
|
||||||
|
QString _renderCommand(const QString& templateStr, const QHash<QString, QString>& variables) const;
|
||||||
|
QHash<QString, QString> _generateVariables(const Image::Data& imageData) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
WallpaperService* m_wallpaperService;
|
WallpaperService* m_wallpaperService;
|
||||||
const Config::ActionConfigItems& m_actionConfig;
|
const Config::ActionConfigItems& m_actionConfig;
|
||||||
Image::Manager& m_imageManager;
|
Image::Manager& m_imageManager;
|
||||||
Palette::Manager& m_paletteManager;
|
Palette::Manager& m_paletteManager;
|
||||||
|
bool m_disableActions;
|
||||||
|
|
||||||
bool m_isProcessing = false;
|
bool m_isProcessing = false;
|
||||||
bool m_hasSelected = false;
|
bool m_hasSelected = false;
|
||||||
|
|
||||||
|
bool m_stateCaptured = false;
|
||||||
|
QString m_pendingPreviewId;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Service
|
} // namespace WallReel::Core::Service
|
||||||
|
|||||||
@@ -1,54 +1,103 @@
|
|||||||
#include "Service/wallpaper.hpp"
|
#include "Service/wallpaper.hpp"
|
||||||
|
|
||||||
#include <QColor>
|
#include <qprocess.h>
|
||||||
#include <iostream>
|
|
||||||
|
#include <QColor>
|
||||||
|
|
||||||
#include "Utils/texttemplate.hpp"
|
|
||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
|
|
||||||
WALLREEL_DECLARE_SENDER("WallpaperService")
|
WALLREEL_DECLARE_SENDER("WallpaperService")
|
||||||
|
|
||||||
WallReel::Core::Service::WallpaperService::WallpaperService(
|
namespace WallReel::Core::Service {
|
||||||
const Config::ActionConfigItems& actionConfig,
|
|
||||||
const Palette::Manager& paletteManager,
|
WallpaperService::WallpaperService(int previewDebounceTime, QObject* parent)
|
||||||
QObject* parent)
|
: QObject(parent) {
|
||||||
: QObject(parent), m_actionConfig(actionConfig), m_paletteManager(paletteManager) {
|
|
||||||
m_previewDebounceTimer = new QTimer(this);
|
m_previewDebounceTimer = new QTimer(this);
|
||||||
m_previewDebounceTimer->setSingleShot(true);
|
m_previewDebounceTimer->setSingleShot(true);
|
||||||
m_previewDebounceTimer->setInterval(m_actionConfig.previewDebounceTime);
|
m_previewDebounceTimer->setInterval(previewDebounceTime);
|
||||||
connect(m_previewDebounceTimer, &QTimer::timeout, this, [this]() {
|
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);
|
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,
|
connect(m_previewProcess,
|
||||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
this,
|
this,
|
||||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
WR_DEBUG(QString("Preview process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||||
emit previewCompleted();
|
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);
|
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,
|
connect(m_selectProcess,
|
||||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
this,
|
this,
|
||||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
WR_DEBUG(QString("Select process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||||
emit selectCompleted();
|
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);
|
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,
|
connect(m_restoreProcess,
|
||||||
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||||
this,
|
this,
|
||||||
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
[this](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
WR_DEBUG(QString("Restore process finished with exit code %1 and exit status %2").arg(exitCode).arg(exitStatus));
|
bool success = exitCode == 0 && exitStatus == QProcess::NormalExit;
|
||||||
emit restoreCompleted();
|
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");
|
WR_DEBUG("Stopping all wallpaper service processes");
|
||||||
if (m_previewProcess->state() != QProcess::NotRunning) {
|
if (m_previewProcess->state() != QProcess::NotRunning) {
|
||||||
m_previewProcess->kill();
|
m_previewProcess->kill();
|
||||||
@@ -65,74 +114,32 @@ void WallReel::Core::Service::WallpaperService::stopAll() {
|
|||||||
m_previewDebounceTimer->stop();
|
m_previewDebounceTimer->stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Service::WallpaperService::preview(const Image::Data& imageData) {
|
void WallpaperService::preview(const QString& command) {
|
||||||
m_pendingImageData = &imageData;
|
m_pendingPreviewCommand = command;
|
||||||
m_previewDebounceTimer->start();
|
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) {
|
if (m_selectProcess->state() != QProcess::NotRunning) {
|
||||||
WR_WARN("Previous select command is still running. Ignoring new command.");
|
WR_WARN("Previous select command is still running. Ignoring new command.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WR_DEBUG(QString("Select wallpaper: %1").arg(imageData.getFullPath()));
|
_doSelect(command);
|
||||||
_doSelect(imageData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Service::WallpaperService::restore() {
|
void WallpaperService::restore(const QString& command) {
|
||||||
if (m_restoreProcess->state() != QProcess::NotRunning) {
|
if (m_restoreProcess->state() != QProcess::NotRunning) {
|
||||||
WR_WARN("Previous restore command is still running. Ignoring new command.");
|
WR_WARN("Previous restore command is still running. Ignoring new command.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WR_DEBUG("Restore state");
|
WR_DEBUG("Restore state");
|
||||||
_doRestore();
|
_doRestore(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
QHash<QString, QString> WallReel::Core::Service::WallpaperService::_generateVariables(const Image::Data& imageData) {
|
void WallpaperService::_doPreview(const QString& command) {
|
||||||
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);
|
|
||||||
if (command.isEmpty()) {
|
if (command.isEmpty()) {
|
||||||
WR_DEBUG("No preview command configured. Skipping preview action.");
|
WR_DEBUG("No preview command configured. Skipping preview action.");
|
||||||
emit previewCompleted();
|
emit previewCompleted(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WR_DEBUG(QString("Executing preview command: %1").arg(command));
|
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);
|
m_previewProcess->start("sh", QStringList() << "-c" << command);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Service::WallpaperService::_doSelect(const Image::Data& imageData) {
|
void WallpaperService::_doSelect(const QString& command) {
|
||||||
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);
|
|
||||||
if (command.isEmpty()) {
|
if (command.isEmpty()) {
|
||||||
WR_DEBUG("No select command configured. Skipping select action.");
|
WR_DEBUG("No select command configured. Skipping select action.");
|
||||||
emit selectCompleted();
|
emit selectCompleted(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WR_DEBUG(QString("Executing select command: %1").arg(command));
|
WR_DEBUG(QString("Executing select command: %1").arg(command));
|
||||||
m_selectProcess->start("sh", QStringList() << "-c" << command);
|
m_selectProcess->start("sh", QStringList() << "-c" << command);
|
||||||
}
|
}
|
||||||
|
|
||||||
void WallReel::Core::Service::WallpaperService::_doRestore() {
|
void WallpaperService::_doRestore(const QString& command) {
|
||||||
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);
|
|
||||||
if (command.isEmpty()) {
|
if (command.isEmpty()) {
|
||||||
WR_DEBUG("Restore command is empty after rendering. Skipping restore action.");
|
WR_DEBUG("Restore command is empty. Skipping restore action.");
|
||||||
emit restoreCompleted();
|
emit restoreCompleted(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
WR_DEBUG(QString("Executing restore command: %1").arg(command));
|
WR_DEBUG(QString("Executing restore command: %1").arg(command));
|
||||||
m_restoreProcess->start("sh", QStringList() << "-c" << command);
|
m_restoreProcess->start("sh", QStringList() << "-c" << command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Service
|
||||||
|
|||||||
@@ -4,43 +4,33 @@
|
|||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "Config/data.hpp"
|
|
||||||
#include "Image/data.hpp"
|
|
||||||
#include "Palette/manager.hpp"
|
|
||||||
|
|
||||||
namespace WallReel::Core::Service {
|
namespace WallReel::Core::Service {
|
||||||
|
|
||||||
class WallpaperService : public QObject {
|
class WallpaperService : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
WallpaperService(
|
WallpaperService(int previewDebounceTime, QObject* parent = nullptr);
|
||||||
const Config::ActionConfigItems& actionConfig,
|
|
||||||
const Palette::Manager& paletteManager,
|
|
||||||
QObject* parent = nullptr);
|
|
||||||
|
|
||||||
void stopAll();
|
void stopAll();
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void preview(const Image::Data& imageData); // execute after 500ms of inactivity
|
void preview(const QString& command); // execute after 500ms of inactivity
|
||||||
void select(const Image::Data& imageData); // execute immediately, ignore if already running
|
void select(const QString& command); // execute immediately, ignore if already running
|
||||||
void restore(); // execute immediately, ignore if already running
|
void restore(const QString& command); // execute immediately, ignore if already running
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void previewCompleted();
|
void previewCompleted(bool success);
|
||||||
void selectCompleted();
|
void selectCompleted(bool success);
|
||||||
void restoreCompleted();
|
void restoreCompleted(bool success);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void _doPreview(const Image::Data& imageData);
|
void _doPreview(const QString& command);
|
||||||
void _doSelect(const Image::Data& imageData);
|
void _doSelect(const QString& command);
|
||||||
void _doRestore();
|
void _doRestore(const QString& command);
|
||||||
QHash<QString, QString> _generateVariables(const Image::Data& imageData);
|
|
||||||
|
|
||||||
const Config::ActionConfigItems& m_actionConfig;
|
|
||||||
const Palette::Manager& m_paletteManager;
|
|
||||||
QTimer* m_previewDebounceTimer;
|
QTimer* m_previewDebounceTimer;
|
||||||
const Image::Data* m_pendingImageData;
|
QString m_pendingPreviewCommand;
|
||||||
QProcess* m_previewProcess;
|
QProcess* m_previewProcess;
|
||||||
QProcess* m_selectProcess;
|
QProcess* m_selectProcess;
|
||||||
QProcess* m_restoreProcess;
|
QProcess* m_restoreProcess;
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ void AppOptions::parseArgs(QApplication& app) {
|
|||||||
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
|
QCommandLineOption configFileOption(QStringList() << "c" << "config-file", "Specify a custom configuration file", "file");
|
||||||
parser.addOption(configFileOption);
|
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.
|
// Not parser.process(a->arguments()) because we want to handle exit logics ourselves.
|
||||||
// parser.process(...) will do something like exit(...) that will terminate
|
// parser.process(...) will do something like exit(...) that will terminate
|
||||||
// the application brutally and produce unwanted warnings.
|
// the application brutally and produce unwanted warnings.
|
||||||
@@ -111,7 +117,7 @@ void AppOptions::parseArgs(QApplication& app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parser.isSet(configFileOption)) {
|
if (parser.isSet(configFileOption)) {
|
||||||
QString path = parser.value(configFileOption);
|
QString path = Utils::expandPath(parser.value(configFileOption));
|
||||||
if (Utils::checkFile(path)) {
|
if (Utils::checkFile(path)) {
|
||||||
configPath = path;
|
configPath = path;
|
||||||
} else {
|
} else {
|
||||||
@@ -120,6 +126,21 @@ void AppOptions::parseArgs(QApplication& app) {
|
|||||||
return;
|
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
|
} // namespace WallReel::Core
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ class AppOptions {
|
|||||||
QString configPath;
|
QString configPath;
|
||||||
QStringList appendDirs;
|
QStringList appendDirs;
|
||||||
QString errorText;
|
QString errorText;
|
||||||
|
QString applyPath; // -a --apply
|
||||||
bool clearCache = false; // -C --clear-cache
|
bool clearCache = false; // -C --clear-cache
|
||||||
|
bool disableActions = false; // -D --disable-actions
|
||||||
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
|
bool doReturn = false; ///< Indicates whether the application should exit after parsing arguments.
|
||||||
|
|
||||||
AppOptions();
|
AppOptions();
|
||||||
|
|||||||
+52
-8
@@ -1,9 +1,15 @@
|
|||||||
#include <qapplication.h>
|
|
||||||
#include <qobject.h>
|
#include <qobject.h>
|
||||||
|
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QQmlApplicationEngine>
|
#include <QQmlApplicationEngine>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
#include <QSocketNotifier>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/signalfd.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
}
|
||||||
|
|
||||||
#include "Core/Provider/bootstrap.hpp"
|
#include "Core/Provider/bootstrap.hpp"
|
||||||
#include "Core/Provider/carousel.hpp"
|
#include "Core/Provider/carousel.hpp"
|
||||||
@@ -20,14 +26,29 @@ int main(int argc, char* argv[]) {
|
|||||||
// 1. QQmlApplicationEngine (with all QML objects)
|
// 1. QQmlApplicationEngine (with all QML objects)
|
||||||
// 2. provider (manages states and connections)
|
// 2. provider (manages states and connections)
|
||||||
// 3. bootstrap (manages lifecycle of all managers)
|
// 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);
|
QApplication a(argc, argv);
|
||||||
a.setApplicationName(APP_NAME);
|
a.setApplicationName(APP_NAME);
|
||||||
a.setApplicationVersion(APP_VERSION);
|
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;
|
using namespace Qt::StringLiterals;
|
||||||
a.setWindowIcon(QIcon(u":/%1.svg"_s.arg(APP_NAME)));
|
a.setWindowIcon(QIcon(u":/icon.svg"_s));
|
||||||
#else
|
#else
|
||||||
a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME)));
|
a.setWindowIcon(QIcon(u":/%1.svg"_qs.arg(APP_NAME)));
|
||||||
#endif
|
#endif
|
||||||
@@ -35,6 +56,27 @@ int main(int argc, char* argv[]) {
|
|||||||
{
|
{
|
||||||
Logger::init();
|
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;
|
AppOptions options;
|
||||||
options.parseArgs(a);
|
options.parseArgs(a);
|
||||||
|
|
||||||
@@ -48,6 +90,10 @@ int main(int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!options.applyPath.isEmpty()) {
|
||||||
|
return bootstrap.apply(options.applyPath) ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
Provider::Carousel provider(&a, bootstrap);
|
Provider::Carousel provider(&a, bootstrap);
|
||||||
qmlRegisterSingletonInstance(
|
qmlRegisterSingletonInstance(
|
||||||
@@ -66,13 +112,11 @@ int main(int argc, char* argv[]) {
|
|||||||
[]() { QCoreApplication::exit(-1); },
|
[]() { QCoreApplication::exit(-1); },
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 8, 0)
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
engine.loadFromModule(UIMODULE_URI, u"Main"_s);
|
engine.loadFromModule(UIMODULE_URI, u"Main"_s);
|
||||||
#elif QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
|
||||||
engine.loadFromModule(UIMODULE_URI, u"Main"_qs);
|
|
||||||
#else
|
#else
|
||||||
engine.addImportPath(u"qrc:/"_qs));
|
engine.addImportPath(u"qrc:/"_qs);
|
||||||
engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs));
|
engine.load(QUrl(u"qrc:/WallReel/UI/Main.qml"_qs));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
+16
-20
@@ -47,11 +47,6 @@
|
|||||||
"theme": {
|
"theme": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"defaultPalette": {
|
|
||||||
"type": "string",
|
|
||||||
"default": "",
|
|
||||||
"description": "Name of the default palette to use"
|
|
||||||
},
|
|
||||||
"palettes": {
|
"palettes": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
@@ -126,10 +121,10 @@
|
|||||||
"default": "",
|
"default": "",
|
||||||
"description": "Key of value to save, used as {{ key }} in onRestore command"
|
"description": "Key of value to save, used as {{ key }} in onRestore command"
|
||||||
},
|
},
|
||||||
"default": {
|
"fallback": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"default": "",
|
"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": {
|
"command": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -179,6 +174,7 @@
|
|||||||
"image_focus_scale": {
|
"image_focus_scale": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
"default": 1.5,
|
"default": 1.5,
|
||||||
|
"minimum": 1.0,
|
||||||
"description": "Scale of the focused image (relative to unfocused image)"
|
"description": "Scale of the focused image (relative to unfocused image)"
|
||||||
},
|
},
|
||||||
"window_width": {
|
"window_width": {
|
||||||
@@ -193,23 +189,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sort": {
|
"cache": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"type": {
|
"saveSortMethod": {
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"name",
|
|
||||||
"date",
|
|
||||||
"size"
|
|
||||||
],
|
|
||||||
"default": "date",
|
|
||||||
"description": "Initial sorting type"
|
|
||||||
},
|
|
||||||
"descending": {
|
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true,
|
"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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
title: WALLREEL
|
||||||
|
section: 1
|
||||||
|
header: User Commands
|
||||||
|
footer: WallReel 2.0.0
|
||||||
|
date: 2026-03-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
wallreel - Choose and set desktop wallpapers with customizable themes and actions
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
**wallreel** [*options*]
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
**wallreel** is a Qt6 application for browsing wallpaper images, previewing candidates,
|
||||||
|
and applying a selected image.
|
||||||
|
|
||||||
|
Configuration is loaded from a JSON file. CLI options are available for logging,
|
||||||
|
one-shot operations, and runtime overrides.
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
**-h, --help**
|
||||||
|
: Display help for command-line options.
|
||||||
|
|
||||||
|
**-v, --version**
|
||||||
|
: Display version information.
|
||||||
|
|
||||||
|
**-V, --verbose**
|
||||||
|
: Set log level to DEBUG (default is INFO).
|
||||||
|
|
||||||
|
**-C, --clear-cache**
|
||||||
|
: Clear image cache and exit.
|
||||||
|
|
||||||
|
**-q, --quiet**
|
||||||
|
: Suppress log output.
|
||||||
|
|
||||||
|
**-d, --append-dir** _dir_
|
||||||
|
: Append an additional wallpaper search directory.
|
||||||
|
|
||||||
|
This option can be provided multiple times.
|
||||||
|
|
||||||
|
**-c, --config-file** _file_
|
||||||
|
: Use a custom configuration file.
|
||||||
|
|
||||||
|
**-D, --disable-actions**
|
||||||
|
: Disable actions defined in the configuration file.
|
||||||
|
|
||||||
|
**-a, --apply** _file_
|
||||||
|
: Apply the specified image as wallpaper and exit.
|
||||||
|
|
||||||
|
In this mode, the configuration is still parsed. Action placeholders are resolved
|
||||||
|
from the selected image and any captured state values.
|
||||||
|
|
||||||
|
# BEHAVIOR NOTES
|
||||||
|
|
||||||
|
- CLI options are generally optional; configuration is the preferred customization path.
|
||||||
|
- Some options are mutually exclusive (for example `--verbose` and `--quiet`).
|
||||||
|
- With `--apply`, WallReel executes configured selection actions without opening the UI.
|
||||||
|
|
||||||
|
# FILES
|
||||||
|
|
||||||
|
`~/.config/wallreel/config.json`
|
||||||
|
: Default configuration file location.
|
||||||
|
|
||||||
|
`~/.cache/wallreel/`
|
||||||
|
: Runtime cache location.
|
||||||
|
|
||||||
|
# EXAMPLES
|
||||||
|
|
||||||
|
Run with default configuration:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wallreel
|
||||||
|
```
|
||||||
|
|
||||||
|
Use a custom configuration file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wallreel --config-file ~/.config/wallreel/config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Append additional search directories:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wallreel --append-dir ~/Pictures/Wallpapers --append-dir ~/Art
|
||||||
|
```
|
||||||
|
|
||||||
|
Apply a wallpaper and exit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wallreel --apply ~/Pictures/wallpaper.jpg
|
||||||
|
```
|
||||||
|
|
||||||
|
# EXIT STATUS
|
||||||
|
|
||||||
|
Returns `0` on success. Returns a non-zero value on failure.
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
**wallreel**(5)
|
||||||
|
|
||||||
|
# AUTHOR
|
||||||
|
|
||||||
|
Uyanide <github.com/Uyanide>
|
||||||
@@ -0,0 +1,233 @@
|
|||||||
|
---
|
||||||
|
title: WALLREEL
|
||||||
|
section: 5
|
||||||
|
header: File Formats Manual
|
||||||
|
footer: WallReel 2.0.0
|
||||||
|
date: 2026-03-24
|
||||||
|
---
|
||||||
|
|
||||||
|
# NAME
|
||||||
|
|
||||||
|
wallreel-config - configuration format for wallreel
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
|
||||||
|
`~/.config/wallreel/config.json`
|
||||||
|
|
||||||
|
# DESCRIPTION
|
||||||
|
|
||||||
|
WallReel reads configuration from a JSON document. The root object is divided into
|
||||||
|
five sections:
|
||||||
|
|
||||||
|
- `wallpaper`
|
||||||
|
- `theme`
|
||||||
|
- `action`
|
||||||
|
- `style`
|
||||||
|
- `cache`
|
||||||
|
|
||||||
|
For complete machine-readable validation details, refer to `config.schema.json`.
|
||||||
|
|
||||||
|
# WALLPAPER SECTION
|
||||||
|
|
||||||
|
Defines where WallReel looks for images and what to exclude.
|
||||||
|
|
||||||
|
If both `paths` and `dirs` are empty or omitted, WallReel defaults to recursively
|
||||||
|
scanning the user's Pictures directory and treating all supported image files as
|
||||||
|
wallpaper candidates.
|
||||||
|
|
||||||
|
`paths` (array of string, default: `[]`)
|
||||||
|
: Exact paths to specific image files.
|
||||||
|
|
||||||
|
`dirs` (array of object, default: `[]`)
|
||||||
|
: Directories to scan for images.
|
||||||
|
|
||||||
|
Each item has:
|
||||||
|
|
||||||
|
- `path` (string)
|
||||||
|
- `recursive` (boolean)
|
||||||
|
|
||||||
|
`excludes` (array of string, default: `[]`)
|
||||||
|
: Exclude patterns as regular expressions.
|
||||||
|
|
||||||
|
# THEME SECTION
|
||||||
|
|
||||||
|
Configures color palettes.
|
||||||
|
|
||||||
|
A dominant color is extracted from each wallpaper. If a palette is selected,
|
||||||
|
WallReel picks the closest palette color as the primary color.
|
||||||
|
|
||||||
|
`palettes` (array of object, default: `[]`)
|
||||||
|
: Custom palette definitions.
|
||||||
|
|
||||||
|
Each palette has:
|
||||||
|
|
||||||
|
- `name` (string)
|
||||||
|
- `colors` (array)
|
||||||
|
|
||||||
|
Each color item has:
|
||||||
|
|
||||||
|
- `name` (string)
|
||||||
|
- `value` (hex string, for example `"#89b4fa"`)
|
||||||
|
|
||||||
|
# ACTION SECTION
|
||||||
|
|
||||||
|
Configures commands executed for preview, selection, and restore behavior.
|
||||||
|
|
||||||
|
`previewDebounceTime` (integer, default: `300`)
|
||||||
|
: Debounce interval in milliseconds for preview actions.
|
||||||
|
|
||||||
|
`printSelected` (boolean, default: `true`)
|
||||||
|
: Print selected wallpaper path to stdout on confirmation.
|
||||||
|
|
||||||
|
`printPreview` (boolean, default: `false`)
|
||||||
|
: Print previewed wallpaper path to stdout on preview.
|
||||||
|
|
||||||
|
`onSelected` (string, default: `""`)
|
||||||
|
: Command executed when a wallpaper is confirmed.
|
||||||
|
|
||||||
|
`onPreview` (string, default: `""`)
|
||||||
|
: Command executed when a wallpaper is previewed.
|
||||||
|
|
||||||
|
`saveState` (array of object, default: `[]`)
|
||||||
|
: Commands for capturing system values before changing wallpaper.
|
||||||
|
|
||||||
|
Each item has:
|
||||||
|
|
||||||
|
- `key` (placeholder key)
|
||||||
|
- `fallback` (default value)
|
||||||
|
- `command` (stdout-mapped command)
|
||||||
|
- `timeout` (milliseconds)
|
||||||
|
|
||||||
|
`onRestore` (string, default: `""`)
|
||||||
|
: Command executed on restore. Saved state keys are usable as placeholders.
|
||||||
|
|
||||||
|
`quitOnSelected` (boolean, default: `false`)
|
||||||
|
: Exit application immediately after confirming a selection.
|
||||||
|
|
||||||
|
`restoreOnClose` (boolean, default: `true`)
|
||||||
|
: Run `onRestore` when application closes without a final selection.
|
||||||
|
|
||||||
|
## ACTION PLACEHOLDERS
|
||||||
|
|
||||||
|
The following placeholders are available in `onSelected`, `onPreview`, and
|
||||||
|
`onRestore` (where applicable):
|
||||||
|
|
||||||
|
`{{ path }}`
|
||||||
|
: Full path of selected or previewed wallpaper.
|
||||||
|
|
||||||
|
`{{ name }}`
|
||||||
|
: File name of selected or previewed wallpaper.
|
||||||
|
|
||||||
|
`{{ size }}`
|
||||||
|
: Size in bytes of selected or previewed wallpaper.
|
||||||
|
|
||||||
|
`{{ palette }}`
|
||||||
|
: Selected palette name (`"null"` if none).
|
||||||
|
|
||||||
|
`{{ colorName }}`
|
||||||
|
: Chosen primary color name (`"null"` if none).
|
||||||
|
|
||||||
|
`{{ colorHex }}`
|
||||||
|
: Chosen primary color hex (`"null"` if none).
|
||||||
|
|
||||||
|
`{{ domColorHex }}`
|
||||||
|
: Dominant color hex extracted from the wallpaper.
|
||||||
|
|
||||||
|
`{{ <key> }}`
|
||||||
|
: Value of a saved state item with matching key.
|
||||||
|
|
||||||
|
# STYLE SECTION
|
||||||
|
|
||||||
|
Controls window layout and thumbnail dimensions.
|
||||||
|
|
||||||
|
`image_width` (integer, default: `320`)
|
||||||
|
: Width of each thumbnail.
|
||||||
|
|
||||||
|
`image_height` (integer, default: `180`)
|
||||||
|
: Height of each thumbnail.
|
||||||
|
|
||||||
|
`image_focus_scale` (number, default: `1.5`)
|
||||||
|
: Focus scale multiplier for highlighted thumbnail.
|
||||||
|
|
||||||
|
`window_width` (integer, default: `750`)
|
||||||
|
: Initial window width.
|
||||||
|
|
||||||
|
`window_height` (integer, default: `500`)
|
||||||
|
: Initial window height.
|
||||||
|
|
||||||
|
# CACHE SECTION
|
||||||
|
|
||||||
|
Controls persisted UI state.
|
||||||
|
|
||||||
|
`saveSortMethod` (boolean, default: `true`)
|
||||||
|
: Persist sort method and direction.
|
||||||
|
|
||||||
|
`savePalette` (boolean, default: `true`)
|
||||||
|
: Persist selected palette.
|
||||||
|
|
||||||
|
`maxImageEntries` (integer, default: `1000`)
|
||||||
|
: Maximum number of image cache entries. Older entries are evicted.
|
||||||
|
|
||||||
|
# EXAMPLE
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"$schema": "https://raw.githubusercontent.com/Uyanide/WallReel/refs/heads/master/config.schema.json",
|
||||||
|
"wallpaper": {
|
||||||
|
"paths": ["/home/user/Pictures/favorite.jpg"],
|
||||||
|
"dirs": [
|
||||||
|
{
|
||||||
|
"path": "/home/user/Pictures/Wallpapers",
|
||||||
|
"recursive": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"excludes": ["\\.gif$"]
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"palettes": [
|
||||||
|
{
|
||||||
|
"name": "Dark",
|
||||||
|
"colors": [
|
||||||
|
{ "name": "blue", "value": "#89b4fa" },
|
||||||
|
{ "name": "red", "value": "#f38ba8" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"previewDebounceTime": 500,
|
||||||
|
"quitOnSelected": true,
|
||||||
|
"onPreview": "swww img {{ path }}",
|
||||||
|
"onSelected": "cp {{ path }} ~/.config/wallpaper/current/ && swww img {{ path }}",
|
||||||
|
"saveState": [
|
||||||
|
{
|
||||||
|
"key": "current_wp",
|
||||||
|
"fallback": "/home/user/Pictures/default.jpg",
|
||||||
|
"command": "find ~/.config/wallpaper/current -type f | head -n 1",
|
||||||
|
"timeout": 1000
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"onRestore": "swww img {{ current_wp }}"
|
||||||
|
},
|
||||||
|
"style": {
|
||||||
|
"image_width": 640,
|
||||||
|
"image_height": 400,
|
||||||
|
"image_focus_scale": 1.2,
|
||||||
|
"window_width": 1280,
|
||||||
|
"window_height": 720
|
||||||
|
},
|
||||||
|
"cache": {
|
||||||
|
"saveSortMethod": true,
|
||||||
|
"savePalette": true,
|
||||||
|
"maxImageEntries": 300
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
# SEE ALSO
|
||||||
|
|
||||||
|
**wallreel**(1)
|
||||||
|
|
||||||
|
# AUTHOR
|
||||||
|
|
||||||
|
Uyanide <github.com/Uyanide>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
Reference in New Issue
Block a user