🚧 wip: chekkupointo
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
||||
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(CORELIB_NAME "wallreel-core")
|
||||
|
||||
@@ -89,7 +89,7 @@ void TestConfigMgr::testDefaults() {
|
||||
QVERIFY(config.getActionConfig().onSelected.isEmpty());
|
||||
QVERIFY(config.getActionConfig().onPreview.isEmpty());
|
||||
QVERIFY(config.getActionConfig().onRestore.isEmpty());
|
||||
QVERIFY(config.getActionConfig().saveState.isEmpty());
|
||||
QVERIFY(config.getActionConfig().saveStateConfig.isEmpty());
|
||||
}
|
||||
|
||||
void TestConfigMgr::testFullConfigParsing() {
|
||||
|
||||
@@ -7,6 +7,7 @@ qt_add_qml_module(${CORELIB_NAME}
|
||||
Image/provider.hpp Image/provider.cpp
|
||||
Palette/data.hpp
|
||||
Palette/manager.hpp Palette/manager.cpp
|
||||
Config/data.hpp
|
||||
Config/manager.hpp Config/manager.cpp
|
||||
logger.hpp logger.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 <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QProcessEnvironment>
|
||||
#include <QStandardPaths>
|
||||
#include <QTimer>
|
||||
|
||||
#include "Utils/misc.hpp"
|
||||
#include "logger.hpp"
|
||||
@@ -179,13 +181,27 @@ void WallReel::Core::Config::Manager::_loadActionConfig(const QJsonObject& root)
|
||||
m_actionConfig.printPreview = val.toBool();
|
||||
}
|
||||
}
|
||||
if (config.contains("saveState")) {
|
||||
const auto& val = config["saveState"];
|
||||
if (val.isObject()) {
|
||||
QJsonObject obj = val.toObject();
|
||||
for (const auto& key : obj.keys()) {
|
||||
if (obj[key].isString()) {
|
||||
m_actionConfig.saveState.insert(key, obj[key].toString());
|
||||
if (config.contains("saveState") && config["saveState"].isArray()) {
|
||||
const QJsonArray& arr = config["saveState"].toArray();
|
||||
for (const auto& item : arr) {
|
||||
if (item.isObject()) {
|
||||
QJsonObject obj = item.toObject();
|
||||
ActionConfigItems::SaveStateItem sItem;
|
||||
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()));
|
||||
}
|
||||
|
||||
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
|
||||
#define WALLREEL_CONFIGMGR_HPP
|
||||
|
||||
#include <QColor>
|
||||
#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
|
||||
#include "data.hpp"
|
||||
|
||||
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 {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -148,7 +51,10 @@ class Manager : public QObject {
|
||||
return QSize{m_styleConfig.imageWidth, m_styleConfig.imageHeight} * m_styleConfig.imageFocusScale;
|
||||
}
|
||||
|
||||
const QString m_configDir;
|
||||
Q_INVOKABLE void captureState();
|
||||
|
||||
signals:
|
||||
void stateCaptured();
|
||||
|
||||
private:
|
||||
void _loadConfig(const QString& configPath);
|
||||
@@ -158,8 +64,10 @@ class Manager : public QObject {
|
||||
void _loadActionConfig(const QJsonObject& config);
|
||||
void _loadStyleConfig(const QJsonObject& config);
|
||||
void _loadSortConfig(const QJsonObject& config);
|
||||
void _onCaptureResult(const QString& key, const QString& value);
|
||||
|
||||
private:
|
||||
const QString m_configDir;
|
||||
WallpaperConfigItems m_wallpaperConfig;
|
||||
PaletteConfigItems m_paletteConfig;
|
||||
ActionConfigItems m_actionConfig;
|
||||
@@ -167,6 +75,8 @@ class Manager : public QObject {
|
||||
SortConfigItems m_sortConfig;
|
||||
|
||||
QStringList m_wallpapers;
|
||||
|
||||
int m_pendingCaptures = 0;
|
||||
};
|
||||
|
||||
} // namespace WallReel::Core::Config
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
#include <QTimer>
|
||||
#include <atomic>
|
||||
|
||||
#include "Config/manager.hpp"
|
||||
#include "Config/data.hpp"
|
||||
#include "provider.hpp"
|
||||
|
||||
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
|
||||
#define WALLREEL_PALETTE_MANAGER_HPP
|
||||
|
||||
#include "Config/manager.hpp"
|
||||
#include "Config/data.hpp"
|
||||
#include "data.hpp"
|
||||
|
||||
namespace WallReel::Core::Palette {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <QProcess>
|
||||
#include <QTimer>
|
||||
|
||||
#include "Config/manager.hpp"
|
||||
#include "Config/data.hpp"
|
||||
#include "Image/data.hpp"
|
||||
|
||||
namespace WallReel::Core {
|
||||
|
||||
@@ -6,4 +6,5 @@ qt_add_qml_module(${UILIB_NAME}
|
||||
Pages/LoadingScreen.qml
|
||||
Pages/CarouselScreen.qml
|
||||
Modules/Carousel.qml
|
||||
Modules/TitleBar.qml
|
||||
)
|
||||
|
||||
@@ -6,10 +6,10 @@ import WallReel.UI.Pages
|
||||
ApplicationWindow {
|
||||
width: Config.windowWidth
|
||||
height: Config.windowHeight
|
||||
minimumWidth: width
|
||||
maximumWidth: width
|
||||
minimumHeight: height
|
||||
maximumHeight: height
|
||||
// minimumWidth: width
|
||||
// maximumWidth: width
|
||||
// minimumHeight: height
|
||||
// maximumHeight: height
|
||||
visible: true
|
||||
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
|
||||
spacing: 20
|
||||
|
||||
Label {
|
||||
text: (ImageModel.dataAt(carousel.currentIndex, "imgName") ?? "") + " (" + (carousel.currentIndex + 1) + "/" + carousel.count + ")"
|
||||
font.pixelSize: 12
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
TitleBar {
|
||||
title: ImageModel.dataAt(carousel.currentIndex, "imgName") ?? ""
|
||||
index: carousel.currentIndex
|
||||
totalCount: carousel.count
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
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