🚧 wip: chekkupointo
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
project(Wallpaper_Carousel VERSION 0.1 LANGUAGES CXX)
|
project(WallReel VERSION 0.1 LANGUAGES CXX)
|
||||||
|
|
||||||
set(EXECUTABLE_NAME "wallreel")
|
set(EXECUTABLE_NAME "wallreel")
|
||||||
set(CORELIB_NAME "wallreel-core")
|
set(CORELIB_NAME "wallreel-core")
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ void TestConfigMgr::testDefaults() {
|
|||||||
QVERIFY(config.getActionConfig().onSelected.isEmpty());
|
QVERIFY(config.getActionConfig().onSelected.isEmpty());
|
||||||
QVERIFY(config.getActionConfig().onPreview.isEmpty());
|
QVERIFY(config.getActionConfig().onPreview.isEmpty());
|
||||||
QVERIFY(config.getActionConfig().onRestore.isEmpty());
|
QVERIFY(config.getActionConfig().onRestore.isEmpty());
|
||||||
QVERIFY(config.getActionConfig().saveState.isEmpty());
|
QVERIFY(config.getActionConfig().saveStateConfig.isEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
void TestConfigMgr::testFullConfigParsing() {
|
void TestConfigMgr::testFullConfigParsing() {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ qt_add_qml_module(${CORELIB_NAME}
|
|||||||
Image/provider.hpp Image/provider.cpp
|
Image/provider.hpp Image/provider.cpp
|
||||||
Palette/data.hpp
|
Palette/data.hpp
|
||||||
Palette/manager.hpp Palette/manager.cpp
|
Palette/manager.hpp Palette/manager.cpp
|
||||||
|
Config/data.hpp
|
||||||
Config/manager.hpp Config/manager.cpp
|
Config/manager.hpp Config/manager.cpp
|
||||||
logger.hpp logger.cpp
|
logger.hpp logger.cpp
|
||||||
wallpaperservice.hpp wallpaperservice.cpp
|
wallpaperservice.hpp wallpaperservice.cpp
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#ifndef WALLREEL_CONFIG_DATA_HPP
|
||||||
|
#define WALLREEL_CONFIG_DATA_HPP
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
|
#include <QHash>
|
||||||
|
#include <QList>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QSize>
|
||||||
|
#include <QString>
|
||||||
|
#include <QStringList>
|
||||||
|
|
||||||
|
// Config entries:
|
||||||
|
//
|
||||||
|
// wallpaper.paths array [] List of paths to images.
|
||||||
|
// wallpaper.dirs array [] Directories to search for images.
|
||||||
|
// wallpaper.dirs[].path string "" Path to the directory.
|
||||||
|
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
||||||
|
// wallpaper.excludes array [] Exclude patterns (regex)
|
||||||
|
//
|
||||||
|
// palettes array []
|
||||||
|
// palettes[].name string "" Name of the palette
|
||||||
|
// palettes[].colors array [] List of colors in the palette
|
||||||
|
// palettes[].colors[].name string "" Name of the color
|
||||||
|
// palettes[].colors[].value string "" Color value in hex format, e.g. "#ff0000" for red
|
||||||
|
//
|
||||||
|
// action.previewDebounceTime number 300 Debounce time for preview action in milliseconds
|
||||||
|
// action.printSelected boolean true Whether to print the selected wallpaper path to stdout on confirm
|
||||||
|
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
|
||||||
|
// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path)
|
||||||
|
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path)
|
||||||
|
// action.saveState array [] Useful for restore command
|
||||||
|
// action.saveState[].key string "" Key of value to save, used as {{ key }} in onRestore command
|
||||||
|
// action.saveState[].default string "" Value to save, used when "cmd" is not set or command execution fails or output is empty
|
||||||
|
// action.saveState[].cmd 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.onRestore string "" Command to execute on restore ({{ key }} -> value defined or obtained in saveState)
|
||||||
|
//
|
||||||
|
// style.image_width number 320 Width of each image
|
||||||
|
// style.image_height number 200 Height of each image
|
||||||
|
// style.image_focus_scale number 1.5 Scale of the focused image (relative to unfocused image)
|
||||||
|
// style.window_width number 750 Initial window width
|
||||||
|
// style.window_height number 500 Initial window height
|
||||||
|
//
|
||||||
|
// sort.type string "name" Sorting type: "none", "name", "date", "size"
|
||||||
|
// sort.reverse boolean false Whether to reverse the sorting order
|
||||||
|
// Normal order: name: lexicographical, e.g. "a.jpg" before "b.jpg"
|
||||||
|
// date: older before newer
|
||||||
|
// size: smaller before larger
|
||||||
|
|
||||||
|
namespace WallReel::Core::Config {
|
||||||
|
|
||||||
|
inline const QString s_DefaultConfigFileName = "config.json";
|
||||||
|
|
||||||
|
enum class SortType : int {
|
||||||
|
None = 0, // "none"
|
||||||
|
Name, // "name"
|
||||||
|
Date, // "date"
|
||||||
|
Size, // "size"
|
||||||
|
};
|
||||||
|
|
||||||
|
struct WallpaperConfigItems {
|
||||||
|
struct WallpaperDirConfigItem {
|
||||||
|
QString path;
|
||||||
|
bool recursive;
|
||||||
|
};
|
||||||
|
|
||||||
|
QStringList paths;
|
||||||
|
QList<WallpaperDirConfigItem> dirs;
|
||||||
|
QList<QRegularExpression> excludes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PaletteConfigItems {
|
||||||
|
struct PaletteColorConfigItem {
|
||||||
|
QString name;
|
||||||
|
QColor value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PaletteConfigItem {
|
||||||
|
QString name;
|
||||||
|
QList<PaletteColorConfigItem> colors;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<PaletteConfigItem> palettes;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ActionConfigItems {
|
||||||
|
struct SaveStateItem {
|
||||||
|
QString key;
|
||||||
|
QString defaultVal;
|
||||||
|
QString cmd;
|
||||||
|
int timeout = 3000;
|
||||||
|
};
|
||||||
|
|
||||||
|
QList<SaveStateItem> saveStateConfig;
|
||||||
|
QHash<QString, QString> saveState;
|
||||||
|
QString onSelected;
|
||||||
|
QString onPreview;
|
||||||
|
QString onRestore;
|
||||||
|
int previewDebounceTime = 300; // milliseconds
|
||||||
|
bool printSelected = true;
|
||||||
|
bool printPreview = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StyleConfigItems {
|
||||||
|
double imageFocusScale = 1.5;
|
||||||
|
int imageWidth = 320;
|
||||||
|
int imageHeight = 200;
|
||||||
|
int windowWidth = 750;
|
||||||
|
int windowHeight = 500;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SortConfigItems {
|
||||||
|
SortType type = SortType::Name;
|
||||||
|
bool reverse = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Config
|
||||||
|
|
||||||
|
#endif // WALLREEL_CONFIG_DATA_HPP
|
||||||
@@ -5,8 +5,10 @@
|
|||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QProcess>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QTimer>
|
||||||
|
|
||||||
#include "Utils/misc.hpp"
|
#include "Utils/misc.hpp"
|
||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
@@ -179,13 +181,27 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
|||||||
m_actionConfig.printPreview = val.toBool();
|
m_actionConfig.printPreview = val.toBool();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (config.contains("saveState")) {
|
if (config.contains("saveState") && config["saveState"].isArray()) {
|
||||||
const auto& val = config["saveState"];
|
const QJsonArray& arr = config["saveState"].toArray();
|
||||||
if (val.isObject()) {
|
for (const auto& item : arr) {
|
||||||
QJsonObject obj = val.toObject();
|
if (item.isObject()) {
|
||||||
for (const auto& key : obj.keys()) {
|
QJsonObject obj = item.toObject();
|
||||||
if (obj[key].isString()) {
|
ActionConfigItems::SaveStateItem sItem;
|
||||||
m_actionConfig.saveState.insert(key, obj[key].toString());
|
if (obj.contains("key") && obj["key"].isString()) {
|
||||||
|
sItem.key = obj["key"].toString();
|
||||||
|
}
|
||||||
|
if (obj.contains("default") && obj["default"].isString()) {
|
||||||
|
sItem.defaultVal = obj["default"].toString();
|
||||||
|
}
|
||||||
|
if (obj.contains("cmd") && obj["cmd"].isString()) {
|
||||||
|
sItem.cmd = obj["cmd"].toString();
|
||||||
|
}
|
||||||
|
if (obj.contains("timeout") && obj["timeout"].isDouble()) {
|
||||||
|
sItem.timeout = obj["timeout"].toInt();
|
||||||
|
}
|
||||||
|
if (!sItem.key.isEmpty()) {
|
||||||
|
m_actionConfig.saveStateConfig.append(sItem);
|
||||||
|
m_actionConfig.saveState.insert(sItem.key, sItem.defaultVal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,3 +355,98 @@ void WallReel::Core::Config::Manager::_loadWallpapers() {
|
|||||||
|
|
||||||
Logger::info(QString("Found %1 files").arg(paths.size()));
|
Logger::info(QString("Found %1 files").arg(paths.size()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void WallReel::Core::Config::Manager::captureState() {
|
||||||
|
m_pendingCaptures = 0;
|
||||||
|
|
||||||
|
const auto& items = m_actionConfig.saveStateConfig;
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
emit stateCaptured();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& item : items) {
|
||||||
|
if (!item.cmd.isEmpty()) {
|
||||||
|
m_pendingCaptures++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_pendingCaptures == 0) {
|
||||||
|
emit stateCaptured();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& item : items) {
|
||||||
|
if (item.cmd.isEmpty()) continue;
|
||||||
|
|
||||||
|
QProcess* process = new QProcess(this);
|
||||||
|
QTimer* timer = nullptr;
|
||||||
|
if (item.timeout > 0) {
|
||||||
|
timer = new QTimer(this);
|
||||||
|
timer->setSingleShot(true);
|
||||||
|
timer->setInterval(item.timeout > 0 ? item.timeout : std::numeric_limits<int>::max());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString key = item.key;
|
||||||
|
QString defaultVal = item.defaultVal;
|
||||||
|
|
||||||
|
auto onFinished = [this, process, timer, key, defaultVal](const QString& output, bool success) {
|
||||||
|
if (timer) {
|
||||||
|
timer->stop();
|
||||||
|
timer->deleteLater();
|
||||||
|
}
|
||||||
|
|
||||||
|
process->disconnect();
|
||||||
|
|
||||||
|
QString result = success ? output : defaultVal;
|
||||||
|
if (result.isEmpty()) result = defaultVal;
|
||||||
|
|
||||||
|
_onCaptureResult(key, result);
|
||||||
|
process->deleteLater();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (timer) {
|
||||||
|
// Timeout handler
|
||||||
|
connect(timer, &QTimer::timeout, this, [process, onFinished, key]() {
|
||||||
|
Logger::warn(QString("Timeout capturing state for key '%1'").arg(key));
|
||||||
|
if (process->state() != QProcess::NotRunning) {
|
||||||
|
process->kill();
|
||||||
|
} else {
|
||||||
|
onFinished(QString(), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finished handler
|
||||||
|
connect(process, &QProcess::finished, this, [process, onFinished](int exitCode, QProcess::ExitStatus exitStatus) {
|
||||||
|
bool success = (exitStatus == QProcess::NormalExit && exitCode == 0);
|
||||||
|
QString output;
|
||||||
|
if (success) {
|
||||||
|
output = QString::fromUtf8(process->readAllStandardOutput()).trimmed();
|
||||||
|
}
|
||||||
|
onFinished(output, success);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Error handler
|
||||||
|
connect(process, &QProcess::errorOccurred, this, [process, onFinished, key](QProcess::ProcessError error) {
|
||||||
|
if (error == QProcess::FailedToStart) {
|
||||||
|
Logger::warn(QString("Failed to start state command for key '%1'").arg(key));
|
||||||
|
onFinished(QString(), false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (timer) {
|
||||||
|
timer->start();
|
||||||
|
}
|
||||||
|
process->startCommand(item.cmd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void WallReel::Core::Config::Manager::_onCaptureResult(const QString& key, const QString& value) {
|
||||||
|
// This is all in main thread, so no lock needed
|
||||||
|
m_actionConfig.saveState[key] = value;
|
||||||
|
m_pendingCaptures--;
|
||||||
|
if (m_pendingCaptures == 0) {
|
||||||
|
emit stateCaptured();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,107 +1,10 @@
|
|||||||
#ifndef WALLREEL_CONFIGMGR_HPP
|
#ifndef WALLREEL_CONFIGMGR_HPP
|
||||||
#define WALLREEL_CONFIGMGR_HPP
|
#define WALLREEL_CONFIGMGR_HPP
|
||||||
|
|
||||||
#include <QColor>
|
#include "data.hpp"
|
||||||
#include <QObject>
|
|
||||||
#include <QRegularExpression>
|
|
||||||
#include <QSize>
|
|
||||||
#include <QString>
|
|
||||||
#include <QStringList>
|
|
||||||
|
|
||||||
// Config entries:
|
|
||||||
//
|
|
||||||
// wallpaper.paths array [] List of paths to images.
|
|
||||||
// wallpaper.dirs array [] Directories to search for images.
|
|
||||||
// wallpaper.dirs[].path string "" Path to the directory.
|
|
||||||
// wallpaper.dirs[].recursive boolean false Whether to search the directory recursively.
|
|
||||||
// wallpaper.excludes array [] Exclude patterns (regex)
|
|
||||||
//
|
|
||||||
// palettes array []
|
|
||||||
// palettes[].name string "" Name of the palette
|
|
||||||
// palettes[].colors array [] List of colors in the palette
|
|
||||||
// palettes[].colors[].name string "" Name of the color
|
|
||||||
// palettes[].colors[].value string "" Color value in hex format, e.g. "#ff0000" for red
|
|
||||||
//
|
|
||||||
// action.previewDebounceTime number 300 Minimum debounce time for preview action in milliseconds
|
|
||||||
// action.printSelected boolean false Whether to print the selected wallpaper path to stdout on confirm
|
|
||||||
// action.printPreview boolean false Whether to print the previewed wallpaper path to stdout on preview
|
|
||||||
// action.saveState object {} Key-value pairs to save the state, useful for restore command
|
|
||||||
// action.onRestore string "" Command to execute on restore ({{ key }} -> value in saveState)
|
|
||||||
// action.onSelected string "" Command to execute on confirmation ({{ path }} -> full path)
|
|
||||||
// action.onPreview string "" Command to execute on preview ({{ path }} -> full path)
|
|
||||||
//
|
|
||||||
// style.image_width number 320 Width of each image
|
|
||||||
// style.image_height number 200 Height of each image
|
|
||||||
// style.image_focus_scale number 1.5 Scale of the focused image (relative to unfocused image)
|
|
||||||
// style.window_width number 750 Initial window width
|
|
||||||
// style.window_height number 500 Initial window height
|
|
||||||
//
|
|
||||||
// sort.type string "name" Sorting type: "none", "name", "date", "size"
|
|
||||||
// sort.reverse boolean false Whether to reverse the sorting order
|
|
||||||
// Normal order: name: lexicographical, e.g. "a.jpg" before "b.jpg"
|
|
||||||
// date: older before newer
|
|
||||||
// size: smaller before larger
|
|
||||||
|
|
||||||
namespace WallReel::Core::Config {
|
namespace WallReel::Core::Config {
|
||||||
|
|
||||||
static const QString s_DefaultConfigFileName = "config.json";
|
|
||||||
|
|
||||||
enum class SortType : int {
|
|
||||||
None = 0, // "none"
|
|
||||||
Name, // "name"
|
|
||||||
Date, // "date"
|
|
||||||
Size, // "size"
|
|
||||||
};
|
|
||||||
|
|
||||||
struct WallpaperConfigItems {
|
|
||||||
struct WallpaperDirConfigItem {
|
|
||||||
QString path;
|
|
||||||
bool recursive;
|
|
||||||
};
|
|
||||||
|
|
||||||
QStringList paths;
|
|
||||||
QList<WallpaperDirConfigItem> dirs;
|
|
||||||
QList<QRegularExpression> excludes;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PaletteConfigItems {
|
|
||||||
struct PaletteColorConfigItem {
|
|
||||||
QString name;
|
|
||||||
QColor value;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PaletteConfigItem {
|
|
||||||
QString name;
|
|
||||||
QList<PaletteColorConfigItem> colors;
|
|
||||||
};
|
|
||||||
|
|
||||||
QList<PaletteConfigItem> palettes;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ActionConfigItems {
|
|
||||||
|
|
||||||
QHash<QString, QString> saveState;
|
|
||||||
QString onSelected;
|
|
||||||
QString onPreview;
|
|
||||||
QString onRestore;
|
|
||||||
int previewDebounceTime = 300; // milliseconds
|
|
||||||
bool printSelected = false;
|
|
||||||
bool printPreview = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct StyleConfigItems {
|
|
||||||
double imageFocusScale = 1.5;
|
|
||||||
int imageWidth = 320;
|
|
||||||
int imageHeight = 200;
|
|
||||||
int windowWidth = 750;
|
|
||||||
int windowHeight = 500;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct SortConfigItems {
|
|
||||||
SortType type = SortType::Name;
|
|
||||||
bool reverse = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Manager : public QObject {
|
class Manager : public QObject {
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
@@ -148,7 +51,10 @@ class Manager : public QObject {
|
|||||||
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString m_configDir;
|
Q_INVOKABLE void captureState();
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void stateCaptured();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void _loadConfig(const QString& configPath);
|
void _loadConfig(const QString& configPath);
|
||||||
@@ -158,8 +64,10 @@ class Manager : public QObject {
|
|||||||
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 _loadSortConfig(const QJsonObject& config);
|
||||||
|
void _onCaptureResult(const QString& key, const QString& value);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
const QString m_configDir;
|
||||||
WallpaperConfigItems m_wallpaperConfig;
|
WallpaperConfigItems m_wallpaperConfig;
|
||||||
PaletteConfigItems m_paletteConfig;
|
PaletteConfigItems m_paletteConfig;
|
||||||
ActionConfigItems m_actionConfig;
|
ActionConfigItems m_actionConfig;
|
||||||
@@ -167,6 +75,8 @@ class Manager : public QObject {
|
|||||||
SortConfigItems m_sortConfig;
|
SortConfigItems m_sortConfig;
|
||||||
|
|
||||||
QStringList m_wallpapers;
|
QStringList m_wallpapers;
|
||||||
|
|
||||||
|
int m_pendingCaptures = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace WallReel::Core::Config
|
} // namespace WallReel::Core::Config
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include "Config/manager.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "provider.hpp"
|
#include "provider.hpp"
|
||||||
|
|
||||||
namespace WallReel::Core::Image {
|
namespace WallReel::Core::Image {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
#include "domcolor.hpp"
|
||||||
|
|
||||||
|
static constexpr int scaleMaxWidth = 128;
|
||||||
|
static constexpr int scaleMaxHeight = 128;
|
||||||
|
// See /misc/ColorArgTest/index.html for visualizing the effect of different powers
|
||||||
|
static constexpr double weightSaturationPower = 2.0;
|
||||||
|
static constexpr double weightLightnessPower = 2.0;
|
||||||
|
static constexpr int numBins = 36; // 360 degrees / 10 degrees per bin
|
||||||
|
|
||||||
|
static double getWeight(const QColor& color) {
|
||||||
|
int h, s, l;
|
||||||
|
color.getHsl(&h, &s, &l);
|
||||||
|
if (h < 0) return 0.0; // Skip undefined hues
|
||||||
|
|
||||||
|
double sNorm = s / 255.0;
|
||||||
|
|
||||||
|
double dist = std::abs(l - 128.0) / 128.0;
|
||||||
|
double lNorm = 1.0 - dist;
|
||||||
|
|
||||||
|
// // satPower=2, lightPower=2
|
||||||
|
// return std::pow(sNorm, weightSaturationPower) * std::pow(lNorm, weightLightnessPower);
|
||||||
|
// Since these are just simple squares, no need to use std::pow() here
|
||||||
|
return sNorm * sNorm * lNorm * lNorm;
|
||||||
|
}
|
||||||
|
|
||||||
|
static QColor getWeightedDominantColor(const QImage& image) {
|
||||||
|
if (image.isNull()) return QColor();
|
||||||
|
|
||||||
|
// QImage scaledImg = image.scaled(128, 128, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
|
QImage* scaledImg = nullptr;
|
||||||
|
if (image.width() > ::scaleMaxWidth || image.height() > ::scaleMaxHeight) {
|
||||||
|
scaledImg = new QImage(image.scaled(::scaleMaxWidth, ::scaleMaxHeight, Qt::IgnoreAspectRatio, Qt::SmoothTransformation));
|
||||||
|
} else {
|
||||||
|
scaledImg = new QImage(image);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct HueBin {
|
||||||
|
double totalWeight = 0.0;
|
||||||
|
double sumR = 0.0;
|
||||||
|
double sumG = 0.0;
|
||||||
|
double sumB = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
QVector<HueBin> bins(::numBins);
|
||||||
|
|
||||||
|
for (int y = 0; y < scaledImg->height(); ++y) {
|
||||||
|
for (int x = 0; x < scaledImg->width(); ++x) {
|
||||||
|
QColor color = scaledImg->pixelColor(x, y);
|
||||||
|
|
||||||
|
int hue = color.hue();
|
||||||
|
if (hue < 0) continue; // Skip grayscale pixels
|
||||||
|
|
||||||
|
double weight = getWeight(color);
|
||||||
|
// Filter out low-weight pixels to reduce noise
|
||||||
|
if (weight < 0.05) continue;
|
||||||
|
|
||||||
|
int binIndex = (hue / 10) % ::numBins;
|
||||||
|
|
||||||
|
bins[binIndex].totalWeight += weight;
|
||||||
|
bins[binIndex].sumR += color.redF() * weight;
|
||||||
|
bins[binIndex].sumG += color.greenF() * weight;
|
||||||
|
bins[binIndex].sumB += color.blueF() * weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete scaledImg;
|
||||||
|
|
||||||
|
// Find the bin with the highest total weight
|
||||||
|
int maxBinIndex = -1;
|
||||||
|
double maxWeight = 0.0;
|
||||||
|
for (int i = 0; i < ::numBins; ++i) {
|
||||||
|
if (bins[i].totalWeight > maxWeight) {
|
||||||
|
maxWeight = bins[i].totalWeight;
|
||||||
|
maxBinIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all filtered (e.g. grayscale image), return a default color
|
||||||
|
if (maxBinIndex == -1 || maxWeight <= 0.0) {
|
||||||
|
return QColor(Qt::gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the average color for the winning bin
|
||||||
|
const HueBin& winningBin = bins[maxBinIndex];
|
||||||
|
|
||||||
|
int finalR = std::round((winningBin.sumR / winningBin.totalWeight) * 255.0);
|
||||||
|
int finalG = std::round((winningBin.sumG / winningBin.totalWeight) * 255.0);
|
||||||
|
int finalB = std::round((winningBin.sumB / winningBin.totalWeight) * 255.0);
|
||||||
|
|
||||||
|
return QColor(finalR, finalG, finalB);
|
||||||
|
}
|
||||||
|
|
||||||
|
QColor WallReel::Core::Palette::getDominantColor(const QImage& image) {
|
||||||
|
return ::getWeightedDominantColor(image);
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#ifndef WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
|
||||||
|
#define WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
namespace WallReel::Core::Palette {
|
||||||
|
|
||||||
|
QColor getDominantColor(const QImage& image);
|
||||||
|
|
||||||
|
} // namespace WallReel::Core::Palette
|
||||||
|
|
||||||
|
#endif // WALLREEL_CORE_PALETTE_DOMCOLOR_HPP
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#ifndef WALLREEL_PALETTE_MANAGER_HPP
|
#ifndef WALLREEL_PALETTE_MANAGER_HPP
|
||||||
#define WALLREEL_PALETTE_MANAGER_HPP
|
#define WALLREEL_PALETTE_MANAGER_HPP
|
||||||
|
|
||||||
#include "Config/manager.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "data.hpp"
|
#include "data.hpp"
|
||||||
|
|
||||||
namespace WallReel::Core::Palette {
|
namespace WallReel::Core::Palette {
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include "Config/manager.hpp"
|
#include "Config/data.hpp"
|
||||||
#include "Image/data.hpp"
|
#include "Image/data.hpp"
|
||||||
|
|
||||||
namespace WallReel::Core {
|
namespace WallReel::Core {
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ qt_add_qml_module(${UILIB_NAME}
|
|||||||
Pages/LoadingScreen.qml
|
Pages/LoadingScreen.qml
|
||||||
Pages/CarouselScreen.qml
|
Pages/CarouselScreen.qml
|
||||||
Modules/Carousel.qml
|
Modules/Carousel.qml
|
||||||
|
Modules/TitleBar.qml
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ import WallReel.UI.Pages
|
|||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
width: Config.windowWidth
|
width: Config.windowWidth
|
||||||
height: Config.windowHeight
|
height: Config.windowHeight
|
||||||
minimumWidth: width
|
// minimumWidth: width
|
||||||
maximumWidth: width
|
// maximumWidth: width
|
||||||
minimumHeight: height
|
// minimumHeight: height
|
||||||
maximumHeight: height
|
// maximumHeight: height
|
||||||
visible: true
|
visible: true
|
||||||
title: qsTr("Hello World")
|
title: qsTr("Hello World")
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string title: ""
|
||||||
|
// start from 0
|
||||||
|
property int index: 0
|
||||||
|
property int totalCount: 0
|
||||||
|
property int maxTitleLength: 50
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: (root.index + 1) + " / " + root.totalCount
|
||||||
|
font.pixelSize: 12
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Label {
|
||||||
|
text: root.title.length > root.maxTitleLength ? root.title.substring(0, root.maxTitleLength) + "..." : root.title
|
||||||
|
font.pixelSize: 12
|
||||||
|
anchors.centerIn: parent
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -41,10 +41,11 @@ Item {
|
|||||||
anchors.margins: 20
|
anchors.margins: 20
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
|
||||||
Label {
|
TitleBar {
|
||||||
text: (ImageModel.dataAt(carousel.currentIndex, "imgName") ?? "") + " (" + (carousel.currentIndex + 1) + "/" + carousel.count + ")"
|
title: ImageModel.dataAt(carousel.currentIndex, "imgName") ?? ""
|
||||||
font.pixelSize: 12
|
index: carousel.currentIndex
|
||||||
Layout.alignment: Qt.AlignHCenter
|
totalCount: carousel.count
|
||||||
|
Layout.fillWidth: true
|
||||||
}
|
}
|
||||||
|
|
||||||
Carousel {
|
Carousel {
|
||||||
|
|||||||
@@ -0,0 +1,216 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Color Arg Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
.left-panel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
border-right: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
#image-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 500px;
|
||||||
|
margin-top: 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.control-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
#color-box {
|
||||||
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
|
border: 1px solid #000;
|
||||||
|
margin-top: 20px;
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="left-panel">
|
||||||
|
<input type="file" id="image-upload" accept="image/*" />
|
||||||
|
<br />
|
||||||
|
<img id="image-preview" alt="Image Preview" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="saturation-power">Saturation Power: <span id="sat-val">1.0</span></label>
|
||||||
|
<input type="range" id="saturation-power" min="1" max="5" step="0.1" value="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label for="lightness-power">Lightness Power: <span id="light-val">1.0</span></label>
|
||||||
|
<input type="range" id="lightness-power" min="1" max="5" step="0.1" value="1" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="color-box"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<canvas id="hidden-canvas" style="display: none"></canvas>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const imageUpload = document.getElementById('image-upload');
|
||||||
|
const imagePreview = document.getElementById('image-preview');
|
||||||
|
const hiddenCanvas = document.getElementById('hidden-canvas');
|
||||||
|
const ctx = hiddenCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const satSlider = document.getElementById('saturation-power');
|
||||||
|
const lightSlider = document.getElementById('lightness-power');
|
||||||
|
const satValDisplay = document.getElementById('sat-val');
|
||||||
|
const lightValDisplay = document.getElementById('light-val');
|
||||||
|
const colorBox = document.getElementById('color-box');
|
||||||
|
|
||||||
|
let currentPixelBuffer = null;
|
||||||
|
|
||||||
|
function rgbToHsl(r, g, b) {
|
||||||
|
r /= 255;
|
||||||
|
g /= 255;
|
||||||
|
b /= 255;
|
||||||
|
const max = Math.max(r, g, b);
|
||||||
|
const min = Math.min(r, g, b);
|
||||||
|
let h,
|
||||||
|
s,
|
||||||
|
l = (max + min) / 2;
|
||||||
|
|
||||||
|
if (max === min) {
|
||||||
|
h = s = 0;
|
||||||
|
} else {
|
||||||
|
const d = max - min;
|
||||||
|
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||||
|
switch (max) {
|
||||||
|
case r:
|
||||||
|
h = (g - b) / d + (g < b ? 6 : 0);
|
||||||
|
break;
|
||||||
|
case g:
|
||||||
|
h = (b - r) / d + 2;
|
||||||
|
break;
|
||||||
|
case b:
|
||||||
|
h = (r - g) / d + 4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [h * 360, s * 255, l * 255];
|
||||||
|
}
|
||||||
|
|
||||||
|
function foobar(rgbaBuffer, satPower, lightPower) {
|
||||||
|
const numBins = 36;
|
||||||
|
const bins = Array.from({ length: numBins }, () => ({
|
||||||
|
totalWeight: 0,
|
||||||
|
sumR: 0,
|
||||||
|
sumG: 0,
|
||||||
|
sumB: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (let i = 0; i < rgbaBuffer.length; i += 4) {
|
||||||
|
const r = rgbaBuffer[i];
|
||||||
|
const g = rgbaBuffer[i + 1];
|
||||||
|
const b = rgbaBuffer[i + 2];
|
||||||
|
const a = rgbaBuffer[i + 3];
|
||||||
|
|
||||||
|
if (a < 5) continue;
|
||||||
|
|
||||||
|
const [h, s, l] = rgbToHsl(r, g, b);
|
||||||
|
|
||||||
|
if (s === 0) continue;
|
||||||
|
|
||||||
|
const sNorm = s / 255.0;
|
||||||
|
const wS = Math.pow(sNorm, satPower);
|
||||||
|
|
||||||
|
const dist = Math.abs(l - 128.0) / 128.0;
|
||||||
|
const lNorm = 1.0 - dist;
|
||||||
|
const wL = Math.pow(lNorm, lightPower);
|
||||||
|
|
||||||
|
const weight = wS * wL;
|
||||||
|
|
||||||
|
if (weight < 0.05) continue;
|
||||||
|
|
||||||
|
const binIndex = Math.floor(h / 10) % numBins;
|
||||||
|
|
||||||
|
bins[binIndex].totalWeight += weight;
|
||||||
|
bins[binIndex].sumR += r * weight;
|
||||||
|
bins[binIndex].sumG += g * weight;
|
||||||
|
bins[binIndex].sumB += b * weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
let maxBinIndex = -1;
|
||||||
|
let maxWeight = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < numBins; i++) {
|
||||||
|
if (bins[i].totalWeight > maxWeight) {
|
||||||
|
maxWeight = bins[i].totalWeight;
|
||||||
|
maxBinIndex = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (maxBinIndex === -1 || maxWeight <= 0) {
|
||||||
|
return `rgb(128, 128, 128)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const winningBin = bins[maxBinIndex];
|
||||||
|
const finalR = Math.round(winningBin.sumR / winningBin.totalWeight);
|
||||||
|
const finalG = Math.round(winningBin.sumG / winningBin.totalWeight);
|
||||||
|
const finalB = Math.round(winningBin.sumB / winningBin.totalWeight);
|
||||||
|
|
||||||
|
return `rgb(${finalR}, ${finalG}, ${finalB})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateColor() {
|
||||||
|
if (!currentPixelBuffer) return;
|
||||||
|
|
||||||
|
const satPower = parseFloat(satSlider.value);
|
||||||
|
const lightPower = parseFloat(lightSlider.value);
|
||||||
|
|
||||||
|
satValDisplay.textContent = satPower.toFixed(1);
|
||||||
|
lightValDisplay.textContent = lightPower.toFixed(1);
|
||||||
|
|
||||||
|
const color = foobar(currentPixelBuffer, satPower, lightPower);
|
||||||
|
colorBox.style.backgroundColor = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
imageUpload.addEventListener('change', function (e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = function (event) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = function () {
|
||||||
|
imagePreview.src = event.target.result;
|
||||||
|
imagePreview.style.display = 'block';
|
||||||
|
|
||||||
|
hiddenCanvas.width = img.width;
|
||||||
|
hiddenCanvas.height = img.height;
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
|
const imageData = ctx.getImageData(0, 0, img.width, img.height);
|
||||||
|
currentPixelBuffer = imageData.data;
|
||||||
|
|
||||||
|
updateColor();
|
||||||
|
};
|
||||||
|
img.src = event.target.result;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
satSlider.addEventListener('input', updateColor);
|
||||||
|
lightSlider.addEventListener('input', updateColor);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user